import { path2js } from './_path.js';
import {
  transform2js,
  transformArc,
  transformsMultiply,
} from './_transforms.js';
import { attrsGroupsDefaults, referencesProps } from './_collections.js';
import { collectStylesheet, computeStyle } from '../lib/style.js';

import { includesUrlReference, removeLeadingZero } from '../lib/svgo/tools.js';

const regNumericValues = /[-+]?(\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?/g;

/**
 * Apply transformation(s) to the Path data.
 *
 * @type {import('../lib/types.js').Plugin<{
 *   transformPrecision: number,
 *   applyTransformsStroked: boolean,
 * }>}
 */
export const applyTransforms = (root, params) => {
  const stylesheet = collectStylesheet(root);
  return {
    element: {
      enter: (node) => {
        if (node.attributes.d == null) {
          return;
        }

        // stroke and stroke-width can be redefined with <use>
        if (node.attributes.id != null) {
          return;
        }

        // if there are no 'stroke' attr and references to other objects such as
        // gradients or clip-path which are also subjects to transform.
        if (
          node.attributes.transform == null ||
          node.attributes.transform === '' ||
          // styles are not considered when applying transform
          // can be fixed properly with new style engine
          node.attributes.style != null ||
          Object.entries(node.attributes).some(
            ([name, value]) =>
              referencesProps.has(name) && includesUrlReference(value),
          )
        ) {
          return;
        }

        const computedStyle = computeStyle(stylesheet, node);
        const transformStyle = computedStyle.transform;

        // Transform overridden in <style> tag which is not considered
        if (
          transformStyle.type === 'static' &&
          transformStyle.value !== node.attributes.transform
        ) {
          return;
        }

        const matrix = transformsMultiply(
          transform2js(node.attributes.transform),
        );

        const stroke =
          computedStyle.stroke?.type === 'static'
            ? computedStyle.stroke.value
            : null;

        const strokeWidth =
          computedStyle['stroke-width']?.type === 'static'
            ? computedStyle['stroke-width'].value
            : null;
        const transformPrecision = params.transformPrecision;

        if (
          computedStyle.stroke?.type === 'dynamic' ||
          computedStyle['stroke-width']?.type === 'dynamic'
        ) {
          return;
        }

        const scale = Number(
          Math.hypot(matrix.data[0], matrix.data[1]).toFixed(
            transformPrecision,
          ),
        );

        if (stroke && stroke != 'none') {
          if (!params.applyTransformsStroked) {
            return;
          }

          // stroke cannot be transformed with different vertical and horizontal scale or skew
          if (
            (matrix.data[0] !== matrix.data[3] ||
              matrix.data[1] !== -matrix.data[2]) &&
            (matrix.data[0] !== -matrix.data[3] ||
              matrix.data[1] !== matrix.data[2])
          ) {
            return;
          }

          // apply transform to stroke-width, stroke-dashoffset and stroke-dasharray
          if (scale !== 1) {
            if (node.attributes['vector-effect'] !== 'non-scaling-stroke') {
              node.attributes['stroke-width'] = (
                strokeWidth || attrsGroupsDefaults.presentation['stroke-width']
              )
                .trim()
                .replace(regNumericValues, (num) =>
                  removeLeadingZero(Number(num) * scale),
                );

              if (node.attributes['stroke-dashoffset'] != null) {
                node.attributes['stroke-dashoffset'] = node.attributes[
                  'stroke-dashoffset'
                ]
                  .trim()
                  .replace(regNumericValues, (num) =>
                    removeLeadingZero(Number(num) * scale),
                  );
              }

              if (node.attributes['stroke-dasharray'] != null) {
                node.attributes['stroke-dasharray'] = node.attributes[
                  'stroke-dasharray'
                ]
                  .trim()
                  .replace(regNumericValues, (num) =>
                    removeLeadingZero(Number(num) * scale),
                  );
              }
            }
          }
        }

        const pathData = path2js(node);
        applyMatrixToPathData(pathData, matrix.data);

        // remove transform attr
        delete node.attributes.transform;
      },
    },
  };
};

/**
 * @param {ReadonlyArray<number>} matrix
 * @param {number} x
 * @param {number} y
 * @returns {[number, number]}
 */
const transformAbsolutePoint = (matrix, x, y) => {
  const newX = matrix[0] * x + matrix[2] * y + matrix[4];
  const newY = matrix[1] * x + matrix[3] * y + matrix[5];
  return [newX, newY];
};

/**
 * @param {ReadonlyArray<number>} matrix
 * @param {number} x
 * @param {number} y
 * @returns {[number, number]}
 */
const transformRelativePoint = (matrix, x, y) => {
  const newX = matrix[0] * x + matrix[2] * y;
  const newY = matrix[1] * x + matrix[3] * y;
  return [newX, newY];
};

/**
 * @param {ReadonlyArray<import('../lib/types.js').PathDataItem>} pathData
 * @param {ReadonlyArray<number>} matrix
 */
const applyMatrixToPathData = (pathData, matrix) => {
  /** @type {[number, number]} */
  const start = [0, 0];
  /** @type {[number, number]} */
  const cursor = [0, 0];

  for (const pathItem of pathData) {
    let { command, args } = pathItem;

    // moveto (x y)
    if (command === 'M') {
      cursor[0] = args[0];
      cursor[1] = args[1];
      start[0] = cursor[0];
      start[1] = cursor[1];
      const [x, y] = transformAbsolutePoint(matrix, args[0], args[1]);
      args[0] = x;
      args[1] = y;
    }
    if (command === 'm') {
      cursor[0] += args[0];
      cursor[1] += args[1];
      start[0] = cursor[0];
      start[1] = cursor[1];
      const [x, y] = transformRelativePoint(matrix, args[0], args[1]);
      args[0] = x;
      args[1] = y;
    }

    // horizontal lineto (x)
    // convert to lineto to handle two-dimensional transforms
    if (command === 'H') {
      command = 'L';
      args = [args[0], cursor[1]];
    }
    if (command === 'h') {
      command = 'l';
      args = [args[0], 0];
    }

    // vertical lineto (y)
    // convert to lineto to handle two-dimensional transforms
    if (command === 'V') {
      command = 'L';
      args = [cursor[0], args[0]];
    }
    if (command === 'v') {
      command = 'l';
      args = [0, args[0]];
    }

    // lineto (x y)
    if (command === 'L') {
      cursor[0] = args[0];
      cursor[1] = args[1];
      const [x, y] = transformAbsolutePoint(matrix, args[0], args[1]);
      args[0] = x;
      args[1] = y;
    }
    if (command === 'l') {
      cursor[0] += args[0];
      cursor[1] += args[1];
      const [x, y] = transformRelativePoint(matrix, args[0], args[1]);
      args[0] = x;
      args[1] = y;
    }

    // curveto (x1 y1 x2 y2 x y)
    if (command === 'C') {
      cursor[0] = args[4];
      cursor[1] = args[5];
      const [x1, y1] = transformAbsolutePoint(matrix, args[0], args[1]);
      const [x2, y2] = transformAbsolutePoint(matrix, args[2], args[3]);
      const [x, y] = transformAbsolutePoint(matrix, args[4], args[5]);
      args[0] = x1;
      args[1] = y1;
      args[2] = x2;
      args[3] = y2;
      args[4] = x;
      args[5] = y;
    }
    if (command === 'c') {
      cursor[0] += args[4];
      cursor[1] += args[5];
      const [x1, y1] = transformRelativePoint(matrix, args[0], args[1]);
      const [x2, y2] = transformRelativePoint(matrix, args[2], args[3]);
      const [x, y] = transformRelativePoint(matrix, args[4], args[5]);
      args[0] = x1;
      args[1] = y1;
      args[2] = x2;
      args[3] = y2;
      args[4] = x;
      args[5] = y;
    }

    // smooth curveto (x2 y2 x y)
    if (command === 'S') {
      cursor[0] = args[2];
      cursor[1] = args[3];
      const [x2, y2] = transformAbsolutePoint(matrix, args[0], args[1]);
      const [x, y] = transformAbsolutePoint(matrix, args[2], args[3]);
      args[0] = x2;
      args[1] = y2;
      args[2] = x;
      args[3] = y;
    }
    if (command === 's') {
      cursor[0] += args[2];
      cursor[1] += args[3];
      const [x2, y2] = transformRelativePoint(matrix, args[0], args[1]);
      const [x, y] = transformRelativePoint(matrix, args[2], args[3]);
      args[0] = x2;
      args[1] = y2;
      args[2] = x;
      args[3] = y;
    }

    // quadratic Bézier curveto (x1 y1 x y)
    if (command === 'Q') {
      cursor[0] = args[2];
      cursor[1] = args[3];
      const [x1, y1] = transformAbsolutePoint(matrix, args[0], args[1]);
      const [x, y] = transformAbsolutePoint(matrix, args[2], args[3]);
      args[0] = x1;
      args[1] = y1;
      args[2] = x;
      args[3] = y;
    }
    if (command === 'q') {
      cursor[0] += args[2];
      cursor[1] += args[3];
      const [x1, y1] = transformRelativePoint(matrix, args[0], args[1]);
      const [x, y] = transformRelativePoint(matrix, args[2], args[3]);
      args[0] = x1;
      args[1] = y1;
      args[2] = x;
      args[3] = y;
    }

    // smooth quadratic Bézier curveto (x y)
    if (command === 'T') {
      cursor[0] = args[0];
      cursor[1] = args[1];
      const [x, y] = transformAbsolutePoint(matrix, args[0], args[1]);
      args[0] = x;
      args[1] = y;
    }
    if (command === 't') {
      cursor[0] += args[0];
      cursor[1] += args[1];
      const [x, y] = transformRelativePoint(matrix, args[0], args[1]);
      args[0] = x;
      args[1] = y;
    }

    // elliptical arc (rx ry x-axis-rotation large-arc-flag sweep-flag x y)
    if (command === 'A') {
      transformArc(cursor, args, matrix);
      cursor[0] = args[5];
      cursor[1] = args[6];
      // reduce number of digits in rotation angle
      if (Math.abs(args[2]) > 80) {
        const a = args[0];
        const rotation = args[2];
        args[0] = args[1];
        args[1] = a;
        args[2] = rotation + (rotation > 0 ? -90 : 90);
      }
      const [x, y] = transformAbsolutePoint(matrix, args[5], args[6]);
      args[5] = x;
      args[6] = y;
    }
    if (command === 'a') {
      transformArc([0, 0], args, matrix);
      cursor[0] += args[5];
      cursor[1] += args[6];
      // reduce number of digits in rotation angle
      if (Math.abs(args[2]) > 80) {
        const a = args[0];
        const rotation = args[2];
        args[0] = args[1];
        args[1] = a;
        args[2] = rotation + (rotation > 0 ? -90 : 90);
      }
      const [x, y] = transformRelativePoint(matrix, args[5], args[6]);
      args[5] = x;
      args[6] = y;
    }

    // closepath
    if (command === 'z' || command === 'Z') {
      cursor[0] = start[0];
      cursor[1] = start[1];
    }

    pathItem.command = command;
    pathItem.args = args;
  }
};
