import { ComponentType, ReactElement, isValidElement, ComponentClass } from 'react';
import { isPlainObject } from '@ali/lowcode-utils';
import { isI18nData, SettingTarget, InitialItem, FilterItem, isJSSlot, isJSExpression } from '@ali/lowcode-types';

type Field = SettingTarget;

export enum DISPLAY_TYPE {
  NONE = 'none', // => condition'plain'
  PLAIN = 'plain',
  INLINE = 'inline',
  BLOCK = 'block',
  ACCORDION = 'accordion',
  TAB = 'tab', // => 'accordion'
  ENTRY = 'entry',
}

// from vision 5.4
export interface OldPropConfig {
  /**
   * composite share the namespace
   * group just be tie up together
   */
  type?: 'composite' | 'group'; // => composite as objectSetter
  /**
   * when type is composite or group
   */
  items?: OldPropConfig[]; // => items
  /**
   * property name: the field key in props of schema
   */
  name: string; // =>
  title?: string; // =>
  tip?: {
    content?: string;
    url?: string;
  };
  defaultValue?: any; // => extraProps.defaultValue
  initialValue?: any | ((value: any, defaultValue: any) => any); // => initials.initialValue
  initial?: (value: any, defaultValue: any) => any; // => initials.initialValue

  display?: DISPLAY_TYPE; // => fieldExtraProps
  fieldStyle?: DISPLAY_TYPE; // => fieldExtraProps
  setter?: ComponentClass | ISetterConfig[] | string | SetterGetter; // =>
  supportVariable?: boolean; // => use MixedSetter
  /**
   * the prop should be collapsed while display value is accordion
   */
  collapse?: boolean; // => extraProps.defaultCollapsed
  /**
   * alias to collapse
   */
  collapsed?: boolean; // => extraProps.defaultCollapsed
  fieldCollapsed?: boolean; // => extraProps.defaultCollapsed
  /**
   * if a prop is declared as disabled, it will not be saved into
   * schema
   */
  disabled?: boolean | ReturnBooleanFunction; // => hide & virtual ? thinkof global transform
  /**
   * will not export data to schema
   */
  ignore?: boolean | ReturnBooleanFunction; // => ?virtualProp ? thinkof global transform
  hidden?: boolean | ReturnBooleanFunction; // => condition

  /**
   * when use getValue(), accessor shall be called as initializer
   */
  accessor?(this: Field, value: any): any; // => getValue
  /**
   * when current prop value mutate, the mutator function shall be called
   */
  mutator?( // => setValue
    this: Field,
    value: any,
    hotValue: any,
  ): /*
    preValue: any, // => x
    preHotValue: any, // => x
    */
  void;
  /**
   * other values' change will trigger sync function here
   */
  sync?(this: Field, value: any): void; // => autorun
  /**
   * user click var to change current field to
   * variable setting field
   */
  useVariableChange?(this: Field, data: { isUseVariable: boolean }): any; // => as MixinSetter param

  slotName?: string;
  slotTitle?: string;
  initialChildren?: any; // schema
  allowTextInput: boolean;
  liveTextEditing?: any;
}

// from vision 5.4
export interface OldPrototypeConfig {
  packageName: string; // => npm.package
  /**
   * category display in the component pane
   * component will be hidden while the value is: null
   */
  category: string; // => tags
  componentName: string; // =>
  docUrl?: string; // =>
  defaultProps?: any; // => ?
  /**
   * extra actions on the outline of current selected node
   * by default we have: remove / clone
   */
  extraActions?: Array<ComponentType<any> | ReactElement> | (() => ReactElement); // => configure.component.actions
  title?: string; // =>
  icon?: ComponentType<any> | ReactElement; // =>
  view: ComponentType; // => ?
  initialChildren?: (props: any) => any[]; // => snippets

  /**
   * Props configurations of node
   */
  configure: OldPropConfig[]; // => configure.props
  snippets?: any[]; // => snippets
  transducers?: any; // => ?
  /**
   * Selector expression rectangle of a node, it is usually a querySelector string
   * @example '.classname > div'
   */
  rectSelector?: string; // => configure.component.rectSelector
  context?: {
    // => ?
    [contextInfoName: string]: any;
  };

