/* eslint-disable no-new-func */

import { camelcase, isEmptyObj, isSlot } from '../common';

import {
  IComponent,
  IPageComponent,
  IPlatformApp,
  IPlatformListener,
  IBasicComponent,
  IComplexComponent,
  deserializeAttributes,
  deserializeValue,
  PropBindType,
  ActionType,
  IDataBind,
  processComponentModule,
  serializeDynamicMap,
  deserializeDynamicMap,
  EComponentType,
  IPlatformComponent,
  processCommonStyle2CSSProperties,
} from '@cloudbase/cals';
import { uniq as _uniq, map as _map, get as lodashGet, cloneDeep } from 'lodash-es';
import { runtime } from './index';

/**
 * utils start
 */

function getMetaInfoBySourceKey(sourceKey: string) {
  if (!sourceKey) {
    return {};
  }
  const [materialName, name] = sourceKey.split(':');
  return {
    materialName,
    name,
    variableName: camelcase(`${materialName}_${name}`),
  };
}

interface ISyncProp {
  changeEvent: string;
  valueFromEvent: string;
}

export function getComponentsInfo(dependencies) {
  const outputObj = {};
  dependencies.forEach(({ name: materialName, components, isComposite }) => {
    if (isComposite) {
      components.forEach((component) => {
        let { lowCodes, componentInstances, ...compItem } = component;
        const sourceKey = `${materialName}:${compItem.name}`;
        outputObj[sourceKey] = {
          isComposite,
          ...compItem,
        };
      });
    } else {
      components.forEach(({ name, meta, emitEvents, events = undefined, isContainer, compConfig, dataForm }) => {
        outputObj[`${materialName}:${name}`] = {
          isComposite,
          ...meta,
          events,
          emitEvents,
          isContainer,
          compConfig,
          dataForm,
        };
      });
    }
  });
  return outputObj;
}

function getInputProps(componentsMetaMap) {
  const outputObj = {};
  for (let key in componentsMetaMap) {
    let component = componentsMetaMap[key];
    const sourceKey = key;
    if (component.isComposite) {
      let compItem = component;
      Object.keys(compItem.dataForm || {}).forEach((key) => {
        const inputProps = compItem.dataForm[key]?.inputProp || compItem.dataForm[key]?.syncProp;
        if (inputProps) {
          outputObj[sourceKey] = {
            [key]: inputProps,
            ...(outputObj[sourceKey] || {}),
          };
        }
      });
    } else {
      const inputProps = component?.inputProps || component?.syncProps;
      if (inputProps) {
        outputObj[sourceKey] = inputProps;
      }
    }
  }
  return outputObj;
}

/**
 * utils end
 */

function splitDynamicMap(data) {
  const staticData = {};
  const dataBinds = [];
  for (let key in data) {
    let meta = data[key];
    if (!meta.type || meta.type === PropBindType.static) {
      staticData[key] = meta.value;
    } else if (meta.type === PropBindType.slot) {
      continue;
    } else {
      dataBinds.push({
        type: meta.type,
        bindDataPath: meta.value,
        propertyPath: key,
      });
    }
  }
  return { staticData, dataBinds };
}

export interface IOriginKeyInfo {
  sourceKey: string;
  name: string;
  materialName: string;
  materialVersion: string;
  key: string;
  variableName: string;
  type?: ActionType;
  isComposite?: boolean;
  isPlainProps?: boolean;
  entry?: string;
}

interface IGenerateOptions {
  isComposite?: boolean;
  looseError?: boolean;
}

function walkThroughCals<P>(
  cals,
  process: (
    item: P & {
      extra: Required<IPlatformComponent>['extra'] & {
        __parent;
      };
    },
  ) => void,
) {
  let { items = [] } = cals;
  cals.extra = cals.extra || {};
  if (cals?.directives?.[':if'] === false) {
    cals.items = [];
  }
  const filteredItems = [];
  for (let item of items) {
    if (item?.directives?.[':if'] === false) {
      continue;
    }
    filteredItems.push(item);
    item.extra = item.extra || {};
    item.extra.__parent = cals;
    walkThroughCals(item, process);
  }
  cals.items = filteredItems;
  process(cals);
  return cals;
}

