// @ts-nocheck
import React from "react";
import PropTypes from "prop-types";
import classNames from "classnames";

import { Select, Balloon } from "@alifd/next";
import AddressCascader from "./AddressCascader";

import { loadTownData, traceTownParents } from "./util";

import { AddressCompE, setStateAsync } from "./Common";

import { last, zipObject, dropRight } from "./lodash-alt";

import {
  NodeID,
  NodeSelection,
  AddressNode,
  AddressNodeSet,
  AddressNodeRoot,
  AddressNodeInternal,
  AddressNodeLeaf,
  AddressDataset,
  AddressRequestParams,
  CascaderSelectOnChangeExtraData,
  NodeIDStrict,
  LabelChineseSimplified,
} from "./Types";
import {
  dataset,
  AddressCompProps,
  AddressCompState,
  nodeChainFromNode,
  selectNodeFromChain,
  selectionToChain,
  foldNodeChainToSelection,
  anyValueToSelectionId,
  CANNOT_REACH,
  maybeGet,
  addressEqual,
  id,
} from "./Common";
// qualified import for name conflict
import * as C from "./Common";

interface AddressMultiProps extends AddressCompProps {
  root: NodeSelection | NodeID;
  value: NodeSelection[];
  defaultValue: NodeSelection[];
  placeholder: any;
  onChange: (value: NodeSelection[], nodes: AddressNode[]) => void;
  size: "small" | "medium" | "large";
  // not available for now ...
  hasClear: boolean;
  popupContainer: React.ReactNode | Function;
  onLabelUpdate: (label: string) => void;
  popupProps: object;
  popupClassName: string;
}

interface AddressMultiState extends AddressCompState {
  value: NodeSelection[];
  cascadeRootNodeID: NodeIDStrict | null;
  uiOverlayVisible: boolean;

  expandedValue: NodeIDStrict;
}

export default class AddressMulti extends React.Component<
  AddressMultiProps,
  AddressMultiState
