import { NodePath, PluginObj, types as t } from '@babel/core';
import { Config as SVGRConfig, transform } from '@svgr/core';
import chalk from 'chalk';
import { promises as fs } from 'fs';
import { globby } from 'globby';
import lodash from 'lodash';
import path from 'path';
import prettier, { resolveConfig } from 'prettier';

import {
  getAttribute,
  hasAttribute,
  removeAttribute,
  setAttribute,
} from './babel-utils';

const { camelCase, upperFirst } = lodash;

interface ProcessSvgStringOptions {
  log?: typeof console.log;
  warn?: typeof console.warn;
  error?: typeof console.error;
  inputFilepath?: string;
  outputFilepath?: string;
  fileName?: string;
  componentName?: string;
  svgoConfig?: SVGRConfig['svgoConfig'];
  prettierConfig?: SVGRConfig['prettierConfig'];
  generationDate?: Date;
  headerComment?: string | ((generationDate?: Date) => string);
}

const IS_DEV = process.env.NODE_ENV === 'development';

const CLEAN_OUTPUT_DIRECTORY = true;

/**
 * Generates a build path given a path that is relative to the repository root
 */
function getBuildPath(...rootRelativePath: string[]) {
  const relativePath = path.relative(
    import.meta.dirname,
    path.resolve(import.meta.dirname, '..', ...rootRelativePath)
  );
  return relativePath.endsWith(path.sep)
    ? relativePath
    : `${relativePath}${path.sep}`;
}

function getRelativeImportPath(relativePath: string) {
  // Remove the extension
  const importPath = path.join(
    path.dirname(relativePath),
    path.basename(relativePath, path.extname(relativePath))
  );
  // Prepend `./` if necessary
  return importPath.startsWith(path.sep) || importPath.startsWith('.')
    ? importPath
    : `./${importPath}`;
}

// style-dictionary directory
const RESOLVED_BASE_PATH = path.resolve(import.meta.dirname, '..');

// Relative to style-dictionary
const ICON_PATH = './assets/icon';
const OUTPUT_PATH = IS_DEV
  ? './build/web/icons/'
  : getBuildPath('ui/app-ui/src/icons/generated');
const PRETTIER_CONFIG_PATH = getBuildPath('ui/app-ui/.prettierrc');

// Default configurations
const svgoConfig: SVGRConfig['svgoConfig'] = {
  plugins: [
    {
      name: 'preset-default',
      params: {
        overrides: {
          // See https://svgo.dev/docs/preset-default/
          removeViewBox: false,
          collapseGroups: false,
        },
      },
    },
    {
      name: 'ensureRootGroup',
      fn: () => ({
        element: {
          enter: (node) => {
            if (node.name === 'svg') {
              // Check if we have a single root group element already
              const hasDirectGroup =
                node.children.length === 1 &&
                node.children[0].type === 'element' &&
                node.children[0].name === 'g';
              // Create a single group to contain the children
              if (!hasDirectGroup && node.children.length > 0) {
                node.children = [
                  {
                    type: 'element',
                    name: 'g',
                    attributes: {},
                    children: node.children,
                  },
                ];
              }
            }
          },
        },
      }),
    },
  ],
};

const prettierConfig = (await resolveConfig(PRETTIER_CONFIG_PATH)) || undefined;

// Utilities
function generateHeaderComment(date = new Date()) {
  return [
    'Do not edit directly, this file was auto-generated.',
    date && `Generated on ${date.toUTCString()}`,
  ]
    .filter((line) => line)
    .join('\n');
}

