import common, { calculateMathMLDimensions, hasKatex, renderKatex } from '../common/common.js';
import * as svgDrawCommon from '../common/svgDrawCommon.js';
import { ZERO_WIDTH_SPACE, parseFontSize } from '../../utils.js';
import { sanitizeUrl } from '@braintree/sanitize-url';
import * as configApi from '../../config.js';

export const ACTOR_TYPE_WIDTH = 18 * 2;
const TOP_ACTOR_CLASS = 'actor-top';
const BOTTOM_ACTOR_CLASS = 'actor-bottom';
const ACTOR_BOX_CLASS = 'actor-box';
const ACTOR_MAN_FIGURE_CLASS = 'actor-man';

export const drawRect = function (elem, rectData) {
  return svgDrawCommon.drawRect(elem, rectData);
};

export const drawPopup = function (elem, actor, minMenuWidth, textAttrs, forceMenus) {
  if (actor.links === undefined || actor.links === null || Object.keys(actor.links).length === 0) {
    return { height: 0, width: 0 };
  }

  const links = actor.links;
  const actorCnt = actor.actorCnt;
  const rectData = actor.rectData;

  var displayValue = 'none';
  if (forceMenus) {
    displayValue = 'block !important';
  }

  const g = elem.append('g');
  g.attr('id', 'actor' + actorCnt + '_popup');
  g.attr('class', 'actorPopupMenu');
  g.attr('display', displayValue);
  var actorClass = '';
  if (rectData.class !== undefined) {
    actorClass = ' ' + rectData.class;
  }

  let menuWidth = rectData.width > minMenuWidth ? rectData.width : minMenuWidth;

  const rectElem = g.append('rect');
  rectElem.attr('class', 'actorPopupMenuPanel' + actorClass);
  rectElem.attr('x', rectData.x);
  rectElem.attr('y', rectData.height);
  rectElem.attr('fill', rectData.fill);
  rectElem.attr('stroke', rectData.stroke);
  rectElem.attr('width', menuWidth);
  rectElem.attr('height', rectData.height);
  rectElem.attr('rx', rectData.rx);
  rectElem.attr('ry', rectData.ry);
  if (links != null) {
    var linkY = 20;
    for (let key in links) {
      var linkElem = g.append('a');
      var sanitizedLink = sanitizeUrl(links[key]);
      linkElem.attr('xlink:href', sanitizedLink);
      linkElem.attr('target', '_blank');

      _drawMenuItemTextCandidateFunc(textAttrs)(
        key,
        linkElem,
        rectData.x + 10,
        rectData.height + linkY,
        menuWidth,
        20,
        { class: 'actor' },
        textAttrs
      );

      linkY += 30;
    }
  }

  rectElem.attr('height', linkY);

  return { height: rectData.height + linkY, width: menuWidth };
};

const popupMenuToggle = function (popId) {
  return (
    "var pu = document.getElementById('" +
    popId +
    "'); if (pu != null) { pu.style.display = pu.style.display == 'block' ? 'none' : 'block'; }"
  );
};

export const drawKatex = async function (elem, textData, msgModel = null) {
  let textElem = elem.append('foreignObject');
  const lines = await renderKatex(textData.text, configApi.getConfig());

  const divElem = textElem
    .append('xhtml:div')
    .attr('style', 'width: fit-content;')
    .attr('xmlns', 'http://www.w3.org/1999/xhtml')
    .html(lines);
  const dim = divElem.node().getBoundingClientRect();

  textElem.attr('height', Math.round(dim.height)).attr('width', Math.round(dim.width));

  if (textData.class === 'noteText') {
    const rectElem = elem.node().firstChild;

    rectElem.setAttribute('height', dim.height + 2 * textData.textMargin);
    const rectDim = rectElem.getBBox();

    textElem
      .attr('x', Math.round(rectDim.x + rectDim.width / 2 - dim.width / 2))
      .attr('y', Math.round(rectDim.y + rectDim.height / 2 - dim.height / 2));
  } else if (msgModel) {
    let { startx, stopx, starty } = msgModel;
    if (startx > stopx) {
      const temp = startx;
      startx = stopx;
      stopx = temp;
    }

    // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
    textElem.attr('x', Math.round(startx + Math.abs(startx - stopx) / 2 - dim.width / 2));
    if (textData.class === 'loopText') {
      textElem.attr('y', Math.round(starty));
    } else {
      textElem.attr('y', Math.round(starty - dim.height));
    }
  }

  return [textElem];
};

export const drawText = function (elem, textData) {
  let prevTextHeight = 0;
  let textHeight = 0;
  const lines = textData.text.split(common.lineBreakRegex);

  const [_textFontSize, _textFontSizePx] = parseFontSize(textData.fontSize);

  let textElems = [];
  let dy = 0;
  let yfunc = () => textData.y;
  if (
    textData.valign !== undefined &&
    textData.textMargin !== undefined &&
    textData.textMargin > 0
  ) {
    switch (textData.valign) {
      case 'top':
      case 'start':
        yfunc = () => Math.round(textData.y + textData.textMargin);
        break;
      case 'middle':
      case 'center':
        yfunc = () =>
          Math.round(textData.y + (prevTextHeight + textHeight + textData.textMargin) / 2);
        break;
      case 'bottom':
      case 'end':
        yfunc = () =>
          Math.round(
            textData.y +
              (prevTextHeight + textHeight + 2 * textData.textMargin) -
              textData.textMargin
          );
        break;
    }
  }

  if (
    textData.anchor !== undefined &&
    textData.textMargin !== undefined &&
    textData.width !== undefined
  ) {
    switch (textData.anchor) {
      case 'left':
      case 'start':
        textData.x = Math.round(textData.x + textData.textMargin);
        textData.anchor = 'start';
        textData.dominantBaseline = 'middle';
        textData.alignmentBaseline = 'middle';
        break;
      case 'middle':
      case 'center':
        textData.x = Math.round(textData.x + textData.width / 2);
        textData.anchor = 'middle';
        textData.dominantBaseline = 'middle';
        textData.alignmentBaseline = 'middle';
        break;
      case 'right':
      case 'end':
        textData.x = Math.round(textData.x + textData.width - textData.textMargin);
        textData.anchor = 'end';
        textData.dominantBaseline = 'middle';
        textData.alignmentBaseline = 'middle';
        break;
    }
  }

  for (let [i, line] of lines.entries()) {
    if (
      textData.textMargin !== undefined &&
      textData.textMargin === 0 &&
      _textFontSize !== undefined
    ) {
      dy = i * _textFontSize;
    }

    const textElem = elem.append('text');
    textElem.attr('x', textData.x);
    textElem.attr('y', yfunc());
    if (textData.anchor !== undefined) {
      textElem
        .attr('text-anchor', textData.anchor)
        .attr('dominant-baseline', textData.dominantBaseline)
        .attr('alignment-baseline', textData.alignmentBaseline);
    }
    if (textData.fontFamily !== undefined) {
      textElem.style('font-family', textData.fontFamily);
    }
    if (_textFontSizePx !== undefined) {
      textElem.style('font-size', _textFontSizePx);
    }
    if (textData.fontWeight !== undefined) {
      textElem.style('font-weight', textData.fontWeight);
    }
    if (textData.fill !== undefined) {
      textElem.attr('fill', textData.fill);
    }
    if (textData.class !== undefined) {
      textElem.attr('class', textData.class);
    }
    if (textData.dy !== undefined) {
      textElem.attr('dy', textData.dy);
    } else if (dy !== 0) {
      textElem.attr('dy', dy);
    }

    const text = line || ZERO_WIDTH_SPACE;
    if (textData.tspan) {
      const span = textElem.append('tspan');
      span.attr('x', textData.x);
      if (textData.fill !== undefined) {
        span.attr('fill', textData.fill);
      }
      span.text(text);
    } else {
      textElem.text(text);
    }
    if (
      textData.valign !== undefined &&
      textData.textMargin !== undefined &&
      textData.textMargin > 0
    ) {
      textHeight += (textElem._groups || textElem)[0][0].getBBox().height;
      prevTextHeight = textHeight;
    }

    textElems.push(textElem);
  }

  return textElems;
};

export const drawLabel = function (elem, txtObject) {
  /**
   * @param {any} x
   * @param {any} y
   * @param {any} width
   * @param {any} height
   * @param {any} cut
   * @returns {any}
   */
  function genPoints(x, y, width, height, cut) {
    return (
      x +
      ',' +
      y +
      ' ' +
      (x + width) +
      ',' +
      y +
      ' ' +
      (x + width) +
      ',' +
      (y + height - cut) +
      ' ' +
      (x + width - cut * 1.2) +
      ',' +
      (y + height) +
      ' ' +
      x +
      ',' +
      (y + height)
    );
  }
  const polygon = elem.append('polygon');
  polygon.attr('points', genPoints(txtObject.x, txtObject.y, txtObject.width, txtObject.height, 7));
  polygon.attr('class', 'labelBox');

  txtObject.y = txtObject.y + txtObject.height / 2;

  drawText(elem, txtObject);
  return polygon;
};

