/**
 * The contents of this file are subject to the license and copyright
 * detailed in the LICENSE and NOTICE files at the root of the source
 * tree and available online at
 *
 * http://www.dspace.org/license/
 */

import { TSESTree } from '@typescript-eslint/utils';
import { RuleContext } from '@typescript-eslint/utils/ts-eslint';
import { readFileSync } from 'fs';
import { basename } from 'path';
import ts, { Identifier } from 'typescript';

import {
  getComponentClassName,
  isPartOfViewChild,
} from './angular';
import {
  isPartOfClassDeclaration,
  isPartOfTypeExpression,
} from './typescript';

/**
 * Couples a themeable Component to its ThemedComponent wrapper
 */
export interface ThemeableComponentRegistryEntry {
  basePath: string;
  baseFileName: string,
  baseClass: string;

  wrapperPath: string;
  wrapperFileName: string,
  wrapperClass: string;
}

function isAngularComponentDecorator(node: ts.Node) {
  if (node.kind === ts.SyntaxKind.Decorator && node.parent.kind === ts.SyntaxKind.ClassDeclaration) {
    const decorator = node as ts.Decorator;

    if (decorator.expression.kind === ts.SyntaxKind.CallExpression) {
      const method = decorator.expression as ts.CallExpression;

      if (method.expression.kind === ts.SyntaxKind.Identifier) {
        return (method.expression as Identifier).text === 'Component';
      }
    }
  }

  return false;
}

function findImportDeclaration(source: ts.SourceFile, identifierName: string): ts.ImportDeclaration | undefined {
  return ts.forEachChild(source, (topNode: ts.Node) => {
    if (topNode.kind === ts.SyntaxKind.ImportDeclaration) {
      const importDeclaration = topNode as ts.ImportDeclaration;

      if (importDeclaration.importClause?.namedBindings?.kind === ts.SyntaxKind.NamedImports) {
        const namedImports = importDeclaration.importClause?.namedBindings as ts.NamedImports;

        for (const element of namedImports.elements) {
          if (element.name.text === identifierName) {
            return importDeclaration;
          }
        }
      }
    }

    return undefined;
  });
}

/**
 * Listing of all themeable Components
 */
class ThemeableComponentRegistry {
  public readonly entries: Set<ThemeableComponentRegistryEntry>;
  public readonly byBaseClass: Map<string, ThemeableComponentRegistryEntry>;
  public readonly byWrapperClass: Map<string, ThemeableComponentRegistryEntry>;
  public readonly byBasePath: Map<string, ThemeableComponentRegistryEntry>;
  public readonly byWrapperPath: Map<string, ThemeableComponentRegistryEntry>;

  constructor() {
    this.entries = new Set();
    this.byBaseClass = new Map();
    this.byWrapperClass = new Map();
    this.byBasePath = new Map();
    this.byWrapperPath = new Map();
  }

  public initialize(prefix = '') {
    if (this.entries.size > 0) {
      return;
    }

    function registerWrapper(path: string) {
      const source = getSource(path);

      function traverse(node: ts.Node) {
        if (node.parent !== undefined && isAngularComponentDecorator(node)) {
          const classNode = node.parent as ts.ClassDeclaration;

          if (classNode.name === undefined || classNode.heritageClauses === undefined) {
            return;
          }

          const wrapperClass = classNode.name?.escapedText as string;

          for (const heritageClause of classNode.heritageClauses) {
            for (const type of heritageClause.types) {
              if ((type as any).expression.escapedText === 'ThemedComponent') {
                if (type.kind !== ts.SyntaxKind.ExpressionWithTypeArguments || type.typeArguments === undefined) {
                  continue;
                }

                const firstTypeArg = type.typeArguments[0] as ts.TypeReferenceNode;
                const baseClass = (firstTypeArg.typeName as ts.Identifier)?.escapedText;

                if (baseClass === undefined) {
                  continue;
                }

                const importDeclaration = findImportDeclaration(source, baseClass);

                if (importDeclaration === undefined) {
                  continue;
                }

                const basePath = resolveLocalPath((importDeclaration.moduleSpecifier as ts.StringLiteral).text, path);

                themeableComponents.add({
                  baseClass,
                  basePath: basePath.replace(new RegExp(`^${prefix}`), ''),
                  baseFileName: basename(basePath).replace(/\.ts$/, ''),
                  wrapperClass,
                  wrapperPath: path.replace(new RegExp(`^${prefix}`), ''),
                  wrapperFileName: basename(path).replace(/\.ts$/, ''),
                });
              }
            }
          }

          return;
        } else {
          ts.forEachChild(node, traverse);
        }
      }

      traverse(source);
    }

    const glob = require('glob');

    // note: this outputs Unix-style paths on Windows
    const wrappers: string[] = glob.GlobSync(prefix + 'src/app/**/themed-*.component.ts', { ignore: 'node_modules/**' }).found;

    for (const wrapper of wrappers) {
      registerWrapper(wrapper);
    }
  }

