import {
  NodeSchema,
  BlockSchema,
  DocumentSchema,
  EventSchema,
  IFileSchema
} from '../types';
import { PAGE_NAME, BLOCK_NAME, LIFE_CYCLES_LIST } from '../constants';
import {
  isTextChildren,
  isJSExpression,
  parseExpression,
  isJSFunction,
  parseFunction,
  upperFirstCamelCase,
  logger,
  cloneDeep
} from '../../utils';
import { IFile } from '../models';

import { IHandlersConfig } from '../config';

import { vIf, vShow, vModel, vOn, vBind, vFor, hasVShow } from './directives';

type CompositeNodeScheam = NodeSchema | BlockSchema | DocumentSchema;

enum EventModifier {
  passive = 'passive',
  capture = 'capture',
  once = 'once',
  stop = 'stop',
  prevent = 'prevent',
  self = 'self'
}

function createEventType(type: string, event: EventSchema) {
  const SuffixModifiers = [
    EventModifier.capture,
    EventModifier.once,
    EventModifier.passive
  ];
  // 存在需要挂载后缀的事件修饰符
  const suffix = event.modifiers?.find((n) =>
    SuffixModifiers.includes(n as any)
  );
  if (suffix) {
    const map = {
      capture: 'Capture',
      once: 'Once',
      passive: 'OnceCapture'
    };
    return `on${upperFirstCamelCase(type)}${(map as any)[suffix]}`;
  }
  return `on${upperFirstCamelCase(type)}`;
}

function createEvents(
  context: any,
  events: Record<string, EventSchema> = {},
  Vue: any
) {
  const result: Record<string, any> = {};
  for (let [name, value] of Object.entries(events)) {
    const evnetType = createEventType(name, value);
    if (isJSFunction(value.handler)) {
      const modifiers = value.modifiers || [];
      const func = parseFunction(value.handler, context);
      result[evnetType] = Vue.withModifiers(func, modifiers);
    }
  }
  return result;
}

function createNodeProps(
  context: any,
  schema: CompositeNodeScheam,
  model: IFile,
  Vue: any
) {
  const {
    id,
    props = {},
    className,
    style,
    ref,
    key,
    events,
    directives
  } = schema;
  const result: Record<string, any> = {};
  // Vue组件 ref, 在设计模式时，包裹组件设置ref提供给设计用
  result.ref = model
    ? (el: any) => {
        if (!el || !id) return;

        model.instanceMap[id] = el;
        let dom = el.$el || el;
        // 如果是text节点，取兄弟节点
        if (dom.nodeType === 3 && dom.nextSibling) {
          dom = dom.nextSibling;
        }
        dom.__vtj__ = id;
        // todo: ref 支持函数
        if (ref && context.__vtj_refs__) {
          context.__vtj_refs__[ref] = el;
        }
      }
    : ref;

  // 组件props
  for (let [name, value] of Object.entries(props)) {
    result[name] =
      isJSExpression(value) || isJSFunction(value)
        ? parseExpression(value, context)
        : value;
  }

  // 组件key
  if (key) {
    result.key = isJSExpression(key) ? parseExpression(key, context) : key;
  }

  // 样式类名
  if (className) {
    result['class'] = isJSExpression(className)
      ? parseExpression(className, context)
      : className;
  }

  // style 样式
  if (style) {
    result.style = isJSExpression(style)
      ? parseExpression(style, context)
      : style;
  }

  // 处理v-show指令
  if (directives && hasVShow(directives)) {
    result.style = Object.assign(result.style || {}, {
      display: vShow(directives, context) ? '' : 'none'
    });
  }

  // 组件事件
  if (events) {
    Object.assign(result, createEvents(context, events, Vue));
  }

  // 处理 v-bind
  Object.assign(result, vBind(directives, context));

  // 处理 v-on
  Object.assign(result, vOn(directives, context));

  // 处理v-model
  Object.assign(result, vModel(directives, context));

  return result;
}