export function getComponentSchema(
  ctx: {
    app;
    $app;
    $page;
    this;
  },
  page: IPageComponent,
  isComposite = false,
  componentsInfoMap: { [componentSourceKey: string]: any } = {},
  wrapperClass?: string,
  options?: {
    isComposite?: boolean;
    looseError?: boolean;
  },
) {
  const componentInputProps = getInputProps(componentsInfoMap) || {};
  const copyJson = JSON.parse(JSON.stringify(page));
  const compWidgets = {};
  const compDataBinds = {};

  const componentSchemaJson = walkThroughCals<IBasicComponent | IComplexComponent>(copyJson, (component) => {
    if (isSlot(component)) {
      return;
    }
    const { component: componentName, module, attributes = {} } = component;
    const { commonStyle = {} } = component.extra || {};
    const processedCommonStyle = attributes.style ? attributes.style : processCommonStyle2CSSProperties(commonStyle);
    if (processedCommonStyle) {
      component.extra.commonStyle = processedCommonStyle;
    }
    const sourceKey = `${module}:${componentName}`;
    const { class: className = '' } = attributes;

    const deserializeCtx = { page };

    let dynamicStyle = component[':style']
      ? deserializeValue(deserializeCtx, ':style', component[':style']).value
      : undefined;
    let dynamicClassList = component[':class']
      ? deserializeValue(deserializeCtx, ':class', component[':class']).value
      : undefined;

    let dynamicIf = component.directives?.[':if']
      ? deserializeValue(deserializeCtx, ':if', component.directives?.[':if']).value
      : undefined;

    let dynamicFor = component.directives?.[':for']
      ? deserializeValue(deserializeCtx, ':for', component.directives?.[':for']).value
      : undefined;

    if (
      dynamicIf != undefined &&
      (dynamicIf.value == undefined || dynamicIf.value === true || dynamicIf.value === false)
    ) {
      dynamicIf = undefined;
    }

    if (dynamicFor) {
      let { type, value } = dynamicFor;
      if (type === PropBindType.expression) {
        dynamicFor.value = value.replace(/^(.*?in\s)/, '');
      }
    }

    const data = deserializeAttributes(deserializeCtx, attributes);

    const { dataBinds, staticData } = splitDynamicMap(data);

    const componentInfo = componentsInfoMap[sourceKey];
    if ((componentInfo as any)?.compConfig) {
      (component.extra as any).compConfig = (componentInfo as any)?.compConfig;
    }
    if ((componentInfo as any)?.events) {
      (component.extra as any).emitEvents = (componentInfo as any)?.events.map((item) => item.name);
    } else if ((componentInfo as any)?.emitEvents) {
      (component.extra as any).emitEvents = (componentInfo as any).emitEvents.map((item) => item.eventName);
    }

    // 生成 widgets/dataBinds
    if (!isSlot(component) && component.id) {
      const parentComponent = component.extra.__parent;
      let _visible = undefined;
      switch (component.directives?.[':if']) {
        case true:
        case false: {
          _visible = component.directives?.[':if'];
          break;
        }
        case undefined: {
          _visible = true;
          break;
        }
      }
      compWidgets[component.id] = {
        ...Object.entries(staticData).reduce((data, [propsKey, propsValue]) => {
          let value = propsValue;
          if (value && Array.isArray(value)) {
            value = value.map((_, index) => {
              const block = attributes[propsKey]?.[index]; // 因为 value 里的是序列化过的，尤其是选区的是时候
              const blockInstances = block?.selectableBlock?.listeners;
              if (blockInstances) {
                block.selectableBlock.listenerInstances = generateListnerInstances(ctx, blockInstances || [], {
                  isComposite,
                  looseError: options?.looseError,
                });
                return block;
              } else {
                // 普通的非选区的，返回staticData 里的值
                return _;
              }
            });
          }
          data[propsKey] = value;
          return data;
        }, {}),
        _visible,
        style: { ...processedCommonStyle },
        classList: className.split(/\s/).filter((item) => !!item),
        widgetType: sourceKey,
        _parentId: parentComponent
          ? isSlot(parentComponent)
            ? parentComponent.extra?.__parent?.id
            : parentComponent.id
          : null,
        getConfig: () => {
          return (componentInfo as any)?.compConfig ?? {};
        },
        _waForKey: component.directives?.key,
      };

      const processedDataBinds = [...dataBinds];
      if (dynamicIf?.value) {
        processedDataBinds.push({
          type: dynamicIf.type,
          bindDataPath: dynamicIf.value,
          propertyPath: '_visible',
        });
      }
      if (dynamicFor?.value) {
        processedDataBinds.push({
          type: dynamicFor.type,
          bindDataPath: dynamicFor.value,
          propertyPath: '_waFor',
        });
      }
      if (dynamicStyle?.value) {
        processedDataBinds.push({
          type: dynamicStyle.type,
          bindDataPath: dynamicStyle.value,
          propertyPath: 'style',
        });
      }
      if (dynamicClassList) {
        processedDataBinds.push({
          type: dynamicClassList.type,
          bindDataPath: dynamicClassList.value,
          propertyPath: 'classList',
        });
      }

      if (processedDataBinds.length > 0) {
        compDataBinds[component.id] = generateDataBinds(ctx, processedDataBinds, {
          isComposite,
          looseError: options?.looseError,
        });
      }
    }

    (component as any).listenerInstances = generateListnerInstances(ctx, component.listeners || [], {
      isComposite,
      looseError: options?.looseError,
    });

    delete component.listeners;
    if (!isEmptyObj(attributes)) {
      // 如果是复合组件的根节点，则补充 wrapperClass
      if (isComposite) {
        if (!component?.extra?.__parent?.extra?.__parent) {
          component.attributes = {};
          component.attributes.class = `${component.attributes.class} ${wrapperClass}`;
        }
      }

      // 组件双向绑定
      const syncProps = componentInputProps[sourceKey];
      if (syncProps) {
        Object.keys(syncProps).forEach((syncPropKey) => {
          const syncProp = syncProps[syncPropKey];
          let syncPropArr: ISyncProp[] = syncProp ?? [];

          // 统一转成数组处理
          if (!Array.isArray(syncProp)) {
            syncPropArr = [syncProp];
          } else {
            syncPropArr = syncPropArr;
          }

          syncPropArr.forEach(({ changeEvent, valueFromEvent }) => {
            // 双向绑定需要优先第一个执行，否则会出现输入框的拼音被打断的问题

            function withContext(context) {
              let instance = new runtime.Function(
                'context',
                `return ({ event, forItems }) => {
                  const $for = forItems;
                  const wid = ${isComposite ? 'this.widgets' : 'context.$page.widgets'}.${component.id};
                  const widgetData = $for.forIndexes && $for.forIndexes.length > 0 ? context.get(wid, forItems.forIndexes) : wid;
                  widgetData.${syncPropKey} = ${valueFromEvent}
                }`,
              )(context);

              return ctx.this ? instance.bind(this) : instance;
            }
            (component as any).listenerInstances.unshift({
              trigger: changeEvent,
              instanceFunction: withContext({ ...ctx, get: lodashGet }),
            });
          });
        });
      }
    }
  });

  return {
    widgets: compWidgets,
    dataBinds: compDataBinds,
    componentSchema: componentSchemaJson,
  };
}

