import { Size } from "@noya-app/noya-geometry";
import {
  getCurrentPlatform,
  handleKeyboardEvent,
  KeyMap,
} from "@noya-app/noya-keymap";
import { isDeepEqual } from "@noya-app/noya-utils";
import { assignRef, useLateReferenceState } from "@noya-app/react-utils";
import React, {
  ComponentProps,
  forwardRef,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useRef,
  useState,
} from "react";
import { InfiniteCanvas } from "../components/InfiniteCanvas";
import { convertOffsetPoint, convertPoint } from "../utils/pointUtils";
import {
  BaseMode,
  Plugin,
  PluginSystem,
  PluginSystemContextProperties,
  PluginSystemData,
  PluginSystemEventHandlerMap,
  PluginSystemMode,
  PluginSystemOptions,
} from "./PluginSystem";
import {
  AnyCanvasPlugin,
  AnyCanvasPluginSystem,
  BaseCanvasData,
  getCanvasTransform,
  initialBaseCanvasData,
  initialPointerData,
  ReactEventHandlerMap,
} from "./baseCanvas";
import {
  BaseContextProperties,
  DOMContextProperties,
} from "./contextProperties";
import {
  PointerData,
  pointerPlugin,
  UniquePointerContextProperties,
} from "./pointerPlugin";

type AllBaseContextProperties<
  Mode extends BaseMode,
  Data extends BaseCanvasData & PointerData,
> = BaseContextProperties<Mode, Data> & DOMContextProperties;

type ContextPropertiesWithoutBase<
  Mode extends BaseMode,
  Data extends BaseCanvasData & PointerData,
  ContextProperties extends object,
> = Omit<ContextProperties, keyof AllBaseContextProperties<Mode, Data>>;

// Fix covariance/contravariance
type ContextPropertiesBaseFixType<
  Mode extends BaseMode,
  Data extends BaseCanvasData & PointerData,
  ContextProperties extends object,
> = ContextPropertiesWithoutBase<Mode, Data, ContextProperties> &
  AllBaseContextProperties<Mode, Data>;

type PluginSystemToReactCanvas<PS extends AnyCanvasPluginSystem> = ReactCanvas<
  PluginSystemMode<PS>,
  PluginSystemData<PS>,
  PluginSystemContextProperties<PS>,
  PluginSystemEventHandlerMap<PS>
>;

const AnyDataContext = React.createContext<
  (BaseCanvasData & PointerData) | undefined
>(undefined);
const AnyModeContext = React.createContext<BaseMode | undefined>(undefined);
const AnyContextPropertiesContext = React.createContext<
  ContextPropertiesBaseFixType<any, any, any> | undefined
>(undefined);

export class ReactCanvas<
  Mode extends BaseMode,
  Data extends BaseCanvasData & PointerData,
  ContextProperties extends object,
  EventHandlerMap extends ReactEventHandlerMap,
