// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { readFile, copyFile, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { assert } from './utils/assert.mjs';

const DESIGN_ASSETS_PATH = process.env.DESIGN_ASSETS_PATH;
assert(DESIGN_ASSETS_PATH != null, 'Missing DESIGN_ASSETS_PATH');

const DESKTOP_ROOT_DIR = join(import.meta.dirname, '..');
const DESIGN_ROOT_DIR = join(DESKTOP_ROOT_DIR, DESIGN_ASSETS_PATH);

const DESIGN_SYMBOLS_FONT_PATH = join(
  DESIGN_ROOT_DIR,
  'Signal Symbols',
  'Font',
  'SignalSymbolsVariable.woff2'
);

const DESKTOP_SYMBOLS_FONT_PATH = join(
  DESKTOP_ROOT_DIR,
  'fonts',
  'signal-symbols',
  'SignalSymbolsVariable.woff2'
);

const DESIGN_SYMBOLS_JSON_PATH = join(
  DESIGN_ROOT_DIR,
  'Signal Symbols',
  'Font',
  'symbols.json'
);

const DESKTOP_SYMBOLS_DEFS_PATH = join(
  DESKTOP_ROOT_DIR,
  'ts',
  'axo',
  '_internal',
  'AxoSymbolDefs.generated.std.ts'
);

/**
 * @typedef {Readonly<{ name: string; unicode: string }>} Glyph
 * @typedef {ReadonlyArray<Glyph>} Glyphs
 * @typedef {string | { ltr: string; rtl: string }} AxoSymbolBidi
 * @typedef {{ icon: AxoSymbolBidi | null; inline: AxoSymbolBidi | null }} AxoSymbolValue
 * @typedef {Readonly<{ name: string; value: AxoSymbolValue }>} AxoSymbol
 */

async function copyVariableFontFile() {
  console.log('Copying variable font file...');
  await copyFile(DESIGN_SYMBOLS_FONT_PATH, DESKTOP_SYMBOLS_FONT_PATH);
}