// convert data binds to functions for performance & simplicity
export function generateDataBinds(ctx, dataBinds: IDataBind[], options?: IGenerateOptions) {
  const dataBindFuncs = {};

  dataBinds.forEach((bind: IDataBind) => {
    if (!bind.bindDataPath) {
      return console.warn('无 bindDataPath', bind.propertyPath);
    }
    let databindFunction = () => {};
    let funcCode = `function (){}`;
    try {
      if (bind.type === PropBindType.forItem) {
        funcCode = `(forItems) => forItems.${bind.bindDataPath}`;
      } else if ((bind.type as any) === 'scope') {
        funcCode = `() => ({__type: "scopedValue", getValue: ($scope)=>$scope.${bind.bindDataPath}})}`;
      } else if (bind.type === PropBindType.expression) {
        if (options?.isComposite) {
          funcCode = ` (forItems, $context) => {const $for = forItems; return (\n${
            bind.bindDataPath.replace(/\$comp/g, 'this.$WEAPPS_COMP').replace(/;$/, '') || 'undefined'
          }\n)}`;
        } else {
          const code = bind.bindDataPath.replace(/;$/, '');
          funcCode =
            /\$scope\./.test(code) || code === '$scope'
              ? `(forItems, event, $context) => ({__type: "scopedValue", getValue: ($scope) => { const $for = forItems; return (\n${
                  code || 'undefined'
                }\n)}})`
              : `(forItems, event, $context) => { const $for = forItems; return (\n${code || 'undefined'}\n)}`;
        }
      } else if (bind.type === PropBindType.prop) {
        let { bindDataPath } = bind;
        const isNegated = bindDataPath.startsWith('!');
        if (isNegated) bindDataPath = bindDataPath.replace(/^!/, '');
        if (options?.isComposite) {
          // 复合组件在预览时其实就是page，所以沿用page的变量即可
          funcCode = `() => ${isNegated ? '!' : ''}${'this.$WEAPPS_COMP'}.props.data.${bindDataPath}`;
        } else {
          // 复合组件在预览时其实就是page，所以沿用page的变量即可
          funcCode = `() => ${isNegated ? '!' : ''}$page.props.data.${bindDataPath}`;
        }
      } else {
        const isGlobal = bind.bindDataPath.startsWith('global.');

        let returnValue = '';
        const PREFIX_MAP = {
          [PropBindType.state]: 'state',
          [PropBindType.computed]: 'computed',
          // [PropBindType.dataVar]: 'dataVar',
          [PropBindType.stateData]: 'dataset.state',
          [PropBindType.paramData]: 'dataset.params',
        };
        switch (bind.type) {
          case PropBindType.state:
          case PropBindType.computed:
          // case PropBindType.dataVar:
          case PropBindType.stateData:
          case PropBindType.paramData: {
            if (isGlobal) {
              returnValue = bind.bindDataPath.replace(/^global./, `app.${PREFIX_MAP[bind.type]}.`);
            } else {
              if (options?.isComposite) {
                returnValue = bind.bindDataPath
                  .replace(/^comp-\w+./, `this.$WEAPPS_COMP.${PREFIX_MAP[bind.type]}.`)
                  .replace(/^\$\w+_\d+./, `this.$WEAPPS_COMP.${PREFIX_MAP[bind.type]}.`);
              } else {
                returnValue = bind.bindDataPath.replace(/^\w+./, `$page.${PREFIX_MAP[bind.type]}.`);
              }
            }
            funcCode = `() => ${returnValue}`;
            break;
          }
        }
      }
      databindFunction = new runtime.Function(
        'context',
        `const { $page, app, $app } = context || {};
        return ${funcCode}`,
      )(ctx);
    } catch (e) {
      if (options?.looseError) {
        console.error('generate dynamic data error:', e);
        databindFunction = () => {
          throw e;
        };
      } else {
        throw e;
      }
    }
    dataBindFuncs[bind.propertyPath] = databindFunction;
  });
  return dataBindFuncs;
}

