import { useStore } from "../store";
import {
  components,
  DrawItem,
  ShapeType,
  ComponentSnapInfo,
} from "../components";
import { computed, reactive, ref, toRaw, watch, watchEffect } from "vue";
import {
  createLine,
  eqNGDire,
  eqPoint,
  lineIntersection,
  lineLen,
  linePointLen,
  linePointProjection,
  numEq,
  Pos,
  vector,
  vector2IncludedAngle,
  verticalVector,
  zeroEq,
} from "@/utils/math";
import { globalWatch, installGlobalVar } from "./use-global-vars";
import { BaseItem } from "../components/util";
import {
  ScaleVectorType,
  useGetTransformerOperDirection,
  useGetTransformerOperType,
  useTransformer,
} from "./use-transformer";
import { Transform } from "konva/lib/Util";
import { useCacheUnitTransform, useViewerInvertTransform } from "./use-viewer";
import { MathUtils } from "three";
import { arrayInsert, mergeFuns, rangMod } from "@/utils/shared";

export type SnapInfo = ComponentSnapInfo & Pick<DrawItem, "id">;
const useStoreSnapInfos = () => {
  const store = useStore();
  const types = Object.keys(components) as ShapeType[];
  const infos = reactive(new Set<SnapInfo>());
  const cleanups = [] as Array<() => void>;

  for (const type of types) {
    const comp = components[type];
    if (!("getSnapInfos" in comp)) continue;

    cleanups.push(
      globalWatch(
        () => store.data[type]?.map((item) => item),
        (items, _, onCleanup) => {
          if (!items) return;
          for (const item of items) {
            const snaps = computed(() => {
              if (item.ref) {
                return comp.getSnapInfos!(item as any) as SnapInfo[];
              } else {
                return [];
              }
            });
            const snapInfoWatchStop = watch(
              snaps,
              (snaps, _, onCleanup) => {
                snaps.forEach((snap) => {
                  snap.id = item.id;
                  infos.add(snap);
                });
                onCleanup(() => {
                  snaps.forEach((snap) => infos.delete(snap));
                });
              },
              { immediate: true }
            );
            const existsWatchStop = watchEffect(() => {
              if (!items.includes(item as any)) {
                snapInfoWatchStop();
                existsWatchStop();
              }
            });
            onCleanup(() => {
              snapInfoWatchStop();
              existsWatchStop();
            });
          }
        },
        { immediate: true }
      )
    );
  }

  return {
    infos: computed(() => Array.from(infos.values())),
    cleanup: mergeFuns(cleanups),
  };
};

export const useCustomSnapInfos = installGlobalVar(() => {
  const infos = ref<ComponentSnapInfo[]>([]);
  return {
    infos,
    add: (snap: ComponentSnapInfo) => {
      infos.value.push(snap);
    },
    remove: (snap: ComponentSnapInfo) => {
      const index = infos.value.findIndex((p) => toRaw(p) === toRaw(snap));
      if (index !== -1) {
        infos.value.splice(index, 1);
      }
    },
  };
});

export const useGlobalSnapInfos = installGlobalVar(() => {
  const storeInfos = useStoreSnapInfos();
  const customInfos = useCustomSnapInfos();
  return {
    var: computed(() => [...customInfos.infos.value, ...storeInfos.infos.value] as SnapInfo[]),
    onDestroy: storeInfos.cleanup,
  };
}, Symbol("snapInfos"));

export const useSnapConfig = () => {
  const unitTransform = useCacheUnitTransform();
  return {
    get snapOffset() {
      return unitTransform.getPixel(5);
    },
  };
};

export type SnapResultInfo = {
  attractSnaps: AttractSnapInfo[];
  clear: () => void;
};
export const useSnapResultInfo = installGlobalVar(() => {
  const snapInfo = reactive({
    attractSnaps: [],
    clear() {
      snapInfo.attractSnaps.length = 0;
    },
  }) as SnapResultInfo;
  return snapInfo;
}, Symbol("snapResultInfo"));

