/*
 * Tencent is pleased to support the open source community by making TMagicEditor available.
 *
 * Copyright (C) 2021 THL A29 Limited, a Tencent company.  All rights reserved.
 *
 * 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 { execSync } = require('child_process');
const { exit } = require('process');
const fs = require('fs');
const path = require('path');

const { parse, prettyPrint, types } = require('recast');
const n = types.namedTypes;

const pluginList = {};
let cwd = __dirname;
const pageRoot = () => path.resolve(cwd, '..');
const entryPath = () => path.resolve(pageRoot(), './src');

const makeCamelCase = function (name) {
  if (typeof name !== 'string') {
    return '';
  }
  return name.replace(/-(\w)/g, ($0, $1) => $1.toUpperCase());
};

const parseEntry = function ({ ast, package, indexPath }) {
  if (!ast.program) {
    console.log(`${package} 入口文件不合法`);
    return exit(1);
  }

  const tokens = getASTTokenByTraverse({ ast, indexPath });
  let { config, value, event } = tokens;
  const { importComponentSource, importComponentToken } = tokens;

  if (!config) {
    console.log(`${package} config 文件声明不合法`);
    return exit(1);
  }
  if (!value) {
    console.log(`${package} value 文件声明不合法`);
    return exit(1);
  }
  if (!event) {
    // event 非必须，不需要 exit
    console.log(`${package} event 文件声明缺失`);
  }

  const findIndex = importComponentToken.indexOf(exportDefaultToken);
  let component = '';
  if (findIndex > -1) {
    component = path.resolve(path.dirname(indexPath), importComponentSource[findIndex]);
  }

  if (!component) {
    console.log(`${package} component 文件声明不合法`);
    return exit(1);
  }

  const reg = /^.*[/\\]node_modules[/\\](.*)/;
  [, config] = config.match(reg) || [, config];
  [, value] = value.match(reg) || [, value];
  [, component] = component.match(reg) || [, component];
  [, event] = event.match(reg) || [, event];

  return {
    config,
    value,
    component,
    event,
  };
};

const getASTTokenByTraverse = ({ ast, indexPath }) => {
  let config = '';
  let value = '';
  let event = '';
  const importComponentToken = [];
  const importComponentSource = [];

  types.visit(ast, {
    visitImportDeclaration(p) {
      const { node } = p;
      const { specifiers, source } = node;

      importComponentToken.push(specifiers[0].local.name);
      importComponentSource.push(source.value);

      this.traverse(p);
    },
    visitExportNamedDeclaration(p) {
      const { node } = p;
      const { specifiers, source } = node;
      const name = specifiers[0].exported.name.toLowerCase();

      if (name === 'value') {
        value = path.resolve(path.dirname(indexPath), source.value);
      } else if (name === 'config') {
        config = path.resolve(path.dirname(indexPath), source.value);
      } else if (name === 'event') {
        event = path.resolve(path.dirname(indexPath), source.value);
      }

      this.traverse(p);
    },
    visitExportDefaultDeclaration(p) {
      const { node } = p;
      const { declaration } = node;
      exportDefaultToken = declaration.name;
      this.traverse(p);
    },
  });

  return {
    config,
    value,
    event,
    importComponentToken,
    importComponentSource,
  };
};

const generateEntry = function ({ entries, type = 'build', componentFileAffix }) {
  const commonArgs = { entries, componentFileAffix };

  generateEntryFile({ entryFile: 'comp-entry.ts', type: 'component', ...commonArgs });
  if (type === 'build') {
    generateEntryFile({ entryFile: 'config-entry.ts', type: 'config', ...commonArgs });
    generateEntryFile({ entryFile: 'value-entry.ts', type: 'value', ...commonArgs });
    generateEntryFile({ entryFile: 'event-entry.ts', type: 'event', ...commonArgs });
  }
};

const generateEntryFile = function ({ entries, entryFile, type, componentFileAffix }) {
  const list = [];
  const importDeclarations = [];

  entries.forEach((entry) => {
    if (!entry.entry[type]) return;

    const name = makeCamelCase(entry.type);
    importDeclarations.push(
      `import ${name} from '${entry.entry[type]}${
        type === 'component' && !entry.entry[type].includes(componentFileAffix) ? componentFileAffix : ''
      }'`,
    );
    list.push(`'${entry.type}': ${name}`);
  });

  const exportToken = `${type}s`;
  const capitalToken = exportToken.charAt(0).toUpperCase() + exportToken.slice(1);
  let jsString = '';
  let exportData = `window.magicPreset${capitalToken} = ${exportToken};`;

  if (type === 'component') {
    const pList = [];

    Object.keys(pluginList).forEach((pluginType) => {
      const name = makeCamelCase(pluginType);
      importDeclarations.push(`import ${name} from '${pluginList[pluginType]}'`);
      pList.push(`'${pluginType}': ${name}`);
    });

    exportData = `const plugins = {${pList.join(',')}};
      const entry = {${exportToken}, plugins};
      window.magicPreset${capitalToken} = entry;
      export default entry;`;

    jsString += `${importDeclarations.join(';')}
      const ${exportToken}: Record<string, any> = {
        ${list.join(',')}
      }
      ${exportData}`;
  } else {
    jsString += `${importDeclarations.join(';')}
      (function(){
        const ${exportToken}: Record<string, any> = {
          ${list.join(',')}
        }
        ${exportData}
      })()`;
  }

  fs.writeFileSync(
    path.resolve(entryPath(), entryFile),
    prettyPrint(parse(jsString, { parser: require('recast/parsers/typescript') }), {
      tabWidth: 2,
      trailingComma: true,
      quote: 'single',
    }).code,
    { encoding: 'utf-8' },
  );
};

const installPackage = function (package) {
  try {
    execSync(`node -e "require.resolve('${package}')"`, { stdio: 'ignore' });
  } catch (e) {
    execSync(`npm install ${package} --registry https://mirrors.tencent.com/npm/`, {
      stdio: 'inherit',
      cwd: pageRoot(),
    });
  }
};

const getIndexPath = function (entry) {
  if (fs.lstatSync(entry).isFile()) {
    return entry;
  }

  if (fs.lstatSync(entry).isDirectory()) {
    const files = fs.readdirSync(entry);
    const [index] = files.filter((file) => file.split('.')[0] === 'index');

    return path.resolve(entry, index);
  }

  return entry;
};

const typeComponent = 1;
const typePlugin = 2;
const typeComponentPackage = 3;

const getComponentPackageImports = function ({ result, properties, indexPath, importDeclarations }) {
  result.type = typeComponentPackage;
  result.imports = [];

  properties.forEach((property) => {
    const [propertyMatch] = importDeclarations.filter((i) => {
      const [specifier] = i.specifiers;

      if (n.ImportDefaultSpecifier.check(specifier) && specifier.local.name === property.value.name) {
        return true;
      }

      return false;
    });

    if (propertyMatch) {
      result.imports.push({
        type: property.key.name,
        name: propertyMatch.specifiers[0].local.name,
        indexPath: getIndexPath(path.resolve(path.dirname(indexPath), propertyMatch.source.value)),
      });
    }
  });

  return result;
};

const isPlugin = function (properties) {
  const [match] = properties.filter((property) => property.key.name === 'install');

  return !!match;
};

/**
 *  1 判断是否组件包
 *  2 判断是组件还是插件
 *  3 组件插件分开写入 comp-entry.ts
 * @param {*} ast
 * @param {String} indexPath
 * @return {Object} { type: '', imports: [] } 返回传入组件的类型。如果是组件包，imports 中包含所有子组件的入口文件路径
 */
