/*
 * Copyright (c) 2024 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.
 */

const ts = require('typescript');
const path = require('path');
const fs = require('fs');

/**
 * Directory location to API .d.ts files.
 */
const SOURCE_DTS_DIR = process.argv[2];

/**
 * Directory location to component JSON files.
 */
const COMPONENT_OUTPUT_DIR = process.argv[3];

/**
 * Directory location to form component JSON files.
 */
const FORM_COMPONENT_OUTPUT_DIR = process.argv[4];

/**
 * Map _special_ extend class name to method names under the class.
 * 
 * @type {Map<string, string[]>}
 */
const SPECIAL_EXTEND_ATTRS = new Map();

/**
 * _special_ extend class names
 */
const SPECIAL_EXTEND_ATTR_NAMES = [
  'DynamicNode', 
  'BaseSpan', 
  'ScrollableCommonMethod',
  'CommonShapeMethod',
  'SecurityComponentMethod'
];

/**
 * _special_ component data array. Each data contains:
 * 
 * - componentName: the component JSON file name.
 * - apiName: the API .d.ts file name.
 * - className: the class name in the API .d.ts file.
 * - includeClassName: whether needs to include the class name in the component JSON file.
 */
const SPECIAL_COMPONENTS = [
  {
    componentName: 'common_attrs',
    apiName: 'common',
    className: 'CommonMethod',
    includeClassName: false,
  }
];

/**
 * API .d.ts file names to skip finding components.
 */
const COMPONENT_WHITE_LIST = ['common'];

/**
 * API .d.ts file names to skip finding form components.
 */
const FORM_COMPONENT_WHITE_LIST = ['common'];

registerSpecialExtends(path.join(SOURCE_DTS_DIR, 'common.d.ts'));
registerSpecialExtends(path.join(SOURCE_DTS_DIR, 'span.d.ts'));
registerSpecialExtends(path.join(SOURCE_DTS_DIR, 'security_component.d.ts'));
generateSpecialTargetFiles();
generateTargetFile(SOURCE_DTS_DIR);

/**
 * Register _special_ extend classes (i.e. classes except CommonMethod that Component attributes' extends from).
 * 
 * Registered classes will be recognized later when an Component attribute extends any of them, 
 * then parse those methods in the extend class as the methods of the Component attribute.
 * 
 * @param {string} filePath File location of the special extend classes.
 */
function registerSpecialExtends(filePath) {
  const content = fs.readFileSync(filePath, 'utf8');
  const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);

  ts.forEachChild(sourceFile, (node) => {
    if (isSpecialExtendClass(node)) {
      const specialExtendClassName = node.name.getText();
      const attrs = getAttrs(node, false);
      SPECIAL_EXTEND_ATTRS.set(specialExtendClassName, attrs);
    }
  });
}

/**
 * Generate or update component JSON files for the special extend classes (e.g. CommonMethod).
 */
function generateSpecialTargetFiles() {
  SPECIAL_COMPONENTS.forEach((special) => {
    const { componentName, apiName, className, includeClassName } = special;

    const apiFilePath = path.join(SOURCE_DTS_DIR, `${apiName}.d.ts`);

    const content = fs.readFileSync(apiFilePath, 'utf8');
    const sourceFile = ts.createSourceFile(apiFilePath, content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);

    ts.forEachChild(sourceFile, (node) => {
      if (!(ts.isClassDeclaration(node) && node.name && ts.isIdentifier(node.name) &&
        node.name.getText() === className)) {
        return;
      }

      const [flags, isForm] = getFlags(node);
      const component = { attrs: Array.from(new Set(getAttrs(node, false))), ...flags };
      const formComponent = { attrs: Array.from(new Set(getAttrs(node, isForm))), ...flags };

      if (includeClassName) {
        component.name = className;
        formComponent.name = className;
      }

      generateFormAndComponents(componentName, component, formComponent, isForm);
    });
  });
}

/**
 * Generate or update component JSON files for the Component attributes.
 * 
 * @param {string} filePath Directory location of API .d.ts files.
 */