export type AttractSnapInfo = {
  ref: ComponentSnapInfo;
  current: ComponentSnapInfo;
  offset: number;
  angle: number;
  join: Pos;
  refDirection: Pos;
  refLinkNdx: number;
  currentLinkNdx: number;
};
// TODO 返回结果按照self.point参照线多少排序， 子数组按照offset排序
export const filterAttractSnapInfos = (
  refInfos: ComponentSnapInfo[],
  selfInfos: ComponentSnapInfo[],
  filters: (
    self: ComponentSnapInfo,
    ref: ComponentSnapInfo,
    result: AttractSnapInfo[]
  ) =>
    | { items?: AttractSnapInfo[]; stop?: boolean; stopSelf?: boolean }
    | AttractSnapInfo[],
  sortKey?: "offset" | "angle"
) => {
  const attractSnapInfosGroups: AttractSnapInfo[][] = [];
  for (const self of selfInfos) {
    if (self.point.view) continue;
    const attractSnapInfos: AttractSnapInfo[] = [];
    for (const ref of refInfos) {
      let infos = filters(self, ref, attractSnapInfos);
      const stop = Array.isArray(infos) ? false : !!infos.stop;
      const stopRef = Array.isArray(infos) ? false : !!infos.stopSelf;
      infos = Array.isArray(infos) ? infos : infos.items!;
      if (infos && sortKey) {
        for (const info of infos) {
          arrayInsert(
            attractSnapInfos,
            info,
            (a, b) => b[sortKey] < a[sortKey]
          );
        }
      }
      if (stop) {
        return attractSnapInfosGroups;
      }
      if (stopRef) {
        break;
      }
    }
    arrayInsert(
      attractSnapInfosGroups,
      attractSnapInfos,
      (a, b) => a.length < b.length
    );
  }
  return attractSnapInfosGroups;
};

type FilterAttrib = {
  maxOffset?: number;
  maxAngle?: number;
  type?: "inter" | "projection" | "all" | "none";
};
const getAttractSnapInfos = (
  ref: ComponentSnapInfo,
  self: ComponentSnapInfo,
  filter: FilterAttrib
) => {
  filter.type = filter.type || "all";

  const limitAngle = "maxAngle" in filter;
  const limitOffset = "maxOffset" in filter;
  const isAll = filter.type === "all";

  const attractSnapInfos: AttractSnapInfo[] = [];
  for (let i = 0; i < self.linkAngle.length; i++) {
    for (let j = 0; j < ref.linkAngle.length; j++) {
      const angle = Math.abs(ref.linkAngle[j] - self.linkAngle[i]);
      if (limitAngle && angle > filter.maxAngle!) {
        continue;
      }

      let join: Pos | null = null;
      let offset: number | null = null;

      const checkOffset = (getJoin: () => Pos | null) => {
        join = getJoin();
        offset = join && lineLen(self.point, join);
        const adopt = join && (!limitOffset || offset! < filter.maxOffset!);
        if (!adopt) {
          join = null;
          offset = null;
        }
        return adopt;
      };

      if (filter.type === "none") {
        const adopt = checkOffset(() => ref.point);
        if (!adopt) continue;
      } else {
        const refLine = [
          ref.point,
          vector(ref.point).add(ref.linkDirections[j]),
        ];
        if (filter.type === "projection" || isAll) {
          const adopt = checkOffset(() =>
            linePointProjection(refLine, self.point)
          );
          if (!adopt && !isAll) continue;
        }

        const curLine = [
          self.point,
          vector(self.point).add(self.linkDirections[i]),
        ];
        if (!join && !checkOffset(() => lineIntersection(refLine, curLine))) {
          continue;
        }
      }
      attractSnapInfos.push({
        ref,
        current: self,
        refLinkNdx: j,
        currentLinkNdx: i,
        angle,
        join: join!,
        offset: offset!,
        refDirection: ref.linkDirections[j],
      });
    }
  }
  return attractSnapInfos;
};

