import { Clusterer as ClustererElement, useYMaps } from "@pbe/react-yandex-maps";
import type {
  ReactElement,
  FC,
  ReactNode,
} from "react";
import {
  useCallback,
  useMemo,
  useState,
  useEffect,
  useContext,
} from "react";
import type {
  Clusterer, IClassConstructor, IEvent, ILayout, IGeoObject, IPointGeometry,
} from "yandex-maps";
import { constants } from "../../../constants";
import { mapDefaults, optionsDefaults } from "../config";
import type {
  TCluster, TClusterOptions, TFactoryCallback, TMapActionArg, TMapContext, TMapInstance, ILayoutBalloonCluster,
} from "../config/types";
import { MapContext } from "../context";
import {
  bindEvents, getNodeFromElementInstance, manageGeoObjectClass, managePaneVariables,
} from "../lib";
import { ClustererInstanceLayout } from "../lib/clusterer";

type TMapClustererProps = {
  cluster: TCluster;
  onReady?: (instance: Clusterer) => void;
  children?: ReactNode;
};

/**
 * Внутренний компонент содержимого карты
 * @returns {ReactElement}
 */
const MapClusterer: FC<TMapClustererProps> = ({
  onReady, cluster, children,
}): ReactElement => {
  type TClusterActionType = "clusterClick" | "clusterBalloonOpen" | "clusterBalloonClose";

  const {
    containerRef, getCN, onAction, api,
  } = useContext<TMapContext>(MapContext);

  const ymaps = useYMaps(api?.load ?? mapDefaults.api!.load);

  const [ instance, setInstance ] = useState<Clusterer | null>(null);

  const [ layout, setLayout ] = useState<{
    icon: IClassConstructor<ILayout> | "";
    balloon: IClassConstructor<ILayout> | "";
  }>({
    icon: "",
    balloon: "",
  });

  // balloon activity
  const handleBalloonActivity = useCallback<TFactoryCallback<ILayoutBalloonCluster>>((type, Balloon) => {

    const onBalloonClick = (e: IEvent<MouseEvent>) => {
      const originalEvent = e
        .get("domEvent").originalEvent;
      const target: EventTarget | null = originalEvent.target;
      if (target && (target as HTMLElement).hasAttribute("data-js-map-cluster-balloon-close")) {
        originalEvent.preventDefault();
        const balloon = Balloon.getData()?.balloon;

        if (balloon) {
          balloon.close();
        }
      }
      if (target && (target as HTMLElement).hasAttribute("data-js-map-cluster-balloon-tab")) {
        originalEvent.preventDefault();

        const pointId = (target as HTMLElement).getAttribute("data-js-map-cluster-balloon-tab");
        const data = Balloon.getData();
        const activePoint = data?.properties.points.find((p: { id: string; }) => p.id === pointId);

        if (activePoint) {
          Balloon.getData().properties.activePoint = activePoint;
          Balloon.rebuild();
          manageGeoObjectClass(Balloon.getElement() as HTMLElement, constants.stateClasses.visible, true);
        }
      }
    };

    const getActionArgs = (type: TClusterActionType): TMapActionArg => {
      return {
        el: Balloon.getElement(),
        ref: instance,
        stateActions: {},
        getData: () => {
          return {
            type,
            cluster,
          };
        },
      };
    };

    const manageVars = (isAdd = true) => {
      const paneNode = Balloon.getParentElement();
      queueMicrotask(() => {
        managePaneVariables(containerRef, isAdd, paneNode);
      });
    };

    switch (type) {
      case "build": {
        Balloon.events.add("click", onBalloonClick);
        const parentLayout: HTMLElement = Balloon.getParentElement();
        parentLayout.classList.add(getCN("balloons"));
        (parentLayout.parentNode as HTMLElement)!.style.zIndex = cluster.options?.panelZIndex ?? "6003";
        manageVars(true);
        break;
      }
      case "rebuild": {
        manageVars(true);
        break;
      }
      case "destroy": {
        Balloon.events.remove("click", onBalloonClick as (event: object | IEvent<unknown, unknown>) => void);
        if (typeof onAction === "function") {
          onAction(getActionArgs("clusterBalloonClose"));
        }
        manageVars(false);
        break;
      }
    }
  }, [ instance, onAction, getCN, containerRef, cluster.options ]);

  useEffect(() => {
    if (!instance) {
      return;
    }
    const commonArgs = {
      ymaps, getCN, props: cluster, instance,
    };
    Promise.all([
      ClustererInstanceLayout({ ...commonArgs, type: "icon" }),
      ClustererInstanceLayout({ ...commonArgs, type: "balloon", callback: handleBalloonActivity }),
    ])
      .then(([ icon, balloon ]) => {
        return setLayout({
          icon, balloon,
        });
      })
      .catch((err: Error) => {
        console.error(err);
      });
  }, [ ymaps, instance, getCN, cluster, handleBalloonActivity ]);

  const currentOptions = useMemo<TClusterOptions>(() => {
    const size = 48;

    const { icon } = layout;

    return {
      // Основные настройки кластеризации
      gridSize: 64,
      groupByCoordinates: false,
      minimumClusterSize: 2,

      // Настройки поведения
      clusterDisableClickZoom: true,
      clusterOpenBalloonOnClick: false,

      // Настройки внешнего вида кластеров
      clusterIconLayout: icon,
      clusterIconImageSize: [ size, size ],
      clusterIconImageOffset: [ size / -2, size / -2 ],
      clusterIconShape: {
        type: "Rectangle",
        coordinates: [
          [ 0, 0 ],
          [ size, size ],
        ],
      },

      // Настройки балуна
      hasBalloon: false,
      hasHint: false,

      // Отключение стандартного поведения
      clusterHideIconOnBalloonOpen: false,
      clusterBalloonAutoPan: false,
      geoObjectHideIconOnBalloonOpen: false,

      ...cluster.options,
    };
  }, [ cluster.options, layout ]);

  const handleClick = async (event: IEvent) => {
    const obj: IGeoObject<IPointGeometry> & {
      options: TClusterOptions & {
        _name: string;
      };
    } & Clusterer = event.get("target");

    if (obj.options._name === "cluster") {
      const el = await getNodeFromElementInstance(obj);
      const getOnActionFn = (type: TClusterActionType) => {
        if (typeof onAction === "function") {
          onAction({
            event,
            ref: instance,
            el,
            stateActions: {},
            getData: () => {
              return {
                type,
                cluster,
              };
            },
          });
        }
      };

      const defaultFn = async () => {
        const coordinates = (obj?.geometry)?.getCoordinates();
        const map: TMapInstance = obj.getMap();
        if (map && coordinates) {
          if (instance?.balloon) { // balloon activity
            if (instance!.balloon.isOpen()) {
              await instance!.balloon.close(true)
                .then(() => {
                  return getOnActionFn("clusterBalloonClose");
                });
            } else {
              const points = obj?.getGeoObjects().map((obj, index) => {
                const balloonHeader = obj.properties.get("balloonHeader", "" as unknown as object) as unknown as string;
                const balloonContent = obj.properties.get("balloonContent", "" as unknown as object) as unknown as string;
                const id = obj.properties.get("idPoint", "" as unknown as object) as unknown as string;
                const coords = obj.properties.get("coords", "" as unknown as object) as unknown as string[];
                return {
                  index,
                  id,
                  coords,
                  balloonHeader: balloonHeader || `# ${(index + 1).toString()}`,
                  balloonContent: balloonContent || coords.join(", "),
                };
              });
              const activePoint = points.find((item) => item.index === 0);
              const map = instance.getMap();
              await map.panTo(coordinates, {
                checkZoomRange: true,
                duration: 200,
                useMapMargin: true,
              })
                .then(() => {
                  return instance!.balloon.open(coordinates, { // set actual data
                    properties: {
                      points,
                      activePoint,
                    },
                    balloon: instance!.balloon,
                  })
                    // @ts-expect-error (api error)
                    .then(() => getNodeFromElementInstance(instance.balloon))
                    .then((node: HTMLElement | null) => manageGeoObjectClass(node, constants.stateClasses.visible, true))
                    .then(() => {
                      return getOnActionFn("clusterBalloonOpen");
                    })
                    .catch((err: Error) => {
                      console.error(err);
                    });
                });
            }
          } else { // zoom
            await map.panTo(coordinates, {
              checkZoomRange: true,
              duration: 200,
              useMapMargin: true,
            })
              .then(() => {
                const maxZoom = map.options.get("maxZoom", optionsDefaults.maxZoom as unknown as object) as unknown as number;
                const newZoom: number = map.getZoom() + 3;
                return map.setZoom(newZoom > maxZoom ? maxZoom : newZoom);
              })
              .catch((err: Error) => {
                console.error(err);
              });
          }
        }
      };
      if (typeof onAction === "function") {
        getOnActionFn("clusterClick");
        if (!event.getSourceEvent()?.isDefaultPrevented()) {
          await defaultFn();
        }
      } else {
        await defaultFn();
      }
    }
  };

  useEffect(() => {
    if (instance && ymaps) {
      const events = [
        {
          event: "click",
          handler: handleClick,
        },
      ];
      // unbind current events
      bindEvents(instance, events, false);

      // bind current events
      bindEvents(instance, events, true);

      // set balloons
      if (!instance.balloon
        && instance.options.get("hasBalloon", false as unknown as object) as unknown as boolean && layout.balloon) {
        const map = instance.getMap();
        // @ts-expect-error (api error)
        instance.balloon = new ymaps.Balloon(map, {
          autoPan: false,
          panelMaxMapArea: 0,
          layout: layout.balloon,
        });
        // @ts-expect-error (api error)
        instance.balloon.options.setParent(map.options);
      }

      return () => {
        // unbind on unmount
        bindEvents(instance, events, false);
      };

    }
  }, [ instance, layout, ymaps ]);

  useEffect(() => {
    if (instance && layout.icon && layout.balloon && typeof onReady === "function") {
      onReady(instance);
    }
  }, [ instance, layout ]);

  return (
    <ClustererElement
      ymaps={ymaps}
      instanceRef={(instance: Clusterer) => {
        setInstance(instance);
      }}
      options={currentOptions}
    >
      {children}
    </ClustererElement>
  );

};

export {
  MapClusterer,
};