let actorCnt = -1;

export const fixLifeLineHeights = (diagram, actors, actorKeys, conf) => {
  if (!diagram.select) {
    return;
  }
  actorKeys.forEach((actorKey) => {
    const actor = actors.get(actorKey);
    const actorDOM = diagram.select('#actor' + actor.actorCnt);
    if (!conf.mirrorActors && actor.stopy) {
      actorDOM.attr('y2', actor.stopy + actor.height / 2);
    } else if (conf.mirrorActors) {
      actorDOM.attr('y2', actor.stopy);
    }
  });
};

/**
 * Draws an actor in the diagram with the attached line
 *
 * @param {any} elem - The diagram we'll draw to.
 * @param {any} actor - The actor to draw.
 * @param {any} conf - DrawText implementation discriminator object
 * @param {boolean} isFooter - If the actor is the footer one
 */
const drawActorTypeParticipant = function (elem, actor, conf, isFooter) {
  const actorY = isFooter ? actor.stopy : actor.starty;
  const center = actor.x + actor.width / 2;
  const centerY = actorY + actor.height;

  const boxplusLineGroup = elem.append('g').lower();
  var g = boxplusLineGroup;

  if (!isFooter) {
    actorCnt++;
    if (Object.keys(actor.links || {}).length && !conf.forceMenus) {
      g.attr('onclick', popupMenuToggle(`actor${actorCnt}_popup`)).attr('cursor', 'pointer');
    }
    g.append('line')
      .attr('id', 'actor' + actorCnt)
      .attr('x1', center)
      .attr('y1', centerY)
      .attr('x2', center)
      .attr('y2', 2000)
      .attr('class', 'actor-line 200')
      .attr('stroke-width', '0.5px')
      .attr('stroke', '#999')
      .attr('name', actor.name);

    g = boxplusLineGroup.append('g');
    actor.actorCnt = actorCnt;

    if (actor.links != null) {
      g.attr('id', 'root-' + actorCnt);
    }
  }

  const rect = svgDrawCommon.getNoteRect();
  var cssclass = 'actor';
  if (actor.properties?.class) {
    cssclass = actor.properties.class;
  } else {
    rect.fill = '#eaeaea';
  }
  if (isFooter) {
    cssclass += ` ${BOTTOM_ACTOR_CLASS}`;
  } else {
    cssclass += ` ${TOP_ACTOR_CLASS}`;
  }
  rect.x = actor.x;
  rect.y = actorY;
  rect.width = actor.width;
  rect.height = actor.height;
  rect.class = cssclass;
  rect.rx = 3;
  rect.ry = 3;
  rect.name = actor.name;
  const rectElem = drawRect(g, rect);
  actor.rectData = rect;

  if (actor.properties?.icon) {
    const iconSrc = actor.properties.icon.trim();
    if (iconSrc.charAt(0) === '@') {
      svgDrawCommon.drawEmbeddedImage(g, rect.x + rect.width - 20, rect.y + 10, iconSrc.substr(1));
    } else {
      svgDrawCommon.drawImage(g, rect.x + rect.width - 20, rect.y + 10, iconSrc);
    }
  }

  _drawTextCandidateFunc(conf, hasKatex(actor.description))(
    actor.description,
    g,
    rect.x,
    rect.y,
    rect.width,
    rect.height,
    { class: `actor ${ACTOR_BOX_CLASS}` },
    conf
  );

  let height = actor.height;
  if (rectElem.node) {
    const bounds = rectElem.node().getBBox();
    actor.height = bounds.height;
    height = bounds.height;
  }

  return height;
};

/**
 * Draws an actor in the diagram with the attached line
 *
 * @param {any} elem - The diagram we'll draw to.
 * @param {any} actor - The actor to draw.
 * @param {any} conf - DrawText implementation discriminator object
 * @param {boolean} isFooter - If the actor is the footer one
 */
const drawActorTypeCollections = function (elem, actor, conf, isFooter) {
  const actorY = isFooter ? actor.stopy : actor.starty;
  const center = actor.x + actor.width / 2;
  const centerY = actorY + actor.height;

  const boxplusLineGroup = elem.append('g').lower();
  var g = boxplusLineGroup;

  if (!isFooter) {
    actorCnt++;
    if (Object.keys(actor.links || {}).length && !conf.forceMenus) {
      g.attr('onclick', popupMenuToggle(`actor${actorCnt}_popup`)).attr('cursor', 'pointer');
    }
    g.append('line')
      .attr('id', 'actor' + actorCnt)
      .attr('x1', center)
      .attr('y1', centerY)
      .attr('x2', center)
      .attr('y2', 2000)
      .attr('class', 'actor-line 200')
      .attr('stroke-width', '0.5px')
      .attr('stroke', '#999')
      .attr('name', actor.name);

    g = boxplusLineGroup.append('g');
    actor.actorCnt = actorCnt;

    if (actor.links != null) {
      g.attr('id', 'root-' + actorCnt);
    }
  }

  const rect = svgDrawCommon.getNoteRect();
  var cssclass = 'actor';
  if (actor.properties?.class) {
    cssclass = actor.properties.class;
  } else {
    rect.fill = '#eaeaea';
  }
  if (isFooter) {
    cssclass += ` ${BOTTOM_ACTOR_CLASS}`;
  } else {
    cssclass += ` ${TOP_ACTOR_CLASS}`;
  }
  rect.x = actor.x;
  rect.y = actorY;
  rect.width = actor.width;
  rect.height = actor.height;
  rect.class = cssclass;
  // rect.rx = 3;
  // rect.ry = 3;
  rect.name = actor.name;

  // 🔹 DRAW STACKED RECTANGLES
  const offset = 6;
  const shadowRect = {
    ...rect,
    x: rect.x + offset,
    y: rect.y + (isFooter ? +offset : -offset),
    class: 'actor',
  };
  drawRect(g, shadowRect);
  const rectElem = drawRect(g, rect); // draw main rectangle on top
  actor.rectData = rect;

  if (actor.properties?.icon) {
    const iconSrc = actor.properties.icon.trim();
    if (iconSrc.charAt(0) === '@') {
      svgDrawCommon.drawEmbeddedImage(g, rect.x + rect.width - 20, rect.y + 10, iconSrc.substr(1));
    } else {
      svgDrawCommon.drawImage(g, rect.x + rect.width - 20, rect.y + 10, iconSrc);
    }
  }

  _drawTextCandidateFunc(conf, hasKatex(actor.description))(
    actor.description,
    g,
    rect.x,
    rect.y,
    rect.width,
    rect.height,
    { class: `actor ${ACTOR_BOX_CLASS}` },
    conf
  );

  let height = actor.height;
  if (rectElem.node) {
    const bounds = rectElem.node().getBBox();
    actor.height = bounds.height;
    height = bounds.height;
  }

  return height;
};

const drawActorTypeQueue = function (elem, actor, conf, isFooter) {
  const actorY = isFooter ? actor.stopy : actor.starty;
  const center = actor.x + actor.width / 2;
  const centerY = actorY + actor.height;

  const boxplusLineGroup = elem.append('g').lower();
  let g = boxplusLineGroup;

  if (!isFooter) {
    actorCnt++;
    if (Object.keys(actor.links || {}).length && !conf.forceMenus) {
      g.attr('onclick', popupMenuToggle(`actor${actorCnt}_popup`)).attr('cursor', 'pointer');
    }
    g.append('line')
      .attr('id', 'actor' + actorCnt)
      .attr('x1', center)
      .attr('y1', centerY)
      .attr('x2', center)
      .attr('y2', 2000)
      .attr('class', 'actor-line 200')
      .attr('stroke-width', '0.5px')
      .attr('stroke', '#999')
      .attr('name', actor.name);

    g = boxplusLineGroup.append('g');
    actor.actorCnt = actorCnt;

    if (actor.links != null) {
      g.attr('id', 'root-' + actorCnt);
    }
  }

  const rect = svgDrawCommon.getNoteRect();
  let cssclass = 'actor';
  if (actor.properties?.class) {
    cssclass = actor.properties.class;
  } else {
    rect.fill = '#eaeaea';
  }

  if (isFooter) {
    cssclass += ` ${BOTTOM_ACTOR_CLASS}`;
  } else {
    cssclass += ` ${TOP_ACTOR_CLASS}`;
  }

  rect.x = actor.x;
  rect.y = actorY;
  rect.width = actor.width;
  rect.height = actor.height;
  rect.class = cssclass;
  rect.name = actor.name;

  // Cylinder dimensions
  const ry = rect.height / 2;
  const rx = ry / (2.5 + rect.height / 50);

  // Cylinder base group
  const cylinderGroup = g.append('g');
  const cylinderArc = g.append('g');

  // Main cylinder body
  cylinderGroup
    .append('path')
    .attr(
      'd',
      `M ${rect.x},${rect.y + ry}
    a ${rx},${ry} 0 0 0 0,${rect.height}
    h ${rect.width - 2 * rx}
    a ${rx},${ry} 0 0 0 0,-${rect.height}
    Z
  `
    )
    .attr('class', cssclass);
  cylinderArc
    .append('path')
    .attr(
      'd',
      `M ${rect.x},${rect.y + ry}
      a ${rx},${ry} 0 0 0 0,${rect.height}`
    )
    .attr('stroke', '#666')
    .attr('stroke-width', '1px')
    .attr('class', cssclass);

  cylinderGroup.attr('transform', `translate(${rx}, ${-(rect.height / 2)})`);
  cylinderArc.attr('transform', `translate(${rect.width - rx}, ${-rect.height / 2})`);

  actor.rectData = rect;

  if (actor.properties?.icon) {
    const iconSrc = actor.properties.icon.trim();
    const iconX = rect.x + rect.width - 20;
    const iconY = rect.y + 10;
    if (iconSrc.charAt(0) === '@') {
      svgDrawCommon.drawEmbeddedImage(g, iconX, iconY, iconSrc.substr(1));
    } else {
      svgDrawCommon.drawImage(g, iconX, iconY, iconSrc);
    }
  }

  _drawTextCandidateFunc(conf, hasKatex(actor.description))(
    actor.description,
    g,
    rect.x,
    rect.y,
    rect.width,
    rect.height,
    { class: `actor ${ACTOR_BOX_CLASS}` },
    conf
  );

  let height = actor.height;
  const lastPath = cylinderGroup.select('path:last-child');
  if (lastPath.node()) {
    const bounds = lastPath.node().getBBox();
    actor.height = bounds.height;
    height = bounds.height;
  }

  return height;
};