const moveSnap = (
  refInfos: ComponentSnapInfo[],
  selfInfos: ComponentSnapInfo[],
  filter: FilterAttrib
) => {
  filter.maxOffset = filter.maxOffset || 15;
  const exclude = new Map<AttractSnapInfo, AttractSnapInfo>();
  const addExclude = (nor: AttractSnapInfo, act: AttractSnapInfo) => {
    exclude.set(nor, act);
    exclude.set(act, nor);
  };
  const getAttractSnapJoin = (nor: AttractSnapInfo, act: AttractSnapInfo) => {
    if (nor === act || exclude.get(nor) === act) {
      return void 0;
    }
    if (eqPoint(nor.current.point, act.join)) {
      return addExclude(nor, act);
    }
    // console.log(nor.current.point, act.join)

    const norJoin = nor.join;
    const norDire = vector(nor.refDirection);
    const norLine = createLine(norJoin, norDire);

    // TODO 确保移动前后normal的 direction 保持一致
    const useDire = act.refDirection;
    if (eqNGDire(norDire, useDire)) {
      return addExclude(nor, act);
    }
    const useJoin = act.join;
    const useLine = createLine(useJoin, useDire);
    const nuJoin = lineIntersection(norLine, useLine);

    if (!nuJoin || lineLen(nuJoin, norJoin) > filter.maxOffset!) {
      return addExclude(nor, act);
    } else {
      return nuJoin;
    }
  };

  const useAttractSnaps: AttractSnapInfo[] = [];
  let end: Pos | null = null;
  let start: Pos | null = null;

  // TODO 最多参考2个信息
  const attractSnapGroups = filterAttractSnapInfos(
    refInfos,
    selfInfos,
    (self, ref, selfGroup) => {
      const attractSnapInfos = getAttractSnapInfos(ref, self, filter);
      const nor = selfGroup[0] || attractSnapInfos[0];
      const checks = [...selfGroup, ...attractSnapInfos];
      // TODO 尽快提前结束
      for (const check of checks) {
        const join = getAttractSnapJoin(nor, check);
        if (join) {
          end = join;
          start = nor.current.point;
          useAttractSnaps.push(nor, check);
          return { stop: true };
        }
      }
      return attractSnapInfos;
    },
    "offset"
  );

  if (!end) {
    if (!attractSnapGroups.length || !attractSnapGroups[0].length) return null;
    const nor = attractSnapGroups[0][0];

    end = nor.join;
    start = nor.current.point;
    useAttractSnaps.push(nor);

    // TODO 如果没有同一个点的两线段，则使用2垂直的两点线段
    const move = vector(end!).sub(start!);
    for (let i = 0; i < attractSnapGroups.length; i++) {
      let j = i === 0 ? 1 : 0;
      for (; j < attractSnapGroups[i].length; j++) {
        const attractSnap = attractSnapGroups[i][j];
        const rDire = attractSnap.refDirection;
        const angle = vector2IncludedAngle(nor.refDirection, rDire);
        if (!numEq(rangMod(angle, Math.PI), Math.PI / 2)) {
          continue;
        }

        const cPoint = vector(attractSnap.current.point).add(move);
        const rPoint = attractSnap.ref.point;
        const inter = lineIntersection(
          createLine(cPoint, nor.refDirection),
          createLine(rPoint, rDire)
        );

        if (inter) {
          useAttractSnaps.push(attractSnap);
          end = vector(end).add(inter.sub(cPoint));
          break;
        }
      }
      if (j !== attractSnapGroups[i].length) break;
    }
  }

  const norMove = vector(end!).sub(start!);
  return {
    useAttractSnaps,
    transform: new Transform().translate(norMove.x, norMove.y),
  };
};