async function generateAxoSymbolDefs() {
  console.log('Generating axo symbol defs...');

  const text = await readFile(DESIGN_SYMBOLS_JSON_PATH, 'utf-8');

  /** @type {Glyphs} */
  const glyphs = JSON.parse(text);
  /** @type {Map<string, Glyph>} */
  const glyphsByName = new Map();
  for (const glyph of glyphs) {
    glyphsByName.set(glyph.name, glyph);
  }

  /**
   * @param {string} name
   * @returns {boolean}
   */
  function isLeftOnly(name) {
    return name.includes('left') && !name.includes('right');
  }

  /**
   * @param {string} name
   * @returns {boolean}
   */
  function isRightOnly(name) {
    return name.includes('right') && !name.includes('left');
  }

  /** @type {Array<AxoSymbol>} */
  const symbols = [];

  /**
   * @param {string} name
   * @returns {string}
   */
  function stripInline(name) {
    return name.replace(/-inline$/, '');
  }

  /**
   * @param {Glyph} glyph
   * @returns {string}
   */
  function getSymbolName(glyph) {
    const base = stripInline(glyph.name);
    if (isLeftOnly(glyph.name)) {
      return base.replace('left', '[start]');
    }
    if (isRightOnly(glyph.name)) {
      return base.replace('right', '[end]');
    }
    return base;
  }

  /**
   * @param {Glyph} glyph
   * @returns {AxoSymbolBidi}
   */
  function getSymbolBidi(glyph) {
    /** @type {string | null} */
    let otherName = null;
    if (isLeftOnly(glyph.name)) {
      otherName = glyph.name.replace('left', 'right');
    }
    if (isRightOnly(glyph.name)) {
      otherName = glyph.name.replace('right', 'left');
    }
    if (otherName != null) {
      const other = glyphsByName.get(otherName);
      if (other == null) {
        throw new Error(`Missing glyph ${otherName} for ${glyph.name}`);
      }
      return { ltr: glyph.unicode, rtl: other.unicode };
    }
    return glyph.unicode;
  }

  /** @type {Set<string>} */
  const seen = new Set();

  /**
   * @param {Glyph} glyph
   * @returns {AxoSymbolValue}
   */
  function getSymbolValue(glyph) {
    /** @type {Glyph | null} */
    let icon;
    /** @type {Glyph | null} */
    let inline;
    if (glyph.name.endsWith('-inline')) {
      icon = glyphsByName.get(stripInline(glyph.name)) ?? null;
      inline = glyph;
    } else {
      icon = glyph;
      inline = glyphsByName.get(`${glyph.name}-inline`) ?? null;
    }

    if (icon != null) {
      seen.add(icon.name);
    }
    if (inline != null) {
      seen.add(inline.name);
    }

    return {
      icon: icon != null ? getSymbolBidi(icon) : null,
      inline: inline != null ? getSymbolBidi(inline) : null,
    };
  }

  for (const glyph of glyphs) {
    if (seen.has(glyph.name)) {
      continue;
    }

    symbols.push({
      name: getSymbolName(glyph),
      value: getSymbolValue(glyph),
    });
  }

  let res = '';
  res += '// Copyright 2025 Signal Messenger, LLC\n';
  res += '// SPDX-License-Identifier: AGPL-3.0-only\n';
  res += '\n';
  res +=
    '// WARNING: This file is automatically generated, do not edit directly\n';
  {
    res += '\n';
    res += '// Can be used in <AxoSymbol.Icon>\n';
    res += 'export type AxoSymbolIconName =\n';
    const lines = [];
    for (const symbol of symbols) {
      if (symbol.value.icon != null) {
        lines.push(`  | '${symbol.name}'`);
      }
    }
    res += lines.join('\n');
    res += ';\n';
  }
  {
    res += '\n';
    res += '// Symbols that can only be used in <AxoSymbol.InlineGlyph>\n';
    res += 'type AxoSymbolInlineGlyphOnlyName =\n';
    const lines = [];
    for (const symbol of symbols) {
      if (symbol.value.icon == null && symbol.value.inline != null) {
        lines.push(`  | '${symbol.name}'`);
      }
    }
    res += lines.join('\n');
    res += ';\n';
  }
  {
    res += '\n';
    res +=
      '// Symbols with an inline-specific glyph that override the icon glyph\n';
    res += 'type AxoSymbolInlineGlyphOverrideName =\n';
    const lines = [];
    for (const symbol of symbols) {
      if (symbol.value.icon != null && symbol.value.inline != null) {
        lines.push(`  | '${symbol.name}'`);
      }
    }
    res += lines.join('\n');
    res += ';\n';
  }
  res += '\n';
  res += '// Symbols that can be used in <AxoSymbol.InlineGlyph>\n';
  res += 'export type AxoSymbolInlineGlyphName =\n';
  res += '  | AxoSymbolIconName\n';
  res += '  | AxoSymbolInlineGlyphOnlyName;\n';
  res += '\n';
  res += 'type SymbolDef = string | { ltr: string; rtl: string };\n';
  res += 'type IconDefsName = AxoSymbolIconName;\n';
  res += 'type InlineDefsName =\n';
  res += '  | AxoSymbolInlineGlyphOnlyName\n';
  res += '  | AxoSymbolInlineGlyphOverrideName;\n';
  res += '\n';
  res += 'const IconDefs: Record<string, SymbolDef> = {\n';
  for (const symbol of symbols) {
    if (symbol.value.icon != null) {
      res += '  ';
      if (/^[a-z_]+$/.test(symbol.name)) {
        res += symbol.name;
      } else {
        res += `'${symbol.name}'`;
      }
      res += ': ';
      if (typeof symbol.value.icon === 'string') {
        res += `'\\u{${symbol.value.icon}}'`;
      } else {
        res += '{ ';
        res += `ltr: '\\u{${symbol.value.icon.ltr}}', `;
        res += `rtl: '\\u{${symbol.value.icon.rtl}}'`;
        res += ' }';
      }
      res += ',\n';
    }
  }
  res += '} satisfies Record<IconDefsName, SymbolDef>;\n';

  res += '\n';
  res += 'const InlineDefs: Record<string, SymbolDef> = {\n';
  for (const symbol of symbols) {
    if (symbol.value.inline != null) {
      res += '  ';
      if (/^[a-z_]+$/.test(symbol.name)) {
        res += symbol.name;
      } else {
        res += `'${symbol.name}'`;
      }
      res += ': ';
      if (typeof symbol.value.inline === 'string') {
        res += `'\\u{${symbol.value.inline}}'`;
      } else {
        res += '{ ';
        res += `ltr: '\\u{${symbol.value.inline.ltr}}', `;
        res += `rtl: '\\u{${symbol.value.inline.rtl}}'`;
        res += ' }';
      }
      res += ',\n';
    }
  }
  res += '} satisfies Record<InlineDefsName, SymbolDef>;\n';
  res += '\n';
  res += '/** @testexport */\n';
  res +=
    'export function _getAllAxoSymbolIconNames(): ReadonlyArray<AxoSymbolIconName> {\n';
  res += '  return Object.keys(IconDefs) as Array<AxoSymbolIconName>;\n';
  res += '}\n';
  res += '\n';
  res += '/** @testexport */\n';
  res +=
    'export function _getAllAxoSymbolInlineGlyphNames(): ReadonlyArray<AxoSymbolInlineGlyphName> {\n';
  res += '  return Object.keys(IconDefs) as Array<AxoSymbolIconName>;\n';
  res += '}\n';
  res += '\n';
  res += 'export function getAxoSymbolIcon(\n';
  res += '  name: AxoSymbolIconName,\n';
  res += "  dir: 'ltr' | 'rtl'\n";
  res += '): string {\n';
  res += '  const value = IconDefs[name];\n';
  res += '  if (value == null) {\n';
  res +=
    // eslint-disable-next-line no-template-curly-in-string
    '    throw new TypeError(`Invalid symbol name for icon: ${name}`);\n';
  res += '  }\n';
  res += "  return typeof value === 'string' ? value : value[dir];\n";
  res += '}\n';
  res += '\n';
  res += 'export function getAxoSymbolInlineGlyph(\n';
  res += '  name: AxoSymbolInlineGlyphName,\n';
  res += "  dir: 'ltr' | 'rtl'\n";
  res += '): string {\n';
  res += '  const value = InlineDefs[name] ?? IconDefs[name];\n';
  res += '  if (value == null) {\n';
  res +=
    // eslint-disable-next-line no-template-curly-in-string
    '    throw new TypeError(`Invalid symbol name for inline glyph: ${name}`);\n';
  res += '  }\n';
  res += "  return typeof value === 'string' ? value : value[dir];\n";
  res += '}\n';

  await writeFile(DESKTOP_SYMBOLS_DEFS_PATH, res, 'utf-8');
}

await Promise.all([copyVariableFontFile(), generateAxoSymbolDefs()]);