const drawActorTypeControl = function (elem, actor, conf, isFooter) {
  const actorY = isFooter ? actor.stopy : actor.starty;
  const center = actor.x + actor.width / 2;
  const centerY = actorY + 75;

  const line = elem.append('g').lower();

  if (!isFooter) {
    actorCnt++;
    line
      .append('line')
      .attr('id', 'actor' + actorCnt)
      .attr('x1', center)
      .attr('y1', centerY)
      .attr('x2', center)
      .attr('y2', 2000)
      .attr('class', 'actor-line 200')
      .attr('stroke-width', '0.5px')
      .attr('stroke', '#999')
      .attr('name', actor.name);

    actor.actorCnt = actorCnt;
  }
  const actElem = elem.append('g');
  let cssClass = ACTOR_MAN_FIGURE_CLASS;
  if (isFooter) {
    cssClass += ` ${BOTTOM_ACTOR_CLASS}`;
  } else {
    cssClass += ` ${TOP_ACTOR_CLASS}`;
  }
  actElem.attr('class', cssClass);
  actElem.attr('name', actor.name);

  const rect = svgDrawCommon.getNoteRect();
  rect.x = actor.x;
  rect.y = actorY;
  rect.fill = '#eaeaea';
  rect.width = actor.width;
  rect.height = actor.height;
  rect.class = 'actor';

  const cx = actor.x + actor.width / 2;
  const cy = actorY + 30;
  const r = 18;

  actElem
    .append('defs')
    .append('marker')
    .attr('id', 'filled-head-control')
    .attr('refX', 15.5)
    .attr('refY', 7)
    .attr('markerWidth', 20)
    .attr('markerHeight', 28)
    .attr('orient', '180')
    .append('path')
    .attr('d', 'M 18,7 L9,13 L14,7 L9,1 Z');

  // Draw the base circle
  actElem
    .append('circle')
    .attr('cx', cx)
    .attr('cy', cy)
    .attr('r', r)
    .attr('fill', '#eaeaf7')
    .attr('stroke', '#666')
    .attr('stroke-width', 1.2);

  // Draw looping arrow as arc path
  actElem
    .append('line')
    .attr('marker-end', 'url(#filled-head-control)')
    .attr('transform', `translate(${cx}, ${cy - r})`);

  const bounds = actElem.node().getBBox();
  actor.height = bounds.height + 2 * (conf?.sequence?.labelBoxHeight ?? 0);

  _drawTextCandidateFunc(conf, hasKatex(actor.description))(
    actor.description,
    actElem,
    rect.x,
    rect.y + 30,
    rect.width,
    rect.height,
    { class: `actor ${ACTOR_MAN_FIGURE_CLASS}` },
    conf
  );

  return actor.height;
};

const drawActorTypeEntity = function (elem, actor, conf, isFooter) {
  const actorY = isFooter ? actor.stopy : actor.starty;
  const center = actor.x + actor.width / 2;
  const centerY = actorY + 75;

  const line = elem.append('g').lower();

  const actElem = elem.append('g');
  let cssClass = ACTOR_MAN_FIGURE_CLASS;
  if (isFooter) {
    cssClass += ` ${BOTTOM_ACTOR_CLASS}`;
  } else {
    cssClass += ` ${TOP_ACTOR_CLASS}`;
  }
  actElem.attr('class', cssClass);
  actElem.attr('name', actor.name);

  const rect = svgDrawCommon.getNoteRect();
  rect.x = actor.x;
  rect.y = actorY;
  rect.fill = '#eaeaea';
  rect.width = actor.width;
  rect.height = actor.height;
  rect.class = 'actor';

  const cx = actor.x + actor.width / 2;
  const cy = actorY + 25;
  const r = 18;

  actElem
    .append('circle')
    .attr('cx', cx)
    .attr('cy', cy)
    .attr('r', r)
    .attr('width', actor.width)
    .attr('height', actor.height);

  actElem
    .append('line')
    .attr('x1', cx - r)
    .attr('x2', cx + r)
    .attr('y1', cy + r)
    .attr('y2', cy + r)
    .attr('stroke', '#333')
    .attr('stroke-width', 2);

  const bounds = actElem.node().getBBox();
  actor.height = bounds.height + (conf?.sequence?.labelBoxHeight ?? 0);

  if (!isFooter) {
    actorCnt++;
    line
      .append('line')
      .attr('id', 'actor' + actorCnt)
      .attr('x1', center)
      .attr('y1', centerY)
      .attr('x2', center)
      .attr('y2', 2000)
      .attr('class', 'actor-line 200')
      .attr('stroke-width', '0.5px')
      .attr('stroke', '#999')
      .attr('name', actor.name);

    actor.actorCnt = actorCnt;
  }

  _drawTextCandidateFunc(conf, hasKatex(actor.description))(
    actor.description,
    actElem,
    rect.x,
    rect.y + (!isFooter ? (cy + r - actorY) / 2 : (cy - actorY) / 2 + r + 2),
    rect.width,
    rect.height,
    { class: `actor ${ACTOR_MAN_FIGURE_CLASS}` },
    conf
  );

  if (!isFooter) {
    actElem.attr('transform', `translate(${0}, ${r / 2})`);
  } else {
    actElem.attr('transform', `translate(${0}, ${r / 2})`);
  }

  return actor.height;
};

const drawActorTypeDatabase = function (elem, actor, conf, isFooter) {
  const actorY = isFooter ? actor.stopy : actor.starty;
  const center = actor.x + actor.width / 2;
  const centerY = actorY + actor.height + 2 * conf.boxTextMargin;

  const boxplusLineGroup = elem.append('g').lower();
  let g = boxplusLineGroup;

  if (!isFooter) {
    actorCnt++;
    if (Object.keys(actor.links || {}).length && !conf.forceMenus) {
      g.attr('onclick', popupMenuToggle(`actor${actorCnt}_popup`)).attr('cursor', 'pointer');
    }
    g.append('line')
      .attr('id', 'actor' + actorCnt)
      .attr('x1', center)
      .attr('y1', centerY)
      .attr('x2', center)
      .attr('y2', 2000)
      .attr('class', 'actor-line 200')
      .attr('stroke-width', '0.5px')
      .attr('stroke', '#999')
      .attr('name', actor.name);

    g = boxplusLineGroup.append('g');
    actor.actorCnt = actorCnt;

    if (actor.links != null) {
      g.attr('id', 'root-' + actorCnt);
    }
  }

  const rect = svgDrawCommon.getNoteRect();

  let cssclass = 'actor';
  if (actor.properties?.class) {
    cssclass = actor.properties.class;
  } else {
    rect.fill = '#eaeaea';
  }

  if (isFooter) {
    cssclass += ` ${BOTTOM_ACTOR_CLASS}`;
  } else {
    cssclass += ` ${TOP_ACTOR_CLASS}`;
  }

  rect.x = actor.x;
  rect.y = actorY;
  rect.width = actor.width;
  rect.height = actor.height;
  rect.class = cssclass;
  rect.name = actor.name;

  // Cylinder dimensions
  rect.x = actor.x;
  rect.y = actorY;
  const w = rect.width / 4;
  const h = rect.height * 0.8;
  const rx = w / 2;
  const ry = rx / (2.5 + w / 50);

  // Cylinder base group
  const cylinderGroup = g.append('g');

  const d = `
  M ${rect.x},${rect.y + ry}
  a ${rx},${ry} 0 0 0 ${w},0
  a ${rx},${ry} 0 0 0 -${w},0
  l 0,${h - 2 * ry}
  a ${rx},${ry} 0 0 0 ${w},0
  l 0,-${h - 2 * ry}
`;
  // Draw the main cylinder body
  cylinderGroup
    .append('path')
    .attr('d', d)
    .attr('fill', '#eaeaea')
    .attr('stroke', '#000')
    .attr('stroke-width', 1)
    .attr('class', cssclass);

  if (!isFooter) {
    cylinderGroup.attr('transform', `translate(${w * 1.5}, ${rect.height / 4 - 2 * ry})`);
  } else {
    cylinderGroup.attr('transform', `translate(${w * 1.5}, ${rect.height / 4 - 2 * ry})`);
  }
  actor.rectData = rect;
  _drawTextCandidateFunc(conf, hasKatex(actor.description))(
    actor.description,
    g,
    rect.x,
    rect.y + (!isFooter ? rect.height / 2 : h / 2 + ry),
    rect.width,
    rect.height,
    { class: `actor ${ACTOR_BOX_CLASS}` },
    conf
  );

  const lastPath = cylinderGroup.select('path:last-child');
  if (lastPath.node()) {
    const bounds = lastPath.node().getBBox();
    actor.height = bounds.height + (conf.sequence.labelBoxHeight ?? 0);
  }

  return actor.height;
};