  isContainer?: boolean; // => configure.component.isContainer
  isModal?: boolean; // => configure.component.isModal
  isFloating?: boolean; // => configure.component.isFloating
  descriptor?: string; // => configure.component.descriptor

  // alias to canDragging
  canDraging?: boolean; // => onDrag
  canDragging?: boolean; // => ?

  canOperating?: boolean; // => disabledActions
  canUseCondition?: boolean;
  canLoop?: boolean;
  canContain?: (dragment: Node) => boolean; // => nestingRule

  canDropTo?: ((container: Node) => boolean) | boolean | string | string[]; // => nestingRule
  canDropto?: ((container: Node) => boolean) | boolean | string | string[]; // => nestingRule

  canDropIn?: ((dragment: Node) => boolean) | boolean | string | string[]; // => nestingRule
  canDroping?: ((dragment: Node) => boolean) | boolean | string | string[]; // => nestingRule

  didDropOut?: (dragment: any, container: any) => void; // => hooks
  didDropIn?: (dragment: any, container: any) => void; // => hooks

  /**
   * when sub-node of the current node changed
   * including: sub-node insert / remove
   */
  subtreeModified?(this: Node): any; // => ? hooks

  // => ?
  canResizing?: ((dragment: any, triggerDirection: string) => boolean) | boolean;
  onResizeStart?: (e: MouseEvent, triggerDirection: string, dragment: Node) => void;
  onResize?: (e: MouseEvent, triggerDirection: string, dragment: Node, moveX: number, moveY: number) => void;
  onResizeEnd?: (e: MouseEvent, triggerDirection: string, dragment: Node) => void;
}

export interface ISetterConfig {
  setter?: ComponentClass;
  // use value to decide whether this setter is available
  condition?: (value: any) => boolean;
}

type SetterGetter = (this: Field, value: any) => ComponentClass;

type ReturnBooleanFunction = (this: Field, value: any) => boolean;

