import {
  NodeSchema,
  JSExpression,
  NpmInfo,
  CompositeValue,
  isJSExpression,
} from '@alilc/lowcode-types';

import _ from 'lodash';
import changeCase from 'change-case';
import {
  BuilderComponentPlugin,
  BuilderComponentPluginFactory,
  ChunkType,
  CodePiece,
  FileType,
  ICodeChunk,
  ICodeStruct,
  IContainerInfo,
  PIECE_TYPE,
  HandlerSet,
  IScope,
  NodeGeneratorConfig,
  NodePlugin,
  AttrPlugin,
} from '../../../types';

import { RAX_CHUNK_NAME } from './const';
import { COMMON_CHUNK_NAME } from '../../../const/generator';

import { generateExpression } from '../../../utils/jsExpression';
import {
  createNodeGenerator,
  generateConditionReactCtrl,
  generateReactExprInJS,
} from '../../../utils/nodeToJSX';
import { generateCompositeType } from '../../../utils/compositeType';
import { Scope } from '../../../utils/Scope';
import { parseExpressionGetGlobalVariables } from '../../../utils/expressionParser';
import { transformThis2Context } from '../../../core/jsx/handlers/transformThis2Context';
import { transformJsExpr } from '../../../core/jsx/handlers/transformJsExpression';

export interface PluginConfig {
  fileType: string;

  /** 是否要忽略小程序 */
  ignoreMiniApp?: boolean;
}

