import { withHttps, withQuery, resolveURL } from 'ufo';
import { existsSync, readFileSync, copyFileSync, mkdirSync, writeFileSync } from 'node:fs';
import { resolve, dirname, extname, posix } from 'node:path';
import { ofetch } from 'ofetch';
import { Hookable } from 'hookable';
import deepmerge from 'deepmerge';

const GOOGLE_FONTS_DOMAIN = "fonts.googleapis.com";
function isValidDisplay(display) {
  return ["auto", "block", "swap", "fallback", "optional"].includes(display);
}
function parseStyle(style) {
  if (["wght", "normal", "regular"].includes(style.toLowerCase())) {
    return "wght";
  }
  if (["ital", "italic", "i"].includes(style.toLowerCase())) {
    return "ital";
  }
  return style;
}
function cartesianProduct(...a) {
  return a.length < 2 ? a : a.reduce((a2, b) => a2.flatMap((d) => b.map((e) => [d, e].flat())));
}
function parseFamilyName(name) {
  return decodeURIComponent(name).replace(/\+/g, " ");
}

function constructURL({
  families,
  display,
  subsets,
  text
} = {}) {
  const _subsets = (Array.isArray(subsets) ? subsets : [subsets]).filter(Boolean);
  const family = convertFamiliesToArray(families ?? {});
  if (family.length < 1) {
    return false;
  }
  const query = {
    family
  };
  if (display && isValidDisplay(display)) {
    query.display = display;
  }
  if (_subsets.length > 0) {
    query.subset = _subsets.join(",");
  }
  if (text) {
    query.text = text;
  }
  return withHttps(withQuery(resolveURL(GOOGLE_FONTS_DOMAIN, "css2"), query));
}
function convertFamiliesToArray(families) {
  const result = [];
  Object.entries(families).forEach(([name, values]) => {
    if (!name) {
      return;
    }
    name = parseFamilyName(name);
    if (typeof values === "string" && String(values).includes("..")) {
      result.push(`${name}:wght@${values}`);
      return;
    }
    if (Array.isArray(values) && values.length > 0) {
      result.push(`${name}:wght@${values.join(";")}`);
      return;
    }
    if (Object.keys(values).length > 0) {
      const axes = {};
      let italicWeights = [];
      Object.entries(values).sort(([styleA], [styleB]) => styleA.localeCompare(styleB)).forEach(([style, weight]) => {
        const parsedStyle = parseStyle(style);
        if (parsedStyle === "ital") {
          axes[parsedStyle] = ["0", "1"];
          if (weight === true || weight === 400 || weight === 1) {
            italicWeights = ["*"];
          } else {
            italicWeights = Array.isArray(weight) ? weight.map((w) => String(w)) : [weight];
          }
        } else {
          axes[parseStyle(style)] = Array.isArray(weight) ? weight.map((w) => String(w)) : [weight];
        }
      });
      const strictlyItalic = [];
      if (Object.keys(axes).length === 1 && Object.hasOwn(axes, "ital")) {
        if (!(italicWeights.includes("*") || italicWeights.length === 1 && italicWeights.includes("400"))) {
          axes.wght = italicWeights;
          strictlyItalic.push(...italicWeights);
        }
      } else if (Object.hasOwn(axes, "wght") && !italicWeights.includes("*")) {
        strictlyItalic.push(...italicWeights.filter((w) => !axes.wght.includes(w)));
        axes.wght = [.../* @__PURE__ */ new Set([...axes.wght, ...italicWeights])];
      }
      const axisTagList = Object.keys(axes).sort((axisA, axisB) => {
        const isLowerA = axisA[0] === axisA[0].toLowerCase();
        const isLowerB = axisB[0] === axisB[0].toLowerCase();
        if (isLowerA && !isLowerB) {
          return -1;
        }
        if (!isLowerA && isLowerB) {
          return 1;
        }
        return axisA.localeCompare(axisB);
      });
      if (axisTagList.length === 1 && axisTagList.includes("ital")) {
        result.push(`${name}:ital@1`);
        return;
      }
      let axisTupleArrays = cartesianProduct(...axisTagList.map((tag) => axes[tag]), [[]]);
      const italicIndex = axisTagList.findIndex((i) => i === "ital");
      if (italicIndex !== -1) {
        const weightIndex = axisTagList.findIndex((i) => i === "wght");
        if (weightIndex !== -1) {
          axisTupleArrays = axisTupleArrays.filter((axisTuple) => axisTuple[italicIndex] === "0" && !strictlyItalic.includes(axisTuple[weightIndex]) || axisTuple[italicIndex] === "1" && italicWeights.includes(axisTuple[weightIndex]));
        }
      }
      const axisTupleList = axisTupleArrays.sort((axisTupleA, axisTupleB) => {
        for (let i = 0; i < axisTupleA.length; i++) {
          const compareResult = parseInt(axisTupleA[i]) - parseInt(axisTupleB[i]);
          if (compareResult !== 0) {
            return compareResult;
          }
        }
        return 0;
      }).map((axisTuple) => axisTuple.join(",")).join(";");
      result.push(`${name}:${axisTagList.join(",")}@${axisTupleList}`);
      return;
    }
    if (values) {
      result.push(name);
    }
  });
  return result;
}