const drawActorTypeBoundary = function (elem, actor, conf, isFooter) {
  const actorY = isFooter ? actor.stopy : actor.starty;
  const center = actor.x + actor.width / 2;
  const centerY = actorY + 80;
  const radius = 30;
  const line = elem.append('g').lower();

  if (!isFooter) {
    actorCnt++;
    line
      .append('line')
      .attr('id', 'actor' + actorCnt)
      .attr('x1', center)
      .attr('y1', centerY)
      .attr('x2', center)
      .attr('y2', 2000)
      .attr('class', 'actor-line 200')
      .attr('stroke-width', '0.5px')
      .attr('stroke', '#999')
      .attr('name', actor.name);

    actor.actorCnt = actorCnt;
  }
  const actElem = elem.append('g');
  let cssClass = ACTOR_MAN_FIGURE_CLASS;
  if (isFooter) {
    cssClass += ` ${BOTTOM_ACTOR_CLASS}`;
  } else {
    cssClass += ` ${TOP_ACTOR_CLASS}`;
  }
  actElem.attr('class', cssClass);
  actElem.attr('name', actor.name);

  const rect = svgDrawCommon.getNoteRect();
  rect.x = actor.x;
  rect.y = actorY;
  rect.fill = '#eaeaea';
  rect.width = actor.width;
  rect.height = actor.height;
  rect.class = 'actor';
  rect.rx = 3;
  rect.ry = 3;

  actElem
    .append('line')
    .attr('id', 'actor-man-torso' + actorCnt)
    .attr('x1', actor.x + actor.width / 2 - radius * 2.5)
    .attr('y1', actorY + 10)
    .attr('x2', actor.x + actor.width / 2 - 15)
    .attr('y2', actorY + 10);

  actElem
    .append('line')
    .attr('id', 'actor-man-arms' + actorCnt)
    .attr('x1', actor.x + actor.width / 2 - radius * 2.5)
    .attr('y1', actorY + 0) // starting Y
    .attr('x2', actor.x + actor.width / 2 - radius * 2.5)
    .attr('y2', actorY + 20); // ending Y (26px long, adjust as needed)

  actElem
    .append('circle')
    .attr('cx', actor.x + actor.width / 2)
    .attr('cy', actorY + 10)
    .attr('r', radius)
    .attr('width', actor.width);

  const bounds = actElem.node().getBBox();
  actor.height = bounds.height + (conf.sequence.labelBoxHeight ?? 0);

  _drawTextCandidateFunc(conf, hasKatex(actor.description))(
    actor.description,
    actElem,
    rect.x,
    rect.y + (!isFooter ? radius / 2 : radius / 2),
    rect.width,
    rect.height,
    { class: `actor ${ACTOR_MAN_FIGURE_CLASS}` },
    conf
  );

  if (!isFooter) {
    actElem.attr('transform', `translate(0,${radius / 2 + 7})`);
  } else {
    actElem.attr('transform', `translate(0,${radius / 2 + 7})`);
  }

  // actElem.attr('transform', `translate(${rect.width / 2}, ${actorY + rect.height / 2})`);

  return actor.height;
};

const drawActorTypeActor = function (elem, actor, conf, isFooter) {
  const actorY = isFooter ? actor.stopy : actor.starty;
  const center = actor.x + actor.width / 2;
  const centerY = actorY + 80;

  const line = elem.append('g').lower();

  if (!isFooter) {
    actorCnt++;
    line
      .append('line')
      .attr('id', 'actor' + actorCnt)
      .attr('x1', center)
      .attr('y1', centerY)
      .attr('x2', center)
      .attr('y2', 2000)
      .attr('class', 'actor-line 200')
      .attr('stroke-width', '0.5px')
      .attr('stroke', '#999')
      .attr('name', actor.name);

    actor.actorCnt = actorCnt;
  }
  const actElem = elem.append('g');
  let cssClass = ACTOR_MAN_FIGURE_CLASS;
  if (isFooter) {
    cssClass += ` ${BOTTOM_ACTOR_CLASS}`;
  } else {
    cssClass += ` ${TOP_ACTOR_CLASS}`;
  }
  actElem.attr('class', cssClass);
  actElem.attr('name', actor.name);

  const rect = svgDrawCommon.getNoteRect();
  rect.x = actor.x;
  rect.y = actorY;
  rect.fill = '#eaeaea';
  rect.width = actor.width;
  rect.height = actor.height;
  rect.class = 'actor';
  rect.rx = 3;
  rect.ry = 3;

  actElem
    .append('line')
    .attr('id', 'actor-man-torso' + actorCnt)
    .attr('x1', center)
    .attr('y1', actorY + 25)
    .attr('x2', center)
    .attr('y2', actorY + 45);

  actElem
    .append('line')
    .attr('id', 'actor-man-arms' + actorCnt)
    .attr('x1', center - ACTOR_TYPE_WIDTH / 2)
    .attr('y1', actorY + 33)
    .attr('x2', center + ACTOR_TYPE_WIDTH / 2)
    .attr('y2', actorY + 33);
  actElem
    .append('line')
    .attr('x1', center - ACTOR_TYPE_WIDTH / 2)
    .attr('y1', actorY + 60)
    .attr('x2', center)
    .attr('y2', actorY + 45);
  actElem
    .append('line')
    .attr('x1', center)
    .attr('y1', actorY + 45)
    .attr('x2', center + ACTOR_TYPE_WIDTH / 2 - 2)
    .attr('y2', actorY + 60);

  const circle = actElem.append('circle');
  circle.attr('cx', actor.x + actor.width / 2);
  circle.attr('cy', actorY + 10);
  circle.attr('r', 15);
  circle.attr('width', actor.width);
  circle.attr('height', actor.height);

  const bounds = actElem.node().getBBox();
  actor.height = bounds.height;

  _drawTextCandidateFunc(conf, hasKatex(actor.description))(
    actor.description,
    actElem,
    rect.x,
    rect.y + 35,
    rect.width,
    rect.height,
    { class: `actor ${ACTOR_MAN_FIGURE_CLASS}` },
    conf
  );

  return actor.height;
};

export const drawActor = async function (elem, actor, conf, isFooter) {
  switch (actor.type) {
    case 'actor':
      return await drawActorTypeActor(elem, actor, conf, isFooter);
    case 'participant':
      return await drawActorTypeParticipant(elem, actor, conf, isFooter);
    case 'boundary':
      return await drawActorTypeBoundary(elem, actor, conf, isFooter);
    case 'control':
      return await drawActorTypeControl(elem, actor, conf, isFooter);
    case 'entity':
      return await drawActorTypeEntity(elem, actor, conf, isFooter);
    case 'database':
      return await drawActorTypeDatabase(elem, actor, conf, isFooter);
    case 'collections':
      return await drawActorTypeCollections(elem, actor, conf, isFooter);
    case 'queue':
      return await drawActorTypeQueue(elem, actor, conf, isFooter);
  }
};

export const drawBox = function (elem, box, conf) {
  const boxplusTextGroup = elem.append('g');
  const g = boxplusTextGroup;
  drawBackgroundRect(g, box);
  if (box.name) {
    _drawTextCandidateFunc(conf)(
      box.name,
      g,
      box.x,
      box.y + conf.boxTextMargin + (box.textMaxHeight || 0) / 2,
      box.width,
      0,
      { class: 'text' },
      conf
    );
  }
  g.lower();
};

export const anchorElement = function (elem) {
  return elem.append('g');
};