> {
  private constructor(
    public pluginSystem: PluginSystem<
      Mode,
      Data,
      ContextProperties,
      EventHandlerMap
    >
  ) {}

  static create() {
    return new ReactCanvas(
      PluginSystem.create()
        .addData(initialBaseCanvasData)
        .addData(initialPointerData)
    );
  }

  addPlugin<
    M extends BaseMode,
    D extends BaseCanvasData & PointerData,
    H extends object,
    E extends ReactEventHandlerMap,
  >(plugin: Plugin<M, D, H, E>) {
    return new ReactCanvas(this.pluginSystem.clone().addPlugin(plugin));
  }

  addPlugins<P extends AnyCanvasPlugin[]>(
    ...plugins: P
  ): PluginSystemToReactCanvas<CanvasPluginsToSystem<P>>;
  addPlugins(...plugins: AnyCanvasPlugin[]) {
    const newPluginSystem = plugins.reduce(
      (pluginSystem, plugin) => pluginSystem.addPlugin(plugin),
      this.pluginSystem.clone() as AnyCanvasPluginSystem
    );

    return new ReactCanvas(newPluginSystem);
  }

  addOptions(options: PluginSystemOptions<ContextProperties>) {
    return new ReactCanvas(this.pluginSystem.clone().addOptions(options));
  }

  get initialMode() {
    return this.pluginSystem.mode;
  }

  get initialData() {
    return this.pluginSystem.data;
  }

  PropertiesRefContext = AnyContextPropertiesContext as React.Context<
    ContextPropertiesBaseFixType<Mode, Data, ContextProperties> | undefined
  >;

  ModeContext = AnyModeContext as React.Context<Mode | undefined>;

  DataContext = AnyDataContext as React.Context<Data | undefined>;

  ModeProvider = this.ModeContext.Provider;

  DataProvider = this.DataContext.Provider;

  useContextPropertiesRef = () => {
    const context = useContext(this.PropertiesRefContext);
    if (!context) {
      throw new Error(
        "usePropertiesRefContext must be used within a CanvasProvider"
      );
    }
    return context;
  };

  useMode = () => {
    const context = useContext(this.ModeContext);
    if (!context) {
      throw new Error("useModeContext must be used within a CanvasProvider");
    }
    return context;
  };

  useData = () => {
    const context = useContext(this.DataContext);
    if (!context) {
      throw new Error("useDataContext must be used within a CanvasProvider");
    }
    return context;
  };

  Canvas = forwardRef(
    (
      {
        mode: modeProp,
        data: dataProp,
        contextProperties: withoutBase,
        setMode: setModeProp,
        setData: setDataProp,
        children,
        InfiniteCanvasComponent = InfiniteCanvas,
        ...rest
      }: {
        mode?: Mode;
        data?: Data;
        contextProperties: (
          base: BaseContextProperties<Mode, Data>
        ) => ContextPropertiesWithoutBase<Mode, Data, ContextProperties> &
          UniquePointerContextProperties;
        setMode?: (mode: Mode) => void;
        setData?: (data: Data) => void;
        children?: React.ReactNode;
        InfiniteCanvasComponent?: typeof InfiniteCanvas;
      } & Omit<
        React.ComponentProps<typeof InfiniteCanvas>,
        "onChangeSize" | "children"
      >,
      forwardedRef: React.Ref<HTMLDivElement>
    ) => {
      const [internalMode, setInternalMode] = useState<Mode>(
        modeProp ?? this.initialMode
      );
      const [internalData, setInternalData] = useState<Data>(
        dataProp ?? this.initialData
      );

      const [mode, setMode_] =
        modeProp !== undefined && setModeProp
          ? [modeProp, setModeProp]
          : [internalMode, setInternalMode];

      const [data, setData_] =
        dataProp !== undefined && setDataProp
          ? [dataProp, setDataProp]
          : [internalData, setInternalData];

      const [getMode, setMode] = useLateReferenceState(mode, setMode_);
      const [getData, setData] = useLateReferenceState(data, setData_);

      const {
        PropertiesRefContext: HandlerContext,
        ModeContext,
        DataContext,
      } = this;

      const canvasRef = useRef<HTMLDivElement>(null);

      const base = useMemo(
        (): BaseContextProperties<Mode, Data> => ({
          getMode,
          setMode,
          getData,
          setData,
        }),
        [getMode, setMode, getData, setData]
      );

      const baseRef = useRef(base);

      useEffect(() => {
        baseRef.current = base;
      }, [base]);

      const [readOnlyInternal, setReadOnlyInternal] = useState(false);
      const [shouldHandleKeyboardEvents, setShouldHandleKeyboardEvents] =
        useState(true);

      const domContextProperties: DOMContextProperties = useMemo(
        () => ({
          canvasRef,
          handleKeyboardEvent:
            (keyMap: KeyMap) => (event: React.KeyboardEvent<Element>) => {
              const platform = getCurrentPlatform(navigator);
              return handleKeyboardEvent(event.nativeEvent, platform, keyMap);
            },
          setReadOnly: setReadOnlyInternal,
          setShouldHandleKeyboardEvents,
          getScreenPoint: convertOffsetPoint,
          getCanvasPoint(input) {
            return this.convertPoint(this.getScreenPoint(input), "canvas");
          },
          convertPoint(point, targetCoordinateSystem) {
            const data = baseRef.current.getData();

            return convertPoint(
              data.origin,
              data.zoom,
              point,
              targetCoordinateSystem
            );
          },
          setPointerCapture: (pointerId: number) => {
            canvasRef.current?.setPointerCapture(pointerId);
          },
          releasePointerCapture: (pointerId: number) => {
            canvasRef.current?.releasePointerCapture(pointerId);
          },
        }),
        []
      );

      const contextProperties = useMemo(
        (): ContextPropertiesBaseFixType<Mode, Data, ContextProperties> => ({
          ...base,
          ...domContextProperties,
          ...withoutBase(base),
        }),
        [base, domContextProperties, withoutBase]
      );

      const pluginSystemWithPointer = useMemo(
        () => this.pluginSystem.clone().addPlugin(pointerPlugin()),
        []
      );

      const eventHandlers = pluginSystemWithPointer
        .clone()
        .getHandlers(contextProperties as any);

      const contextPropertiesRef = useRef(contextProperties);

      useEffect(() => {
        contextPropertiesRef.current = contextProperties;
      }, [contextProperties]);

      const handleSizeChange = useCallback((size: Size) => {
        const { getData, setData } = contextPropertiesRef.current;
        const data = getData();
        if (isDeepEqual(size, data.size)) return;
        setData({ ...data, size });
      }, []);

      const handleRef = useCallback(
        (value: HTMLDivElement | null) => {
          assignRef(canvasRef, value);
          assignRef(forwardedRef, value);
        },
        [forwardedRef]
      );

      const OwnProvider = this.ReactCanvasProvider;

      return (
        <HandlerContext.Provider value={contextProperties}>
          <ModeContext.Provider value={mode}>
            <DataContext.Provider value={data}>
              <OwnProvider>
                <InfiniteCanvasComponent
                  ref={handleRef}
                  onChangeSize={handleSizeChange}
                  readOnly={readOnlyInternal}
                  shouldHandleKeyboardEvents={shouldHandleKeyboardEvents}
                  {...rest}
                  {...eventHandlers}
                  style={{
                    cursor: this.pluginSystem.getCursor({ data, mode }),
                    ...rest.style,
                  }}
                >
                  {children}
                </InfiniteCanvasComponent>
              </OwnProvider>
            </DataContext.Provider>
          </ModeContext.Provider>
        </HandlerContext.Provider>
      );
    }
  );

  ReactCanvasProvider = ({ children }: { children: React.ReactNode }) => {
    return (
      <ReactCanvasContext.Provider
        value={this as ReactCanvas<any, any, any, any>}
      >
        {children}
      </ReactCanvasContext.Provider>
    );
  };

  createContextProperties = (
    value: ComponentProps<typeof this.Canvas>["contextProperties"]
  ) => {
    return value;
  };

  getTransform = getCanvasTransform;

  useTransform = () => {
    const data = this.useData();
    return useMemo(
      () => this.getTransform({ origin: data.origin, zoom: data.zoom }),
      [data.origin, data.zoom]
    );
  };
}

