import { Directive } from './Directive';
import {
  NodeSchema,
  PropsSchema,
  JSExpression,
  ComponentProp,
  ComponentDescription,
  EventSchema,
  EventsSchema,
  BlockSchema,
  ComponentEvent,
  DirectiveSchema
} from '../types';
import {
  hashId,
  isTextChildren,
  arrayToMap,
  isJSExpression
} from '../../utils';
import { Prop } from './Prop';
import { Event } from './Event';
import { Document } from './Document';
import { Block } from './Block';
import { BLOCK_NAME, PAGE_NAME } from '../constants';
import { emitter, EVENT_NODE_CHANGE } from './../emitter';

type ChildNodeSchema = Array<NodeSchema | BlockSchema> | string | JSExpression;

function isRoot(name: string, parent: any) {
  return [BLOCK_NAME, PAGE_NAME].includes(name) && !parent;
}

export const NodeNormalPropNames: string[] = [
  'key',
  'name',
  'className',
  'style',
  'ref',
  'slotName',
  'slotParams'
];

export class Node {
  readonly id: string;
  name: string = '';
  key?: string;
  props: Record<string, Prop> = {};
  events: Record<string, Event> = {};
  directives: Record<string, Directive> = {};
  className?: string | JSExpression;
  style?: Record<string, any> | JSExpression = {};
  ref?: string;
  slotName?: string;
  slotParams?: string[];
  children?: Array<Node | Block> | string | JSExpression;

  parent: Node | Document | Block | null;
  root: Document | Block;
  // 是否可见
  visible: boolean = true;
  // 是否锁定，锁定后不可修改
  lock: boolean = false;

  // 不需要特殊处理，原样输出的属性
  normal: string[] = NodeNormalPropNames;

  constructor(schema: NodeSchema, parent?: Node | Document | Block) {
    this.id = schema.id || hashId();
    this.name = this.name || schema.name || 'div';
    this.parent = parent || null;
    this.root = isRoot(this.name, this.parent) ? (this as any) : parent?.root;

    if (this.root && this.root.nodeMap) {
      this.root.nodeMap[this.id] = this;
      this.root.addNodeNameRef(this);
    }

    this.load(schema);
  }

  dispose() {
    const children = this.children;
    if (Array.isArray(children)) {
      children.forEach((node) => node.dispose());
    }
    if (this.root && this.root.nodeMap) {
      delete this.root.nodeMap[this.id];
      this.root.removeNodeNameRef(this);
    }
    if (this.parent) {
      this.parent.removeChild(this);
    }
    this.parent = null;
  }

  load(schema: NodeSchema) {
    const { props, children, events, directives = {} } = schema;
    this.normal.forEach((key) => {
      (this as any)[key] = (schema as any)[key] ?? (this as any)[key];
    });
    this.props = this.createProps(props);
    this.children = this.createChildren(children);
    this.events = this.createEvents(events);
    this.directives = this.createDirectives(directives);
  }

  update(schema: NodeSchema, silent?: boolean) {
    const { props, children, events, directives } = schema;
    if (props) {
      this.props = this.createProps(props);
    }
    if (children !== undefined) {
      this.children = this.createChildren(children);
    }
    if (events) {
      this.events = this.createEvents(events);
    }
    if (directives) {
      this.directives = this.createDirectives(directives);
    }
    this.normal.forEach((key) => {
      const value = (schema as any)[key];
      if (typeof value !== 'undefined') {
        (this as any)[key] = value;
      }
    });
    if (!silent) {
      emitter.emit(EVENT_NODE_CHANGE, this);
    }
  }

  createChildren(
    children?: ChildNodeSchema
  ): Array<Node | Block> | string | JSExpression {
    if (!children) return [];
    if (isTextChildren(children)) {
      return children as string;
    }
    if (isJSExpression(children)) {
      return children as JSExpression;
    }
    return (children as Array<NodeSchema | BlockSchema>).map((child) =>
      child.name === BLOCK_NAME
        ? new Block(child as BlockSchema, this as any)
        : new Node(child, this)
    );
  }