// TODO: componentName 若并非大写字符打头，甚至并非是一个有效的 JS 标识符怎么办？？
// FIXME: 我想了下，这块应该放到解析阶段就去做掉，对所有 componentName 做 identifier validate，然后对不合法的做统一替换。
const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?) => {
  const cfg: PluginConfig = {
    fileType: FileType.JSX,
    ...config,
  };

  const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => {
    const next: ICodeStruct = {
      ...pre,
    };

    const ir = next.ir as IContainerInfo;
    const rootScope = Scope.createRootScope();
    const { tolerateEvalErrors = true, evalErrorsHandler = '' } = next.contextData;

    // Rax 构建到小程序的时候，不能给组件起起别名，得直接引用，故这里将所有的别名替换掉
    // 先收集下所有的 alias 的映射
    const componentsNameAliasMap = new Map<string, string>();
    next.chunks.forEach((chunk) => {
      if (isImportAliasDefineChunk(chunk)) {
        componentsNameAliasMap.set(chunk.ext.aliasName, chunk.ext.originalName);
      }
    });

    // 注意：这里其实隐含了一个假设：schema 中的 componentName 应该是一个有效的 JS 标识符，而且是大写字母打头的
    // FIXME: 为了快速修复临时加的逻辑，需要用 pre-process 的方式替代处理。
    const mapComponentNameToAliasOrKeepIt = (componentName: string) =>
      componentsNameAliasMap.get(componentName) || componentName;

    // 然后过滤掉所有的别名 chunks
    next.chunks = next.chunks.filter((chunk) => !isImportAliasDefineChunk(chunk));

    // 如果直接按目前的 React 的方式之间出码 JSX 的话，会有 3 个问题：
    // 1. 小程序出码的时候，循环变量没法拿到
    // 2. 小程序出码的时候，很容易出现 Uncaught TypeError: Cannot read property 'avatar' of undefined 这样的异常(如下图的 50 行) -- 因为若直接出码，Rax 构建到小程序的时候会立即计算所有在视图中用到的变量
    // 3. 通过 this.xxx 能拿到的东西太多了，而且自定义的 methods 可能会无意间破坏 Rax 框架或小程序框架在页面 this 上的东东
    const customHandlers: HandlerSet<string> = {
      expression(input: JSExpression, scope: IScope) {
        return transformJsExpr(generateExpression(input, scope), scope, {
          dontWrapEval: !tolerateEvalErrors,
        });
      },
      function(input, scope: IScope) {
        return transformThis2Context(input.value || 'null', scope);
      },
    };

    // 创建代码生成器
    const commonNodeGenerator = createNodeGenerator({
      handlers: customHandlers,
      tagMapping: mapComponentNameToAliasOrKeepIt,
      nodePlugins: [generateReactExprInJS, generateConditionReactCtrl, generateRaxLoopCtrl],
      attrPlugins: [generateNodeAttrForRax.bind({ cfg })],
    });

    // 生成 JSX 代码
    const jsxContent = commonNodeGenerator(ir, rootScope);

    if (!cfg.ignoreMiniApp) {
      next.chunks.push({
        type: ChunkType.STRING,
        fileType: cfg.fileType,
        name: COMMON_CHUNK_NAME.ExternalDepsImport,
        content: "import { isMiniApp as __$$isMiniApp } from 'universal-env';",
        linkAfter: [],
      });
    }

    next.chunks.push({
      type: ChunkType.STRING,
      fileType: cfg.fileType,
      name: RAX_CHUNK_NAME.ClassRenderPre,
      // TODO: setState, dataSourceMap, reloadDataSource, utils, i18n, i18nFormat, getLocale, setLocale 这些在 Rax 的编译模式下不能在视图中直接访问，需要转化成 this.xxx
      content: `
        const __$$context = this._context;
        const { state, setState, dataSourceMap, reloadDataSource, utils, constants, i18n, i18nFormat, getLocale, setLocale } = __$$context;
      `,
      linkAfter: [RAX_CHUNK_NAME.ClassRenderBegin],
    });

    next.chunks.push({
      type: ChunkType.STRING,
      fileType: cfg.fileType,
      name: RAX_CHUNK_NAME.ClassRenderJSX,
      content: `return ${jsxContent};`,
      linkAfter: [RAX_CHUNK_NAME.ClassRenderBegin, RAX_CHUNK_NAME.ClassRenderPre],
    });

    next.chunks.push({
      type: ChunkType.STRING,
      fileType: cfg.fileType,
      name: COMMON_CHUNK_NAME.CustomContent,
      content: [
        tolerateEvalErrors &&
          `
        function __$$eval(expr) {
          try {
            return expr();
          } catch (error) { 
            ${evalErrorsHandler}
          }
        }

        function __$$evalArray(expr) {
          const res = __$$eval(expr);
          return Array.isArray(res) ? res : [];
        }
        `,
        `
        function __$$createChildContext(oldContext, ext) {
          return Object.assign({}, oldContext, ext);
        }
      `,
      ]
        .filter(Boolean)
        .join('\n'),
      linkAfter: [COMMON_CHUNK_NAME.FileExport],
    });

    return next;

    function generateRaxLoopCtrl(
      nodeItem: NodeSchema,
      scope: IScope,
      config?: NodeGeneratorConfig,
      next?: NodePlugin,
    ): CodePiece[] {
      if (nodeItem.loop) {
        const loopItemName = nodeItem.loopArgs?.[0] || 'item';
        const loopIndexName = nodeItem.loopArgs?.[1] || 'index';
        const subScope = scope.createSubScope([loopItemName, loopIndexName]);
        const pieces: CodePiece[] = next ? next(nodeItem, subScope, config) : [];

        const loopDataExpr = tolerateEvalErrors
          ? `__$$evalArray(() => (${transformThis2Context(
              generateCompositeType(nodeItem.loop, scope, { handlers: config?.handlers }),
              scope,
            )}))`
          : `(${transformThis2Context(
              generateCompositeType(nodeItem.loop, scope, { handlers: config?.handlers }),
              scope,
            )})`;

        pieces.unshift({
          value: `${loopDataExpr}.map((${loopItemName}, ${loopIndexName}) => ((__$$context) => (`,
          type: PIECE_TYPE.BEFORE,
        });

        pieces.push({
          value: `))(__$$createChildContext(__$$context, { ${loopItemName}, ${loopIndexName} })))`,
          type: PIECE_TYPE.AFTER,
        });

        return pieces;
      }

      return next ? next(nodeItem, scope, config) : [];
    }
  };

  return plugin;
};

