/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import * as ts from 'typescript';

import {
  IFileLog,
  LogType
} from './utils';
import {
  LogData,
  LogDataFactory
} from './fast_build/ark_compiler/logger';
import {
  ArkTSErrorDescription,
  ErrorCode
} from './fast_build/ark_compiler/error_code';
import creatAstNodeUtils from './create_ast_node_utils';
import { CompileEvent, createAndStartEvent, stopEvent } from './performance';

export const reExportCheckLog: IFileLog = new creatAstNodeUtils.FileLog();
export const reExportNoCheckMode: string = 'noCheck';
const reExportStrictMode: string = 'strict';

export interface LazyImportOptions {
  autoLazyImport: boolean;
  reExportCheckMode: string;
  autoLazyFilter: Object;
}

export function processJsCodeLazyImport(id: string, code: string,
  autoLazyImport: boolean, reExportCheckMode: string, metaInfo: Object, autoLazyFilter: Object, parentEvent?: CompileEvent): string {
  const eventProcessJsCodeLazyImport: CompileEvent = createAndStartEvent(parentEvent, 'process Js code lazy import');
  let sourceNode: ts.SourceFile = ts.createSourceFile(id, code, ts.ScriptTarget.ES2021, true, ts.ScriptKind.JS);
  if (autoLazyImport) {
    sourceNode = transformLazyImport(metaInfo, sourceNode, autoLazyFilter, undefined, eventProcessJsCodeLazyImport);
  }
  lazyImportReExportCheck(sourceNode, reExportCheckMode, eventProcessJsCodeLazyImport);
  code = autoLazyImport ? ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }).printFile(sourceNode) : code;
  stopEvent(eventProcessJsCodeLazyImport);
  return code;
}

export function transformLazyImport(metaInfo: Object, sourceNode: ts.SourceFile,
  autoLazyFilter: Object, resolver?: Object, parentEvent?: CompileEvent): ts.SourceFile {
  const eventTransformLazyImport: CompileEvent = createAndStartEvent(parentEvent, 'transform lazy import');
  if (isNotAutoLazyImport(metaInfo, autoLazyFilter)) {
    return sourceNode;
  }
  const moduleNodeTransformer: ts.TransformerFactory<ts.SourceFile> = context => {
    const visitor: ts.Visitor = node => {
      if (ts.isImportDeclaration(node)) {
        return updateImportDecl(node, resolver);
      }
      return node;
    };
    return node => ts.visitEachChild(node, visitor, context);
  };

  const result: ts.SourceFile =
    ts.transform(sourceNode, [moduleNodeTransformer]).transformed[0];
  stopEvent(eventTransformLazyImport);
  return result;
}

function isNotAutoLazyImport(metaInfo: Object, autoLazyFilter: Object): boolean {
  if (!autoLazyFilter || Object.keys(autoLazyFilter).length === 0) {
    return false;
  }
  const pkgName: string = metaInfo?.pkgName;
  const includeList: string[] = autoLazyFilter?.include;
  const excludeList: string[] = autoLazyFilter?.exclude;
  if (pkgName && includeList && includeList.length > 0) {
    return !includeList.includes(pkgName);
  }
  if (pkgName && excludeList && excludeList.length > 0) {
    return excludeList.includes(pkgName);
  }
  return false;
}

