//

const ANGLE_EPSILON = Math.PI / 90;

const UNIT = 1 / 18; const ARROW_MARGIN = 8 * UNIT; const LABEL_DISTANCE = 5 * UNIT;

class DiagramModifier extends Modifier {

modify(element) {
  let arrowElements = this.findChildren(element, "math-arrow");
  let cellElements = this.findChildren(element, "math-cellwrap").map((child) => child.children[0]);
  let backgroundColor = this.getBackgroundColor(element);
  let graphic = this.createGraphic(element);
  element.appendChild(graphic);
  for (let arrowElement of arrowElements) {
    let arrowSpec = this.determineArrowSpec(graphic, arrowElement, cellElements, arrowElements);
    let arrows = this.createArrows(arrowSpec, backgroundColor);
    graphic.append(...arrows);
    let labelPoint = this.determineLabelPoint(graphic, arrowElement, arrowSpec);
    let fontRatio = this.getFontSize(graphic) / this.getFontSize(arrowElement);
    arrowElement.style.left = "" + (labelPoint[0] * fontRatio) + "em";
    arrowElement.style.top = "" + (labelPoint[1] * fontRatio) + "em";
  }
  let pathElements = Array.from(graphic.children).filter((child) => child.localName == "path");
  let extrusion = this.calcExtrusion(graphic, arrowElements.concat(pathElements));
  element.style.marginTop = "" + extrusion.top + "em";
  element.style.marginBottom = "" + extrusion.bottom + "em";
  element.style.marginLeft = "" + extrusion.left + "em";
  element.style.marginRight = "" + extrusion.right + "em";
}
determineArrowSpec(graphic, arrowElement, cellElements, arrowElements) {
  let spec = {};
  let startConfigString = arrowElement.getAttribute("data-start");
  let endConfigString = arrowElement.getAttribute("data-end");
  let startConfig = this.parseEdgeConfig(startConfigString, graphic, cellElements, arrowElements);
  let endConfig = this.parseEdgeConfig(endConfigString, graphic, cellElements, arrowElements);
  if (startConfig && endConfig) {
    let bendAngleString = arrowElement.getAttribute("data-bend");
    if (bendAngleString) {
      spec.bendAngle = parseFloat(bendAngleString) * Math.PI / 180;
    }
    let shiftString = arrowElement.getAttribute("data-shift");
    if (shiftString) {
      spec.shift = parseFloat(shiftString) * UNIT;
    }
    let startElement = startConfig.element;
    let endElement = endConfig.element;
    let startDimension = startConfig.dimension;
    let endDimension = endConfig.dimension;
    if (startConfig.point) {
      spec.startPoint = startConfig.point;
    } else {
      spec.startPoint = this.calcEdgePoint(startDimension, endDimension, spec.bendAngle, spec.shift);
    }
    if (endConfig.point) {
      spec.endPoint = endConfig.point;
    } else {
      spec.endPoint = this.calcEdgePoint(endDimension, startDimension, -spec.bendAngle, -spec.shift);
    }
  } else {
    spec.startPoint = [0, 0];
    spec.endPoint = [0, 0];
  }
  let labelPositionString = arrowElement.getAttribute("data-pos");
  if (labelPositionString) {
    spec.labelPosition = parseFloat(labelPositionString) / 100;
  }
  let lineCountString = arrowElement.getAttribute("data-line");
  if (lineCountString) {
    spec.lineCount = parseInt(lineCountString);
  }
  let dashed = !!arrowElement.getAttribute("data-dash");
  if (dashed) {
    spec.dashed = true;
  }
  let inverted = !!arrowElement.getAttribute("data-inv");
  if (inverted) {
    spec.inverted = true;
  }
  let mark = !!arrowElement.getAttribute("data-mark");
  if (mark) {
    spec.mark = true;
  }
  let tipKindsString = arrowElement.getAttribute("data-tip");
  spec.tipKinds = this.parseTipKinds(tipKindsString, spec.lineCount);
  spec.intrudedStartPoint = this.calcIntrudedPoint(spec.startPoint, spec.endPoint, spec.bendAngle, spec.tipKinds.start);
  spec.intrudedEndPoint = this.calcIntrudedPoint(spec.endPoint, spec.startPoint, -spec.bendAngle, spec.tipKinds.end);
  return spec;
}
determineLabelPoint(graphic, labelElement, arrowSpec) {
  let labelDimension = this.calcDimension(graphic, labelElement);
  let startPoint = arrowSpec.startPoint;
  let endPoint = arrowSpec.endPoint;
  let bendAngle = arrowSpec.bendAngle;
  let position = (arrowSpec.labelPosition == undefined) ? 0.5 : arrowSpec.labelPosition;
  let basePoint = [0, 0];
  let angle = 0;
  if (bendAngle != undefined) {
    let controlPoint = this.calcControlPoint(startPoint, endPoint, bendAngle);
    let basePointX = (1 - position) * (1 - position) * startPoint[0] + 2 * (1 - position) * position * controlPoint[0] + position * position * endPoint[0];
    let basePointY = (1 - position) * (1 - position) * startPoint[1] + 2 * (1 - position) * position * controlPoint[1] + position * position * endPoint[1];
    let speedX = -2 * (1 - position) * startPoint[0] + 2 * (1 - 2 * position) * controlPoint[0] + 2 * position * endPoint[0];
    let speedY = -2 * (1 - position) * startPoint[1] + 2 * (1 - 2 * position) * controlPoint[1] + 2 * position * endPoint[1];
    basePoint = [basePointX, basePointY];
    angle = this.calcAngle([0, 0], [speedX, speedY]) + Math.PI / 2;
  } else {
    let basePointX = (1 - position) * startPoint[0] + position * endPoint[0];
    let basePointY = (1 - position) * startPoint[1] + position * endPoint[1];
    basePoint = [basePointX, basePointY];
    angle = this.calcAngle(startPoint, endPoint) + Math.PI / 2;
  }
  if (arrowSpec.inverted) {
    angle += Math.PI;
  }
  angle = this.normalizeAngle(angle);
  let point;
  if (arrowSpec.mark) {
    let pointX = basePoint[0] + labelDimension.northWest[0] - labelDimension.center[0];
    let pointY = basePoint[1] + labelDimension.northWest[0] - labelDimension.center[1];
    point = [pointX, pointY];
  } else {
    point = this.calcLabelPoint(basePoint, labelDimension, angle, arrowSpec.lineCount);
  }
  return point;
}
calcEdgePoint(baseDimension, destinationDimension, bendAngle, shift) {
  let margin = ARROW_MARGIN;
  let angle = this.calcAngle(baseDimension.center, destinationDimension.center) + (bendAngle || 0);
  let shiftAngle = angle + Math.PI / 2;
  let southWestAngle = this.calcAngle(baseDimension.center, baseDimension.southWestMargined);
  let southEastAngle = this.calcAngle(baseDimension.center, baseDimension.southEastMargined);
  let northEastAngle = this.calcAngle(baseDimension.center, baseDimension.northEastMargined);
  let northWestAngle = this.calcAngle(baseDimension.center, baseDimension.northWestMargined);
  let x = 0;
  let y = 0;
  angle = this.normalizeAngle(angle);
  shiftAngle = this.normalizeAngle(shiftAngle);
  if (angle >= southWestAngle && angle <= southEastAngle) {
    x = baseDimension.center[0] + (baseDimension.center[1] - baseDimension.southMargined[1]) / Math.tan(angle);
    y = baseDimension.southMargined[1];
  } else if (angle >= southEastAngle && angle <= northEastAngle) {
    x = baseDimension.eastMargined[0];
    y = baseDimension.center[1] + (baseDimension.center[0] - baseDimension.eastMargined[0]) * Math.tan(angle);
  } else if (angle >= northEastAngle && angle <= northWestAngle) {
    x = baseDimension.center[0] + (baseDimension.center[1] - baseDimension.northMargined[1]) / Math.tan(angle);
    y = baseDimension.northMargined[1];
  } else if (angle >= northWestAngle || angle <= southWestAngle) {
    x = baseDimension.westMargined[0];
    y = baseDimension.center[1] + (baseDimension.center[0] - baseDimension.westMargined[0]) * Math.tan(angle);
  }
  if (shift) {
    x += Math.cos(shiftAngle) * shift;
    y -= Math.sin(shiftAngle) * shift;
  }
  return [x, y];
}
calcIntrudedPoint(basePoint, destinationPoint, bendAngle, tipKind) {
  if (tipKind != "none") {
    let angle = this.calcAngle(basePoint, destinationPoint) + (bendAngle || 0);
    let distance = DATA["arrow"][tipKind]["extrusion"];
    angle = this.normalizeAngle(angle);
    let intrudedPointX = basePoint[0] + distance * Math.cos(angle);
    let intrudedPointY = basePoint[1] - distance * Math.sin(angle);
    let intrudedPoint = [intrudedPointX, intrudedPointY];
    return intrudedPoint;
  } else {
    return basePoint;
  }
}
calcLabelPoint(basePoint, labelDimension, angle, lineCount) {
  let distance = LABEL_DISTANCE + ((lineCount || 1) - 1) * 0.09;
  let direction = "east";
  if (angle <= -Math.PI + ANGLE_EPSILON) {
    direction = "east";
  } else if (angle <= -Math.PI / 2 - ANGLE_EPSILON) {
    direction = "northEast";
  } else if (angle <= -Math.PI / 2 + ANGLE_EPSILON) {
    direction = "north";
  } else if (angle <= -ANGLE_EPSILON) {
    direction = "northWest";
  } else if (angle <= ANGLE_EPSILON) {
    direction = "west";
  } else if (angle <= Math.PI / 2 - ANGLE_EPSILON) {
    direction = "southWest";
  } else if (angle <= Math.PI / 2 + ANGLE_EPSILON) {
    direction = "south";
  } else if (angle <= Math.PI - ANGLE_EPSILON) {
    direction = "southEast";
  } else {
    direction = "east";
  }
  let x = basePoint[0] + Math.cos(angle) * distance + labelDimension.northWest[0] - labelDimension[direction][0];
  let y = basePoint[1] - Math.sin(angle) * distance + labelDimension.northWest[1] - labelDimension[direction][1];
  return [x, y];
}
parseEdgeConfig(string, graphic, cellElements, arrowElements) {
  let config = null;
  let match = string.match(/(?:(\d+)|([A-Za-z]\w*))(?:\:(\w+))?/);
  if (match) {
    let element = null;
    if (match[1]) {
      let number = parseInt(match[1]) - 1;
      element = cellElements[number];
    } else if (match[2]) {
      let candidates = cellElements.map((candidate) => candidate.parentNode).concat(arrowElements);
      let name = match[2];
      element = candidates.find((candidate) => candidate.getAttribute("data-name") == name);
    }
    if (element) {
      let dimension = this.calcDimension(graphic, element);
      let point = null;
      if (match[3]) {
        point = this.parsePoint(match[3], dimension);
      }
      config = {element, dimension, point};
    }
  }
  return config;
}
parsePoint(string, dimension) {
  let point = null;
  let match;
  if (match = string.match(/^n(|w|e)|s(|w|e)|w|e|c$/)) {
    if (string == "nw") {
      point = dimension.northWestMargined;
    } else if (string == "n") {
      point = dimension.northMargined;
    } else if (string == "ne") {
      point = dimension.northEastMargined;
    } else if (string == "e") {
      point = dimension.eastMargined;
    } else if (string == "se") {
      point = dimension.southEastMargined;
    } else if (string == "s") {
      point = dimension.southMargined;
    } else if (string == "sw") {
      point = dimension.southWestMargined;
    } else if (string == "w") {
      point = dimension.westMargined;
    } else if (string == "c") {
      point = dimension.center;
    }
  } else if (match = string.match(/^(t|r|b|l)([\d.]+)$/)) {
    let direction = match[1];
    let position = parseFloat(match[2]) / 100;
    let pointX = null;
    let pointY = null;
    if (direction == "t") {
      pointX = (1 - position) * dimension.northWestMargined[0] + position * dimension.northEastMargined[0];
      pointY = dimension.northMargined[1];
    } else if (direction == "r") {
      pointX = dimension.eastMargined[0];
      pointY = (1 - position) * dimension.northEastMargined[1] + position * dimension.southEastMargined[1];
    } else if (direction == "b") {
      pointX = (1 - position) * dimension.southWestMargined[0] + position * dimension.southEastMargined[0];
      pointY = dimension.southMargined[1];
    } else if (direction == "l") {
      pointX = dimension.westMargined[0];
      pointY = (1 - position) * dimension.northWestMargined[1] + position * dimension.southWestMargined[1];
    }
    if (pointX != null && pointY != null) {
      point = [pointX, pointY];
    }
  }
  return point;
}
parseTipKinds(string, lineCount) {
  let tipKinds = {start: "none", end: "normal"};
  if (string != null) {
    let specifiedTipKinds = string.split(/\s*,\s*/);
    for (let specifiedTipKind of specifiedTipKinds) {
      let spec = DATA["arrow"][specifiedTipKind];
      if (spec) {
        tipKinds[spec.edge] = specifiedTipKind;
      }
      if (specifiedTipKind == "none") {
        tipKinds.end = "none";
      }
    }
  }
  if (lineCount == 2) {
    if (tipKinds.start != "none") {
      tipKinds.start = "d" + tipKinds.start;
    }
    if (tipKinds.end != "none") {
      tipKinds.end = "d" + tipKinds.end;
    }
  } else if (lineCount == 3) {
    if (tipKinds.start != "none") {
      tipKinds.start = "t" + tipKinds.start;
    }
    if (tipKinds.end != "none") {
      tipKinds.end = "t" + tipKinds.end;
    }
  }
  return tipKinds;
}
calcAngle(basePoint, destinationPoint) {
  let x = destinationPoint[0] - basePoint[0];
  let y = destinationPoint[1] - basePoint[1];
  let angle = -Math.atan2(y, x);
  return angle;
}
normalizeAngle(angle) {
  let normalizedAngle = (angle + Math.PI) % (Math.PI * 2) - Math.PI;
  return normalizedAngle;
}
createArrows(arrowSpec, backgroundColor) {
  let startPoint = arrowSpec.intrudedStartPoint;
  let endPoint = arrowSpec.intrudedEndPoint;
  let bendAngle = arrowSpec.bendAngle;
  let lineCount = (arrowSpec.lineCount == undefined) ? 1 : arrowSpec.lineCount;
  let command = "M " + startPoint[0] + " " + startPoint[1];
  if (bendAngle != undefined) {
    let controlPoint = this.calcControlPoint(startPoint, endPoint, bendAngle)
    command += " Q " + controlPoint[0] + " " + controlPoint[1] + ", " + endPoint[0] + " " + endPoint[1];
  } else {
    command += " L " + endPoint[0] + " " + endPoint[1];
  }
  let arrows = [];
  for (let i = 0 ; i < lineCount ; i ++) {
    let arrow = this.createSvgElement("path");
    arrow.setAttribute("d", command);
    if (arrowSpec.tipKinds.start != "none" && i == lineCount - 1) {
      arrow.setAttribute("marker-start", "url(#tip-" + arrowSpec.tipKinds.start +")");
    }
    if (arrowSpec.tipKinds.end != "none" && i == lineCount - 1) {
      arrow.setAttribute("marker-end", "url(#tip-" + arrowSpec.tipKinds.end + ")");
    }
    if (arrowSpec.dashed && i % 2 == 0) {
      arrow.classList.add("dashed");
    }
    if (i == 0) {
      arrow.classList.add("base");
    } else if (i == 1) {
      arrow.classList.add("cover");
      arrow.style.stroke = backgroundColor;
    } else if (i == 2) {
      arrow.classList.add("front");
    }
    if (lineCount == 2) {
      arrow.classList.add("double");
    } else if (lineCount == 3) {
      arrow.classList.add("triple");
    }
    arrows.push(arrow);
  }
  return arrows;
}
calcControlPoint(startPoint, endPoint, bendAngle) {
  let x = (endPoint[0] + startPoint[0] + (endPoint[1] - startPoint[1]) * Math.tan(bendAngle)) / 2;
  let y = (endPoint[1] + startPoint[1] - (endPoint[0] - startPoint[0]) * Math.tan(bendAngle)) / 2;
  return [x, y];
}
calcExtrusion(graphic, elements) {
  let fontSize = this.getFontSize(graphic);
  let xOffset =  window.pageXOffset;
  let yOffset =  window.pageYOffset;
  let graphicRect = graphic.getBoundingClientRect();
  let graphicTop = graphicRect.top + yOffset
  let graphicBottom = graphicRect.bottom + yOffset;
  let graphicLeft = graphicRect.left + xOffset;
  let graphicRight = graphicRect.right + xOffset;
  let extrusion = {top: 0, bottom: 0, left: 0, right: 0};
  for (let element of elements) {
    let rect = element.getBoundingClientRect();
    let topExtrusion = -(rect.top + yOffset - graphicTop) / fontSize;
    let bottomExtrusion = (rect.bottom + yOffset - graphicBottom) / fontSize;
    let leftExtrusion = -(rect.left + xOffset - graphicLeft) / fontSize;
    let rightExtrusion = (rect.right + xOffset - graphicRight) / fontSize;
    if (topExtrusion > extrusion.top) {
      extrusion.top = topExtrusion;
    }
    if (bottomExtrusion > extrusion.bottom) {
      extrusion.bottom = bottomExtrusion;
    }
    if (leftExtrusion > extrusion.left) {
      extrusion.left = leftExtrusion;
    }
    if (rightExtrusion > extrusion.right) {
      extrusion.right = rightExtrusion;
    }
  }
  return extrusion;
}
createGraphic(element) {
  let width = this.getWidth(element);
  let height = this.getHeight(element);
  let graphic = this.createSvgElement("svg");
  graphic.setAttribute("viewBox", "0 0 " + width + " " + height);
  let definitionElement = this.createSvgElement("defs");
  let tipSpecKeys = Object.keys(DATA["arrow"]);
  for (let tipSpecKey of tipSpecKeys) {
    let tipSpec = DATA["arrow"][tipSpecKey];
    let markerElement = this.createSvgElement("marker");
    let markerPathElement = this.createSvgElement("path");
    markerElement.setAttribute("id", "tip-" + tipSpecKey);
    markerElement.setAttribute("refX", tipSpec["x"]);
    markerElement.setAttribute("refY", tipSpec["y"]);
    markerElement.setAttribute("markerWidth", tipSpec["width"]);
    markerElement.setAttribute("markerHeight", tipSpec["height"]);
    markerElement.setAttribute("markerUnits", "userSpaceOnUse");
    markerElement.setAttribute("orient", "auto");
    markerPathElement.setAttribute("d", tipSpec["command"]);
    markerElement.appendChild(markerPathElement);
    definitionElement.appendChild(markerElement);
  }
  graphic.appendChild(definitionElement);
  return graphic;
}
createSvgElement(name) {
  let element = document.createElementNS("http://www.w3.org/2000/svg", name);
  return element;
}
calcDimension(graphic, element) {
  let dimension = {};
  let margin = ARROW_MARGIN;
  let fontSize = this.getFontSize(graphic)
  let graphicTop = graphic.getBoundingClientRect().top + window.pageYOffset;
  let graphicLeft = graphic.getBoundingClientRect().left + window.pageXOffset;
  let top = (element.getBoundingClientRect().top + window.pageYOffset - graphicTop) / fontSize;
  let left = (element.getBoundingClientRect().left + window.pageXOffset - graphicLeft) / fontSize;
  let width = this.getWidth(element, graphic);
  let height = this.getHeight(element, graphic);
  let lowerHeight = this.getLowerHeight(element, graphic);
  dimension.northWest = [left, top];
  dimension.north = [left + width / 2, top];
  dimension.northEast = [left + width, top];
  dimension.west = [left, top + height - lowerHeight];
  dimension.center = [left + width / 2, top + height - lowerHeight];
  dimension.east = [left + width, top + height - lowerHeight];
  dimension.southWest = [left, top + height];
  dimension.south = [left + width / 2, top + height];
  dimension.southEast = [left + width, top + height];
  dimension.northWestMargined = [left - margin, top - margin];
  dimension.northMargined = [left + width / 2, top - margin];
  dimension.northEastMargined = [left + width + margin, top - margin];
  dimension.westMargined = [left - margin, top + height - lowerHeight];
  dimension.centerMargined = [left + width / 2, top + height - lowerHeight];
  dimension.eastMargined = [left + width + margin, top + height - lowerHeight];
  dimension.southWestMargined = [left - margin, top + height + margin];
  dimension.southMargined = [left + width / 2, top + height + margin];
  dimension.southEastMargined = [left + width + margin, top + height + margin];
  return dimension;
}
getBackgroundColor(element) {
  let currentElement = element;
  let color = "white";
  while (currentElement && currentElement instanceof Element) {
    let currentColor = window.getComputedStyle(currentElement).backgroundColor;
    if (currentColor != "rgba(0, 0, 0, 0)" && currentColor != "transparent") {
      color = currentColor;
      break;
    }
    currentElement = currentElement.parentNode;
  }
  return color;
}

}