import { Transform } from 'stream';
import Sax from 'sax';
import { SVGPathData } from 'svg-pathdata';
import svgShapesToPath from './svgshapes2svgpath.js';
import {
  type Matrix,
  scale,
  translate,
  compose,
  fromDefinition,
  fromTransformAttribute,
} from 'transformation-matrix';

import { YError } from 'yerror';
import debug from 'debug';

const warn = debug('svgicons2svgfont');

export { fileSorter } from './filesorter.js';
export * from './iconsdir.js';
export * from './metadata.js';

function matrixFromTransformAttribute(transformAttributeString): Matrix {
  return compose(
    fromDefinition(fromTransformAttribute(transformAttributeString)),
  );
}

// Rendering
function tagShouldRender(curTag, parents) {
  let values;

  return !parents.some((tag) => {
    if (
      'undefined' !== typeof tag.attributes.display &&
      'none' === tag.attributes.display.toLowerCase()
    ) {
      return true;
    }
    if (
      'undefined' !== typeof tag.attributes.width &&
      0 === parseFloat(tag.attributes.width)
    ) {
      return true;
    }
    if (
      'undefined' !== typeof tag.attributes.height &&
      0 === parseFloat(tag.attributes.height)
    ) {
      return true;
    }
    if ('undefined' !== typeof tag.attributes.viewBox) {
      values = tag.attributes.viewBox.split(/\s*,*\s|\s,*\s*|,/);
      if (0 === parseFloat(values[2]) || 0 === parseFloat(values[3])) {
        return true;
      }
    }
    return false;
  });
}

// According to the document (http://www.w3.org/TR/SVG/painting.html#FillProperties)
// fill <paint> none|currentColor|inherit|<color>
//     [<icccolor>]|<funciri> (not support yet)
function getTagColor(currTag, parents) {
  const defaultColor = 'black';
  const fillVal = currTag.attributes.fill;
  let color;
  const parentsLength = parents.length;

  if ('none' === fillVal) {
    return color;
  }
  if ('currentColor' === fillVal) {
    return defaultColor;
  }
  if ('inherit' === fillVal) {
    if (0 === parentsLength) {
      return defaultColor;
    }
    return getTagColor(
      parents[parentsLength - 1],
      parents.slice(0, parentsLength - 1),
    );
    // this might be null.
    // For example: <svg ><path fill="inherit" /> </svg>
    // in this case getTagColor should return null
    // recursive call, the bottom element should be svg,
    // and svg didn't fill color, so just return null
  }

  return fillVal;
}

export type SVGIcons2SVGFontStreamOptions = {
  fontName: string;
  fontId: string;
  fixedWidth: boolean;
  descent: number;
  ascent?: number;
  round: number;
  metadata: string;
  usePathBounds: boolean;
  normalize?: boolean;
  preserveAspectRatio?: boolean;
  centerHorizontally?: boolean;
  centerVertically?: boolean;
  fontWeight?: number;
  fontHeight?: number;
  fontStyle?: string;
  callback?: (glyphs: Glyph[]) => void;
};

export type Glyph = {
  name: string;
  width: number;
  height: number;
  defaultHeight?: number;
  defaultWidth?: number;
  unicode: string[];
  paths?: SVGPathData[];
};

export class SVGIcons2SVGFontStream extends Transform {
  private _options: SVGIcons2SVGFontStreamOptions;
  glyphs: Glyph[];

  constructor(options: Partial<SVGIcons2SVGFontStreamOptions>) {
    super({ objectMode: true });

    this.glyphs = [];

    this._options = {
      ...options,
      fontName: options.fontName || 'iconfont',
      fontId: options.fontId || options.fontName || 'iconfont',
      fixedWidth: options.fixedWidth || false,
      descent: options.descent || 0,
      round: options.round || 10e12,
      metadata: options.metadata || '',
      usePathBounds: options.usePathBounds || false,
    };
  }