function createSlotsChildren(
  Vue: any,
  context: any,
  schema: CompositeNodeScheam,
  components: Record<string, any>,
  model: IFile,
  handlers: IHandlersConfig,
  global: Window,
  parentId: string
) {
  const slots: Record<string, CompositeNodeScheam[]> = {
    default: []
  };
  const children = schema.children as CompositeNodeScheam[];
  children.forEach((n) => {
    const slotName = n.slotName || 'default';
    if (slotName) {
      if (!slots[slotName]) {
        slots[slotName] = [n];
      } else {
        slots[slotName].push(n);
      }
    }
  });
  const result: Record<string, any> = {};
  Object.keys(slots).forEach((name) => {
    const items = slots[name];
    const cloneParentContext = cloneDeep(context.context[parentId]);
    result[name] = ((items, parentContext) => {
      return (scope: any) => {
        return items.map((child: CompositeNodeScheam) => {
          // 记录插槽参数上下文;
          setContext(context, child.id as string, parentId, {
            ...parentContext,
            ...scope
          });
          return child.name === BLOCK_NAME
            ? Vue.h(createSchemaRenderer(Vue, global), {
                schema: child,
                components,
                model,
                handlers,
                ...createNodeProps(context, child, model, Vue)
              })
            : createNode(
                Vue,
                context,
                child,
                components,
                model,
                handlers,
                global,
                parentId
              );
        });
      };
    })(items, cloneParentContext);
  });
  return result;
}

function createChildren(
  Vue: any,
  context: any,
  schema: CompositeNodeScheam,
  components: Record<string, any>,
  model: IFile,
  handlers: IHandlersConfig,
  global: Window,
  parentId: string
) {
  let children: any;
  // 文本节点
  if (isTextChildren(schema.children)) {
    children = schema.children;
    // 表达式的节点
  } else if (isJSExpression(schema.children)) {
    children = parseExpression(schema.children, context);
  } else {
    // 多个子节点
    children = createSlotsChildren(
      Vue,
      context,
      schema,
      components,
      model,
      handlers,
      global,
      parentId
    );
  }
  return children;
}

function setContext(context: any, id: string, parentId?: string, value?: any) {
  if (!context.context[id]) {
    context.context[id] = {};
    if (parentId) {
      const parentContext = context.context[parentId] || {};
      context.context[id].__proto__ = parentContext;
    }
  }
  Object.assign(context.context[id], value || {});
}

function createNode(
  Vue: any,
  context: any,
  schema: CompositeNodeScheam,
  components: Record<string, any> = {},
  model: IFile,
  handlers: IHandlersConfig,
  global: Window,
  parentId?: string
) {
  // 模型设置了不可见，不渲染
  if (model && schema.id) {
    const node = model.nodeMap[schema.id];
    if (node && !node.visible) return null;
  }

  // 记录上下文变量
  setContext(context, schema.id as string, parentId);

  // 无标签名，不处理
  if (!schema?.name) return null;

  // 检测组件和html标签
  let component = components[schema.name] || schema.name;
  // 无标签名，不需要处理
  if (!component) {
    return null;
  }

  // 匹配 v-if 指令，v-if=false, 不渲染
  const condition = vIf(schema.directives, context);
  if (!condition) return null;

  // todo:  用div标签包裹页面和区块，仅测试，后续需要删除
  if ([PAGE_NAME, BLOCK_NAME].includes(component)) {
    component = 'div';
  }

  return vFor(schema.id as string, schema.directives, context, () => {
    return Vue.h(
      component,
      createNodeProps(context, schema, model, Vue),
      createChildren(
        Vue,
        context,
        schema,
        components,
        model,
        handlers,
        global,
        schema.id as string
      )
    );
  });
}

function createLifecycles() {
  const result: Record<string, any> = {};
  for (let name of LIFE_CYCLES_LIST) {
    result[name] = function () {
      const { schema } = this;
      const hook = schema?.lifeCycles ? schema.lifeCycles[name] : null;
      if (!hook) return;
      const fn = isJSFunction(hook) ? parseFunction(hook, this as any) : null;
      try {
        fn && fn.apply(this, arguments);
      } catch (e) {
        logger.error(`生命周期 ${name} 出错`);
      }
    };
  }
  return result;
}

function createState(context: any, schema: IFileSchema) {
  const result: Record<string, any> = {};
  const state = schema.state || {};
  for (let [name, value] of Object.entries(state)) {
    result[name] =
      isJSExpression(value) || isJSFunction(value)
        ? parseExpression(value, context)
        : value;
  }
  return result;
}