const typeAssertion = function ({ ast, indexPath }) {
  const result = {
    type: '',
    imports: [],
  };
  const { importDeclarations, variableDeclarations, exportDefaultName, exportDefaultNode } =
    getAssertionTokenByTraverse(ast);

  if (exportDefaultName) {
    importDeclarations.every((node) => {
      const [specifier] = node.specifiers;

      // 从 import 语句中找到 export default 的变量，认为是组件
      if (n.ImportDefaultSpecifier.check(specifier) && specifier.local.name === exportDefaultName) {
        result.type = typeComponent;
        return false;
      }

      return true;
    });

    if (result.type) return result;

    variableDeclarations.every((node) => {
      const [variable] = node.declarations;

      // 从声明变量语句中找到 export default 的变量，认为是组件包
      if (
        n.Identifier.check(variable.id) &&
        variable.id.name === exportDefaultName &&
        n.ObjectExpression.check(variable.init)
      ) {
        if (isPlugin(variable.init.properties)) {
          result.type = typePlugin;
          return false;
        }

        // 从组件包声明中找到对应子组件入口文件
        getComponentPackageImports({ result, properties: variable.init.properties, indexPath, importDeclarations });
      }

      return true;
    });
  }

  if (exportDefaultNode) {
    if (isPlugin(exportDefaultNode.properties)) {
      result.type = typePlugin;
    } else {
      getComponentPackageImports({ result, properties: variable.init.properties, indexPath, importDeclarations });
    }
  }

  return result;
};