export function upgradePropConfig(config: OldPropConfig, collector: ConfigCollector) {
  const {
    type,
    name,
    title,
    tip,
    slotName,
    slotTitle,
    initialChildren,
    allowTextInput,
    initialValue,
    defaultValue,
    display,
    fieldStyle,
    collapse,
    collapsed,
    fieldCollapsed,
    hidden,
    disabled,
    items,
    ignore,
    initial,
    sync,
    accessor,
    mutator,
    setter,
    useVariableChange,
    supportVariable,
    liveTextEditing,
  } = config;

  const extraProps: any = {};
  const newConfig: any = {
    type: type === 'group' ? 'group' : 'field',
    name,
    title,
    extraProps,
  };

  if (tip) {
    if (typeof title !== 'object' || isI18nData(title) || isValidElement(title)) {
      newConfig.title = {
        label: title,
        tip: tip.content,
        docUrl: tip.url,
      };
    } else {
      newConfig.title = {
        ...(title as any),
        tip: tip.content,
        docUrl: tip.url,
      };
    }
  }

  if (display || fieldStyle) {
    extraProps.display = display || fieldStyle;
    if (extraProps.display === DISPLAY_TYPE.TAB) {
      extraProps.display = DISPLAY_TYPE.ACCORDION;
    }
  }

  if (collapse || collapsed || fieldCollapsed || extraProps.display === DISPLAY_TYPE.ENTRY) {
    extraProps.defaultCollapsed = true;
  }
  function isDisabled(field: Field) {
    if (typeof disabled === 'function') {
      return disabled.call(field, field.getValue()) === true;
    }
    return disabled === true;
  }
  function isHidden(field: Field) {
    if (typeof hidden === 'function') {
      return hidden.call(field, field.getValue()) === true;
    }
    return hidden === true;
  }
  if (extraProps.display === DISPLAY_TYPE.NONE) {
    extraProps.display = undefined;
    extraProps.condition = () => false;
  } else if (hidden != null || disabled != null) {
    extraProps.condition = (field: Field) => !(isHidden(field) || isDisabled(field));
  }

  if (type === 'group') {
    newConfig.items = items ? upgradeConfigure(items, collector) : [];
    return newConfig;
  }

  if (defaultValue !== undefined) {
    extraProps.defaultValue = defaultValue;
  } else if (typeof initialValue !== 'function') {
    extraProps.defaultValue = initialValue;
  }

  let initialFn = (slotName ? null : initial) || initialValue;
  if (slotName && initialValue === true) {
    initialFn = (field: any, value: any) => {
      if (isJSSlot(value)) {
        return {
          title: slotTitle || title,
          ...value,
        };
      }
      return {
        type: 'JSSlot',
        title: slotTitle || title,
        value: initialChildren,
      };
    };
  }

  if (accessor && !slotName) {
    extraProps.getValue = (field: Field, fieldValue: any) => {
      return accessor.call(field, fieldValue);
    };
    if (!initialFn) {
      initialFn = accessor;
    }
  }

  const setterInitial = getInitialFromSetter(setter);

  collector.addInitial({
    // FIXME! name should be "xxx.xxx"
    name: slotName || name,
    initial: (field: Field, currentValue: any) => {
      // FIXME! read from prototype.defaultProps
      const defaults = extraProps.defaultValue;

      if (typeof initialFn !== 'function') {
        initialFn = defaultInitial;
      }

      const v = initialFn.call(field, currentValue, defaults);

      if (setterInitial) {
        return setterInitial.call(field, v, defaults);
      }

      return v;
    },
  });

  if (ignore != null || disabled != null) {
    collector.addFilter({
      // FIXME! name should be "xxx.xxx"
      name: slotName || name,
      filter: (field: Field, currentValue: any) => {
        let disabledValue: boolean;
        if (typeof disabled === 'function') {
          disabledValue = disabled.call(field, currentValue) === true;
        }
        else {
          disabledValue = disabled === true;
        }
        if (disabledValue) {
          return false;
        }
        if (typeof ignore === 'function') {
          return ignore.call(field, currentValue) !== true;
        }
        return ignore !== true;
      }
    });
  }

  if (sync) {
    extraProps.autorun = (field: Field) => {
      const value = sync.call(field, field.getValue());
      if (value !== undefined) {
        field.setValue(value);
      }
    };
  }
  if (mutator && !slotName) {
    extraProps.setValue = (field: Field, value: any) => {
      mutator.call(field, value, value);
    };
  }

  if (slotName) {
    newConfig.name = slotName;
    if (!newConfig.title && slotTitle) {
      newConfig.title = slotTitle;
    }
    const setters: any[] = [
      {
        componentName: 'SlotSetter',
        initialValue: (field: any, value: any) => {
          if (isJSSlot(value)) {
            return {
              title: slotTitle || title,
              ...value,
            };
          }
          return {
            type: 'JSSlot',
            title: slotTitle || title,
            value: value == null ? initialChildren : value,
          };
        },
      },
    ];
    if (allowTextInput !== false) {
      setters.unshift('I18nSetter');
    }
    if (supportVariable) {
      setters.push('VariableSetter');
    }
    newConfig.setter = setters.length > 1 ? setters : setters[0];

    return newConfig;
  }

  let primarySetter: any;
  if (type === 'composite') {
    const initials: InitialItem[] = [];
    const filters: FilterItem[] = [];
    const objItems = items
      ? upgradeConfigure(items,
        {
          addInitial: (item) => {
            initials.push(item);
          },
          addFilter: (item) => {
            filters.push(item);
          }
        }
      )
      : [];
    const initial = (target: SettingTarget, value?: any) => {
      // TODO:
      const defaults = extraProps.defaultValue;
      const data: any = {};
      initials.forEach((item) => {
        // FIXME! Target may be a wrong
        data[item.name] = item.initial(target, isPlainObject(value) ? value[item.name] : null);
      });
      return data;
    };
    collector.addInitial({
      name,
      initial,
    });
    primarySetter = {
      componentName: 'ObjectSetter',
      props: {
        config: {
          items: objItems,
        },
      },
      initialValue: (field: Field) => {
        return initial(field, field.getValue());
      },
    };
  } else if (setter) {
    if (Array.isArray(setter)) {
      // FIXME! read initial from setter
      primarySetter = setter.map(({ setter, condition }) => {
        return {
          componentName: setter,
          condition: condition
            ? (field: Field) => {
                return condition.call(field, field.getValue());
              }
            : null,
        };
      });
    } else {
      primarySetter = setter;
    }
  }
  if (!primarySetter) {
    primarySetter = 'I18nSetter';
  }
  if (supportVariable) {
    const setters = Array.isArray(primarySetter)
      ? primarySetter.concat('VariableSetter')
      : [primarySetter, 'VariableSetter'];
    primarySetter = {
      componentName: 'MixedSetter',
      props: {
        setters,
        onSetterChange: (field: Field, name: string) => {
          if (useVariableChange) {
            useVariableChange.call(field, { isUseVariable: name === 'VariableSetter' });
          }
        },
      },
    };
  }
  newConfig.setter = primarySetter;

  if (liveTextEditing) {
    extraProps.liveTextEditing = liveTextEditing;
  }

  return newConfig;
}