/**
 * Draws an activation in the diagram
 *
 * @param {any} elem - Element to append activation rect.
 * @param {any} bounds - Activation box bounds.
 * @param {any} verticalPos - Precise y coordinate of bottom activation box edge.
 * @param {any} conf - Sequence diagram config object.
 * @param {any} actorActivations - Number of activations on the actor.
 */
export const drawActivation = function (elem, bounds, verticalPos, conf, actorActivations) {
  const rect = svgDrawCommon.getNoteRect();
  const g = bounds.anchored;
  rect.x = bounds.startx;
  rect.y = bounds.starty;
  rect.class = 'activation' + (actorActivations % 3); // Will evaluate to 0, 1 or 2
  rect.width = bounds.stopx - bounds.startx;
  rect.height = verticalPos - bounds.starty;
  drawRect(g, rect);
};

/**
 * Draws a loop in the diagram
 *
 * @param {any} elem - Element to append the loop to.
 * @param {any} loopModel - LoopModel of the given loop.
 * @param {any} labelText - Text within the loop.
 * @param {any} conf - Diagram configuration
 * @returns {any}
 */
export const drawLoop = async function (elem, loopModel, labelText, conf) {
  const {
    boxMargin,
    boxTextMargin,
    labelBoxHeight,
    labelBoxWidth,
    messageFontFamily: fontFamily,
    messageFontSize: fontSize,
    messageFontWeight: fontWeight,
  } = conf;
  const g = elem.append('g');
  const drawLoopLine = function (startx, starty, stopx, stopy) {
    return g
      .append('line')
      .attr('x1', startx)
      .attr('y1', starty)
      .attr('x2', stopx)
      .attr('y2', stopy)
      .attr('class', 'loopLine');
  };
  drawLoopLine(loopModel.startx, loopModel.starty, loopModel.stopx, loopModel.starty);
  drawLoopLine(loopModel.stopx, loopModel.starty, loopModel.stopx, loopModel.stopy);
  drawLoopLine(loopModel.startx, loopModel.stopy, loopModel.stopx, loopModel.stopy);
  drawLoopLine(loopModel.startx, loopModel.starty, loopModel.startx, loopModel.stopy);
  if (loopModel.sections !== undefined) {
    loopModel.sections.forEach(function (item) {
      drawLoopLine(loopModel.startx, item.y, loopModel.stopx, item.y).style(
        'stroke-dasharray',
        '3, 3'
      );
    });
  }

  let txt = svgDrawCommon.getTextObj();
  txt.text = labelText;
  txt.x = loopModel.startx;
  txt.y = loopModel.starty;
  txt.fontFamily = fontFamily;
  txt.fontSize = fontSize;
  txt.fontWeight = fontWeight;
  txt.anchor = 'middle';
  txt.valign = 'middle';
  txt.tspan = false;
  txt.width = labelBoxWidth || 50;
  txt.height = labelBoxHeight || 20;
  txt.textMargin = boxTextMargin;
  txt.class = 'labelText';

  drawLabel(g, txt);
  txt = getTextObj();
  txt.text = loopModel.title;
  txt.x = loopModel.startx + labelBoxWidth / 2 + (loopModel.stopx - loopModel.startx) / 2;
  txt.y = loopModel.starty + boxMargin + boxTextMargin;
  txt.anchor = 'middle';
  txt.valign = 'middle';
  txt.textMargin = boxTextMargin;
  txt.class = 'loopText';
  txt.fontFamily = fontFamily;
  txt.fontSize = fontSize;
  txt.fontWeight = fontWeight;
  txt.wrap = true;

  let textElem = hasKatex(txt.text) ? await drawKatex(g, txt, loopModel) : drawText(g, txt);

  if (loopModel.sectionTitles !== undefined) {
    for (const [idx, item] of Object.entries(loopModel.sectionTitles)) {
      if (item.message) {
        txt.text = item.message;
        txt.x = loopModel.startx + (loopModel.stopx - loopModel.startx) / 2;
        txt.y = loopModel.sections[idx].y + boxMargin + boxTextMargin;
        txt.class = 'loopText';
        txt.anchor = 'middle';
        txt.valign = 'middle';
        txt.tspan = false;
        txt.fontFamily = fontFamily;
        txt.fontSize = fontSize;
        txt.fontWeight = fontWeight;
        txt.wrap = loopModel.wrap;

        if (hasKatex(txt.text)) {
          loopModel.starty = loopModel.sections[idx].y;
          await drawKatex(g, txt, loopModel);
        } else {
          drawText(g, txt);
        }
        let sectionHeight = Math.round(
          textElem
            .map((te) => (te._groups || te)[0][0].getBBox().height)
            .reduce((acc, curr) => acc + curr)
        );
        loopModel.sections[idx].height += sectionHeight - (boxMargin + boxTextMargin);
      }
    }
  }

  loopModel.height = Math.round(loopModel.stopy - loopModel.starty);
  return g;
};

/**
 * Draws a background rectangle
 *
 * @param {any} elem Diagram (reference for bounds)
 * @param {any} bounds Shape of the rectangle
 */
export const drawBackgroundRect = function (elem, bounds) {
  svgDrawCommon.drawBackgroundRect(elem, bounds);
};