const getAssertionTokenByTraverse = (ast) => {
  const importDeclarations = [];
  const variableDeclarations = [];
  let exportDefaultName = '';
  let exportDefaultNode = undefined;

  types.visit(ast, {
    visitImportDeclaration(p) {
      importDeclarations.push(p.node);
      this.traverse(p);
    },
    visitVariableDeclaration(p) {
      variableDeclarations.push(p.node);
      this.traverse(p);
    },
    visitExportDefaultDeclaration(p) {
      const { node } = p;
      const { declaration } = node;

      // 导出的是变量名
      if (n.Identifier.check(declaration)) {
        exportDefaultName = declaration.name;
      }

      // 导出的是对象的字面量
      if (n.ObjectExpression.check(declaration)) {
        exportDefaultNode = declaration;
      }

      this.traverse(p);
    },
  });

  return {
    importDeclarations,
    variableDeclarations,
    exportDefaultName,
    exportDefaultNode,
  };
};

const start = function ({ type, componentFileAffix, units, workingDir }) {
  cwd = workingDir;
  const entries = [];

  Object.keys(units).forEach((componentType) => {
    const package = units[componentType];

    installPackage(package);

    const indexPath = require.resolve(package);
    const indexCode = fs.readFileSync(indexPath, { encoding: 'utf-8', flag: 'r' });
    const ast = parse(indexCode, { parser: require('recast/parsers/typescript') });
    const result = typeAssertion({ ast, indexPath });

    if (result.type === typeComponent) {
      // 组件
      const entry = parseEntry({ ast, package, indexPath });
      entries.push({ type: componentType, entry });
    } else if (result.type === typeComponentPackage) {
      // 组件包
      result.imports.forEach((i) => {
        const affixReg = new RegExp(`${componentFileAffix}$`);
        if (affixReg.test(i.indexPath)) {
          entries.push({ type: i.type, entry: { component: i.indexPath } });
        } else {
          const indexCode = fs.readFileSync(i.indexPath, { encoding: 'utf-8', flag: 'r' });
          const ast = parse(indexCode);
          const entry = parseEntry({ ast, package: `${package} | ${i.name}`, indexPath: i.indexPath });
          entries.push({ type: i.type, entry });
        }
      });
    } else if (result.type === typePlugin) {
      // 插件
      pluginList[componentType] = package;
    }
  });

  generateEntry({ entries, type, componentFileAffix });
};

module.exports = start;