type AddInitial = (initialItem: InitialItem) => void;
type AddFilter = (filterItem: FilterItem) => void;

type ConfigCollector = {
  addInitial: AddInitial,
  addFilter: AddFilter,
}

function getInitialFromSetter(setter: any) {
  return setter && (
      setter.initial || setter.Initial
      || (setter.type && (setter.type.initial || setter.type.Initial))
    ) || null; // eslint-disable-line
}

function defaultInitial(value: any, defaultValue: any) {
  return value == null ? defaultValue : value;
}


export function upgradeConfigure(items: OldPropConfig[], collector: ConfigCollector) {
  const configure: any[] = [];
  let ignoreSlotName: any = null;
  items.forEach((config) => {
    if (config.slotName) {
      ignoreSlotName = config.slotName;
    } else if (ignoreSlotName) {
      if (config.name === ignoreSlotName) {
        ignoreSlotName = null;
        return;
      }
      ignoreSlotName = null;
    }
    configure.push(upgradePropConfig(config, collector));
  });
  return configure;
}

export function upgradeActions(actions?: Array<ComponentType<any> | ReactElement> | (() => ReactElement)) {
  if (!actions) {
    return null;
  }
  if (!Array.isArray(actions)) {
    actions = [actions];
  }
  return actions.map((content) => {
    const type: any = isValidElement(content) ? content.type : content;
    if (typeof content === 'function') {
      const fn = content as () => ReactElement;
      content = (({ node }: any) => {
        fn.call(node);
      }) as any;
    }
    return {
      name: type.displayName || type.name || 'anonymous',
      content,
      important: true,
    };
  });
}

/**
 * 升级
 */