function createMethods(context: any, schema: IFileSchema) {
  const result: Record<string, any> = {};
  const methods = schema.methods || {};
  for (let [name, value] of Object.entries(methods)) {
    if (isJSFunction(value)) {
      result[name] = parseFunction(value, context);
    } else {
      logger.warn(`method ${name} is not a function`);
    }
  }
  return result;
}

function createComputed(Vue: any, context: any, schema: IFileSchema) {
  const result: Record<string, any> = {};
  const computed = schema.computed || {};
  for (let [name, value] of Object.entries(computed)) {
    if (isJSFunction(value)) {
      result[name] = Vue.computed(parseFunction(value, context));
    } else {
      logger.warn(`method ${name} is not a function`);
    }
  }
  return result;
}

function createWatch(Vue: any, context: any, schema: IFileSchema) {
  const watchers: (() => void)[] = [];
  const watch = schema.watch || {};
  for (let [name, value] of Object.entries(watch)) {
    const watchFunction = parseFunction(
      {
        type: 'JSFunction',
        value: `(function(){ return ${name};})`
      },
      context
    );
    const handler = parseFunction(value.handler, context);
    const w = Vue.watch(watchFunction, handler, {
      deep: value.deep,
      immediate: value.immediate
    });
    watchers.push(w);
  }
  return watchers;
}

function createDataSources(context: any, schema: IFileSchema) {
  const { handlers = {} } = context;
  const dataSources = Object.values(schema.dataSources || {});
  const result: Record<string, any> = {};
  const dataSourceFactory = (handlers as IHandlersConfig).dataSourceFactory;
  if (!dataSourceFactory) return result;
  for (let n of dataSources) {
    const item = cloneDeep(n);
    const transform = item.transform;
    if (transform && isJSFunction(transform)) {
      item.transform = parseFunction(transform, context);
    }
    result[item.id] = dataSourceFactory(item);
  }
  return result;
}

function adoptedStyleSheets(global: Window, id: string, css: string) {
  const styleSheet = new (global as any).CSSStyleSheet();
  styleSheet.id = id;
  styleSheet.replaceSync(css);
  const doc: any = global.document;
  const adoptedStyleSheets = doc.adoptedStyleSheets;
  const sheets = Array.from(adoptedStyleSheets).filter((n: any) => n.id !== id);
  doc.adoptedStyleSheets = [...sheets, styleSheet];
}

export function createSchemaRenderer(Vue: any, global: Window = self) {
  const mixin = {
    data() {
      return {
        __vtj_refs__: Vue.markRaw({}),
        __watchers__: Vue.markRaw([])
      };
    },
    mounted() {
      const _self = this as any;
      // todo: Block之间没有隔离，同一个页面中的ref名称相同会被覆盖，待优化
      //@ts-ignore
      Object.assign(_self.$refs.__proto__, _self.__vtj_refs__);
      const schema: IFileSchema = _self.schema || {};
      const { id, css = '' } = schema;
      if (id) {
        adoptedStyleSheets(global, id, css);
      }
    },
    created() {
      const _self = this as any;
      const { model, schema } = _self;
      if (model) {
        model.renderer = _self;
      }
      _self.__watchers__ = createWatch(Vue, _self, schema);
    },
    beforeUnmount() {
      const _self = this as any;
      const { model } = _self;
      const watchers: (() => void)[] = _self.__watchers__ || [];
      watchers.forEach((unwatch) => unwatch());
      if (model) {
        model.renderer = null;
        model.instanceMap = {};
      }
    }
  };

  return Vue.defineComponent({
    name: 'SchemaRenderer',
    mixins: [mixin],
    props: {
      schema: {
        type: Object,
        default() {
          return {};
        }
      },
      components: Object,
      // 页面或区块 IFile
      model: Object,
      handlers: {
        type: Object,
        default() {
          return Object.create(null);
        }
      }
    },
    data() {
      const _self = this as any;
      const { schema } = _self;
      return {
        // // 设置非响应式上下文
        context: Vue.markRaw({}),
        state: createState(_self, schema),
        computed: createComputed(Vue, _self, schema),
        dataSources: Vue.markRaw(createDataSources(_self, schema)),
        methods: Vue.markRaw(createMethods(_self, schema))
      };
    },
    render() {
      const { schema, components = {}, model, handlers } = this as any;
      return createNode(Vue, this, schema, components, model, handlers, global);
    },
    ...createLifecycles()
  });
}