  createProps(props?: PropsSchema) {
    if (!props) return {};
    const { name, root } = this;
    const componentProps: ComponentProp[] =
      root && root.componentMap ? root.componentMap[name]?.props || [] : [];
    const componentPropsMap = arrayToMap(componentProps, 'name');
    const result: Record<string, Prop> = {};
    for (const [name, value] of Object.entries(props)) {
      const map = componentPropsMap[name] || {};
      const opts = {
        name,
        value: value ?? map.defaultValue,
        defaultValue: map.defaultValue
      };
      result[name] = new Prop(opts);
    }
    return result;
  }

  createEvents(events?: EventsSchema) {
    if (!events) return {};
    const result: Record<string, Event> = {};
    for (const [name, event] of Object.entries(events)) {
      result[name] = new Event(this, event);
    }
    return result;
  }

  createDirectives(directives: Record<string, DirectiveSchema>) {
    if (!directives) return {};
    const result: Record<string, Directive> = {};
    for (const [name, directive] of Object.entries(directives)) {
      result[name] = new Directive(this, directive);
    }
    return result;
  }

  getPropsSchema(props: Record<string, Prop>): PropsSchema {
    const schema: PropsSchema = {};
    Object.entries(props).forEach(([key, prop]) => {
      if (!prop.isUnset) {
        schema[key] = prop.getValue();
      }
    });
    return schema;
  }

  getEventsShema(evnets: Record<string, Event>): EventsSchema {
    const schema: EventsSchema = {};
    Object.entries(evnets).forEach(([key, event]) => {
      schema[key] = event.getSchema();
    });
    return schema;
  }
  getDirectivesShema(directives: Record<string, Directive>) {
    const schema: Record<string, DirectiveSchema> = {};
    Object.entries(directives).forEach(([key, directive]) => {
      schema[key] = directive.getSchema();
    });
    return schema;
  }

  getChildrenSchema(
    children: Array<Node | Block> | string | JSExpression = []
  ): ChildNodeSchema {
    if (isTextChildren(children)) {
      return children as string;
    }
    if (isJSExpression(children)) {
      return children as JSExpression;
    }
    return (children as Array<Node | Block>).map((n) => n.toSchema());
  }

  toSchema(): NodeSchema {
    const schema = this.normal.reduce((result, key) => {
      result[key] = (this as any)[key];
      return result;
    }, {} as Record<string, any>);

    const { id, name, props, events, directives, children } = this;

    return {
      ...schema,
      id,
      name,
      props: this.getPropsSchema(props),
      children: this.getChildrenSchema(children),
      events: this.getEventsShema(events),
      directives: this.getDirectivesShema(directives)
    } as NodeSchema;
  }

  removeChild(node: Node | Block) {
    const children = this.children;
    if (Array.isArray(children)) {
      const index = children.indexOf(node);
      if (index > -1) {
        node.parent = null;
        children.splice(index, 1);
      }
    }
    emitter.emit(EVENT_NODE_CHANGE, this);
  }

  appendChild(node: Node | Block) {
    const children = this.children;
    if (Array.isArray(children)) {
      node.parent = this;
      node.root = this.root;
      children.push(node);
    }
    emitter.emit(EVENT_NODE_CHANGE, this);
  }

  insertAfter(node: Node | Block, after: Node | Block) {
    const children = this.children;
    if (Array.isArray(children)) {
      const index = children.indexOf(after);
      if (index > -1) {
        node.parent = this;
        children.splice(index + 1, 0, node);
      }
    }
    emitter.emit(EVENT_NODE_CHANGE, this);
  }