function generateTargetFile(filePath) {
  const files = [];
  readFile(filePath, files);

  const program = ts.createProgram(files, {});
  const checker = program.getTypeChecker();
  const sourceFiles = program.getSourceFiles()
    .filter((f) => files.includes(toUnixPath(f.fileName)));

  sourceFiles.forEach((sourceFile) => {
    const sourceFilePath = path.parse(toUnixPath(sourceFile.fileName));
    const baseName = path.basename(sourceFilePath.name, path.extname(sourceFilePath.name));

    const [component, formComponent, isForm] = findComponent(sourceFile, checker, program);

    Object.keys(component).length > 0 && 
      generateFormAndComponents(baseName, component, formComponent, isForm);
  });
}

/**
 * Generate or update component JSON files for form and non-form components.
 * 
 * @param {string} componentName Component name.
 * @param {object} component Component JSON data.
 * @param {object} formComponent Form component JSON data.
 * @param {boolean} isForm Whether the component can be a form component.
 */
function generateFormAndComponents(componentName, component, formComponent, isForm) {
  if (!COMPONENT_WHITE_LIST.includes(componentName)) {
    if (isForm && !FORM_COMPONENT_WHITE_LIST.includes(componentName)) {
      const formComponentFileName = path.join(FORM_COMPONENT_OUTPUT_DIR, `${componentName}.json`);
      generateComponentJSONFile(formComponentFileName, formComponent);
    }
    const componentFileName = path.join(COMPONENT_OUTPUT_DIR, `${componentName}.json`);
    generateComponentJSONFile(componentFileName, component);
  }
}

/**
 * Write file utility for generating or updating component JSON files.
 * 
 * This only updates the `attrs` attribute in the JSON file.
 * 
 * @param {string} filePath File location to write the component JSON file.
 * @param {object} component component JSON object.
 */
function generateComponentJSONFile(filePath, component) {
  if (!fs.existsSync(filePath)) {
    fs.writeFileSync(path.resolve(filePath), JSON.stringify(component, null, 2));
    return;
  }
  const source = JSON.parse(fs.readFileSync(filePath, 'utf8'));
  const updateComponent = { ...source, ...component };
  fs.writeFileSync(path.resolve(filePath), JSON.stringify(updateComponent, null, 2));
}

/**
 * Read file utility for reading all files in the directory `dir`.
 * 
 * Add all file pathes to the array `fileDir`.
 * 
 * @param {string} dir Directory location for reading all files. 
 * @param {string[]} fileDir All file pathes array. 
 */
function readFile(dir, fileDir) {
  const files = fs.readdirSync(dir);
  files.forEach((element) => {
    const filePath = path.join(dir, element);
    const status = fs.statSync(filePath);
    if (status.isDirectory()) {
      readFile(filePath, fileDir);
    } else {
      fileDir.push(path.resolve(filePath));
    }
  });
}

/**
 * Find any Component attribute in the API .d.ts file.
 * 
 * @param {ts.SourceFile} sourceFile .d.ts file.
 * @param {ts.TypeChecker} checker type checker.
 * @param {ts.Program} program program.
 * @returns {[object, object, boolean]} A component JSON object, a form component JSON object, 
 * and a boolean to indicate whether the Component attribute can be a form component.
 */
function findComponent(sourceFile, checker, program) {
  let component = {};
  let formComponent = {};
  let isForm = false;

  ts.forEachChild(sourceFile, (node) => {
    if (isClass(node)) {
      const [flags, _isForm] = getFlags(node);
      component = {
        name: node.name.getText().replace(/Attribute$/, ''),
        attrs: Array.from(new Set([
          ...getAttrs(node, false),
          ...getCommonAttrs(node)
        ])),
        ...flags
      };
      formComponent = {
        name: node.name.getText().replace(/Attribute$/, ''),
        attrs: Array.from(new Set([
          ...getAttrs(node, _isForm),
          ...getCommonAttrs(node)
        ])),
        ...flags
      };
      isForm = _isForm;
    }
  });

  return [component, formComponent, isForm];
}

/**
 * Check whether a AST Node is a class.
 * 
 * @param {ts.Node} node 
 * @returns {boolean}
 */
function isClass(node) {
  return ts.isClassDeclaration(node) && node.name && ts.isIdentifier(node.name) &&
    /Attribute$/.test(node.name.getText());
}

/**
 * Check whether a AST Node is a _special_ extend class.
 * 
 * @param {ts.Node} node 
 * @returns {boolean}
 */