> {
  static propTypes = {
    className: PropTypes.string,
    style: PropTypes.object,
    value: PropTypes.arrayOf(
      PropTypes.oneOfType([
        PropTypes.object,
        PropTypes.number,
        PropTypes.string,
      ])
    ),
    defaultValue: PropTypes.arrayOf(
      PropTypes.oneOfType([
        PropTypes.object,
        PropTypes.number,
        PropTypes.string,
      ])
    ),
    root: PropTypes.oneOfType([
      PropTypes.string,
      PropTypes.number,
      PropTypes.object,
    ]),
    level: PropTypes.number,
    disabled: PropTypes.bool,
    placeholder: PropTypes.node,
    requestAddressUrl: PropTypes.string,
    requestTownUrl: PropTypes.string,
    requestAddressLevelUrl: PropTypes.string,
    showItemCount: PropTypes.number,
    maxWidth: PropTypes.number,
    size: PropTypes.oneOf(["small", "medium", "large"]),
    hasClear: PropTypes.bool,
    popupContainer: PropTypes.func,
    ignored: PropTypes.arrayOf(PropTypes.string),
    dataOverride: PropTypes.objectOf(PropTypes.arrayOf(PropTypes.string)),
    preprocessor: PropTypes.func,
    hiddenData: PropTypes.arrayOf(PropTypes.string),
    hasToSelectToLastLevel: PropTypes.oneOfType([
      PropTypes.bool,
      PropTypes.number,
    ]),
    onLabelUpdate: PropTypes.func,
    popupProps: PropTypes.object,
    popupClassName: PropTypes.string,
    _IS_CN_ADDRESS_MULTI_: PropTypes.bool,
  };

  static defaultProps = {
    level: 4,
    placeholder: "请选择地址",
    requestAddressUrl: "//division-data.alicdn.com/simple/addr_4_1111_1_0.js",
    requestTownUrl:
      "//lsp.wuliu.taobao.com/locationservice/addr/output_address_town.do",
    requestAddressLevelUrl:
      "//lsp.wuliu.taobao.com/locationservice/addr/outputParentDivisons.do",
    root: "0",
    defaultValue: [],
    showItemCount: 8,
    maxWidth: 256,
    size: "medium",
    hasClear: false,
    popupContainer: null,
    ignored: [],
    dataOverride: {},
    preprocessor: id,
    hiddenData: [],
    disabled: false,
    _IS_CN_ADDRESS_MULTI_: true,
  };

  allNodesSet: AddressNodeSet | null;
  hiddenMap: { [id: string]: any } = {};

  constructor(props: AddressMultiProps) {
    super(props);
    this.state = {
      value: [],
      cascadeRootNodeID: null,
      paneRoot: null,
      addressTree: {} as AddressNodeInternal,
      uiOverlayVisible: false,

      expandedValue: "1",
    };

    this.updateHiddenMap(this.props.hiddenData);

    this.handleCascadeChange = this.handleCascadeChange.bind(this);
    this.handleLoadData = this.handleLoadData.bind(this);
    this.handleOverlayVisibleChange =
      this.handleOverlayVisibleChange.bind(this);
    this.getOverlayContainer = this.getOverlayContainer.bind(this);

    this.asyncInitialize();
  }

  asyncInitialize = async () => {
    // PROBLEM: how to deal with these state + async?
    const defaultValue: NodeSelection[] = this.props.defaultValue;
    const initialValue = this.props.value || defaultValue; // props.defaultValue

    await this.dataset();
    await this.normalizeAndUpdateCascadeRoot(this.props.root);

    const temps = await Promise.all(
      initialValue.map((v) =>
        this.normalizeValueReturningAddressAndNodeChain(v)
      )
    );

    // TODO: fix this equality logic ...
    if ((this.props.value || defaultValue) == initialValue) {
      // 过滤无效
      const filters = (temps || []).filter((item) => {
        return Object.keys(item.address || {}).length;
      });
      const value = filters.map((t) => t.address);
      await this.setStateAsync({ value });
      this.triggerLabelUpdate(initialValue);
    }
  };

  updateHiddenMap(hiddenList: NodeIDStrict[]) {
    const ret = zipObject(hiddenList, new Array(hiddenList.length).fill(true));
    this.hiddenMap = ret;
    return ret;
  }

  async componentWillReceiveProps(nextProps: AddressMultiProps) {
    if (
      "root" in nextProps &&
      (this.state.cascadeRootNodeID == null ||
        !addressEqual(nextProps.root, this.state.cascadeRootNodeID))
    ) {
      this.normalizeAndUpdateCascadeRoot(nextProps.root);
    }

    if ("value" in nextProps) {
      let { value } = nextProps;
      if (value === undefined || value === null || (value as any) === "") {
        value = [];
      }
      if (!Array.isArray(value)) {
        console.error("value of AddressMulti must be an array!");
      }
      // console.log(nextProps.value)
      const temps = await Promise.all(
        value.map((v) => this.normalizeValueReturningAddressAndNodeChain(v))
      );
      // TODO: equality testing
      const valueNew = temps.map((t) => t.address);
      this.setState({ value: valueNew });
      this.triggerLabelUpdate(valueNew, nextProps);
    }

    if (
      "hiddenData" in nextProps &&
      this.props.hiddenData != nextProps.hiddenData
    ) {
      this.updateHiddenMap(nextProps.hiddenData);
    }
  }

  getRequestParams = (props = this.props): AddressRequestParams => ({
    addressURL: props.requestAddressUrl,
    townURL: props.requestTownUrl,
    parentURL: props.requestAddressLevelUrl,
    ignored: props.ignored,
    dataOverride: props.dataOverride,
    preprocessor: props.preprocessor,
  });

  // TODO: this is not type safe

  setStateAsync = (updater: object) =>
    new Promise((resolve, reject) =>
      // @ts-ignore
      this.setState(updater, () => resolve())
    );

  dataset = (
    params: AddressRequestParams = this.getRequestParams()
  ): Promise<AddressDataset> => C.dataset(this, this.getRequestParams());

  datasetImmediate = (): AddressCompE => this as AddressCompE;

  loadAndMergeTownList = async (
    node: AddressNodeInternal | AddressNodeRoot,
    params: AddressRequestParams = this.getRequestParams(),
    allNodesSetNow = this.datasetImmediate().allNodesSet
  ) => {
    const townList: AddressNodeLeaf[] = await loadTownData(params, node);
    if (townList.length) {
      node.children = townList;
      node.isLeaf = false;
    } else {
      const nodeIsALeaf = node as any as AddressNodeLeaf;
      nodeIsALeaf.children = undefined;
      nodeIsALeaf.isLeaf = true;
    }
    Object.assign(
      (await this.dataset()).allNodesSet,
      zipObject(
        townList.map((t) => t.id),
        townList
      )
    );

    if (this.datasetImmediate().allNodesSet === allNodesSetNow) {
      this.setState({ addressTree: this.state.addressTree });
    }

    return townList;
  };

  async normalizeValueReturningNodeChain(
    val: NodeID | NodeSelection,
    params = this.getRequestParams()
  ) {
    const traceTownParentsAndUpdateIfDataNotChanged = async (
      town: NodeID,
      { addressTree, allNodesSet }: AddressDataset
    ) => {
      const chain = await traceTownParents(params, town);
      const nodeChain = selectNodeFromChain(addressTree, dropRight(1, chain)),
        node = last(nodeChain);
      if (node) {
        await this.loadAndMergeTownList(
          node as AddressNodeInternal,
          params,
          allNodesSet
        );
      }
      return [...nodeChain, allNodesSet[town.toString()]];
    };

    if (typeof val == "number" || typeof val == "string") {
      const dataset = await this.dataset(params);
      const { addressTree, allNodesSet } = dataset;
      if (allNodesSet[val]) {
        return nodeChainFromNode(allNodesSet[val]);
      } else {
        return await traceTownParentsAndUpdateIfDataNotChanged(val, dataset);
      }
    } else if (typeof val == "object") {
      const val_ = val as NodeSelection;
      const dataset = await this.dataset(params);
      const { addressTree, allNodesSet } = dataset;
      if (
        typeof val_.town == "string" &&
        !this.datasetImmediate().allNodesSet[val_.town]
      ) {
        return await traceTownParentsAndUpdateIfDataNotChanged(
          val_.town,
          dataset
        );
      } else {
        const selectionChain = selectionToChain(val_);
        if (!selectionChain.length) {
          return [];
        }
        return nodeChainFromNode(
          allNodesSet[maybeGet(last(selectionChain)).toString()]
        );
      }
    } else {
      throw `Address normalizeValue(): unknown value of ${typeof val} - ${val}`;
    }
  }

  async normalizeValueReturningAddressAndNodeChain(
    val: NodeID | NodeSelection,
    params?: AddressRequestParams
  ) {
    const nodeChain = await this.normalizeValueReturningNodeChain(val, params);
    return {
      nodeChain,
      address: foldNodeChainToSelection(nodeChain),
    };
  }

  normalizeAndUpdateCascadeRoot = async (root: NodeID | NodeSelection) => {
    if (typeof root == "object" && !("country" in root)) {
      await this.updateCascadeRoot_("0");
      return;
    }
    const rootNode = last(await this.normalizeValueReturningNodeChain(root));
    // console.log("updating Cascade root", root, rootNode)
    if (!rootNode || rootNode.isLeaf) {
      throw new Error(`invalid root node ${root} - ${rootNode}`);
    }
    await this.updateCascadeRoot_(rootNode.id);
  };

  updateCascadeRoot_ = async (root: NodeID) => {
    root = root.toString();
    const updater: any = { cascadeRootNodeID: root };
    if (
      !C.isDescendantOfOne(this.allNodesSet!, this.state.expandedValue, [root])
    ) {
      updater.expandedValue = root;
    }
    await this.setStateAsync(updater);
  };

  // TODO: Next 1.x Select compatibility, and a little bug of TS typechecker
  async handleCascadeChange(
    values: (NodeIDStrict | { value: NodeIDStrict })[]
  ) {
    // console.log(extra)
    // console.log("select change", arguments)
    const ids = values.map((v) =>
      typeof v == "object" ? (v as any).value : v
    );
    const normalized = this.normalizeSelection(ids);
    const nodes = normalized.map(
      (i) => this.datasetImmediate().allNodesSet[i.toString()]
    );
    const nodeChains = nodes.map(nodeChainFromNode);
    const selections = nodeChains.map(foldNodeChainToSelection);
    if (!("value" in this.props)) {
      await setStateAsync(this, { value: selections });
    }
    this.props.onChange && this.props.onChange(selections, nodes);
  }

  async loadTownFromNode(
    value: AddressNodeRoot | AddressNodeInternal
  ): Promise<AddressNode> {
    if (
      this.props.level >= 4 &&
      !value.isLeaf &&
      (!value.children || !value.children.length) &&
      !(value.levelKey == "town")
    ) {
      try {
        await this.loadAndMergeTownList(value);
      } catch (e) {
        console.error(e);
      }
      return value;
    } else {
      return Promise.resolve(value);
    }
  }

  async handleOverlayVisibleChange(value: boolean) {
    const arg = { uiOverlayVisible: value };
    this.setState(arg);
  }

  async loadTownFromNodeId(id: NodeIDStrict): Promise<AddressNode> {
    const value = (await this.dataset()).allNodesSet[id.toString()] as
      | AddressNodeRoot
      | AddressNodeInternal;
    return await this.loadTownFromNode(value);
  }

  // the arg value is the one cached inside CascaderSelect, not our data
  handleLoadData = async (valueClone: AddressNode): Promise<AddressNode> =>
    await this.loadTownFromNodeId(valueClone.id);

  renderCount(displayValues: { label: LabelChineseSimplified }[]) {
    if (!this.state.value.length) {
      return null;
    }

    const text = displayValues.map((n) => n.label).join(", ");
    const trigger = (
      <span className="next-cn-address-multi-total">
        共&nbsp;
        <span className="next-cn-address-multi-total-num">
          {this.state.value.length}
        </span>
        &nbsp;项
      </span>
    );

    return (
      <Balloon.Tooltip
        className="next-cn-address-multi-allvalues"
        trigger={trigger}
        align="b"
      >
        {text}
      </Balloon.Tooltip>
    );
  }

  handleExpandChange = async (expandedValue: NodeIDStrict) => {
    await this.loadTownFromNodeId(expandedValue);
    this.setState({ expandedValue });
  };

  getOverlayContainer() {
    const { popupContainer } = this.props;
    if (this.props.popupContainer) {
      if (typeof popupContainer == "function") {
        return popupContainer();
      }
      return popupContainer;
    }
    return this.refs.outerWrapper;
  }

  triggerLabelUpdate(nodeValues: NodeSelection[], props = this.props) {
    if (typeof props.onLabelUpdate == "function") {
      const nodes = nodeValues
        .map((v) => anyValueToSelectionId(v))
        .map((id) => this.allNodesSet![id.toString()]);
      props.onLabelUpdate.call(
        null,
        nodes.map((node) => node.nameZh).join(", "),
        nodes
      );
    }
  }

  normalizeSelection(ids: NodeIDStrict[]): NodeIDStrict[] {
    // TODO: optimize this "deduplicate" process
    const iteration = (src: NodeIDStrict[]): NodeIDStrict[] | false => {
      const children: Map<NodeIDStrict, Set<NodeIDStrict>> = new Map();
      const ret: NodeIDStrict[][] = [];
      src.forEach((id) => {
        const parentId = this.allNodesSet![id.toString()].parentNode!.id;
        if (!children.has(parentId)) {
          children.set(parentId, new Set());
        }
        children.get(parentId)!.add(id);
      });

      let changed: boolean = false;
      children.forEach((v, k) => {
        if (this.allNodesSet![k.toString()].children!.length == v.size) {
          changed = true;
          ret.push([k]);
        } else {
          ret.push(Array.from(v.values()));
        }
      });
      if (changed) {
        return Array.prototype.concat.apply([], ret);
      } else {
        return false;
      }
    };

    let idsMerged = ids;
    while (true) {
      const t = iteration(idsMerged);
      if (!t) {
        break;
      } else {
        idsMerged = t;
      }
    }

    const ret = idsMerged.filter(
      (id) =>
        !C.isDescendantOfOne(this.allNodesSet as AddressNodeSet, id, idsMerged)
    );
    return ret;
  }

  renderCascader() {
    return (
      <div className="next-cn-address-multi-overlay">
        <AddressCascader
          addressTree={this.state.addressTree}
          allNodesSet={this.allNodesSet as AddressNodeSet}
          expandedValue={this.state.expandedValue}
          onExpand={this.handleExpandChange}
          onChange={this.handleCascadeChange}
          value={this.state.value.map((node) => last(selectionToChain(node)))}
          rootNodeId={anyValueToSelectionId(this.props.root)}
          level={this.props.level}
          hiddenMap={this.hiddenMap}
        />
      </div>
    );
  }

  render() {
    const {
      className,
      style,
      placeholder,
      requestAddressUrl,
      requestTownUrl,
      requestAddressLevelUrl,
      level,
      root,
      onChange,
      value,
      hasClear,
      popupProps,
      popupClassName,
      size,
      ...rest
    } = this.props;

    if (!this.allNodesSet) {
      return (
        <span
          key="dataLoading"
          className={classNames("next-cn-address-loading", className)}
          style={style}
        >
          加载数据中 ...
        </span>
      );
    }

    // TODO: why casting to ReactComponent<any, any> does not work?
    const dataSourceRoot = !this.state.cascadeRootNodeID
      ? []
      : this.datasetImmediate().allNodesSet[
          this.state.cascadeRootNodeID.toString()
        ].children;
    const displayValues = this.state.value
      .map(
        (v) =>
          (this.allNodesSet as AddressNodeSet)[
            anyValueToSelectionId(v).toString()
          ]
      )
      .map((n) => ({ label: n.nameZh!, value: n.id }));

    return (
      <span
        className={classNames("next-cn-address-multi-wrap", className, size)}
        style={style}
        ref="outerWrapper"
      >
        {/* <CascaderSelect
                    multiple={true}
                    { ... rest }
                    className={classNames("next-cn-address-multi", { "next-cn-address-multi-nonempty": !!this.state.value.length })}
                    dataSource={dataSourceRoot}
                    value={this.state.value.map(node => last(selectionToChain(node)))}
                    onChange={this.handleSelectChange}
                    loadData={this.handleLoadData}
                /> */}
        <Select
          className={classNames("next-cn-address-multi", {
            "next-cn-address-multi-nonempty": !!this.state.value.length,
          })}
          size={size}
          placeholder={placeholder}
          popupContent={this.renderCascader()}
          visible={this.state.uiOverlayVisible}
          onVisibleChange={this.handleOverlayVisibleChange}
          onChange={this.handleCascadeChange}
          mode="multiple"
          value={displayValues}
          hasClear={hasClear}
          popupContainer={this.getOverlayContainer}
          disabled={this.props.disabled}
          autoWidth={false}
          popupProps={popupProps}
          popupClassName={popupClassName}
        />
        {this.renderCount(displayValues)}
      </span>
    );
  }
}