type SelfAttitude = {
  rotation: number;
  origin: Pos;
  operTarget: Pos;
  center: Pos;
};
const scaleSnap = (
  refInfos: ComponentSnapInfo[],
  selfInfos: ComponentSnapInfo[],
  filter: Omit<FilterAttrib, "type">,
  type: ScaleVectorType,
  attitude: SelfAttitude
) => {
  const { origin, operTarget } = attitude;
  const attractSnaps: AttractSnapInfo[] = [];
  const operVector = vector(operTarget).sub(origin).normalize();
  const vOperVector = verticalVector(operVector);
  const vLine = createLine(origin, vOperVector);
  const proportional = [
    "top-right",
    "top-left",
    "bottom-right",
    "bottom-left",
  ].includes(type);

  const limitOffset = filter.maxOffset;
  const asFilter: FilterAttrib = { ...filter, type: "none" };
  delete asFilter.maxOffset;

  const exclude = new Set<AttractSnapInfo>();
  const getAttractSnapJoin = (nor: AttractSnapInfo) => {
    if (exclude.has(nor)) return;
    if (
      eqNGDire(nor.refDirection, operVector) ||
      eqNGDire(nor.refDirection, nor.current.linkDirections[nor.currentLinkNdx])
    ) {
      exclude.add(nor);
      return;
    }
    const cur = nor.current.point;
    if (
      eqNGDire(vOperVector, nor.refDirection) &&
      zeroEq(linePointLen(vLine, nor.ref.point))
    ) {
      exclude.add(nor);
      attractSnaps.push(nor);
      return;
    }
    const refLine = [
      nor.ref.point,
      vector(nor.ref.point).add(nor.refDirection),
    ];
    const norLine = proportional
      ? [origin, cur]
      : [cur, vector(cur).add(operVector)];
    const join = lineIntersection(refLine, norLine);
    if (!join || (limitOffset && lineLen(join, cur) > limitOffset)) {
      exclude.add(nor);
      return;
    }

    if (
      Math.abs(lineLen(vector(join).sub(origin).normalize(), operVector)) > 0.3
    ) {
      exclude.add(nor);
      return;
    }

    return join;
  };

  let useAttractSnap: AttractSnapInfo;
  let useJoin: Pos;

  // TODO 最多参考1个信息
  filterAttractSnapInfos(refInfos, selfInfos, (self, ref) => {
    if (eqPoint(self.point, origin)) return { stopSelf: true };
    const attractSnapInfos = getAttractSnapInfos(ref, self, asFilter);
    for (const info of attractSnapInfos) {
      const join = getAttractSnapJoin(info);
      if (join) {
        info.join = join;
        useJoin = join;
        useAttractSnap = info;
        return { stop: true };
      }
    }
    return attractSnapInfos;
  });
  if (!useAttractSnap! || !useJoin!) return null;

  const rotation = Math.atan2(operVector.y, operVector.x);
  const invRotateTransform = new Transform()
    .rotate(-rotation)
    .translate(-origin.x, -origin.y);

  const t = invRotateTransform.point(useJoin!);
  const c = invRotateTransform.point(useAttractSnap.current.point);

  const currentFactor = vector({
    x: numEq(t.x, c.x) ? 1 : t.x / c.x,
    y: numEq(t.y, c.y) ? 1 : t.y / c.y,
  });
  if (proportional) {
    currentFactor.y = currentFactor.x;
  }

  attractSnaps.push(useAttractSnap);
  return {
    useAttractSnaps: attractSnaps,
    transform: new Transform()
      .multiply(invRotateTransform.copy().invert())
      .scale(currentFactor.x, currentFactor.y)
      .multiply(invRotateTransform),
  };
};

export const useSnap = (
  snapInfos: { value: ComponentSnapInfo[] } = useGlobalSnapInfos()
) => {
  const snapResultInfo = useSnapResultInfo();
  const snapConfig = useSnapConfig();
  const afterHandler = (result: ReturnType<typeof moveSnap>) => {
    if (result) {
      snapResultInfo.attractSnaps = result.useAttractSnaps;
      return result.transform;
    }
    return null;
  };

  const move = (selfSnapInfos: ComponentSnapInfo[]) => {
    const result = moveSnap(snapInfos.value, selfSnapInfos, {
      maxOffset: snapConfig.snapOffset,
    });
    return afterHandler(result);
  };
  const scale = (
    selfSnapInfos: ComponentSnapInfo[],
    attitude: SelfAttitude & { type: ScaleVectorType }
  ) => {
    const result = scaleSnap(
      snapInfos.value,
      selfSnapInfos,
      { maxOffset: snapConfig.snapOffset },
      attitude.type,
      attitude
    );
    return afterHandler(result);
  };
  return {
    move,
    scale,
    clear: snapResultInfo.clear,
  };
};

export const useComponentSnap = (componentId: string) => {
  const store = useStore();
  const type = componentId ? store.getType(componentId) : undefined;
  const comp = type && components[type];
  const api = type && comp?.getSnapInfos;
  if (!api) return null;

  const snapInfos = useGlobalSnapInfos();
  const refSnapInfos = computed(() =>
    snapInfos.value.filter((p) => !("id" in p) || p.id !== componentId)
  );
  const baseSnap = useSnap(refSnapInfos);
  const getOperType = useGetTransformerOperType();
  const getTransformerOperDirection = useGetTransformerOperDirection();
  const transformer = useTransformer();
  const viewerInvertTransform = useViewerInvertTransform();

  const snap = (item: BaseItem) => {
    const operateType = getOperType();
    const selfSnapInfos = api(item as any);
    baseSnap.clear();

    // move
    if (!operateType) {
      return baseSnap.move(selfSnapInfos);
    } else if (operateType !== "rotater") {
      const direction = getTransformerOperDirection()!;
      const node = transformer.nodes()[0];
      const rect = node.getClientRect();
      const attitude = {
        center: viewerInvertTransform.value.point({
          x: rect.x + rect.width / 2,
          y: rect.y + rect.height / 2,
        }),
        operTarget: direction[1],
        rotation: MathUtils.degToRad(node.rotation()),
        origin: direction[0],
        type: operateType,
      };
      return baseSnap.scale(selfSnapInfos, attitude);
    }
  };

  return [snap, baseSnap.clear] as const;
};
