import { Placemark, useYMaps } from "@pbe/react-yandex-maps";
import { isMediaQuery } from "@web3r/flowerkit/css";
import type { FC, ReactNode, RefObject } from "react";
import {
  useMemo, useEffect, useState, useCallback, useRef, useContext,
} from "react";
import type { IEvent, Placemark as PlacemarkInstance } from "yandex-maps";
import { constants } from "../../../constants";
import { mapDefaults, pointDefaults } from "../config";
import type {
  TPoint,
  TPointOptions,
  TMapContext,
  TFactoryCallback,
  TMapActionArg,
  TConstructorInstance,
  TCluster,
  TPanelData, TMapInstance, ILayoutHint, ILayoutBalloon, ILayoutIcon, TBalloonInstance,
} from "../config/types";
import { MapContext } from "../context";
import {
  bindEvents, manageGeoObjectClass, getNodeFromElementInstance, getMapMoveWithPx,
} from "../lib";
import {
  PlacemarkInstanceLayout,
  getNodeFromPlacemarkEvent,
  getPropFromInstanceLayout,
  handlePreventableEvent,
  getPointDataForCallback,
  getInView,
  getWithBalloon,
  getNextClustersByPointClick,
  handleBalloonDestroy,
} from "../lib/placemark";

type TMapPlacemarkProps = {
  point: TPoint;
  cluster: TCluster;
  activePointsCountRef: RefObject<number>;
  isPanel: boolean;
};


/**
 * Внутренний компонент содержимого карты

 * @returns {ReactElement}
 */
