import fs from "fs";
import path from "path";
import cheerio from "cheerio";
import { getSvgBoundingBox } from "./bounding-box";
import { extractSpecs } from "./specs";
import {
  SvgSymbolData,
  SvgSymbolDef,
  SvgSymbolElement,
  SvgSymbolGradientStop,
} from "./svg-symbol";
import toml from "toml";
import { validateSvgSymbolMetadata } from "./svg-symbol-metadata";
import { withoutNulls } from "./util";
import { clampedBytesToRGBColor } from "./color-util";

const SUPPORTED_SVG_TAG_ARRAY: SvgSymbolElement["tagName"][] = ["g", "path"];
const SUPPORTED_SVG_TAGS = new Set(SUPPORTED_SVG_TAG_ARRAY);

const MY_DIR = __dirname;
export const SVG_SYMBOLS_DIR = path.join(MY_DIR, "..", "assets", "symbols");
const VOCAB_JSON_PATH = path.join(
  MY_DIR,
  "_svg-vocabulary-pretty-printed.json"
);
const VOCAB_TS_PATH = path.join(MY_DIR, "_svg-vocabulary.ts");
const SVG_EXT = ".svg";

function onlyTags(
  elements: cheerio.Element[] | cheerio.Cheerio
): cheerio.TagElement[] {
  const result: cheerio.TagElement[] = [];

  for (let i = 0; i < elements.length; i++) {
    const el = elements[i];
    if (el.type === "tag") {
      result.push(el);
    }
  }

  return result;
}

function isSupportedSvgTag(
  tagName: string
): tagName is SvgSymbolElement["tagName"] {
  return SUPPORTED_SVG_TAGS.has(tagName as any);
}

const SVG_ATTRIB_TO_PROP_MAP: {
  [key: string]: keyof SvgSymbolElement["props"] | undefined;
} = {
  id: "id",
  fill: "fill",
  stroke: "stroke",
  d: "d",
  "stroke-linejoin": "strokeLinejoin",
  "stroke-linecap": "strokeLinecap",
  "stroke-width": "strokeWidth",
  "fill-rule": "fillRule",
};

function attribsToProps(el: cheerio.TagElement): any {
  const { attribs } = el;
  const result: any = {};

  for (let attrib of Object.keys(attribs)) {
    const prop = SVG_ATTRIB_TO_PROP_MAP[attrib];

    if (!prop) {
      throw new Error(`Unknown SVG attribute '${attrib}' in <${el.tagName}>!`);
    }

    result[prop] = attribs[attrib];
  }

  return result;
}

function getNonEmptyAttrib(el: cheerio.TagElement, attr: string): string {
  const result = el.attribs[attr];

  if (!result) {
    throw new Error(
      `Expected <${el.tagName}> to have a non-empty '${attr}' attribute!`
    );
  }

  return result;
}

function parseGradientStops(
  els: cheerio.TagElement["children"]
): SvgSymbolGradientStop[] {
  const stops: SvgSymbolGradientStop[] = [];
  for (let el of els) {
    if (el.type === "tag") {
      if (el.tagName !== "stop") {
        throw new Error(
          `Expected an SVG gradient to only contain <stop> elements!`
        );
      }
      const style = getNonEmptyAttrib(el, "style");
      const color = style.match(/stop-color\:rgb\((\d+),(\d+),(\d+)\)/);
      if (!color) {
        throw new Error(`Expected "${style}" to contain a stop-color!`);
      }
      const rgb = Array.from(color)
        .slice(1)
        .map((value) => parseInt(value));
      stops.push({
        offset: getNonEmptyAttrib(el, "offset"),
        color: clampedBytesToRGBColor(rgb),
      });
    }
  }
  return stops;
}

function parseLinearGradient(el: cheerio.TagElement): SvgSymbolDef {
  return {
    type: "linearGradient",
    id: getNonEmptyAttrib(el, "id"),
    x1: getNonEmptyAttrib(el, "x1"),
    y1: getNonEmptyAttrib(el, "y1"),
    x2: getNonEmptyAttrib(el, "x2"),
    y2: getNonEmptyAttrib(el, "y2"),
    stops: parseGradientStops(el.children),
  };
}

function parseRadialGradient(el: cheerio.TagElement): SvgSymbolDef {
  return {
    type: "radialGradient",
    id: getNonEmptyAttrib(el, "id"),
    cx: getNonEmptyAttrib(el, "cx"),
    cy: getNonEmptyAttrib(el, "cy"),
    r: getNonEmptyAttrib(el, "r"),
    stops: parseGradientStops(el.children),
  };
}

