import { ISchema, Schema, SchemaOptionsContext, useField, useFieldSchema } from '@formily/react';
import { uid } from '@formily/shared';
import get from 'lodash/get';
import set from 'lodash/set';
import React, { useContext } from 'react';
import { SchemaComponentContext } from '../context';

interface CreateDesignableProps {
  current: Schema;
}

export function createDesignable(options: CreateDesignableProps) {
  return new Designable(options);
}

/**
 *
 */
type Position = 'beforeBegin' | 'afterBegin' | 'beforeEnd' | 'afterEnd';

interface InsertAdjacentOptions {
  wrap?: (s: ISchema) => ISchema;
  removeEmptyParents?: boolean;
}

const generateUid = (s: ISchema) => {
  if (!s['x-uid']) {
    s['x-uid'] = uid();
  }
  Object.keys(s.properties || {}).forEach((key) => {
    generateUid(s.properties[key]);
  });
};

const defaultWrap = (s: ISchema) => s;

export class Designable {
  current: Schema;

  events = {};

  constructor({ current }) {
    this.current = current;
  }

  prepareProperty(schema: ISchema) {
    if (!schema.type) {
      schema.type = 'void';
    }
    if (!schema.name) {
      schema.name = uid();
    }
    // x-uid 仅用于后端查询 schema，如果 current 没有 x-uid 不处理
    if (!this.current['x-uid']) {
      return;
    }
    // if (Schema.isSchemaInstance(schema)) {
    //   return;
    // }
    generateUid(schema);
  }

  on(name: 'afterInsertAdjacent' | 'afterRemove', listener: any) {
    if (!this.events[name]) {
      this.events[name] = [];
    }
    this.events[name].push(listener);
  }

  emit(name: 'afterInsertAdjacent' | 'afterRemove', ...args) {
    if (!this.events[name]) {
      return;
    }
    this.events[name].forEach((fn) => fn.bind(this)(...args));
  }

  insertAdjacent(position: Position, schema: ISchema, options: InsertAdjacentOptions = {}) {
    switch (position) {
      case 'beforeBegin':
        return this.insertBeforeBegin(schema, options);
      case 'afterBegin':
        return this.insertAfterBegin(schema, options);
      case 'beforeEnd':
        return this.insertBeforeEnd(schema, options);
      case 'afterEnd':
        return this.insertAfterEnd(schema, options);
    }
  }

  removeIfChildrenEmpty(schema?: Schema) {
    if (!schema) {
      return;
    }
    let s = schema;
    const count = Object.keys(s.properties || {}).length;
    console.log('removeIfChildrenEmpty', count, s.properties);
    if (count > 0) {
      return;
    }
    let removed;
    while (s.parent) {
      removed = s.parent.removeProperty(s.name);
      const count = Object.keys(s.parent.properties || {}).length;
      if (count > 0) {
        break;
      }
      s = s.parent;
    }
  }

  remove(schema?: Schema, options: { removeEmptyParents?: boolean } = {}) {
    const { removeEmptyParents } = options;
    let s = schema || this.current;
    let removed;
    while (s.parent) {
      removed = s.parent.removeProperty(s.name);
      if (!removeEmptyParents) {
        break;
      }
      const count = Object.keys(s.parent.properties || {}).length;
      if (count > 0) {
        break;
      }
      s = s.parent;
    }
    this.emit('afterRemove', removed);
  }

  insertBeforeBeginOrAfterEnd(schema: ISchema, options: InsertAdjacentOptions = {}) {
    if (!Schema.isSchemaInstance(this.current)) {
      return;
    }
    if (!Schema.isSchemaInstance(schema)) {
      return;
    }
    if (this.current.parent !== schema.parent) {
      return;
    }
    let fromIndex = 0;
    let toIndex = 0;
    this.current.parent.mapProperties((property, key, index) => {
      if (this.current.name === key) {
        toIndex = index;
      }
      if (schema.name === key) {
        fromIndex = index;
      }
    });
    return fromIndex > toIndex ? this.insertBeforeBegin(schema, options) : this.insertAfterEnd(schema, options);
  }

  /**
   * Before the current schema itself.
   */
  insertBeforeBegin(schema: ISchema, options: InsertAdjacentOptions = {}) {
    if (!Schema.isSchemaInstance(this.current)) {
      return;
    }
    const { wrap = defaultWrap, removeEmptyParents } = options;
    const properties = {};
    let start = false;

    if (Schema.isSchemaInstance(schema)) {
      schema.parent.removeProperty(schema.name);
      if (removeEmptyParents) {
        this.removeIfChildrenEmpty(schema.parent);
      }
    }

    this.current.parent.mapProperties((property, key) => {
      if (key === this.current.name) {
        start = true;
      }
      if (start) {
        properties[key] = property;
        this.current.parent.removeProperty(key);
      }
    });

    this.prepareProperty(schema);
    const wrapped = wrap(schema);
    const s = this.current.parent.addProperty(wrapped.name, wrapped);
    s.parent = this.current.parent;
    this.current.parent.setProperties(properties);
    this.emit('afterInsertAdjacent', 'beforeBegin', s);
  }