export function upgradeMetadata(oldConfig: OldPrototypeConfig) {
  const {
    componentName,
    docUrl,
    title,
    icon,
    packageName,
    category,
    extraActions,
    defaultProps,
    initialChildren,
    snippets,
    view,
    configure,
    transducers,
    isContainer,
    rectSelector,
    isModal,
    isFloating,
    descriptor,
    context,
    canOperating,
    canContain,
    canDropTo,
    canDropto,
    canDropIn,
    canDroping,
    canUseCondition,
    canLoop,

    // hooks
    canDraging,
    canDragging, // handleDragging
    // events
    didDropOut, // onNodeRemove
    didDropIn, // onNodeAdd
    subtreeModified, // onSubtreeModified

    canResizing, // resizing
    onResizeStart, // onResizeStart
    onResize, // onResize
    onResizeEnd, // onResizeEnd
  } = oldConfig;

  const meta: any = {
    componentName,
    title,
    icon,
    docUrl,
    devMode: 'procode',
  };

  if (category) {
    meta.tags = [category];
  }
  if (packageName) {
    meta.npm = {
      componentName,
      package: packageName,
    };
  }

  const component: any = {
    isContainer,
    rootSelector: rectSelector,
    isModal,
    isFloating,
    descriptor,
  };

  if (canOperating === false) {
    component.disableBehaviors = '*';
  }
  if (extraActions) {
    component.actions = upgradeActions(extraActions);
  }
  const nestingRule: any = {};
  if (canContain) {
    nestingRule.descendantWhitelist = canContain;
  }
  if (canDropTo != null || canDropto != null) {
    if (canDropTo === false || canDropto === false) {
      nestingRule.parentWhitelist = () => false;
    }
    if (canDropTo !== true && canDropto !== true) {
      nestingRule.parentWhitelist = canDropTo || canDropto;
    }
  }
  if (canDropIn != null || canDroping != null) {
    if (canDropIn === false || canDroping === false) {
      nestingRule.childWhitelist = () => false;
    }
    if (canDropIn !== true && canDroping !== true) {
      nestingRule.childWhitelist = canDropIn || canDroping;
    }
  }
  component.nestingRule = nestingRule;

  // 未考虑清楚的，放在实验性段落
  const experimental: any = {};
  if (context) {
    // for prototype.getContextInfo
    experimental.context = context;
  }
  if (snippets) {
    experimental.snippets = snippets.map((data) => {
      const { schema = {} } = data;
      if (!schema.children && initialChildren && typeof initialChildren !== 'function') {
        schema.children = initialChildren;
      }
      return {
        ...data,
        schema,
      };
    });
  }
  // FIXME! defaultProps for initial input
  // initialChildren maybe a function
  else if (defaultProps || initialChildren) {
    const snippet = {
      screenshot: icon,
      label: title,
      schema: {
        componentName,
        props: defaultProps,
        children: initialChildren,
      },
    };
    if (experimental.snippets) {
      experimental.snippets.push(snippet);
    } else {
      experimental.snippets = [snippet];
    }
  }
  if (initialChildren) {
    experimental.initialChildren =
      typeof initialChildren === 'function'
        ? (field: Field) => {
            return initialChildren.call(field, (field as any).props);
          }
        : initialChildren;
  }
  if (view) {
    experimental.view = view;
  }
  if (transducers) {
    // Array<{ toStatic, toNative }>
    // ? only twice
    experimental.transducers = transducers;
  }
  if (canResizing) {
    // TODO: enhance
    experimental.getResizingHandlers = (currentNode: any) => {
      const directs = ['n', 'w', 's', 'e'];
      if (canResizing === true) {
        return directs;
      }
      return directs.filter((d) => canResizing(currentNode, d));
    };
  }

  const callbacks: any = {};
  if (canDragging != null || canDraging != null) {
    let v = true;
    if (canDragging === false || canDraging === false) {
      v = false;
    }
    callbacks.onMoveHook = () => v;
  }
  if (didDropIn) {
    callbacks.onNodeAdd = didDropIn;
  }
  if (didDropOut) {
    callbacks.onNodeRemove = didDropOut;
  }
  if (subtreeModified) {
    callbacks.onSubtreeModified = (...args: any[]) => {
      // FIXME! args not correct
      subtreeModified.apply(args[0], args as any);
    };
  }
  if (onResize) {
    callbacks.onResize = (e: any, currentNode: any) => {
      // todo: what is trigger?
      const { trigger, deltaX, deltaY } = e;
      onResize(e, trigger, currentNode, deltaX, deltaY);
    };
  }
  if (onResizeStart) {
    callbacks.onResizeStart = (e: any, currentNode: any) => {
      // todo: what is trigger?
      const { trigger } = e;
      onResizeStart(e, trigger, currentNode);
    };
  }
  if (onResizeEnd) {
    callbacks.onResizeEnd = (e: any, currentNode: any) => {
      // todo: what is trigger?
      const { trigger } = e;
      onResizeEnd(e, trigger, currentNode);
    };
  }

  experimental.callbacks = callbacks;

  const initials: InitialItem[] = [];
  const filters: FilterItem[] = [];
  const props = upgradeConfigure(configure || [],
    {
      addInitial: (item) => {
        initials.push(item);
      },
      addFilter: (item) => {
        filters.push(item);
      },
    }
  );
  experimental.initials = initials;
  experimental.filters = filters;

  const supports: any = {};
  if (canUseCondition != null) {
    supports.condition = canUseCondition;
  }
  if (canLoop != null) {
    supports.loop = canLoop;
  }
  meta.configure = { props, component, supports };
  meta.experimental = experimental;
  return meta;
}