export const insertDatabaseIcon = function (elem) {
  elem
    .append('defs')
    .append('symbol')
    .attr('id', 'database')
    .attr('fill-rule', 'evenodd')
    .attr('clip-rule', 'evenodd')
    .append('path')
    .attr('transform', 'scale(.5)')
    .attr(
      'd',
      'M12.258.001l.256.004.255.005.253.008.251.01.249.012.247.015.246.016.242.019.241.02.239.023.236.024.233.027.231.028.229.031.225.032.223.034.22.036.217.038.214.04.211.041.208.043.205.045.201.046.198.048.194.05.191.051.187.053.183.054.18.056.175.057.172.059.168.06.163.061.16.063.155.064.15.066.074.033.073.033.071.034.07.034.069.035.068.035.067.035.066.035.064.036.064.036.062.036.06.036.06.037.058.037.058.037.055.038.055.038.053.038.052.038.051.039.05.039.048.039.047.039.045.04.044.04.043.04.041.04.04.041.039.041.037.041.036.041.034.041.033.042.032.042.03.042.029.042.027.042.026.043.024.043.023.043.021.043.02.043.018.044.017.043.015.044.013.044.012.044.011.045.009.044.007.045.006.045.004.045.002.045.001.045v17l-.001.045-.002.045-.004.045-.006.045-.007.045-.009.044-.011.045-.012.044-.013.044-.015.044-.017.043-.018.044-.02.043-.021.043-.023.043-.024.043-.026.043-.027.042-.029.042-.03.042-.032.042-.033.042-.034.041-.036.041-.037.041-.039.041-.04.041-.041.04-.043.04-.044.04-.045.04-.047.039-.048.039-.05.039-.051.039-.052.038-.053.038-.055.038-.055.038-.058.037-.058.037-.06.037-.06.036-.062.036-.064.036-.064.036-.066.035-.067.035-.068.035-.069.035-.07.034-.071.034-.073.033-.074.033-.15.066-.155.064-.16.063-.163.061-.168.06-.172.059-.175.057-.18.056-.183.054-.187.053-.191.051-.194.05-.198.048-.201.046-.205.045-.208.043-.211.041-.214.04-.217.038-.22.036-.223.034-.225.032-.229.031-.231.028-.233.027-.236.024-.239.023-.241.02-.242.019-.246.016-.247.015-.249.012-.251.01-.253.008-.255.005-.256.004-.258.001-.258-.001-.256-.004-.255-.005-.253-.008-.251-.01-.249-.012-.247-.015-.245-.016-.243-.019-.241-.02-.238-.023-.236-.024-.234-.027-.231-.028-.228-.031-.226-.032-.223-.034-.22-.036-.217-.038-.214-.04-.211-.041-.208-.043-.204-.045-.201-.046-.198-.048-.195-.05-.19-.051-.187-.053-.184-.054-.179-.056-.176-.057-.172-.059-.167-.06-.164-.061-.159-.063-.155-.064-.151-.066-.074-.033-.072-.033-.072-.034-.07-.034-.069-.035-.068-.035-.067-.035-.066-.035-.064-.036-.063-.036-.062-.036-.061-.036-.06-.037-.058-.037-.057-.037-.056-.038-.055-.038-.053-.038-.052-.038-.051-.039-.049-.039-.049-.039-.046-.039-.046-.04-.044-.04-.043-.04-.041-.04-.04-.041-.039-.041-.037-.041-.036-.041-.034-.041-.033-.042-.032-.042-.03-.042-.029-.042-.027-.042-.026-.043-.024-.043-.023-.043-.021-.043-.02-.043-.018-.044-.017-.043-.015-.044-.013-.044-.012-.044-.011-.045-.009-.044-.007-.045-.006-.045-.004-.045-.002-.045-.001-.045v-17l.001-.045.002-.045.004-.045.006-.045.007-.045.009-.044.011-.045.012-.044.013-.044.015-.044.017-.043.018-.044.02-.043.021-.043.023-.043.024-.043.026-.043.027-.042.029-.042.03-.042.032-.042.033-.042.034-.041.036-.041.037-.041.039-.041.04-.041.041-.04.043-.04.044-.04.046-.04.046-.039.049-.039.049-.039.051-.039.052-.038.053-.038.055-.038.056-.038.057-.037.058-.037.06-.037.061-.036.062-.036.063-.036.064-.036.066-.035.067-.035.068-.035.069-.035.07-.034.072-.034.072-.033.074-.033.151-.066.155-.064.159-.063.164-.061.167-.06.172-.059.176-.057.179-.056.184-.054.187-.053.19-.051.195-.05.198-.048.201-.046.204-.045.208-.043.211-.041.214-.04.217-.038.22-.036.223-.034.226-.032.228-.031.231-.028.234-.027.236-.024.238-.023.241-.02.243-.019.245-.016.247-.015.249-.012.251-.01.253-.008.255-.005.256-.004.258-.001.258.001zm-9.258 20.499v.01l.001.021.003.021.004.022.005.021.006.022.007.022.009.023.01.022.011.023.012.023.013.023.015.023.016.024.017.023.018.024.019.024.021.024.022.025.023.024.024.025.052.049.056.05.061.051.066.051.07.051.075.051.079.052.084.052.088.052.092.052.097.052.102.051.105.052.11.052.114.051.119.051.123.051.127.05.131.05.135.05.139.048.144.049.147.047.152.047.155.047.16.045.163.045.167.043.171.043.176.041.178.041.183.039.187.039.19.037.194.035.197.035.202.033.204.031.209.03.212.029.216.027.219.025.222.024.226.021.23.02.233.018.236.016.24.015.243.012.246.01.249.008.253.005.256.004.259.001.26-.001.257-.004.254-.005.25-.008.247-.011.244-.012.241-.014.237-.016.233-.018.231-.021.226-.021.224-.024.22-.026.216-.027.212-.028.21-.031.205-.031.202-.034.198-.034.194-.036.191-.037.187-.039.183-.04.179-.04.175-.042.172-.043.168-.044.163-.045.16-.046.155-.046.152-.047.148-.048.143-.049.139-.049.136-.05.131-.05.126-.05.123-.051.118-.052.114-.051.11-.052.106-.052.101-.052.096-.052.092-.052.088-.053.083-.051.079-.052.074-.052.07-.051.065-.051.06-.051.056-.05.051-.05.023-.024.023-.025.021-.024.02-.024.019-.024.018-.024.017-.024.015-.023.014-.024.013-.023.012-.023.01-.023.01-.022.008-.022.006-.022.006-.022.004-.022.004-.021.001-.021.001-.021v-4.127l-.077.055-.08.053-.083.054-.085.053-.087.052-.09.052-.093.051-.095.05-.097.05-.1.049-.102.049-.105.048-.106.047-.109.047-.111.046-.114.045-.115.045-.118.044-.12.043-.122.042-.124.042-.126.041-.128.04-.13.04-.132.038-.134.038-.135.037-.138.037-.139.035-.142.035-.143.034-.144.033-.147.032-.148.031-.15.03-.151.03-.153.029-.154.027-.156.027-.158.026-.159.025-.161.024-.162.023-.163.022-.165.021-.166.02-.167.019-.169.018-.169.017-.171.016-.173.015-.173.014-.175.013-.175.012-.177.011-.178.01-.179.008-.179.008-.181.006-.182.005-.182.004-.184.003-.184.002h-.37l-.184-.002-.184-.003-.182-.004-.182-.005-.181-.006-.179-.008-.179-.008-.178-.01-.176-.011-.176-.012-.175-.013-.173-.014-.172-.015-.171-.016-.17-.017-.169-.018-.167-.019-.166-.02-.165-.021-.163-.022-.162-.023-.161-.024-.159-.025-.157-.026-.156-.027-.155-.027-.153-.029-.151-.03-.15-.03-.148-.031-.146-.032-.145-.033-.143-.034-.141-.035-.14-.035-.137-.037-.136-.037-.134-.038-.132-.038-.13-.04-.128-.04-.126-.041-.124-.042-.122-.042-.12-.044-.117-.043-.116-.045-.113-.045-.112-.046-.109-.047-.106-.047-.105-.048-.102-.049-.1-.049-.097-.05-.095-.05-.093-.052-.09-.051-.087-.052-.085-.053-.083-.054-.08-.054-.077-.054v4.127zm0-5.654v.011l.001.021.003.021.004.021.005.022.006.022.007.022.009.022.01.022.011.023.012.023.013.023.015.024.016.023.017.024.018.024.019.024.021.024.022.024.023.025.024.024.052.05.056.05.061.05.066.051.07.051.075.052.079.051.084.052.088.052.092.052.097.052.102.052.105.052.11.051.114.051.119.052.123.05.127.051.131.05.135.049.139.049.144.048.147.048.152.047.155.046.16.045.163.045.167.044.171.042.176.042.178.04.183.04.187.038.19.037.194.036.197.034.202.033.204.032.209.03.212.028.216.027.219.025.222.024.226.022.23.02.233.018.236.016.24.014.243.012.246.01.249.008.253.006.256.003.259.001.26-.001.257-.003.254-.006.25-.008.247-.01.244-.012.241-.015.237-.016.233-.018.231-.02.226-.022.224-.024.22-.025.216-.027.212-.029.21-.03.205-.032.202-.033.198-.035.194-.036.191-.037.187-.039.183-.039.179-.041.175-.042.172-.043.168-.044.163-.045.16-.045.155-.047.152-.047.148-.048.143-.048.139-.05.136-.049.131-.05.126-.051.123-.051.118-.051.114-.052.11-.052.106-.052.101-.052.096-.052.092-.052.088-.052.083-.052.079-.052.074-.051.07-.052.065-.051.06-.05.056-.051.051-.049.023-.025.023-.024.021-.025.02-.024.019-.024.018-.024.017-.024.015-.023.014-.023.013-.024.012-.022.01-.023.01-.023.008-.022.006-.022.006-.022.004-.021.004-.022.001-.021.001-.021v-4.139l-.077.054-.08.054-.083.054-.085.052-.087.053-.09.051-.093.051-.095.051-.097.05-.1.049-.102.049-.105.048-.106.047-.109.047-.111.046-.114.045-.115.044-.118.044-.12.044-.122.042-.124.042-.126.041-.128.04-.13.039-.132.039-.134.038-.135.037-.138.036-.139.036-.142.035-.143.033-.144.033-.147.033-.148.031-.15.03-.151.03-.153.028-.154.028-.156.027-.158.026-.159.025-.161.024-.162.023-.163.022-.165.021-.166.02-.167.019-.169.018-.169.017-.171.016-.173.015-.173.014-.175.013-.175.012-.177.011-.178.009-.179.009-.179.007-.181.007-.182.005-.182.004-.184.003-.184.002h-.37l-.184-.002-.184-.003-.182-.004-.182-.005-.181-.007-.179-.007-.179-.009-.178-.009-.176-.011-.176-.012-.175-.013-.173-.014-.172-.015-.171-.016-.17-.017-.169-.018-.167-.019-.166-.02-.165-.021-.163-.022-.162-.023-.161-.024-.159-.025-.157-.026-.156-.027-.155-.028-.153-.028-.151-.03-.15-.03-.148-.031-.146-.033-.145-.033-.143-.033-.141-.035-.14-.036-.137-.036-.136-.037-.134-.038-.132-.039-.13-.039-.128-.04-.126-.041-.124-.042-.122-.043-.12-.043-.117-.044-.116-.044-.113-.046-.112-.046-.109-.046-.106-.047-.105-.048-.102-.049-.1-.049-.097-.05-.095-.051-.093-.051-.09-.051-.087-.053-.085-.052-.083-.054-.08-.054-.077-.054v4.139zm0-5.666v.011l.001.02.003.022.004.021.005.022.006.021.007.022.009.023.01.022.011.023.012.023.013.023.015.023.016.024.017.024.018.023.019.024.021.025.022.024.023.024.024.025.052.05.056.05.061.05.066.051.07.051.075.052.079.051.084.052.088.052.092.052.097.052.102.052.105.051.11.052.114.051.119.051.123.051.127.05.131.05.135.05.139.049.144.048.147.048.152.047.155.046.16.045.163.045.167.043.171.043.176.042.178.04.183.04.187.038.19.037.194.036.197.034.202.033.204.032.209.03.212.028.216.027.219.025.222.024.226.021.23.02.233.018.236.017.24.014.243.012.246.01.249.008.253.006.256.003.259.001.26-.001.257-.003.254-.006.25-.008.247-.01.244-.013.241-.014.237-.016.233-.018.231-.02.226-.022.224-.024.22-.025.216-.027.212-.029.21-.03.205-.032.202-.033.198-.035.194-.036.191-.037.187-.039.183-.039.179-.041.175-.042.172-.043.168-.044.163-.045.16-.045.155-.047.152-.047.148-.048.143-.049.139-.049.136-.049.131-.051.126-.05.123-.051.118-.052.114-.051.11-.052.106-.052.101-.052.096-.052.092-.052.088-.052.083-.052.079-.052.074-.052.07-.051.065-.051.06-.051.056-.05.051-.049.023-.025.023-.025.021-.024.02-.024.019-.024.018-.024.017-.024.015-.023.014-.024.013-.023.012-.023.01-.022.01-.023.008-.022.006-.022.006-.022.004-.022.004-.021.001-.021.001-.021v-4.153l-.077.054-.08.054-.083.053-.085.053-.087.053-.09.051-.093.051-.095.051-.097.05-.1.049-.102.048-.105.048-.106.048-.109.046-.111.046-.114.046-.115.044-.118.044-.12.043-.122.043-.124.042-.126.041-.128.04-.13.039-.132.039-.134.038-.135.037-.138.036-.139.036-.142.034-.143.034-.144.033-.147.032-.148.032-.15.03-.151.03-.153.028-.154.028-.156.027-.158.026-.159.024-.161.024-.162.023-.163.023-.165.021-.166.02-.167.019-.169.018-.169.017-.171.016-.173.015-.173.014-.175.013-.175.012-.177.01-.178.01-.179.009-.179.007-.181.006-.182.006-.182.004-.184.003-.184.001-.185.001-.185-.001-.184-.001-.184-.003-.182-.004-.182-.006-.181-.006-.179-.007-.179-.009-.178-.01-.176-.01-.176-.012-.175-.013-.173-.014-.172-.015-.171-.016-.17-.017-.169-.018-.167-.019-.166-.02-.165-.021-.163-.023-.162-.023-.161-.024-.159-.024-.157-.026-.156-.027-.155-.028-.153-.028-.151-.03-.15-.03-.148-.032-.146-.032-.145-.033-.143-.034-.141-.034-.14-.036-.137-.036-.136-.037-.134-.038-.132-.039-.13-.039-.128-.041-.126-.041-.124-.041-.122-.043-.12-.043-.117-.044-.116-.044-.113-.046-.112-.046-.109-.046-.106-.048-.105-.048-.102-.048-.1-.05-.097-.049-.095-.051-.093-.051-.09-.052-.087-.052-.085-.053-.083-.053-.08-.054-.077-.054v4.153zm8.74-8.179l-.257.004-.254.005-.25.008-.247.011-.244.012-.241.014-.237.016-.233.018-.231.021-.226.022-.224.023-.22.026-.216.027-.212.028-.21.031-.205.032-.202.033-.198.034-.194.036-.191.038-.187.038-.183.04-.179.041-.175.042-.172.043-.168.043-.163.045-.16.046-.155.046-.152.048-.148.048-.143.048-.139.049-.136.05-.131.05-.126.051-.123.051-.118.051-.114.052-.11.052-.106.052-.101.052-.096.052-.092.052-.088.052-.083.052-.079.052-.074.051-.07.052-.065.051-.06.05-.056.05-.051.05-.023.025-.023.024-.021.024-.02.025-.019.024-.018.024-.017.023-.015.024-.014.023-.013.023-.012.023-.01.023-.01.022-.008.022-.006.023-.006.021-.004.022-.004.021-.001.021-.001.021.001.021.001.021.004.021.004.022.006.021.006.023.008.022.01.022.01.023.012.023.013.023.014.023.015.024.017.023.018.024.019.024.02.025.021.024.023.024.023.025.051.05.056.05.06.05.065.051.07.052.074.051.079.052.083.052.088.052.092.052.096.052.101.052.106.052.11.052.114.052.118.051.123.051.126.051.131.05.136.05.139.049.143.048.148.048.152.048.155.046.16.046.163.045.168.043.172.043.175.042.179.041.183.04.187.038.191.038.194.036.198.034.202.033.205.032.21.031.212.028.216.027.22.026.224.023.226.022.231.021.233.018.237.016.241.014.244.012.247.011.25.008.254.005.257.004.26.001.26-.001.257-.004.254-.005.25-.008.247-.011.244-.012.241-.014.237-.016.233-.018.231-.021.226-.022.224-.023.22-.026.216-.027.212-.028.21-.031.205-.032.202-.033.198-.034.194-.036.191-.038.187-.038.183-.04.179-.041.175-.042.172-.043.168-.043.163-.045.16-.046.155-.046.152-.048.148-.048.143-.048.139-.049.136-.05.131-.05.126-.051.123-.051.118-.051.114-.052.11-.052.106-.052.101-.052.096-.052.092-.052.088-.052.083-.052.079-.052.074-.051.07-.052.065-.051.06-.05.056-.05.051-.05.023-.025.023-.024.021-.024.02-.025.019-.024.018-.024.017-.023.015-.024.014-.023.013-.023.012-.023.01-.023.01-.022.008-.022.006-.023.006-.021.004-.022.004-.021.001-.021.001-.021-.001-.021-.001-.021-.004-.021-.004-.022-.006-.021-.006-.023-.008-.022-.01-.022-.01-.023-.012-.023-.013-.023-.014-.023-.015-.024-.017-.023-.018-.024-.019-.024-.02-.025-.021-.024-.023-.024-.023-.025-.051-.05-.056-.05-.06-.05-.065-.051-.07-.052-.074-.051-.079-.052-.083-.052-.088-.052-.092-.052-.096-.052-.101-.052-.106-.052-.11-.052-.114-.052-.118-.051-.123-.051-.126-.051-.131-.05-.136-.05-.139-.049-.143-.048-.148-.048-.152-.048-.155-.046-.16-.046-.163-.045-.168-.043-.172-.043-.175-.042-.179-.041-.183-.04-.187-.038-.191-.038-.194-.036-.198-.034-.202-.033-.205-.032-.21-.031-.212-.028-.216-.027-.22-.026-.224-.023-.226-.022-.231-.021-.233-.018-.237-.016-.241-.014-.244-.012-.247-.011-.25-.008-.254-.005-.257-.004-.26-.001-.26.001z'
    );
};