  /**
   * Just inside the current schema, before its first child.
   *
   * @param schema
   * @returns
   */
  insertAfterBegin(schema: ISchema, options: InsertAdjacentOptions = {}) {
    if (!Schema.isSchemaInstance(this.current)) {
      return;
    }
    const { wrap = defaultWrap, removeEmptyParents } = options;
    if (Schema.isSchemaInstance(schema)) {
      schema.parent.removeProperty(schema.name);
      if (removeEmptyParents) {
        this.removeIfChildrenEmpty(schema.parent);
      }
    }
    const properties = {};
    this.current.mapProperties((schema, key) => {
      properties[key] = schema;
    });
    this.current.properties = {};
    this.prepareProperty(schema);
    const wrapped = wrap(schema);
    const s = this.current.addProperty(wrapped.name, wrapped);
    s.parent = this.current;
    this.current.setProperties(properties);
    this.emit('afterInsertAdjacent', 'afterBegin', s);
  }

  /**
   * Just inside the targetElement, after its last child.
   *
   * @param schema
   * @returns
   */
  insertBeforeEnd(schema: ISchema, options: InsertAdjacentOptions = {}) {
    if (!Schema.isSchemaInstance(this.current)) {
      return;
    }
    const { wrap = defaultWrap, removeEmptyParents } = options;
    if (Schema.isSchemaInstance(schema)) {
      schema.parent.removeProperty(schema.name);
      if (removeEmptyParents) {
        this.removeIfChildrenEmpty(schema.parent);
      }
    }
    this.prepareProperty(schema);
    const wrapped = wrap(schema);
    const s = this.current.addProperty(wrapped.name || uid(), wrapped);
    s.parent = this.current;
    this.emit('afterInsertAdjacent', 'beforeEnd', s);
  }

  /**
   * After the current schema itself.
   */
  insertAfterEnd(schema: ISchema, options: InsertAdjacentOptions = {}) {
    if (!Schema.isSchemaInstance(this.current)) {
      return;
    }
    const properties = {};
    let start = false;
    const { wrap = defaultWrap, removeEmptyParents } = options;
    if (Schema.isSchemaInstance(schema)) {
      schema.parent.removeProperty(schema.name);
      console.log('insertAfterEnd', removeEmptyParents);
      if (removeEmptyParents) {
        this.removeIfChildrenEmpty(schema.parent);
      }
      schema.parent = null;
    }

    this.current.parent.mapProperties((property, key) => {
      if (key === this.current.name) {
        start = true;
      }
      if (start && key !== this.current.name) {
        properties[key] = property;
        this.current.parent.removeProperty(key);
      }
    });

    this.prepareProperty(schema);

    const wrapped = wrap(schema);

    const s = this.current.parent.addProperty(wrapped.name || uid(), wrapped);
    s.parent = this.current.parent;
    this.current.parent.setProperties(properties);
    this.emit('afterInsertAdjacent', 'afterEnd', s);
  }
}

// TODO
export function useDesignable() {
  const { designable, setDesignable, refresh, reset } = useContext(SchemaComponentContext);
  const { components } = useContext(SchemaOptionsContext);
  const DesignableBar = () => {
    return <></>;
  };
  const field = useField();
  const fieldSchema = useFieldSchema();
  const dn = createDesignable({ current: fieldSchema });
  dn.on('afterInsertAdjacent', refresh);
  dn.on('afterRemove', refresh);
  return {
    designable,
    reset,
    refresh,
    setDesignable,
    DesignableBar,
    findComponent(component: any) {
      if (!component) {
        return null;
      }
      if (typeof component !== 'string') {
        return component;
      }
      return get(components, component);
    },
    on: dn.on.bind(dn),
    // TODO
    patch: (key: ISchema | string, value?: any) => {
      const update = (obj: any) => {
        Object.keys(obj).forEach((k) => {
          const val = obj[k];
          if (k === 'title') {
            field.title = val;
            fieldSchema['title'] = val;
          }
          if (k === 'x-component-props') {
            Object.keys(val).forEach((i) => {
              field.componentProps[i] = val[i];
              fieldSchema['x-component-props'][i] = val[i];
            });
          }
        });
      };
      if (typeof key === 'string') {
        const obj = {};
        set(obj, key, value);
        return update(obj);
      }
      update(key);
      refresh();
    },
    remove() {
      dn.remove();
    },
    insertAdjacent(position: Position, schema: ISchema, options: InsertAdjacentOptions = {}) {
      dn.insertAdjacent(position, schema, options);
    },
    insertBeforeBegin(schema: ISchema) {
      dn.insertBeforeBegin(schema);
    },
    insertAfterBegin(schema: ISchema) {
      dn.insertAfterBegin(schema);
    },
    insertBeforeEnd(schema: ISchema) {
      dn.insertBeforeEnd(schema);
    },
    insertAfterEnd(schema: ISchema) {
      dn.insertAfterEnd(schema);
    },
  };
}