function isValidURL(url) {
  return RegExp(GOOGLE_FONTS_DOMAIN).test(url);
}

var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => {
  __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
  return value;
};
class Downloader extends Hookable {
  constructor(url, options) {
    super();
    this.url = url;
    __publicField(this, "config");
    this.config = {
      base64: false,
      overwriting: false,
      outputDir: "./",
      stylePath: "fonts.css",
      fontsDir: "fonts",
      fontsPath: "./fonts",
      headers: [["user-agent", [
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
        "AppleWebKit/537.36 (KHTML, like Gecko)",
        "Chrome/98.0.4758.102 Safari/537.36"
      ].join(" ")]],
      ...options
    };
  }
  async execute() {
    if (!isValidURL(this.url)) {
      throw new Error("Invalid Google Fonts URL");
    }
    const { outputDir, stylePath, headers, fontsPath } = this.config;
    const cssPath = resolve(outputDir, stylePath);
    let overwriting = this.config.overwriting;
    if (!overwriting && existsSync(cssPath)) {
      const currentCssContent = readFileSync(cssPath, "utf-8");
      const currentUrl = (currentCssContent.split(/\r?\n/, 1).shift() || "").replace("/*", "").replace("*/", "").trim();
      if (currentUrl === this.url) {
        return false;
      }
      overwriting = true;
    }
    await this.callHook("download:start");
    const { searchParams } = new URL(this.url);
    const subsets = searchParams.get("subset") ? searchParams.get("subset")?.split(",") : void 0;
    await this.callHook("download-css:before", this.url);
    const _css = await ofetch(this.url, { headers });
    const { fonts: fontsFromCss, css: cssContent } = parseFontsFromCss(_css, fontsPath, subsets);
    await this.callHook("download-css:done", this.url, cssContent, fontsFromCss);
    const fonts = await this.downloadFonts(fontsFromCss);
    await this.callHook("write-css:before", cssPath, cssContent, fonts);
    const newContent = this.writeCss(cssPath, `/* ${this.url} */
${cssContent}`, fonts);
    await this.callHook("write-css:done", cssPath, newContent, cssContent);
    await this.callHook("download:complete");
    return true;
  }
  async downloadFonts(fonts) {
    const { headers, base64, outputDir, fontsDir } = this.config;
    const downloadedFonts = [];
    const _fonts = [];
    for (const font of fonts) {
      const downloadedFont = downloadedFonts.find((f) => f.inputFont === font.inputFont);
      if (downloadedFont) {
        if (base64) {
          font.outputText = downloadedFont.outputText;
        } else {
          copyFileSync(
            resolve(outputDir, fontsDir, downloadedFont.outputFont),
            resolve(outputDir, fontsDir, font.outputFont)
          );
        }
        _fonts.push(font);
        continue;
      }
      await this.callHook("download-font:before", font);
      const response = await ofetch.raw(font.inputFont, { headers, responseType: "arrayBuffer" });
      if (!response?._data) {
        _fonts.push(font);
        continue;
      }
      const buffer = Buffer.from(response?._data);
      if (base64) {
        const mime = response.headers.get("content-type") ?? "font/woff2";
        font.outputText = `url('data:${mime};base64,${buffer.toString("base64")}')`;
      } else {
        const fontPath = resolve(outputDir, fontsDir, font.outputFont);
        mkdirSync(dirname(fontPath), { recursive: true });
        writeFileSync(fontPath, buffer);
      }
      _fonts.push(font);
      await this.callHook("download-font:done", font);
      downloadedFonts.push(font);
    }
    return _fonts;
  }
  writeCss(path, content, fonts) {
    for (const font of fonts) {
      content = content.replace(font.inputText, font.outputText);
    }
    mkdirSync(dirname(path), { recursive: true });
    writeFileSync(path, content, "utf-8");
    return content;
  }
}
function parseFontsFromCss(content, fontsPath, subsets) {
  const css = [];
  const fonts = [];
  const re = {
    face: /\s*(?:\/\*\s*(.*?)\s*\*\/)?[^@]*?@font-face\s*{(?:[^}]*?)}\s*/gi,
    family: /font-family\s*:\s*(?:'|")?([^;]*?)(?:'|")?\s*;/i,
    style: /font-style\s*:\s*([^;]*?)\s*;/i,
    weight: /font-weight\s*:\s*([^;]*?)\s*;/i,
    url: /url\s*\(\s*(?:'|")?\s*([^]*?)\s*(?:'|")?\s*\)\s*?/gi
  };
  let match1;
  while ((match1 = re.face.exec(content)) !== null) {
    const [fontface, subset] = match1;
    const familyRegExpArray = re.family.exec(fontface);
    const family = familyRegExpArray ? familyRegExpArray[1] : "";
    const styleRegExpArray = re.style.exec(fontface);
    const style = styleRegExpArray ? styleRegExpArray[1] : "";
    const weightRegExpArray = re.weight.exec(fontface);
    const weight = weightRegExpArray ? weightRegExpArray[1] : "";
    if (subsets && subsets.length && !subsets.includes(subset)) {
      continue;
    }
    css.push(fontface);
    let match2;
    while ((match2 = re.url.exec(fontface)) !== null) {
      const [forReplace, url] = match2;
      const ext = extname(url).replace(/^\./, "") || "woff2";
      const newFilename = formatFontFileName("{family}-{style}-{weight}-{subset}.{ext}", {
        family: family.replace(/\s+/g, "_"),
        style: style.replace(/\s+/g, "_") || "normal",
        weight: weight.replace(/\s+/g, "_") || "",
        subset: subset || "text",
        ext
      }).replace(/\.$/, "");
      fonts.push({
        inputFont: url,
        outputFont: newFilename,
        inputText: forReplace,
        outputText: `url('${posix.join(fontsPath, newFilename)}')`
      });
    }
  }
  return {
    css: css.join("\n"),
    fonts
  };
}
function formatFontFileName(template, values) {
  return Object.entries(values).filter(([key]) => /^[a-z0-9_-]+$/gi.test(key)).map(([key, value]) => [new RegExp(`([^{]|^){${key}}([^}]|$)`, "g"), `$1${value}$2`]).reduce((str, [regexp, replacement]) => str.replace(regexp, String(replacement)), template).replace(/({|}){2}/g, "$1");
}

function download(url, options) {
  return new Downloader(url, options);
}

function merge(...fonts) {
  return deepmerge.all(fonts);
}

function parse(url) {
  const result = {};
  if (!isValidURL(url)) {
    return result;
  }
  const { searchParams, pathname } = new URL(url);
  if (!searchParams.has("family")) {
    return result;
  }
  const families = convertFamiliesObject(searchParams.getAll("family"), pathname.endsWith("2"));
  if (Object.keys(families).length < 1) {
    return result;
  }
  result.families = families;
  const display = searchParams.get("display");
  if (display && isValidDisplay(display)) {
    result.display = display;
  }
  const subsets = searchParams.get("subset");
  if (subsets) {
    result.subsets = subsets.split(",");
  }
  const text = searchParams.get("text");
  if (text) {
    result.text = text;
  }
  return result;
}
function convertFamiliesObject(families, v2 = true) {
  const result = {};
  families.flatMap((family) => family.split("|")).forEach((family) => {
    if (!family) {
      return;
    }
    if (!family.includes(":")) {
      result[family] = true;
      return;
    }
    const parts = family.split(":");
    if (!parts[1]) {
      return;
    }
    const values = {};
    if (!v2) {
      parts[1].split(",").forEach((style) => {
        const styleParsed = parseStyle(style);
        if (styleParsed === "wght") {
          values.wght = true;
        }
        if (styleParsed === "ital") {
          values.ital = true;
        }
        if (styleParsed === "bold" || styleParsed === "b") {
          values.wght = 700;
        }
        if (styleParsed === "bolditalic" || styleParsed === "bi") {
          values.ital = 700;
        }
      });
    }
    if (v2) {
      let [styles, weights] = parts[1].split("@");
      if (!weights) {
        weights = String(styles).replace(",", ";");
        styles = "wght";
      }
      styles.split(",").forEach((style) => {
        const styleParsed = parseStyle(style);
        values[styleParsed] = weights.split(";").map((weight) => {
          if (/^\+?\d+$/.test(weight)) {
            return parseInt(weight);
          }
          const [pos, w] = weight.split(",");
          const index = styleParsed === "wght" ? 0 : 1;
          if (!w) {
            return weight;
          }
          if (parseInt(pos) !== index) {
            return 0;
          }
          if (/^\+?\d+$/.test(w)) {
            return parseInt(w);
          }
          return w;
        }).filter((v) => parseInt(v.toString()) > 0 || v.toString().includes(".."));
        if (!values[styleParsed].length) {
          values[styleParsed] = true;
          return;
        }
        if (values[styleParsed].length > 1) {
          return;
        }
        const first = values[styleParsed][0];
        if (String(first).includes("..")) {
          values[styleParsed] = first;
        }
        if (first === 1 || first === true) {
          values[styleParsed] = true;
        }
      });
    }
    result[parseFamilyName(parts[0])] = values;
  });
  return result;
}

export { Downloader, constructURL, download, isValidURL, merge, parse };