export const insertComputerIcon = function (elem) {
  elem
    .append('defs')
    .append('symbol')
    .attr('id', 'computer')
    .attr('width', '24')
    .attr('height', '24')
    .append('path')
    .attr('transform', 'scale(.5)')
    .attr(
      'd',
      'M2 2v13h20v-13h-20zm18 11h-16v-9h16v9zm-10.228 6l.466-1h3.524l.467 1h-4.457zm14.228 3h-24l2-6h2.104l-1.33 4h18.45l-1.297-4h2.073l2 6zm-5-10h-14v-7h14v7z'
    );
};

export const insertClockIcon = function (elem) {
  elem
    .append('defs')
    .append('symbol')
    .attr('id', 'clock')
    .attr('width', '24')
    .attr('height', '24')
    .append('path')
    .attr('transform', 'scale(.5)')
    .attr(
      'd',
      'M12 2c5.514 0 10 4.486 10 10s-4.486 10-10 10-10-4.486-10-10 4.486-10 10-10zm0-2c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12zm5.848 12.459c.202.038.202.333.001.372-1.907.361-6.045 1.111-6.547 1.111-.719 0-1.301-.582-1.301-1.301 0-.512.77-5.447 1.125-7.445.034-.192.312-.181.343.014l.985 6.238 5.394 1.011z'
    );
};

/**
 * Setup arrow head and define the marker. The result is appended to the svg.
 *
 * @param elem
 */
export const insertArrowHead = function (elem) {
  elem
    .append('defs')
    .append('marker')
    .attr('id', 'arrowhead')
    .attr('refX', 7.9)
    .attr('refY', 5)
    .attr('markerUnits', 'userSpaceOnUse')
    .attr('markerWidth', 12)
    .attr('markerHeight', 12)
    .attr('orient', 'auto-start-reverse')
    .append('path')
    .attr('d', 'M -1 0 L 10 5 L 0 10 z'); // this is actual shape for arrowhead
};

/**
 * Setup arrow head and define the marker. The result is appended to the svg.
 *
 * @param {any} elem
 */
export const insertArrowFilledHead = function (elem) {
  elem
    .append('defs')
    .append('marker')
    .attr('id', 'filled-head')
    .attr('refX', 15.5)
    .attr('refY', 7)
    .attr('markerWidth', 20)
    .attr('markerHeight', 28)
    .attr('orient', 'auto')
    .append('path')
    .attr('d', 'M 18,7 L9,13 L14,7 L9,1 Z');
};

/**
 * Setup node number. The result is appended to the svg.
 *
 * @param {any} elem
 */
export const insertSequenceNumber = function (elem) {
  elem
    .append('defs')
    .append('marker')
    .attr('id', 'sequencenumber')
    .attr('refX', 15)
    .attr('refY', 15)
    .attr('markerWidth', 60)
    .attr('markerHeight', 40)
    .attr('orient', 'auto')
    .append('circle')
    .attr('cx', 15)
    .attr('cy', 15)
    .attr('r', 6);
  // .style("fill", '#f00');
};