function isSpecialExtendClass(node) {
  return ts.isClassDeclaration(node) && node.name && ts.isIdentifier(node.name) &&
    SPECIAL_EXTEND_ATTR_NAMES.includes(node.name.getText());
}

/**
 * Check whether a AST Node is a method.
 * 
 * @param {ts.Node} node 
 * @returns {boolean}
 */
function isMethod(node) {
  return ts.isMethodDeclaration(node) && node.name && ts.isIdentifier(node.name) &&
    node.name.escapedText;
}

/**
 * Check whether a AST Node is a class and extends any _special_ extend class.
 * 
 * @param {ts.Node} node 
 * @returns {boolean}
 */
function isExtendSpecialDeclaration(node) {
  return ts.isClassDeclaration(node) && node.heritageClauses && node.heritageClauses.length > 0;
}

/**
 * Check whether a method belongs to the form component.
 * 
 * @param {ts.Node} node 
 * @returns {boolean}
 */
function isFormComponent(node) {
  const flags = getFlags(node);
  return flags && flags[1];
}

/**
 * Get all identifiers that this class extends.
 * 
 * @param {ts.Node} node 
 * @returns {ts.Expression[]}
 */
function getExtendIdentifiers(node) {
  if (!ts.isHeritageClause(node)) {
    return [];
  }

  const identifiers = [];
  node.types.forEach((type) => {
    if (ts.isExpressionWithTypeArguments(type) && type.expression) {
      identifiers.push(type.expression);
    }
  });

  return identifiers;
}

/**
 * Get all method names from _special_ extend class.
 * 
 * @param {ts.ClassDeclaration} node 
 * @returns {string[]}
 */
function getCommonAttrs(node) {
  if (!isExtendSpecialDeclaration(node)) {
    return [];
  }

  const attrs = [];
  const heritageClause = node.heritageClauses[0];
  const identifiers = getExtendIdentifiers(heritageClause);
  identifiers.forEach((identifier) => {
    if (SPECIAL_EXTEND_ATTRS.has(identifier.escapedText)) {
      attrs.push(...SPECIAL_EXTEND_ATTRS.get(identifier.escapedText));
    }
  });
  return attrs;
}

/**
 * Get all method names from the class (without any method names from extend class).
 * 
 * @param {ts.ClassDeclaration} node 
 * @param {boolean} shouldFilterForm Whether needs to filter any method names that belongs to form component.
 * @returns {string[]}
 */
function getAttrs(node, shouldFilterForm) {
  const attrs = [];

  ts.forEachChild(node, (child) => {
    if (isMethod(child) && (!shouldFilterForm || isFormComponent(child))) {
      const attrName = child.name.escapedText;
      attrs.push(attrName);
    }
  });

  return attrs;
}

/**
 * Get flags and whether is form component boolean.
 * 
 * @param {ts.Node} node 
 * @returns {[string[], boolean]} Flag names, whether is form component.
 */
function getFlags(node) {
  const tags = parseTags(node);
  const flags = filterFlags(tags);

  if (flags.form) {
    delete flags.form;
    return [flags, true];
  }
  return [flags, false];
}

/**
 * Parse all JSDoc tags from a AST Node.
 * 
 * @param {ts.Node} node 
 * @returns {string[]}
 */
function parseTags(node) {
  const tags = [];
  const jsTags = ts.getJSDocTags(node);

  jsTags.forEach((jsTag) => {
    tags.push(jsTag.tagName.getText());
  });
  return tags;
}

/**
 * Filter useful tags from a tag array.
 * 
 * Currently, we only parse `@form`.
 * 
 * @param {string} tags 
 * @returns {object}
 */
function filterFlags(tags) {
  let form;

  tags.forEach((tag) => {
    const name = typeof tag === 'string' ? tag : tag?.title;

    if (name) {
      if (name === 'form') {
        form = true;
      }
    }
  });

  return {
    ...(form && { form }),
  };
}

/**
 * Convert file path format to linux-style.
 * 
 * @param {string} data file location. 
 * @returns {string}
 */
function toUnixPath(data) {
  if (/^win/.test(require('os').platform())) {
    const fileTmps = data.split(path.sep);
    const newData = path.posix.join(...fileTmps);
    return newData;
  }
  return data;
}