async function processSvgString(
  svgContent: string,
  options?: ProcessSvgStringOptions
) {
  const {
    log = console.log,
    warn = console.warn,
    error = console.error,
    componentName = 'SvgIcon',
    inputFilepath,
    outputFilepath = `${componentName}.tsx`,
    svgoConfig,
    prettierConfig,
    generationDate,
    headerComment = generateHeaderComment,
  } = options || {};

  log(chalk.gray(`Transforming with ${chalk.magenta('SVGR')}...`));
  let outputContent = '';
  try {
    outputContent = await transform(
      svgContent,
      {
        icon: true,
        plugins: ['@svgr/plugin-svgo', '@svgr/plugin-jsx'],
        svgoConfig,
        prettierConfig,
        jsxRuntime: 'automatic',
        jsx: {
          babelConfig: {
            plugins: [
              function flagBadPatternsPlugin(): PluginObj<any> {
                return {
                  visitor: {
                    JSXOpeningElement(path) {
                      if (t.isJSXIdentifier(path.node.name, { name: 'defs' })) {
                        throw new Error(
                          '<defs> is not allowed. You may need to flatten a clipping mask or remove effects/gradients.'
                        );
                      }
                    },
                  },
                };
              },
              function removeFillPlugin(): PluginObj<any> {
                return {
                  visitor: {
                    Program(path) {
                      const commentText =
                        typeof headerComment === 'function'
                          ? headerComment(generationDate || new Date())
                          : headerComment;
                      if (commentText) {
                        path.addComment(
                          'leading',
                          '*\n' +
                            commentText
                              .split('\n')
                              .map((line) => ` * ${line}\n`)
                              .join(''),
                          false
                        );
                      }
                    },
                  },
                };
              },
              function removeFillPlugin(): PluginObj<any> {
                return {
                  visitor: {
                    VariableDeclarator(path: NodePath<t.VariableDeclarator>) {
                      if (
                        t.isIdentifier(path.node.id, { name: componentName })
                      ) {
                        path.node.id.typeAnnotation = t.tsTypeAnnotation(
                          t.tsTypeReference(
                            t.tsQualifiedName(
                              t.identifier('React'),
                              t.identifier('FC')
                            ),
                            t.tsTypeParameterInstantiation([
                              t.tsTypeReference(
                                t.tsQualifiedName(
                                  t.identifier('React'),
                                  t.identifier('SVGProps')
                                ),
                                t.tsTypeParameterInstantiation([
                                  t.tsTypeReference(
                                    t.identifier('SVGSVGElement')
                                  ),
                                ])
                              ),
                            ])
                          )
                        );
                      }
                    },
                    JSXOpeningElement(path: NodePath<t.JSXOpeningElement>) {
                      if (t.isJSXIdentifier(path.node.name, { name: 'svg' })) {
                        setAttribute(path.node, 'fill', 'currentColor');
                      } else if (
                        hasAttribute(path.node, 'fill') &&
                        !t.isStringLiteral(
                          getAttribute(path.node, 'fill')?.value,
                          { value: 'none' }
                        )
                      ) {
                        removeAttribute(path.node, 'fill');
                      }
                    },
                  },
                };
              },
            ],
          },
        },
      },
      { componentName: componentName }
    );
  } catch (e) {
    error(
      chalk.red(
        inputFilepath
          ? `SVGR failed on ${path.relative(RESOLVED_BASE_PATH, inputFilepath)}`
          : `SVGR failed`
      )
    );
    if (e instanceof Error) {
      error(chalk.redBright(e.message.replace(/^[^:]+:\s+/, '')));
    }
    return '';
  }

  // Running prettier separate from SVGR because of unexpected behavior when formatting
  log(chalk.gray(`Formatting with ${chalk.magenta('Prettier')}...`));
  try {
    outputContent = await prettier.format(outputContent, prettierConfig);
  } catch (e) {
    warn(
      chalk.yellow(
        `Prettier failed on ${path.relative(RESOLVED_BASE_PATH, outputFilepath)}`
      )
    );
    warn(e);
  }

  return outputContent;
}

// Give all generated icons the same timestamp
const generationDate = new Date();

// Glob of all icon SVGs
console.log(chalk.bold(`Scanning for SVGs in ${chalk.cyan(ICON_PATH)}`));
const iconSvgs = await globby(
  path.join(RESOLVED_BASE_PATH, ICON_PATH, '*.svg')
);