/**
 * dataset 特殊的绑定格式
 * 要求：入参分别为 app ， page
 * 绑定只有变量，参数，表达式的绑定
 */
export function generateDataBindForDataset(bind: IDataBind) {
  let expr = '';
  const { type, bindDataPath } = bind;
  if (type === PropBindType.static || !type) {
    expr = typeof bindDataPath === 'string' ? `'${bindDataPath}'` : JSON.stringify(bindDataPath);
  } else if (type === PropBindType.expression) {
    expr = bindDataPath;
  } else {
    const isGlobal = bindDataPath.startsWith('global.');
    const isCopositive = bindDataPath.startsWith('comp-') || bindDataPath.startsWith('$comp_');

    const PREFIX_MAP = {
      [PropBindType.state]: 'state',
      [PropBindType.computed]: 'computed',
      [PropBindType.dataVar]: 'dataVar',
      [PropBindType.stateData]: 'dataset.state',
      [PropBindType.paramData]: 'dataset.params',
    };
    switch (type) {
      case PropBindType.state:
      case PropBindType.computed:
      case PropBindType.dataVar:
      case PropBindType.stateData:
      case PropBindType.paramData: {
        if (isCopositive) {
          expr = bindDataPath
            .replace(/^comp-\w+/, `$comp.${PREFIX_MAP[type]}`)
            .replace(/^\$comp_\w+/, `$comp.${PREFIX_MAP[type]}`);
        } else {
          expr = bindDataPath
            .replace(/^\w+./, `${isGlobal ? 'app' : '$page'}.${PREFIX_MAP[type]}.`)
            .replace(/^comp-\w+/, `$comp.${PREFIX_MAP[type]}`)
            .replace(/^\$comp_\w+/, `$comp.${PREFIX_MAP[type]}`);
        }
      }
    }
  }
  return new runtime.Function('app, $page', `const $app = app; return (\n${expr || 'undefined'}\n)`);
}