  _transform(svgIconStream, _unused, svgIconStreamCallback) {
    // Parsing each icons asynchronously
    const saxStream = Sax.createStream(true);
    const parents: (Sax.Tag | Sax.QualifiedTag)[] = [];
    const transformStack: Matrix[] = [];

    function applyTransform(d) {
      const last = transformStack[transformStack.length - 1];
      if (!last) return new SVGPathData(d);
      return new SVGPathData(d).matrix(
        last.a,
        last.b,
        last.c,
        last.d,
        last.e,
        last.f,
      );
    }

    const glyph = svgIconStream.metadata || {};

    // init width and height os they aren't undefined if <svg> isn't renderable
    glyph.width = 0;
    glyph.height = 1;

    glyph.paths = [];
    this.glyphs.push(glyph);

    if ('string' !== typeof glyph.name) {
      this.emit(
        'error',
        new Error(
          `Please provide a name for the glyph at index ${
            this.glyphs.length - 1
          }`,
        ),
      );
    }
    if (
      this.glyphs.some(
        (anotherGlyph) =>
          anotherGlyph !== glyph && anotherGlyph.name === glyph.name,
      )
    ) {
      this.emit(
        'error',
        new Error(`The glyph name "${glyph.name}" must be unique.`),
      );
    }
    if (
      glyph.unicode &&
      glyph.unicode instanceof Array &&
      glyph.unicode.length
    ) {
      if (
        glyph.unicode.some((unicodeA, i) =>
          glyph.unicode.some((unicodeB, j) => i !== j && unicodeA === unicodeB),
        )
      ) {
        this.emit(
          'error',
          new Error(
            `Given codepoints for the glyph "${glyph.name}" contain duplicates.`,
          ),
        );
      }
    } else if ('string' !== typeof glyph.unicode) {
      this.emit(
        'error',
        new Error(`Please provide a codepoint for the glyph "${glyph.name}"`),
      );
    }

    if (
      this.glyphs.some(
        (anotherGlyph) =>
          anotherGlyph !== glyph && anotherGlyph.unicode === glyph.unicode,
      )
    ) {
      this.emit(
        'error',
        new Error(
          `The glyph "${glyph.name}" codepoint seems to be used already elsewhere.`,
        ),
      );
    }

    saxStream.on('opentag', (tag) => {
      let values;
      let color;

      parents.push(tag);

      try {
        const currentTransform = transformStack[transformStack.length - 1];

        if ('undefined' !== typeof tag.attributes.transform) {
          const transform = matrixFromTransformAttribute(
            tag.attributes.transform,
          );
          transformStack.push(
            compose([currentTransform, transform].filter(Boolean)),
          );
        } else {
          transformStack.push(currentTransform);
        }
        // Checking if any parent rendering is disabled and exit if so
        if (!tagShouldRender(tag, parents)) {
          return;
        }

        // Save the view size
        if ('svg' === tag.name) {
          if ('viewBox' in tag.attributes) {
            values = (tag.attributes.viewBox as string).split(
              /\s*,*\s|\s,*\s*|,/,
            );
            const dX = parseFloat(values[0]);
            const dY = parseFloat(values[1]);
            const width = parseFloat(values[2]);
            const height = parseFloat(values[3]);

            // use the viewBox width/height if not specified explictly
            glyph.width =
              'width' in tag.attributes
                ? parseFloat(tag.attributes.width as string)
                : width;
            glyph.height =
              'height' in tag.attributes
                ? parseFloat(tag.attributes.height as string)
                : height;

            transformStack[transformStack.length - 1] = compose(
              [
                transformStack[transformStack.length - 1],
                translate(-dX, -dY),
                scale(glyph.width / width, glyph.height / height),
              ].filter(Boolean),
            );
          } else {
            if ('width' in tag.attributes) {
              glyph.width = parseFloat(tag.attributes.width as string);
            } else {
              warn(
                `⚠️ - Glyph "${glyph.name}" has no width attribute, using current glyph horizontal bounds.`,
              );
              glyph.defaultWidth = true;
            }
            if ('height' in tag.attributes) {
              glyph.height = parseFloat(tag.attributes.height as string);
            } else {
              warn(
                `⚠️ - Glyph "${glyph.name}" has no height attribute, using current glyph vertical bounds.`,
              );
              glyph.defaultHeight = true;
            }
          }
        } else if ('clipPath' === tag.name) {
          // Clipping path unsupported
          warn(
            `🤷 - Found a clipPath element in the icon "${glyph.name}" the result may be different than expected.`,
          );
        } else if ('rect' === tag.name && 'none' !== tag.attributes.fill) {
          glyph.paths.push(
            applyTransform(svgShapesToPath.rectToPath(tag.attributes)),
          );
        } else if ('line' === tag.name && 'none' !== tag.attributes.fill) {
          warn(
            `🤷 - Found a line element in the icon "${glyph.name}" the result could be different than expected.`,
          );
          glyph.paths.push(
            applyTransform(svgShapesToPath.lineToPath(tag.attributes)),
          );
        } else if ('polyline' === tag.name && 'none' !== tag.attributes.fill) {
          warn(
            `🤷 - Found a polyline element in the icon "${glyph.name}" the result could be different than expected.`,
          );
          glyph.paths.push(
            applyTransform(svgShapesToPath.polylineToPath(tag.attributes)),
          );
        } else if ('polygon' === tag.name && 'none' !== tag.attributes.fill) {
          glyph.paths.push(
            applyTransform(svgShapesToPath.polygonToPath(tag.attributes)),
          );
        } else if (
          ['circle', 'ellipse'].includes(tag.name) &&
          'none' !== tag.attributes.fill
        ) {
          glyph.paths.push(
            applyTransform(svgShapesToPath.circleToPath(tag.attributes)),
          );
        } else if (
          'path' === tag.name &&
          tag.attributes.d &&
          'none' !== tag.attributes.fill
        ) {
          glyph.paths.push(applyTransform(tag.attributes.d));
        }

        // According to http://www.w3.org/TR/SVG/painting.html#SpecifyingPaint
        // Map attribute fill to color property
        if ('none' !== tag.attributes.fill) {
          color = getTagColor(tag, parents);
          if ('undefined' !== typeof color) {
            glyph.color = color;
          }
        }
      } catch (err) {
        this.emit(
          'error',
          new Error(
            `Got an error parsing the glyph "${glyph.name}": ${(err as Error)?.message}.`,
          ),
        );
      }
    });

    saxStream.on('error', (err) => {
      this.emit('error', err);
    });

    saxStream.on('closetag', () => {
      transformStack.pop();
      parents.pop();
    });

    saxStream.on('end', () => {
      svgIconStreamCallback();
    });

    svgIconStream.pipe(saxStream);
  }