  private add(entry: ThemeableComponentRegistryEntry) {
    this.entries.add(entry);
    this.byBaseClass.set(entry.baseClass, entry);
    this.byWrapperClass.set(entry.wrapperClass, entry);
    this.byBasePath.set(entry.basePath, entry);
    this.byWrapperPath.set(entry.wrapperPath, entry);
  }
}

export const themeableComponents = new ThemeableComponentRegistry();

/**
 * Construct the AST of a TypeScript source file
 * @param file
 */
function getSource(file: string): ts.SourceFile {
  return ts.createSourceFile(
    file,
    readFileSync(file).toString(),
    ts.ScriptTarget.ES2020,  // todo: actually use tsconfig.json?
    /*setParentNodes */ true,
  );
}

/**
 * Resolve a possibly relative local path into an absolute path starting from the root directory of the project
 */
function resolveLocalPath(path: string, relativeTo: string) {
  if (path.startsWith('src/')) {
    return path;
  } else if (path.startsWith('./')) {
    const parts = relativeTo.split('/');
    return [
      ...parts.slice(0, parts.length - 1),
      path.replace(/^.\//, ''),
    ].join('/') + '.ts';
  } else {
    throw new Error(`Unsupported local path: ${path}`);
  }
}

export function isThemedComponentWrapper(decoratorNode: TSESTree.Decorator): boolean {
  if (decoratorNode.parent.type !== TSESTree.AST_NODE_TYPES.ClassDeclaration) {
    return false;
  }

  if (decoratorNode.parent.superClass?.type !== TSESTree.AST_NODE_TYPES.Identifier) {
    return false;
  }

  return (decoratorNode.parent.superClass as any)?.name === 'ThemedComponent';
}

export function getBaseComponentClassName(decoratorNode: TSESTree.Decorator): string | undefined {
  const wrapperClass = getComponentClassName(decoratorNode);

  if (wrapperClass === undefined) {
    return;
  }

  themeableComponents.initialize();
  const entry = themeableComponents.byWrapperClass.get(wrapperClass);

  if (entry === undefined) {
    return undefined;
  }

  return entry.baseClass;
}

export function isThemeableComponent(className: string): boolean {
  themeableComponents.initialize();
  return themeableComponents.byBaseClass.has(className);
}

export function inThemedComponentOverrideFile(filename: string): boolean {
  const match = filename.match(/src\/themes\/[^\/]+\/(app\/.*)/);

  if (!match) {
    return false;
  }
  themeableComponents.initialize();
  // todo: this is fragile!
  return themeableComponents.byBasePath.has(`src/${match[1]}`);
}

export function allThemeableComponents(): ThemeableComponentRegistryEntry[] {
  themeableComponents.initialize();
  return [...themeableComponents.entries];
}

export function getThemeableComponentByBaseClass(baseClass: string): ThemeableComponentRegistryEntry | undefined {
  themeableComponents.initialize();
  return themeableComponents.byBaseClass.get(baseClass);
}

export function isAllowedUnthemedUsage(usageNode: TSESTree.Identifier) {
  return isPartOfClassDeclaration(usageNode) || isPartOfTypeExpression(usageNode) || isPartOfViewChild(usageNode);
}

export const DISALLOWED_THEME_SELECTORS = 'ds-(base|themed)-';

export function fixSelectors(text: string): string {
  return text.replaceAll(/ds-(base|themed)-/g, 'ds-');
}

/**
 * Determine the theme of the current file based on its path in the project.
 * @param context the current ESLint rule context
 */
export function getFileTheme(context: RuleContext<any, any>): string | undefined {
  // note: shouldn't use plain .filename (doesn't work in DSpace Angular 7.4)
  const m = context.getFilename()?.match(/\/src\/themes\/([^/]+)\//);

  if (m?.length === 2) {
    return m[1];
  }

  return undefined;
}