/**
 * Setup cross head and define the marker. The result is appended to the svg.
 *
 * @param {any} elem
 */
export const insertArrowCrossHead = function (elem) {
  const defs = elem.append('defs');
  const marker = defs
    .append('marker')
    .attr('id', 'crosshead')
    .attr('markerWidth', 15)
    .attr('markerHeight', 8)
    .attr('orient', 'auto')
    .attr('refX', 4)
    .attr('refY', 4.5);
  // The cross
  marker
    .append('path')
    .attr('fill', 'none')
    .attr('stroke', '#000000')
    .style('stroke-dasharray', '0, 0')
    .attr('stroke-width', '1pt')
    .attr('d', 'M 1,2 L 6,7 M 6,2 L 1,7');
  // this is actual shape for arrowhead
};

export const getTextObj = function () {
  return {
    x: 0,
    y: 0,
    fill: undefined,
    anchor: undefined,
    style: '#666',
    width: undefined,
    height: undefined,
    textMargin: 0,
    rx: 0,
    ry: 0,
    tspan: true,
    valign: undefined,
  };
};

export const getNoteRect = function () {
  return {
    x: 0,
    y: 0,
    fill: '#EDF2AE',
    stroke: '#666',
    width: 100,
    anchor: 'start',
    height: 100,
    rx: 0,
    ry: 0,
  };
};

const _drawTextCandidateFunc = (function () {
  /**
   * @param {any} content
   * @param {any} g
   * @param {any} x
   * @param {any} y
   * @param {any} width
   * @param {any} height
   * @param {any} textAttrs
   */
  function byText(content, g, x, y, width, height, textAttrs) {
    const text = g
      .append('text')
      .attr('x', x + width / 2)
      .attr('y', y + height / 2 + 5)
      .style('text-anchor', 'middle')
      .text(content);
    _setTextAttrs(text, textAttrs);
  }

  /**
   * @param {any} content
   * @param {any} g
   * @param {any} x
   * @param {any} y
   * @param {any} width
   * @param {any} height
   * @param {any} textAttrs
   * @param {any} conf
   */
  function byTspan(content, g, x, y, width, height, textAttrs, conf) {
    const { actorFontSize, actorFontFamily, actorFontWeight } = conf;

    const [_actorFontSize, _actorFontSizePx] = parseFontSize(actorFontSize);

    const lines = content.split(common.lineBreakRegex);
    for (let i = 0; i < lines.length; i++) {
      const dy = i * _actorFontSize - (_actorFontSize * (lines.length - 1)) / 2;
      const text = g
        .append('text')
        .attr('x', x + width / 2)
        .attr('y', y)
        .style('text-anchor', 'middle')
        .style('font-size', _actorFontSizePx)
        .style('font-weight', actorFontWeight)
        .style('font-family', actorFontFamily);
      text
        .append('tspan')
        .attr('x', x + width / 2)
        .attr('dy', dy)
        .text(lines[i]);

      text
        .attr('y', y + height / 2.0)
        .attr('dominant-baseline', 'central')
        .attr('alignment-baseline', 'central');

      _setTextAttrs(text, textAttrs);
    }
  }

  /**
   * @param {any} content
   * @param {any} g
   * @param {any} x
   * @param {any} y
   * @param {any} width
   * @param {any} height
   * @param {any} textAttrs
   * @param {any} conf
   */
  function byFo(content, g, x, y, width, height, textAttrs, conf) {
    const s = g.append('switch');
    const f = s
      .append('foreignObject')
      .attr('x', x)
      .attr('y', y)
      .attr('width', width)
      .attr('height', height);

    const text = f
      .append('xhtml:div')
      .style('display', 'table')
      .style('height', '100%')
      .style('width', '100%');

    text
      .append('div')
      .style('display', 'table-cell')
      .style('text-align', 'center')
      .style('vertical-align', 'middle')
      .text(content);

    byTspan(content, s, x, y, width, height, textAttrs, conf);
    _setTextAttrs(text, textAttrs);
  }

  /**
   *
   * @param content
   * @param g
   * @param x
   * @param y
   * @param width
   * @param height
   * @param textAttrs
   * @param conf
   */
  async function byKatex(content, g, x, y, width, height, textAttrs, conf) {
    // TODO duplicate render calls, optimize

    const dim = await calculateMathMLDimensions(content, configApi.getConfig());
    const s = g.append('switch');
    const f = s
      .append('foreignObject')
      .attr('x', x + width / 2 - dim.width / 2)
      .attr('y', y + height / 2 - dim.height / 2)
      .attr('width', dim.width)
      .attr('height', dim.height);

    const text = f.append('xhtml:div').style('height', '100%').style('width', '100%');

    text
      .append('div')
      .style('text-align', 'center')
      .style('vertical-align', 'middle')
      .html(await renderKatex(content, configApi.getConfig()));

    byTspan(content, s, x, y, width, height, textAttrs, conf);
    _setTextAttrs(text, textAttrs);
  }

  /**
   * @param {any} toText
   * @param {any} fromTextAttrsDict
   */
  function _setTextAttrs(toText, fromTextAttrsDict) {
    for (const key in fromTextAttrsDict) {
      if (fromTextAttrsDict.hasOwnProperty(key)) {
        toText.attr(key, fromTextAttrsDict[key]);
      }
    }
  }

  return function (conf, hasKatex = false) {
    if (hasKatex) {
      return byKatex;
    }
    return conf.textPlacement === 'fo' ? byFo : conf.textPlacement === 'old' ? byText : byTspan;
  };
})();

const _drawMenuItemTextCandidateFunc = (function () {
  /**
   * @param {any} content
   * @param {any} g
   * @param {any} x
   * @param {any} y
   * @param {any} width
   * @param {any} height
   * @param {any} textAttrs
   */
  function byText(content, g, x, y, width, height, textAttrs) {
    const text = g
      .append('text')
      .attr('x', x)
      .attr('y', y)
      .style('text-anchor', 'start')
      .text(content);
    _setTextAttrs(text, textAttrs);
  }

  /**
   * @param {any} content
   * @param {any} g
   * @param {any} x
   * @param {any} y
   * @param {any} width
   * @param {any} height
   * @param {any} textAttrs
   * @param {any} conf
   */
  function byTspan(content, g, x, y, width, height, textAttrs, conf) {
    const { actorFontSize, actorFontFamily, actorFontWeight } = conf;

    const lines = content.split(common.lineBreakRegex);
    for (let i = 0; i < lines.length; i++) {
      const dy = i * actorFontSize - (actorFontSize * (lines.length - 1)) / 2;
      const text = g
        .append('text')
        .attr('x', x)
        .attr('y', y)
        .style('text-anchor', 'start')
        .style('font-size', actorFontSize)
        .style('font-weight', actorFontWeight)
        .style('font-family', actorFontFamily);
      text.append('tspan').attr('x', x).attr('dy', dy).text(lines[i]);

      text
        .attr('y', y + height / 2.0)
        .attr('dominant-baseline', 'central')
        .attr('alignment-baseline', 'central');

      _setTextAttrs(text, textAttrs);
    }
  }

  /**
   * @param {any} content
   * @param {any} g
   * @param {any} x
   * @param {any} y
   * @param {any} width
   * @param {any} height
   * @param {any} textAttrs
   * @param {any} conf
   */
  function byFo(content, g, x, y, width, height, textAttrs, conf) {
    const s = g.append('switch');
    const f = s
      .append('foreignObject')
      .attr('x', x)
      .attr('y', y)
      .attr('width', width)
      .attr('height', height);

    const text = f
      .append('xhtml:div')
      .style('display', 'table')
      .style('height', '100%')
      .style('width', '100%');

    text
      .append('div')
      .style('display', 'table-cell')
      .style('text-align', 'center')
      .style('vertical-align', 'middle')
      .text(content);

    byTspan(content, s, x, y, width, height, textAttrs, conf);
    _setTextAttrs(text, textAttrs);
  }

  /**
   * @param {any} toText
   * @param {any} fromTextAttrsDict
   */
  function _setTextAttrs(toText, fromTextAttrsDict) {
    for (const key in fromTextAttrsDict) {
      if (fromTextAttrsDict.hasOwnProperty(key)) {
        toText.attr(key, fromTextAttrsDict[key]);
      }
    }
  }

  return function (conf) {
    return conf.textPlacement === 'fo' ? byFo : conf.textPlacement === 'old' ? byText : byTspan;
  };
})();

export default {
  drawRect,
  drawText,
  drawLabel,
  drawActor,
  drawBox,
  drawPopup,
  anchorElement,
  drawActivation,
  drawLoop,
  drawBackgroundRect,
  insertArrowHead,
  insertArrowFilledHead,
  insertSequenceNumber,
  insertArrowCrossHead,
  insertDatabaseIcon,
  insertComputerIcon,
  insertClockIcon,
  getTextObj,
  getNoteRect,
  fixLifeLineHeights,
  sanitizeUrl,
};