// Remove existing files in output directory and start fresh
if (CLEAN_OUTPUT_DIRECTORY) {
  const outputDirectory = path.join(RESOLVED_BASE_PATH, OUTPUT_PATH);
  try {
    const items = await fs.readdir(outputDirectory);
    for (const item of items) {
      const itemPath = path.join(outputDirectory, item);
      // Skip anything that is not a TSX file
      const stats = await fs.stat(itemPath);
      if (path.extname(itemPath) !== '.tsx' || stats.isDirectory()) {
        continue;
      }
      // Delete the file
      try {
        await fs.unlink(itemPath);
      } catch (error) {
        console.error(
          `Error removing ${chalk.cyan(path.relative(RESOLVED_BASE_PATH, itemPath))}`
        );
      }
    }
  } catch (e) {
    console.error(
      `Error clearing directory ${chalk.cyan(path.relative(RESOLVED_BASE_PATH, outputDirectory))}`
    );
  }
}

// Process each file
const errors: [inputFilepath: string, outputFilepath: string][] = [];
const icons: [componentName: string, relativePath: string][] = [];
for (const inputFilepath of iconSvgs) {
  const name = upperFirst(
    camelCase(path.basename(inputFilepath, path.extname(inputFilepath)))
  );
  const componentName = `${name}Icon`;
  const outputFilepath = path.join(
    RESOLVED_BASE_PATH,
    OUTPUT_PATH,
    `${componentName}.tsx`
  );

  console.log(
    chalk.gray(
      `Reading from ${chalk.cyan(path.relative(RESOLVED_BASE_PATH, inputFilepath))}`
    )
  );

  // Read SVG content
  const svgContent = await fs.readFile(inputFilepath, 'utf8');

  // Run through SVGR and prettier
  const outputContent = await processSvgString(svgContent, {
    inputFilepath,
    outputFilepath,
    componentName,
    log: console.log,
    warn: console.warn,
    error: console.error,
    svgoConfig,
    prettierConfig: { ...prettierConfig, filepath: outputFilepath },
    generationDate,
  });

  if (!outputContent) {
    console.error(
      chalk.bold.red(
        `No content to write to ${path.relative(RESOLVED_BASE_PATH, outputFilepath)}`
      )
    );
    errors.push([inputFilepath, outputFilepath]);
    continue;
  }

  try {
    // Ensure we have the output directory
    await fs.mkdir(path.dirname(outputFilepath), { recursive: true });
    // Write content
    await fs.writeFile(outputFilepath, outputContent);
    const relativePath = path.relative(
      path.join(RESOLVED_BASE_PATH, OUTPUT_PATH),
      outputFilepath
    );
    icons.push([componentName, getRelativeImportPath(relativePath)]);
    console.log(
      chalk.bold(
        `Wrote ${chalk.white(componentName)} to ${chalk.cyan(path.relative(RESOLVED_BASE_PATH, outputFilepath))}`
      )
    );
  } catch (e) {
    console.error(
      `Error writing to ${chalk.cyan(path.relative(RESOLVED_BASE_PATH, outputFilepath))}`
    );
  }
}

console.log(
  chalk.white(`Created component files for ${icons.length} icons! 🌠`)
);

// Create index file
{
  const outputFilepath = path.join(
    RESOLVED_BASE_PATH,
    OUTPUT_PATH,
    'index.tsx'
  );
  const outputContent = [
    '/**',
    generateHeaderComment(generationDate)
      .split('\n')
      .map((line) => ` * ${line}`)
      .join(''),
    ' */',
    icons
      .map(
        ([componentName, relativePath]) =>
          `export { default as ${componentName} } from '${relativePath}';\n`
      )
      .join(''),
  ].join('\n');
  try {
    // Write content
    await fs.writeFile(outputFilepath, outputContent);

    console.log(
      chalk.bold(
        `Wrote index to ${chalk.cyan(path.relative(RESOLVED_BASE_PATH, outputFilepath))}`
      )
    );
  } catch (e) {
    console.error(
      `Error writing to ${chalk.cyan(path.relative(RESOLVED_BASE_PATH, outputFilepath))}`
    );
  }
}

if (errors.length) {
  console.error(
    chalk.bold.redBright(
      `Encountered ${errors.length} ${errors.length === 1 ? 'error' : 'error'} while processing icons:`
    )
  );
  for (const [inputFilepath] of errors) {
    console.error(
      chalk.red(` - ${path.relative(RESOLVED_BASE_PATH, inputFilepath)}`)
    );
  }
  process.exit(1);
}