export const ReactCanvasContext = React.createContext<
  | ReactCanvas<
      BaseMode,
      BaseCanvasData & PointerData,
      {},
      ReactEventHandlerMap
    >
  | undefined
>(undefined);

export const useReactCanvas = () => {
  const context = useContext(ReactCanvasContext);
  if (!context) {
    throw new Error("useReactCanvas must be used within a ReactCanvasProvider");
  }
  return context;
};

export function createCanvas() {
  return ReactCanvas.create();
}

export type CanvasPluginsToSystem<Plugins extends AnyCanvasPlugin[]> =
  CanvasPluginsToSystemRecurse<
    // identity for a union is never
    never,
    BaseCanvasData & PointerData,
    {},
    ReactEventHandlerMap,
    Plugins
  >;
type CanvasPluginsToSystemRecurse<
  Mode extends BaseMode,
  Data extends BaseCanvasData & PointerData,
  UniqueContextProperties extends object,
  EventHandlerMap extends ReactEventHandlerMap,
  Plugins extends readonly AnyCanvasPlugin[],
> = Plugins extends readonly [
  Plugin<infer M, infer D, infer H, infer E>,
  ...infer Tail extends AnyCanvasPlugin[],
]
  ? CanvasPluginsToSystemRecurse<
      Mode | M,
      Data & D,
      // don't include context shared across all plugins
      UniqueContextProperties & Omit<H, keyof BaseContextProperties<any, any>>,
      EventHandlerMap & E,
      Tail
    >
  : PluginSystem<
      Mode,
      Data,
      BaseContextProperties<Mode, Data> & UniqueContextProperties,
      EventHandlerMap
    >;
