const ARROW_MIN_LENGTH = 48;
const ARROW_STYLES_ALLOWED = ['normal', 'right', 'left'];
const ARROW_WEIGHTS_ALLOWED = ['small', 'medium', 'large'];

const ARROW_WEIGHT_METRICS = {
  small: {
    shaft: 8,
    outline: 4,
    headLength: 26,
    headWidth: 24,
    shadowBlur: 2.5,
    padding: 28,
  },
  medium: {
    shaft: 12,
    outline: 4.5,
    headLength: 36,
    headWidth: 34,
    shadowBlur: 3.5,
    padding: 34,
  },
  large: {
    shaft: 16,
    outline: 5,
    headLength: 46,
    headWidth: 44,
    shadowBlur: 4.5,
    padding: 42,
  },
};

function normalizeArrowStyle(style) {
  return ARROW_STYLES_ALLOWED.indexOf(style) >= 0 ? style : 'normal';
}

function normalizeArrowWeight(weight) {
  return ARROW_WEIGHTS_ALLOWED.indexOf(weight) >= 0 ? weight : 'medium';
}

function snapArrowAngle(angle) {
  var snap = Math.PI / 4;
  return Math.round(angle / snap) * snap;
}

function arrowPointFromLengthAngle(point, length, angle) {
  return {
    x: point.x + length * Math.cos(angle),
    y: point.y + length * Math.sin(angle),
  };
}

function arrowGeometryFromPoints(startPoint, endPoint, options) {
  var dx = endPoint.x - startPoint.x;
  var dy = endPoint.y - startPoint.y;
  var angle = Math.atan2(dy, dx);
  var length = Math.sqrt(dx * dx + dy * dy);

  if (options && options.shiftKey) {
    angle = snapArrowAngle(angle);
  }

  length = Math.max(ARROW_MIN_LENGTH, length || ARROW_MIN_LENGTH);
  endPoint = arrowPointFromLengthAngle(startPoint, length, angle);

  return {
    startPoint: { x: startPoint.x, y: startPoint.y },
    endPoint: endPoint,
    length: length,
    angle: angle,
  };
}

function arrowCurveOffset(style, length) {
  if (style === 'right') {
    return -Math.min(120, Math.max(26, length * 0.3));
  }
  if (style === 'left') {
    return Math.min(120, Math.max(26, length * 0.3));
  }
  return 0;
}

function arrowRenderGeometry(style, length, metrics) {
  var curve = arrowCurveOffset(style, length);
  var tip = { x: length, y: 0 };
  var tangentAngle =
    style === 'normal' ? 0 : Math.atan2(-curve, Math.max(1, length * 0.3));
  var baseDistance = metrics.headLength * 0.82;
  var baseCenter = {
    x: tip.x - baseDistance * Math.cos(tangentAngle),
    y: tip.y - baseDistance * Math.sin(tangentAngle),
  };
  var half = metrics.headWidth / 2;
  var perpendicularAngle = tangentAngle + Math.PI / 2;

  return {
    curve: curve,
    tip: tip,
    baseCenter: baseCenter,
    baseLeft: {
      x: baseCenter.x + half * Math.cos(perpendicularAngle),
      y: baseCenter.y + half * Math.sin(perpendicularAngle),
    },
    baseRight: {
      x: baseCenter.x - half * Math.cos(perpendicularAngle),
      y: baseCenter.y - half * Math.sin(perpendicularAngle),
    },
  };
}

function arrowHeadPathFromGeometry(geometry) {
  return (
    'M ' +
    geometry.tip.x +
    ' ' +
    geometry.tip.y +
    ' L ' +
    geometry.baseLeft.x +
    ' ' +
    geometry.baseLeft.y +
    ' Q ' +
    geometry.baseCenter.x +
    ' ' +
    geometry.baseCenter.y +
    ' ' +
    geometry.baseRight.x +
    ' ' +
    geometry.baseRight.y +
    ' Z'
  );
}

function arrowBodyPath(length, style, metrics) {
  var geometry = arrowRenderGeometry(style, length, metrics);

  if (style === 'normal') {
    return 'M 0 0 L ' + geometry.baseCenter.x + ' 0';
  }

  return (
    'M 0 0 C ' +
    length * 0.3 +
    ' ' +
    geometry.curve +
    ' ' +
    length * 0.62 +
    ' ' +
    geometry.curve +
    ' ' +
    geometry.baseCenter.x +
    ' ' +
    geometry.baseCenter.y
  );
}