function updateImportDecl(node: ts.ImportDeclaration, resolver: Object): ts.ImportDeclaration | ts.ImportDeclaration[] {
  const importClause: ts.ImportClause | undefined = node.importClause;
  const moduleRequest: string = (node.moduleSpecifier! as ts.StringLiteral).text.replace(/'|"/g, '');
  // The following cases do not support lazy-import.
  // case1: import '...'
  // case2: import type { t } from '...' or import type t from '...'
  // case3: import lazy { x } from '...'
  if (!importClause || importClause.isTypeOnly || importClause.isLazy) {
    return node;
  }
  // case4: import * as ns from '...'
  // case5: import y, * as ns from '...'
  if (importClause.namedBindings && ts.isNamespaceImport(importClause.namedBindings)) {
    return node;
  }
  // case6: import ... from 'xxx.json'
  if (moduleRequest.endsWith('.json')) {
    return node;
  }
  const modifiers: readonly ts.Modifier[] | undefined =
    ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
  const namedBindings: ts.NamedImportBindings = importClause.namedBindings;
  let newImportClause: ts.ImportClause;
  // The following cases support lazy-import.
  // case1: import { x } from '...' --> import lazy { x } from '...'
  // case2: import y, { x } from '...' --> import lazy y, { x } from '...'
  if (namedBindings && ts.isNamedImports(namedBindings)) {
    // The resolver is used to determine whether type symbols need to be processed.
    // Only TS/ETS files have type symbols.
    if (resolver) {
      // Separate type symbols from runtime values.
      // case1: import { type t, x } from '...' -->  import lazy { x } from '...'; import { type t } from '...';
      // case2: import x { type t } from '...' -->  import lazy x from '...'; import { type t } from '...';
      // case3: import type u { type t, x } from '...' -->  import lazy { x } from '...'; import { type u, type t } from '...';
      const { valueBindings, typeBindings } = splitImportBindings(namedBindings, resolver);
      const typeImportDeclaration: ts.ImportDeclaration | undefined = typeBindings.length > 0 ?
        ts.factory.updateImportDeclaration(
          node,
          node.modifiers,
          ts.factory.createImportClause(false, undefined, ts.factory.createNamedImports(typeBindings)),
          node.moduleSpecifier,
          node.assertClause
        ) : undefined;
      if (valueBindings.length === 0 && !importClause.name) {
        return typeImportDeclaration ?? node;
      }
      const valueImportClause: ts.ImportClause = ts.factory.updateImportClause(
        importClause,
        false,
        importClause.name,
        valueBindings.length > 0 ? ts.factory.updateNamedImports(namedBindings, valueBindings) : undefined
      );
      // @ts-ignore
      valueImportClause.isLazy = true;
      const lazyImportDeclaration: ts.ImportDeclaration = ts.factory.updateImportDeclaration(
        node,
        modifiers,
        valueImportClause,
        node.moduleSpecifier,
        node.assertClause
      );
      return typeImportDeclaration ? [lazyImportDeclaration, typeImportDeclaration] : lazyImportDeclaration;
    } else {
      newImportClause = importClause;
    }
  } else if (!namedBindings && importClause.name) {
    // case3: import y from '...' --> import lazy y from '...'
    newImportClause = importClause;
  }
  // @ts-ignore
  newImportClause.isLazy = true;
  return ts.factory.updateImportDeclaration(node, modifiers, newImportClause, node.moduleSpecifier, node.assertClause);
}

function splitImportBindings(namedBindings: ts.NamedImportBindings, resolver: Object): {
  valueBindings: ts.ImportSpecifier[];
  typeBindings: ts.ImportSpecifier[];
} {
  const valueBindings: ts.ImportSpecifier[] = [];
  const typeBindings: ts.ImportSpecifier[] = [];
  namedBindings.elements.forEach(item => {
    const element = item as ts.ImportSpecifier;
    let targetBindings: ts.ImportSpecifier[];
    if (element.isTypeOnly) {
      targetBindings = typeBindings;
    } else if (resolver.isReferencedAliasDeclaration(element)) {
      targetBindings = valueBindings;
    } else {
      return;
    }
    // import { x } from './y' --> propertyName is undefined
    // import { x as a } from './y' --> propertyName is x
    targetBindings.push(ts.factory.updateImportSpecifier(
      element,
      targetBindings === typeBindings,
      element.propertyName,
      element.name
    ));
  });
  return { valueBindings, typeBindings };
}

export function resetReExportCheckLog(): void {
  reExportCheckLog.cleanUp();
}

export function lazyImportReExportCheck(node: ts.SourceFile, reExportCheckMode: string, parentEvent?: CompileEvent): void {
  if (reExportCheckMode === reExportNoCheckMode) {
    return;
  }
  const eventLazyImportReExportCheck: CompileEvent = createAndStartEvent(parentEvent, 'lazy import re export check');
  reExportCheckLog.sourceFile = node;
  const lazyImportSymbols: Set<string> = new Set();
  const exportSymbols: Map<string, ts.Statement[]> = new Map();
  const result: Map<string, ts.Statement[]> = new Map();
  node.statements.forEach(stmt => {
    collectLazyImportSymbols(stmt, lazyImportSymbols, exportSymbols, result);
    collectLazyReExportSymbols(stmt, lazyImportSymbols, exportSymbols, result);
  });
  for (const [key, statements] of result.entries()) {
    for (const statement of statements) {
      collectReExportErrors(statement, key, reExportCheckMode);
    }
  }
  stopEvent(eventLazyImportReExportCheck);
}

function collectLazyImportSymbols(stmt: ts.Statement, lazyImportSymbols: Set<string>,
  exportSymbols: Map<string, ts.Statement[]>, result: Map<string, ts.Statement[]>): void {
  if (ts.isImportDeclaration(stmt) && stmt.importClause && stmt.importClause.isLazy) {
    // For import lazy x from './y', collect 'x'
    const importClauseName = stmt.importClause.name;
    if (importClauseName) {
      lazyImportSymbols.add(importClauseName.text);
      result.set(importClauseName.text, exportSymbols.get(importClauseName.text) ?? []);
    }
    // For import lazy { x } from './y', collect 'x'
    const importNamedBindings: ts.NamedImportBindings = stmt.importClause.namedBindings;
    if (importNamedBindings && ts.isNamedImports(importNamedBindings) && importNamedBindings.elements.length !== 0) {
      importNamedBindings.elements.forEach((element: ts.ImportSpecifier) => {
        const nameText = element.name.text;
        lazyImportSymbols.add(nameText);
        result.set(nameText, exportSymbols.get(nameText) ?? []);
      });
    }
  }
}

function collectLazyReExportSymbols(stmt: ts.Statement, lazyImportSymbols: Set<string>,
  exportSymbols: Map<string, ts.Statement[]>, result: Map<string, ts.Statement[]>): void {
  // export default x
  if (ts.isExportAssignment(stmt) && ts.isIdentifier(stmt.expression)) {
    const nameText: string = stmt.expression.text;
    const targetMap = lazyImportSymbols.has(nameText) ? result : exportSymbols;
    if (!targetMap.get(nameText)) {
      targetMap.set(nameText, []);
    }
    targetMap.get(nameText).push(stmt);
  }
  // export { x }
  if (ts.isExportDeclaration(stmt) && !stmt.moduleSpecifier &&
    ts.isNamedExports(stmt.exportClause) && stmt.exportClause.elements.length !== 0) {
    stmt.exportClause.elements.forEach((element: ts.ExportSpecifier) => {
      // For example, in 'export { foo as bar }', exportName is 'bar', localName is 'foo'
      const exportName: string = element.name.text;
      const localName: string = element.propertyName ? element.propertyName.text : exportName;
      const targetMap = lazyImportSymbols.has(localName) ? result : exportSymbols;
      if (!targetMap.get(localName)) {
        targetMap.set(localName, []);
      }
      targetMap.get(localName).push(stmt);
    });
  }
}

function collectReExportErrors(node: ts.Node, elementText: string, reExportCheckMode: string): void {
  let pos: number;
  try {
    pos = node.getStart();
  } catch {
    pos = 0;
  }
  let type: LogType = LogType.WARN;
  if (reExportCheckMode === reExportStrictMode) {
    type = LogType.ERROR;
  }
  // reExportCheckMode explanation:
  // - 'noCheck': NoCheck mode. The functionality to block re-exported lazy-import is disabled.
  // - 'strict': Strict mode. It intercepts errors and treats them as critical (LogType.ERROR).
  // - 'compatible': Compatible mode. It logs warnings (LogType.WARN) but does not intercept or block them.
  const errInfo: LogData = LogDataFactory.newInstance(
    ErrorCode.ETS2BUNDLE_EXTERNAL_LAZY_IMPORT_RE_EXPORT_ERROR,
    ArkTSErrorDescription,
    `'${elementText}' of lazy-import is re-export`,
    '',
    ['Please make sure the namedBindings of lazy-import are not be re-exported.',
      'Please check whether the autoLazyImport switch is opened.']
  );
  reExportCheckLog.errors.push({
    type: type,
    message: errInfo.toString(),
    pos: pos
  });
}