/**
 * Attempt to convert the given SVG element into a `SvgSymbolElement`
 * and/or a list of accompanying `SvgSymbolDef` objects.
 *
 * Note that the latter will be "returned" to the caller via the
 * `defsOutput` argument.
 */
function serializeSvgSymbolElement(
  $: cheerio.Root,
  el: cheerio.TagElement,
  defsOutput: SvgSymbolDef[]
): SvgSymbolElement | null {
  const { tagName } = el;
  if (tagName === "radialGradient") {
    defsOutput.push(parseRadialGradient(el));
    return null;
  } else if (tagName === "linearGradient") {
    defsOutput.push(parseLinearGradient(el));
    return null;
  } else if (!isSupportedSvgTag(tagName)) {
    throw new Error(`Unsupported SVG element: <${tagName}>`);
  }
  let children = withoutNulls(
    onlyTags(el.children).map((child) =>
      serializeSvgSymbolElement($, child, defsOutput)
    )
  );
  return {
    tagName,
    props: attribsToProps(el) as any,
    children,
  };
}

function removeEmptyGroups(s: SvgSymbolElement[]): SvgSymbolElement[] {
  return s
    .filter((child) => !(child.tagName === "g" && child.children.length === 0))
    .map((s) => ({
      ...s,
      children: removeEmptyGroups(s.children),
    }));
}

export function convertSvgMarkupToSymbolData(
  filename: string,
  svgMarkup: string
): SvgSymbolData {
  const name = path.basename(filename, SVG_EXT).toLowerCase();
  const $ = cheerio.load(svgMarkup);
  const svgEl = $("svg");
  const outputDefs: SvgSymbolDef[] = [];
  const rawLayers = removeEmptyGroups(
    withoutNulls(
      onlyTags(svgEl.children()).map((ch) =>
        serializeSvgSymbolElement($, ch, outputDefs)
      )
    )
  );
  const [specs, layers] = extractSpecs(rawLayers);
  const bbox = getSvgBoundingBox(layers);
  const defs = outputDefs.length ? outputDefs : undefined;

  const symbol: SvgSymbolData = {
    name,
    bbox,
    layers,
    specs,
    defs,
  };
  return symbol;
}

export function build() {
  const filenames = fs.readdirSync(SVG_SYMBOLS_DIR);
  const vocab: SvgSymbolData[] = [];
  for (let filename of filenames) {
    if (path.extname(filename) === SVG_EXT) {
      let filenames = filename;
      let metaToml: string | null = null;
      const metaFilename = `${path.basename(filename, SVG_EXT)}.toml`;
      const metaFilepath = path.join(SVG_SYMBOLS_DIR, metaFilename);
      if (fs.existsSync(metaFilepath)) {
        filenames += ` and ${metaFilename}`;
        metaToml = fs.readFileSync(metaFilepath, {
          encoding: "utf-8",
        });
      }
      console.log(`Adding ${filenames} to vocabulary.`);
      const svgMarkup = fs.readFileSync(path.join(SVG_SYMBOLS_DIR, filename), {
        encoding: "utf-8",
      });
      const symbol = convertSvgMarkupToSymbolData(filename, svgMarkup);
      if (metaToml) {
        const { metadata, unknownProperties } = validateSvgSymbolMetadata(
          toml.parse(metaToml)
        );
        symbol.meta = metadata;
        if (unknownProperties.length) {
          console.log(
            `WARNING: Found unknown metadata properties ${unknownProperties.join(
              ", "
            )}.`
          );
        }
      }
      vocab.push(symbol);
    }
  }

  console.log(`Writing ${VOCAB_JSON_PATH} (for debugging output).`);
  fs.writeFileSync(VOCAB_JSON_PATH, JSON.stringify(vocab, null, 2));

  // Ugh, we need to write out a TypeScript file instead of importing
  // the JSON directly because otherwise the TS compiler will spend
  // a huge amount of resources doing type inference, which massively
  // slows down type-checking (especially in IDEs and such).
  console.log(`Writing ${VOCAB_TS_PATH} (for importing into code).`);
  const stringified = JSON.stringify(vocab);
  fs.writeFileSync(
    VOCAB_TS_PATH,
    [
      "// This file is auto-generated, please do not modify it.",
      `import type { SvgSymbolData } from "./svg-symbol";`,
      `const _SvgVocabulary: SvgSymbolData[] = JSON.parse(${JSON.stringify(
        stringified
      )});`,
      `export default _SvgVocabulary;`,
    ].join("\n")
  );
}