export default pluginFactory;

function isImportAliasDefineChunk(chunk: ICodeChunk): chunk is ICodeChunk & {
  ext: {
    aliasName: string;
    originalName: string;
    dependency: NpmInfo;
  };
} {
  return (
    chunk.name === COMMON_CHUNK_NAME.ImportAliasDefine &&
    !!chunk.ext &&
    typeof chunk.ext.aliasName === 'string' &&
    typeof chunk.ext.originalName === 'string' &&
    !!(chunk.ext.dependency as NpmInfo | null)?.componentName
  );
}

function generateNodeAttrForRax(
  this: { cfg: PluginConfig },
  attrData: { attrName: string; attrValue: CompositeValue },
  scope: IScope,
  config?: NodeGeneratorConfig,
  next?: AttrPlugin,
): CodePiece[] {
  if (!this.cfg.ignoreMiniApp && /^on/.test(attrData.attrName)) {
    // else: onXxx 的都是事件处理函数需要特殊处理下
    return generateEventHandlerAttrForRax(attrData.attrName, attrData.attrValue, scope, config);
  }

  if (attrData.attrName === 'ref') {
    return [
      {
        name: attrData.attrName,
        value: `__$$context._refsManager.linkRef('${attrData.attrValue}')`,
        type: PIECE_TYPE.ATTR,
      },
    ];
  }

  return next ? next(attrData, scope, config) : [];
}

function generateEventHandlerAttrForRax(
  attrName: string,
  attrValue: CompositeValue,
  scope: IScope,
  config?: NodeGeneratorConfig,
): CodePiece[] {
  // -- 事件处理函数中 JSExpression 转成 JSFunction 来处理，避免当 JSExpression 处理的时候多包一层 eval 而导致 Rax 转码成小程序的时候出问题
  const valueExpr = generateCompositeType(
    isJSExpression(attrValue) ? { type: 'JSFunction', value: attrValue.value } : attrValue,
    scope,
    {
      handlers: config?.handlers,
    },
  );

  // 查询当前作用域下的变量
  const currentScopeVariables = scope.bindings?.getAllBindings() || [];
  if (currentScopeVariables.length <= 0) {
    return [
      {
        type: PIECE_TYPE.ATTR,
        name: attrName,
        value: valueExpr,
      },
    ];
  }

  // 提取出所有的未定义的全局变量
  const undeclaredVariablesInValueExpr = parseExpressionGetGlobalVariables(valueExpr);
  const referencedLocalVariables = _.intersection(
    undeclaredVariablesInValueExpr,
    currentScopeVariables,
  );
  if (referencedLocalVariables.length <= 0) {
    return [
      {
        type: PIECE_TYPE.ATTR,
        name: attrName,
        value: valueExpr,
      },
    ];
  }

  const wrappedAttrValueExpr = [
    '(...__$$args) => {',
    '  if (__$$isMiniApp) {',
    '    const __$$event = __$$args[0];',
    ...referencedLocalVariables.map(
      (localVar) => `const ${localVar} = __$$event.target.dataset.${localVar};`,
    ),
    `    return (${valueExpr}).apply(this, __$$args);`,
    '  } else {',
    `    return (${valueExpr}).apply(this, __$$args);`,
    '  }',
    '}',
  ].join('\n');

  return [
    ...referencedLocalVariables.map((localVar) => ({
      type: PIECE_TYPE.ATTR,
      name: `data-${changeCase.snake(localVar)}`,
      value: localVar,
    })),
    {
      type: PIECE_TYPE.ATTR,
      name: attrName,
      value: wrappedAttrValueExpr,
    },
  ];
}