function generateListnerInstances(
  ctx,
  listenerInstances: (IPlatformListener & { jsCode?: string; noPropagation?: boolean })[],
  options: IGenerateOptions = {
    isComposite: false,
  },
) {
  const { isComposite = false } = options;
  return listenerInstances.map((listener) => {
    const generatedListener: {
      key: string;
      trigger: string;
      isCapturePhase: boolean;
      noPropagation?: boolean;
      instanceFunction: Function;
      data: any;
      dataBinds: any;
    } = {
      key: listener.id,
      trigger: listener.eventName,
      isCapturePhase: !!listener.isCapturePhase,
      noPropagation: listener.noPropagation,
      instanceFunction: () => {},
      data: {},
      dataBinds: {},
    };
    if (listener.type === ActionType.Material) {
      const { name, module } = listener.handler || {};
      generatedListener.instanceFunction = new runtime.Function(
        'context',
        `const { $page, app, $app, actionsMap = {} } = context || {};
        return (...args) => actionsMap["${module}:${name}"]?.apply?.(null, args)`,
      )(ctx);
    } else if (listener.type === ActionType.PropEvent) {
      if (isComposite) {
        const instance = new runtime.Function(
          '{ data, event, forItems }',
          `
          event.detail.data = data;
          this.props.emit('${
            // TODO: CHECK listener.handler.name 具体是什么
            listener.handler.name || listener.jsCode
          }', event.detail);`,
        );
        generatedListener.instanceFunction = ctx.this ? instance.bind(this) : instance;
      } else {
        generatedListener.instanceFunction = new runtime.Function(
          'context',
          `return (...args) => context.$page.props.events.${
            listener.handler?.name || listener.jsCode
          }.apply(null, args)`,
        )(ctx);
      }
    } else if (listener.type === ActionType.Inline) {
      // 内联 handler
      const instance = new runtime.Function(
        '{event, domEvent, customEventData, data, forItems}',
        `return ${listener.jsCode}`,
      );
      if (isComposite) {
        generatedListener.instanceFunction = ctx.this ? instance.bind(this) : instance;
      } else {
        generatedListener.instanceFunction = instance.bind(ctx.$page);
      }
    } else if (listener.type === ActionType.Platform) {
      let name = listener.handler?.name || listener.jsCode;
      generatedListener.instanceFunction = new runtime.Function(
        'context',
        `return ({data}) => context.app.${name}(data)`,
      )(ctx);
    } else if (listener.type === ActionType.DataSource) {
      generatedListener.instanceFunction = new runtime.Function(
        'context',
        `return ({data}) => context.app.cloud.callDataSource(data)`,
      )(ctx);
    } else {
      // Lowcode action(handler)
      if (isComposite) {
        generatedListener.instanceFunction = new runtime.Function(
          '',
          `return this.handler.${listener.handler?.name || listener.jsCode}.bind(this)`,
        ).bind(ctx.this)();
      } else {
        let name = listener.handler?.name || listener.jsCode;
        generatedListener.instanceFunction = new runtime.Function('context', `return context.$page.handler.${name}`)(
          ctx,
        );
      }
    }

    const processedParams = deserializeDynamicMap(
      {
        page: { id: ctx.$page.id, type: EComponentType.PAGE },
      },
      listener.handler?.params,
    );

    const { staticData, dataBinds } = splitDynamicMap(processedParams);

    generatedListener.data = staticData;
    generatedListener.dataBinds = generateDataBinds(ctx, dataBinds, options);
    return generatedListener;
  });
}