  _flush(svgFontFlushCallback) {
    this.glyphs.forEach((glyph) => {
      if (
        glyph.defaultHeight ||
        glyph.defaultWidth ||
        this._options.usePathBounds
      ) {
        const glyphPath = new SVGPathData('');
        (glyph.paths || []).forEach((path) => {
          glyphPath.commands.push(...path.commands);
        });
        const bounds = glyphPath.getBounds();

        if (glyph.defaultHeight || this._options.usePathBounds) {
          glyph.height = bounds.maxY - bounds.minY;
        }
        if (glyph.defaultWidth || this._options.usePathBounds) {
          glyph.width = bounds.maxX - bounds.minX;
        }
      }
    });

    const maxGlyphHeight = this.glyphs.reduce(
      (curMax, glyph) => Math.max(curMax, glyph.height),
      0,
    );
    const maxGlyphWidth = this.glyphs.reduce(
      (curMax, glyph) => Math.max(curMax, glyph.width),
      0,
    );
    const fontHeight = this._options.fontHeight || maxGlyphHeight;
    let fontWidth = maxGlyphWidth;

    if (this._options.normalize) {
      fontWidth = this.glyphs.reduce(
        (curMax, glyph) =>
          Math.max(curMax, (fontHeight / glyph.height) * glyph.width),
        0,
      );
    } else if (this._options.fontHeight) {
      // even if normalize is off, we need to scale the fontWidth if we have a custom fontHeight
      fontWidth *= fontHeight / maxGlyphHeight;
    }

    this._options.ascent =
      'undefined' !== typeof this._options.ascent
        ? this._options.ascent
        : fontHeight - this._options.descent;

    if (
      !this._options.normalize &&
      fontHeight >
        (1 < this.glyphs.length
          ? this.glyphs.reduce(
              (curMin, glyph) => Math.min(curMin, glyph.height),
              Infinity,
            )
          : this.glyphs[0].height)
    ) {
      warn(
        '🤷 - The provided icons do not have the same heights. This could lead' +
          ' to unexpected results. Using the normalize option may help.',
      );
    }
    if (1000 > fontHeight) {
      warn(
        '🤷 - A fontHeight of at least than 1000 is recommended, otherwise ' +
          'further steps (rounding in svg2ttf) could lead to ugly results.' +
          ' Use the fontHeight option to scale icons.',
      );
    }

    // Output the SVG file
    // (find a SAX parser that allows modifying SVG on the fly)
    this.push(
      '<?xml version="1.0" standalone="no"?>\n' +
        '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >\n' +
        '<svg xmlns="http://www.w3.org/2000/svg">\n' +
        (this._options.metadata
          ? '<metadata>' + this._options.metadata + '</metadata>\n'
          : '') +
        '<defs>\n' +
        '  <font id="' +
        this._options.fontId +
        '" horiz-adv-x="' +
        fontWidth +
        '">\n' +
        '    <font-face font-family="' +
        this._options.fontName +
        '"\n' +
        '      units-per-em="' +
        fontHeight +
        '" ascent="' +
        this._options.ascent +
        '"\n' +
        '      descent="' +
        this._options.descent +
        '"' +
        (this._options.fontWeight
          ? '\n      font-weight="' + this._options.fontWeight + '"'
          : '') +
        (this._options.fontStyle
          ? '\n      font-style="' + this._options.fontStyle + '"'
          : '') +
        ' />\n' +
        '    <missing-glyph horiz-adv-x="0" />\n',
    );

    this.glyphs.forEach((glyph) => {
      const ratio = this._options.normalize
        ? fontHeight /
          (this._options.preserveAspectRatio && glyph.width > glyph.height
            ? glyph.width
            : glyph.height)
        : fontHeight / maxGlyphHeight;

      if (!isFinite(ratio)) {
        throw new YError('E_BAD_COMPUTED_RATIO', ratio);
      }

      glyph.width *= ratio;
      glyph.height *= ratio;
      const glyphPath = new SVGPathData('');

      if (this._options.fixedWidth) {
        glyph.width = fontWidth;
      }
      const yOffset = glyph.height - this._options.descent;
      let glyphPathTransform: Matrix = {
        a: 1,
        b: 0,
        c: 0,
        d: -1,
        e: 0,
        f: yOffset,
      }; // ySymmetry
      if (1 !== ratio) {
        glyphPathTransform = compose(glyphPathTransform, scale(ratio, ratio));
      }
      (glyph.paths || []).forEach((path) => {
        glyphPath.commands.push(
          ...path
            .toAbs()
            .matrix(
              glyphPathTransform.a,
              glyphPathTransform.b,
              glyphPathTransform.c,
              glyphPathTransform.d,
              glyphPathTransform.e,
              glyphPathTransform.f,
            ).commands,
        );
      });
      const bounds =
        (this._options.centerHorizontally || this._options.centerVertically) &&
        glyphPath.getBounds();
      if (this._options.centerHorizontally && bounds && 'maxX' in bounds) {
        glyphPath.translate(
          (glyph.width - (bounds.maxX - bounds.minX)) / 2 - bounds.minX,
        );
      }
      if (this._options.centerVertically && bounds && 'maxX' in bounds) {
        glyphPath.translate(
          0,
          (fontHeight - (bounds.maxY - bounds.minY)) / 2 -
            bounds.minY -
            this._options.descent,
        );
      }
      delete glyph.paths;
      const d = glyphPath.round(this._options.round).encode();
      glyph.unicode.forEach((unicode, i) => {
        const unicodeStr = [...unicode]
          .map(
            (char) =>
              '&#x' + char.codePointAt(0)!.toString(16).toUpperCase() + ';',
          )
          .join('');

        this.push(
          '    <glyph glyph-name="' +
            glyph.name +
            (0 === i ? '' : '-' + i) +
            '"\n' +
            '      unicode="' +
            unicodeStr +
            '"\n' +
            '      horiz-adv-x="' +
            glyph.width +
            '" d="' +
            d +
            '" />\n',
        );
      });
    });
    this.push('  </font>\n' + '</defs>\n' + '</svg>\n');
    warn('✅ - Font created');
    if ('function' === typeof this._options.callback) {
      this._options.callback(this.glyphs);
    }
    svgFontFlushCallback();
  }
}