function renderArrowSVG(annotation) {
  var style = normalizeArrowStyle(annotation.arrowStyle);
  var weight = normalizeArrowWeight(annotation.arrowWeight);
  var metrics = ARROW_WEIGHT_METRICS[weight];
  var length = Math.max(ARROW_MIN_LENGTH, annotation.length || ARROW_MIN_LENGTH);
  var padding = metrics.padding;
  var viewHeight = padding * 2;
  var bodyPath = arrowBodyPath(length, style, metrics);
  var headPath = arrowHeadPathFromGeometry(
    arrowRenderGeometry(style, length, metrics)
  );
  var filterId =
    'arrow-shadow-' + String(annotation.id).replace(/[^a-zA-Z0-9_-]/g, '-');

  return (
    '<svg class="arrow-svg" width="' +
    (length + padding * 2) +
    '" height="' +
    viewHeight +
    '" viewBox="' +
    -padding +
    ' ' +
    -padding +
    ' ' +
    (length + padding * 2) +
    ' ' +
    viewHeight +
    '" style="left: -' +
    padding +
    'px" fill="none" xmlns="http://www.w3.org/2000/svg">' +
    '<defs><filter id="' +
    filterId +
    '" x="-50%" y="-80%" width="200%" height="260%"><feDropShadow dx="0" dy="0" stdDeviation="' +
    metrics.shadowBlur +
    '" flood-color="#000000" flood-opacity="0.38"/></filter></defs>' +
    '<g class="arrow-art" filter="url(#' +
    filterId +
    ')">' +
    '<path class="arrow-outline" d="' +
    bodyPath +
    '" stroke="white" stroke-width="' +
    (metrics.shaft + metrics.outline * 2) +
    '" stroke-linecap="round" stroke-linejoin="round"/>' +
    '<path class="arrow-outline" d="' +
    headPath +
    '" fill="white" stroke="white" stroke-width="' +
    metrics.outline * 2 +
    '" stroke-linejoin="round"/>' +
    '<path class="arrow-fill" d="' +
    bodyPath +
    '" stroke="currentColor" stroke-width="' +
    metrics.shaft +
    '" stroke-linecap="round" stroke-linejoin="round"/>' +
    '<path class="arrow-fill" d="' +
    headPath +
    '" fill="currentColor"/>' +
    '</g>' +
    '</svg>'
  );
}

function applyArrowGeometry(annotation, geometry) {
  if (!annotation || !annotation.element) {
    return;
  }

  annotation.startPoint = {
    x: geometry.startPoint.x,
    y: geometry.startPoint.y,
  };
  annotation.endPoint = {
    x: geometry.endPoint.x,
    y: geometry.endPoint.y,
  };
  annotation.length = Math.max(ARROW_MIN_LENGTH, geometry.length || ARROW_MIN_LENGTH);
  annotation.angle = Number.isFinite(geometry.angle) ? geometry.angle : 0;
  annotation.arrowStyle = normalizeArrowStyle(annotation.arrowStyle);
  annotation.arrowWeight = normalizeArrowWeight(annotation.arrowWeight);

  annotation.element.style.transform =
    'translate3d(' +
    annotation.startPoint.x +
    'px, ' +
    annotation.startPoint.y +
    'px, 0) rotateZ(' +
    annotation.angle +
    'rad)';
  annotation.element.innerHTML = renderArrowSVG(annotation);
  annotation.element.style.width = annotation.length + 'px';
  annotation.element.style.height = '1px';
  annotation.element.setAttribute('data-arrow-style', annotation.arrowStyle);
  annotation.element.setAttribute('data-arrow-weight', annotation.arrowWeight);
}

function refreshArrowRendering(annotation) {
  if (!annotation || annotation.type !== 'arrow') {
    return;
  }
  applyArrowGeometry(annotation, {
    startPoint: annotation.startPoint || { x: 0, y: 0 },
    endPoint: annotation.endPoint || arrowPointFromLengthAngle(annotation.startPoint || { x: 0, y: 0 }, annotation.length || 140, annotation.angle || 0),
    length: annotation.length || ARROW_MIN_LENGTH,
    angle: annotation.angle || 0,
  });
}