  insertBefore(node: Node | Block, before: Node | Block) {
    const children = this.children;
    if (Array.isArray(children)) {
      const index = children.indexOf(before);
      if (index > -1) {
        node.parent = this;
        children.splice(index, 0, node);
      }
    }
    emitter.emit(EVENT_NODE_CHANGE, this);
  }

  appendSchema(schema: NodeSchema | BlockSchema, index?: number): Node {
    const node =
      schema.name === BLOCK_NAME
        ? new Block(schema as BlockSchema, this as any)
        : new Node(schema as NodeSchema, this as any);
    const children = this.children;
    if (Array.isArray(children)) {
      if (index === undefined || index > children.length) {
        children.push(node);
      } else {
        children.splice(index, 0, node);
      }
    } else {
      this.children = [node];
    }
    emitter.emit(EVENT_NODE_CHANGE, this);
    return node;
  }

  moveNext() {
    const parent = this.parent;
    if (parent) {
      const children = parent.children;
      if (Array.isArray(children)) {
        const index = children.indexOf(this);
        if (index > -1 && index < children.length - 1) {
          children.splice(index, 1);
          children.splice(index + 1, 0, this);
          emitter.emit(EVENT_NODE_CHANGE, parent);
        }
      }
    }
  }

  movePrev() {
    const parent = this.parent;
    if (parent) {
      const children = parent.children;
      if (Array.isArray(children)) {
        const index = children.indexOf(this);
        if (index > 0) {
          children.splice(index, 1);
          children.splice(index - 1, 0, this);
          emitter.emit(EVENT_NODE_CHANGE, parent);
        }
      }
    }
  }

  copy() {
    const children = this.parent?.children;
    if (Array.isArray(children)) {
      const index = children.indexOf(this);
      const schema = this.toSchema();
      delete schema.id;
      const node = this.parent?.appendSchema(schema, index + 1);
      emitter.emit(EVENT_NODE_CHANGE, this.parent);
      return node;
    }
  }

  allowDrop(desc: ComponentDescription) {
    const { root, name } = this;
    if (!root || !desc) return false;
    const { parentIncludes } = desc;
    const mathParent =
      parentIncludes === true ||
      (parentIncludes !== false &&
        (!parentIncludes || (parentIncludes && parentIncludes.includes(name))));

    const currentDesc = root.componentMap[name] || {};
    const childIncludes = currentDesc.childIncludes;
    const matchChild =
      childIncludes === true ||
      (childIncludes !== false &&
        (!childIncludes || childIncludes.includes(desc.name)));
    return mathParent && matchChild;
  }

  setPropValue(name: string, value: any) {
    const prop = this.props[name];
    if (prop) {
      prop.setValue(value);
    } else {
      const { root } = this;
      const componentProps: ComponentProp[] =
        root && root.componentMap
          ? root.componentMap[this.name]?.props || []
          : [];
      const componentPropsMap = arrayToMap(componentProps, 'name');
      const map = componentPropsMap[name] || {};
      const opts = {
        name,
        value,
        defaultValue: map.defaultValue
      };
      this.props[name] = new Prop(opts);
    }
    emitter.emit(EVENT_NODE_CHANGE, this);
  }

  bindEvent(name: string, event: EventSchema) {
    const binding = this.events[name];
    if (binding) {
      binding.update(event);
    } else {
      this.events[name] = new Event(this, event);
    }
    emitter.emit(EVENT_NODE_CHANGE, this);
  }

  unbindEvent(name: string) {
    delete this.events[name];
    emitter.emit(EVENT_NODE_CHANGE, this);
  }

  setDirective(name: string, schema: DirectiveSchema) {
    const binding = this.directives[name];
    if (binding) {
      binding.update(schema);
    } else {
      this.directives[name] = new Directive(this, schema);
    }
    emitter.emit(EVENT_NODE_CHANGE, this);
  }

  removeDirective(name: string) {
    delete this.directives[name];
    emitter.emit(EVENT_NODE_CHANGE, this);
  }
}