export const MapPlacemark: FC<TMapPlacemarkProps> = ({
  point,
  activePointsCountRef,
  isPanel,
}: TMapPlacemarkProps): ReactNode => {

  const {
    onAction,
    stateActions: { setClusters, setPanelData, setPointState },
    getCN,
    api,
    instanceRef: mapRef,
    balloonRef,
    containerRef,
    maxActivePoints,
    currentClusters,
  } = useContext<TMapContext>(MapContext);

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

  // Panel data
  const pointPanelDataRef = useRef<TPanelData>(null);

  // instance state
  const [ instance, setInstance ] = useState<PlacemarkInstance | null>(null);

  // clicked state
  const [ isActive, setIsActive ] = useState<boolean>(point.state?.isActive ?? pointDefaults.state.isActive);

  // visibility state
  const [ isDisabled, setIsDisabled ] = useState<boolean>(point.state?.isDisabled ?? pointDefaults.state.isDisabled);

  // interactivity state
  const [ isInteract, setIsInteract ] = useState<boolean>(point.state?.isHasInteraction ?? pointDefaults.state.isHasInteraction);

  // prev activity state
  const prevActive = useRef<boolean>(isActive);

  // prev disabled state
  const prevDisabled = useRef<boolean>(isDisabled);

  // stored calculated nextIsActive for onAction callback
  const nextIsActiveRef = useRef<boolean>(false);

  // first sync flag for initial active point handling
  const isFirstStateSyncRef = useRef<boolean>(true);

  // Balloon opening flag
  const isBalloonOpening = useRef<boolean>(false);

  // layout state
  const [ layout, setLayout ] = useState<{
    isReady: boolean;
    instance: {
      icon: TConstructorInstance<ILayoutIcon> | "";
      hint: TConstructorInstance<ILayoutHint> | "";
      balloon: TConstructorInstance<ILayoutBalloon> | "";
    };
  }>({
    isReady: false,
    instance: {
      icon: "",
      hint: "",
      balloon: "",
    },
  });

  // stateActions
  const stateActions = {
    setIsActive,
    setIsDisabled,
    setIsInteract,
    setPanelData,
  };

  const openPanelForPoint = useCallback(() => {
    if (!isPanel) {
      return;
    }
    const panelData = {
      ...getPointDataForCallback(point),
      onClose: () => {
        pointPanelDataRef.current = null;
        setIsActive(false);
      },
    };
    setPanelData((prevData: TPanelData) => {
      if (typeof prevData?.onClose === "function" && prevData?.id !== point.id) {
        prevData.onClose();
      }
      return panelData;
    });
    pointPanelDataRef.current = panelData;
  }, [ isPanel, point, setPanelData ]);

  useEffect(() => {
    if (!isPanel || !isActive || pointPanelDataRef.current) {
      return;
    }
    openPanelForPoint();
  }, [ isPanel, isActive, openPanelForPoint ]);

  useEffect(() => {
    const currentActiveState = point.state?.isActive ?? pointDefaults.state.isActive;
    if (currentActiveState !== isActive) {
      setIsActive(currentActiveState);
    }
    if (point.state?.isDisabled !== isDisabled) {
      setIsDisabled(point.state?.isDisabled ?? pointDefaults.state.isDisabled);
    }
    if (point.state?.isHasInteraction !== isInteract) {
      setIsInteract(point.state?.isHasInteraction ?? pointDefaults.state.isHasInteraction);
    }
  }, [ point.state?.isActive, point.state?.isDisabled, point.state?.isHasInteraction, point ]);

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

    function onBalloonClick(e: IEvent<PointerEvent>) {
      const originalEvent = e.get("domEvent").originalEvent;
      const target: EventTarget | null = originalEvent.target;
      if (target && (target as HTMLElement).hasAttribute("data-js-map-balloon-close")) {
        originalEvent.preventDefault();
        Balloon.events.fire("close");
      }
    }

    // balloon closing
    function onBalloonClose() {
      setIsActive(false);
    }

    const getActionArgs = (type: "balloonOpen" | "balloonClose"): TMapActionArg => {
      return {
        stateActions,
        el: Balloon.getElement(),
        ref: instance,
        getData: () => {
          return {
            type,
            point,
          };
        },
      };
    };
    switch (type) {
      case "build": {
        Balloon.events.add("click", onBalloonClick);
        Balloon.events.add("close", onBalloonClose);

        // append current instance
        balloonRef.current = Balloon;

        const geoObject = Balloon?.getData()?.geoObject;
        if (geoObject) {
          const map: TMapInstance = geoObject.getMap();
          if (map) {
            const balloonNode: HTMLElement = Balloon.getElement();
            const balloonLayout = balloonNode.children[0];
            balloonLayout.classList.add("isAnimating");

            const parentLayout: HTMLElement = Balloon.getParentElement();
            parentLayout.classList.add(getCN("balloons"));

            (parentLayout.parentNode as HTMLElement)!.style.zIndex = point.options?.panelZIndex ?? "6003";

            const coords = geoObject?.geometry?.getCoordinates() as [ number, number ];

            balloonNode.addEventListener("transitionend", async () => {
              const callback = () => {
                balloonLayout.classList.remove("isAnimating");
                if (typeof onAction === "function") {
                  onAction(getActionArgs("balloonOpen"));
                }
              };
              if (isMediaQuery(`(${constants.mediaQueries.mobile})`)) {
                callback();
              } else { // desktop-only centering
                const x = balloonNode.offsetWidth / 2;
                const y = (balloonNode.offsetHeight / 2);
                await getMapMoveWithPx(map, coords, x, y)
                  .then(() => {
                    return callback();
                  })
                  .catch((err: Error) => {
                    console.error(err);
                  });
              }
            }, {
              once: true,
            });
          }
        }
        break;
      }
      case "destroy": {
        // remove current instance
        balloonRef.current = null;

        // @ts-expect-error (api)
        Balloon.events.remove("click", onBalloonClick);
        Balloon.events.remove("close", onBalloonClose);
        handleBalloonDestroy({
          setIsActive,
          onAction,
          getActionArgs: () => getActionArgs("balloonClose"),
        });
        break;
      }
    }
  }, [ instance, getCN, balloonRef, point, stateActions, onAction ]);

  // onClick
  const handleClick = useCallback((event: IEvent) => {

    // @ts-expect-error (api)
    const clickedPointId: string = event.originalEvent?.target?.properties?.get?.("idPoint");

    if (clickedPointId !== point.id) {
      return;
    }

    const getNextByClick = (clusters: TCluster[]) => getNextClustersByPointClick({
      clusters,
      pointId: point.id,
      maxActivePoints,
    });

    nextIsActiveRef.current = getNextByClick(currentClusters).nextIsActive;

    const defaultFn = () => {
      if (isInteract) {
        setClusters((clustersState: TCluster[]) => {
          const { nextClusters, nextIsActive } = getNextByClick(clustersState);
          nextIsActiveRef.current = nextIsActive;
          return nextClusters;
        }, false);
      }
    };

    const action = typeof onAction === "function" ? () => {
      onAction({
        event,
        stateActions,
        el: getNodeFromPlacemarkEvent(event),
        ref: instance,
        getData: () => {
          return {
            type: "pointClick",
            point: getPointDataForCallback(point),
            state: {
              isActive: nextIsActiveRef.current,
              isInteract,
              isDisabled,
            },
          };
        },
      });
    } : undefined;

    handlePreventableEvent({ event, action, defaultFn });
  }, [ point, maxActivePoints, isInteract, onAction, instance, stateActions, setClusters, isDisabled, currentClusters ]);

  // onMouseLeave
  const handleMouseLeave = (event: IEvent) => {

    const defaultFn = async () => {
      const Hint = instance?.hint;
      if (Hint?.isOpen()) {
        await getNodeFromElementInstance(Hint)
          .then((node) => {
            return manageGeoObjectClass(node, constants.stateClasses.visible, false, () => {
              Hint?.close();
            });
          })
          .catch((err: Error) => {
            console.error(err);
          });
      }
    };

    const action = typeof onAction === "function" ? () => {
      onAction({
        event,
        stateActions,
        el: getNodeFromPlacemarkEvent(event),
        ref: instance,
        getData: () => {
          return {
            type: "pointMouseleave",
            point: getPointDataForCallback(point),
          };
        },
      });
    } : undefined;

    handlePreventableEvent({ event, action, defaultFn });

  };

  // onMouseEnter
  const handleMouseEnter = (event: IEvent) => {

    const defaultFn = async () => {
      const Hint = instance?.hint;
      if (!Hint?.isOpen() && !instance?.balloon?.isOpen()) {
        await Hint?.open()
          .then(async () => {
            const node = await getNodeFromElementInstance(Hint);
            return manageGeoObjectClass(node, constants.stateClasses.visible, true);
          })
          .catch((err: Error) => {
            console.error(err);
          });
      }
    };

    const action = typeof onAction === "function" ? () => {
      onAction({
        event,
        stateActions,
        el: getNodeFromPlacemarkEvent(event),
        ref: instance,
        getData: () => {
          return {
            type: "pointMouseenter",
            point: getPointDataForCallback(point),
          };
        },
      });
    } : undefined;

    handlePreventableEvent({ event, action, defaultFn });
  };

  // placemark options
  const computedOptions = useMemo<TPointOptions>(() => {

    // default sizes
    const [ defaultWidth, defaultHeight ] = pointDefaults.icon.size;

    // user sizes
    const [
      currentWidth = defaultWidth,
      currentHeight = defaultHeight,
    ] = point.icon?.size ?? pointDefaults.icon.size;

    // balloon offset
    const balloonOffset: [ number, number ] = [ currentWidth / 2, currentHeight / 2 ];

    // hint offset
    const hintOffset: [ number, number ] = [ currentWidth / 2, currentHeight / 2 ];

    // icon offset
    const iconOffset: [ number, number ] = [ currentWidth / -2, currentHeight / -2 ];

    // layouts
    const { hint, balloon, icon } = layout.instance;

    // elements
    const isBalloon = !!getPropFromInstanceLayout(balloon, "title") || !!getPropFromInstanceLayout(balloon, "description");
    const isHint = !!getPropFromInstanceLayout(hint, "title") || !!getPropFromInstanceLayout(hint, "description");

    // cursor
    const cursor = (isInteract || isHint) ? "pointer" : "default";

    return {
      iconOffset,
      iconShape: {
        type: "Rectangle",
        coordinates: [
          [ 0, 0 ],
          [ currentWidth, currentHeight ],
        ],
      },
      openBalloonOnClick: false,
      openHintOnHover: false,
      hideIconOnBalloonOpen: false,
      balloonAutoPan: true,
      balloonAutoPanUseMapMargin: true,
      balloonAutoPanCheckZoomRange: true,
      balloonShadow: false,
      cursor,
      balloonOffset,
      hintOffset,
      hintLayout: hint,
      hintCloseTimeout: 200,
      hintOpenTimeout: 0,
      iconLayout: icon,
      balloonLayout: balloon,
      balloonCloseTimeout: 200,
      visible: !isDisabled,
      hasBalloon: isBalloon,
      hasHint: isHint,
      draggable: false,
      balloonPanelMaxMapArea: 0,
      ...point.options,
    };
  }, [ isInteract, isDisabled, point.options, layout ]);

  // placemark props
  const computedProperties = useMemo(() => {
    const { hint, balloon } = layout.instance;
    return {
      // balloon
      balloonHeader: getPropFromInstanceLayout(balloon, "title"),
      balloonContent: getPropFromInstanceLayout(balloon, "description"),

      // hint
      hintHeader: getPropFromInstanceLayout(hint, "title"),
      hintContent: getPropFromInstanceLayout(hint, "description"),

      // states
      isDisabled,
      isActive,
      isInteract,

      // meta
      idPoint: point.id,
      coords: point.coords,
    };
  }, [ isInteract, isActive, isDisabled, layout ]);

  useEffect(() => {
    if (!!instance) {

      // set layout
      if (!layout.isReady) {
        const commonArgs = {
          ymaps, props: point, getCN, map: mapRef.current,
        };
        Promise.all([
          PlacemarkInstanceLayout<ILayoutBalloon>({ ...commonArgs, type: "balloon", callback: handleBalloonActivity }),
          PlacemarkInstanceLayout<ILayoutIcon>({ ...commonArgs, type: "icon" }),
          PlacemarkInstanceLayout<ILayoutHint>({ ...commonArgs, type: "hint" }),
        ])
          .then(([ balloon, icon, hint ]) => {
            return setLayout({
              isReady: true,
              instance: {
                balloon,
                icon,
                hint,
              },
            });
          })
          .catch((err: Error) => {
            console.error(err);
          });
      }

      // set/remove events
      const events = [
        {
          event: "click",
          handler: handleClick,
        },
        {
          event: "mouseenter",
          handler: handleMouseEnter,
        },
        {
          event: "mouseleave",
          handler: handleMouseLeave,
        },
      ];
      bindEvents(instance, events, false);
      bindEvents(instance, events, true);

      const shouldSyncInitialActiveState = isFirstStateSyncRef.current && isActive;
      isFirstStateSyncRef.current = false;

      if (shouldSyncInitialActiveState && isPanel) {
        openPanelForPoint();
      }

      // set activity or disabled state (only for interactive point)
      if (isInteract) {

        const isDisabledChanged = prevDisabled.current !== isDisabled;
        const isActivityChanged = prevActive.current !== isActive;

        // set a disabled state
        if (isDisabledChanged) {
          // update prev ref of disabled state
          prevDisabled.current = isDisabled;
        }

        // check activity if changed or initially active
        if (isActivityChanged || shouldSyncInitialActiveState) {

          // update prev ref of activity
          prevActive.current = isActive;

          if (isActive) {

            // increase active count only when state is changed by user interaction
            if (isActivityChanged) {
              activePointsCountRef.current++;
            }

            // show panel if exist
            if (isPanel && isActivityChanged) {
              openPanelForPoint();
            }

          } else {
            // decrease active count
            if (activePointsCountRef.current > 0) {
              activePointsCountRef.current--;
            }

            // clear panel data
            if (isPanel && pointPanelDataRef.current) {
              setPanelData(null);
            }
          }
        }

        // update cluster state
        if (isActivityChanged || isDisabledChanged || shouldSyncInitialActiveState) {
          setPointState(point.id, {
            isActive,
            isDisabled,
          });
        }
      }

      return () => {
        // remove listeners on unmounting
        bindEvents(instance, events, false);
      };
    }
  }, [ instance, isActive, isDisabled, ymaps, handleClick, isPanel, openPanelForPoint ]);

  useEffect(() => {
    // open or close balloon if API are ready
    if (!instance?.balloon) {
      isBalloonOpening.current = false;
      return;
    }

    let isDisposed = false;

    const applyBalloonState = async () => {

      // Update loading ref
      isBalloonOpening.current = true;

      if (isActive) {
        if (instance.balloon.isOpen()) {
          return;
        }

        // When switching active points we should release link to previous balloon
        // so new active point can be opened in the same cycle.
        const prevBalloon = balloonRef.current;
        if (prevBalloon && prevBalloon !== instance.balloon) {
          try {
            if (prevBalloon.isOpen?.()) {
              await prevBalloon.close();
            }
          } catch (err) {
            console.error("[Map] Failed to close previous balloon:", err);
          } finally {
            if (balloonRef.current === prevBalloon) {
              balloonRef.current = null;
            }
          }
        }

        if (isDisposed) {
          return;
        }

        balloonRef.current = instance.balloon;
        await getInView(instance, mapRef, point, containerRef)
          .then(() => getNodeFromElementInstance(instance.balloon))
          .then((node: HTMLElement | null) => manageGeoObjectClass(node, constants.stateClasses.visible, true))
          .catch((err: Error) => {
            console.error(err);
            if (balloonRef.current === instance.balloon) {
              balloonRef.current = null;
            }
          });
      } else {
        if (instance.balloon.isOpen()) { // check if balloon is open
          await getNodeFromElementInstance(instance.balloon)
            .then((node) => {
              return manageGeoObjectClass(node, constants.stateClasses.visible, false, async () => {
                return getWithBalloon(instance, false, containerRef);
              });
            })
            .catch((err: Error) => {
              console.error(err);
            });
        }
        // clear ref if this is current balloon
        if (balloonRef.current === instance.balloon) {
          balloonRef.current = null;
        }
      }
    };

    if (!isBalloonOpening.current) {
      applyBalloonState()
        .finally(() => {
          isBalloonOpening.current = false;
        });
    }
    return () => {
      isDisposed = true;
    };
  }, [ instance?.balloon, isActive, layout.isReady ]);

  return (
    <Placemark
      // @ts-expect-error (api bug)
      instanceRef={(inst: PlacemarkInstance) => {
        if (inst && !instance) {
          setInstance(inst);
        }
      }}
      properties={computedProperties}
      geometry={point.coords}
      options={computedOptions}
    />
  );

};