function startAddArrow(STATE) {
  STATE.setMode('add-arrow');
  STATE.maxZ += 1;

  var itemID = String(Math.random());
  var defaultStyle = normalizeArrowStyle(
    STATE.currentArrowStyle || STATE.arrowDefaults.style
  );
  var defaultWeight = normalizeArrowWeight(STATE.arrowDefaults.weight);
  var startPoint = {
    x: STATE.mouse.clientX || window.innerWidth / 2,
    y: STATE.mouse.clientY || window.innerHeight / 2,
  };
  var endPoint = arrowPointFromLengthAngle(startPoint, 140, -Math.PI / 4);
  var placementMoved = false;
  var annotation = STATE.createArrowAnnotation({
    id: itemID,
    color: STATE.annotationDefaults.arrow,
    arrowStyle: defaultStyle,
    arrowWeight: defaultWeight,
    startPoint: startPoint,
    endPoint: endPoint,
    zIndex: STATE.maxZ,
    preview: true,
  });
  var newElement = annotation.element;

  document.body.style.cursor = 'none';
  document.body.classList.add('show-cursor-none');
  document.querySelector('#draw-canvas').appendChild(newElement);

  function finishAddArrow() {
    newElement.classList.remove('preview');
    newElement.classList.add('draggable');
    document.body.style.cursor = 'auto';
    document.body.classList.remove('show-cursor-none');
    STATE.setMode('free');
    STATE.currentController = null;
    STATE.selectAnnotation(itemID);
    STATE.markDocumentChanged(true);
  }

  function exit(remove) {
    STATE.setMode('free');
    removeHandlers();
    document.body.style.cursor = 'auto';
    document.body.classList.remove('show-cursor-none');
    if (remove) {
      STATE.removeAnnotation(itemID);
    }
  }

  function updatePreviewToMouse(e) {
    startPoint = {
      x: e.clientX,
      y: e.clientY,
    };
    endPoint = arrowPointFromLengthAngle(startPoint, 140, -Math.PI / 4);
    applyArrowGeometry(annotation, arrowGeometryFromPoints(startPoint, endPoint));
    STATE.xOffset[itemID] = annotation.startPoint.x;
    STATE.yOffset[itemID] = annotation.startPoint.y;
  }

  function updateEndpoint(e) {
    var dx = e.clientX - startPoint.x;
    var dy = e.clientY - startPoint.y;
    if (!placementMoved && Math.sqrt(dx * dx + dy * dy) < 4) {
      return;
    }
    placementMoved = true;
    var geometry = arrowGeometryFromPoints(startPoint, { x: e.clientX, y: e.clientY }, {
      shiftKey: e.shiftKey,
    });
    applyArrowGeometry(annotation, geometry);
    STATE.xOffset[itemID] = annotation.startPoint.x;
    STATE.yOffset[itemID] = annotation.startPoint.y;
    if (STATE.syncSelectedArrowResizeHandles) {
      STATE.syncSelectedArrowResizeHandles(annotation);
    }
  }

  function handleMouseDown(e) {
    e.stopPropagation();
    e.preventDefault();
    exit(false);
    startPoint = { x: e.clientX, y: e.clientY };
    placementMoved = false;
    document.addEventListener('mousemove', updateEndpoint);
    document.addEventListener('mouseup', handleMouseUp);
  }

  function handleMouseUp(e) {
    if (placementMoved) {
      updateEndpoint(e);
    }
    document.removeEventListener('mousemove', updateEndpoint);
    document.removeEventListener('mouseup', handleMouseUp);
    finishAddArrow();
  }

  function handleMouseMove(e) {
    updatePreviewToMouse(e);
  }

  function handleKeydown(e) {
    if (e.keyCode === 27) {
      STATE.skipNextCancelOperation = true;
      exit(true);
    }
  }

  function removeHandlers() {
    document.removeEventListener('mousedown', handleMouseDown);
    document.removeEventListener('mousemove', handleMouseMove);
    document.removeEventListener('keydown', handleKeydown);
  }

  document.addEventListener('mousedown', handleMouseDown, false);
  document.addEventListener('mousemove', handleMouseMove, false);
  document.addEventListener('keydown', handleKeydown);

  updatePreviewToMouse({
    clientX: startPoint.x,
    clientY: startPoint.y,
  });

  return {
    cancel: function () {
      return exit(true);
    },
  };
}
