import { useCallback, useEffect, useRef, useState } from 'react';

import { ipcChannels } from '../../../shared/ipc/contracts';
import type {
  SceneNpcSessionSpawn,
  SceneNpcTokensSessionEvent,
  SceneNpcTokensSessionPlacement,
  SceneNpcTokensSessionState,
} from '../../../shared/types';
import {
  asSceneNpcTokenId,
  clampNpcTokenSessionScale,
  clampSceneNpcTokenSizeN,
  DEFAULT_NPC_TOKEN_SESSION_SCALE,
  DEFAULT_SCENE_NPC_TOKEN_SIZE_N,
  emptySceneNpcTokensSessionState,
  newSceneNpcSessionSpawnId,
} from '../../../shared/types/appPlayers';
import { asNpcId } from '../../../shared/types/ids';
import { normalizeNpcDisposition } from '../../../shared/types/npcDisposition';
import { getDndApi } from '../dndApi';

function withScale(state: SceneNpcTokensSessionState): SceneNpcTokensSessionState {
  return {
    ...state,
    scale: clampNpcTokenSessionScale(state.scale),
    spawned: state.spawned ?? {},
  };
}

function isEmptyPlacement(p: SceneNpcTokensSessionPlacement): boolean {
  return (
    p.nx === undefined &&
    p.ny === undefined &&
    p.sizeN === undefined &&
    !p.disposition &&
    !p.inactive
  );
}

function restOf(prev: SceneNpcTokensSessionPlacement | undefined): SceneNpcTokensSessionPlacement {
  return {
    ...(prev?.nx !== undefined ? { nx: prev.nx } : {}),
    ...(prev?.ny !== undefined ? { ny: prev.ny } : {}),
    ...(prev?.sizeN !== undefined ? { sizeN: prev.sizeN } : {}),
    ...(prev?.disposition ? { disposition: prev.disposition } : {}),
    ...(prev?.inactive ? { inactive: true } : {}),
  };
}

function applyEvent(
  prev: SceneNpcTokensSessionState,
  event: SceneNpcTokensSessionEvent,
): SceneNpcTokensSessionState {
  const spawned = prev.spawned ?? {};
  if (event.kind === 'clear') {
    return emptySceneNpcTokensSessionState(prev.revision + 1);
  }
  if (event.kind === 'setScale') {
    const scale = clampNpcTokenSessionScale(event.scale);
    if (prev.scale === scale) return prev;
    return { ...prev, revision: prev.revision + 1, scale, spawned };
  }
  if (event.kind === 'spawn') {
    const npcId = String(event.npcId ?? '');
    const sceneId = String(event.sceneId ?? '');
    if (!npcId || !sceneId) return prev;
    const id = event.placementId
      ? asSceneNpcTokenId(String(event.placementId))
      : newSceneNpcSessionSpawnId();
    const spawn: SceneNpcSessionSpawn = {
      id,
      sceneId,
      npcId: asNpcId(npcId),
      nx: Math.max(0, Math.min(1, event.nx)),
      ny: Math.max(0, Math.min(1, event.ny)),
      sizeN: clampSceneNpcTokenSizeN(
        typeof event.sizeN === 'number' ? event.sizeN : DEFAULT_SCENE_NPC_TOKEN_SIZE_N,
      ),
      disposition: normalizeNpcDisposition(event.disposition),
    };
    return {
      ...prev,
      revision: prev.revision + 1,
      spawned: { ...spawned, [String(id)]: spawn },
    };
  }
  if (event.kind === 'despawn') {
    const placementId = String(event.placementId ?? '');
    if (!placementId) return prev;
    if (!(placementId in spawned) && !(placementId in prev.byPlacementId)) return prev;
    const { [placementId]: _s, ...nextSpawned } = spawned;
    const { [placementId]: _p, ...byPlacementId } = prev.byPlacementId;
    return {
      ...prev,
      revision: prev.revision + 1,
      byPlacementId,
      spawned: nextSpawned,
    };
  }
  if (event.kind === 'setDisposition') {
    const placementId = String(event.placementId ?? '');
    if (!placementId) return prev;
    const disposition = normalizeNpcDisposition(event.disposition);
    const cur = prev.byPlacementId[placementId];
    const spawn = spawned[placementId];
    if (cur?.disposition === disposition && (!spawn || spawn.disposition === disposition)) {
      return prev;
    }
    return {
      revision: prev.revision + 1,
      byPlacementId: {
        ...prev.byPlacementId,
        [placementId]: {
          ...(cur?.nx !== undefined ? { nx: cur.nx } : {}),
          ...(cur?.ny !== undefined ? { ny: cur.ny } : {}),
          ...(cur?.sizeN !== undefined ? { sizeN: cur.sizeN } : {}),
          disposition,
          ...(cur?.inactive ? { inactive: true } : {}),
        },
      },
      scale: prev.scale ?? DEFAULT_NPC_TOKEN_SESSION_SCALE,
      spawned: spawn ? { ...spawned, [placementId]: { ...spawn, disposition } } : spawned,
    };
  }
  if (event.kind === 'setInactive') {
    const placementId = String(event.placementId ?? '');
    if (!placementId) return prev;
    const inactive = Boolean(event.inactive);
    const cur = prev.byPlacementId[placementId];
    if (Boolean(cur?.inactive) === inactive) return prev;
    const next: SceneNpcTokensSessionPlacement = {
      ...(cur?.nx !== undefined ? { nx: cur.nx } : {}),
      ...(cur?.ny !== undefined ? { ny: cur.ny } : {}),
      ...(cur?.sizeN !== undefined ? { sizeN: cur.sizeN } : {}),
      ...(cur?.disposition ? { disposition: cur.disposition } : {}),
      ...(inactive ? { inactive: true } : {}),
    };
    if (isEmptyPlacement(next)) {
      const { [placementId]: _removed, ...rest } = prev.byPlacementId;
      return {
        revision: prev.revision + 1,
        byPlacementId: rest,
        scale: prev.scale ?? DEFAULT_NPC_TOKEN_SESSION_SCALE,
        spawned,
      };
    }
    return {
      revision: prev.revision + 1,
      byPlacementId: {
        ...prev.byPlacementId,
        [placementId]: next,
      },
      scale: prev.scale ?? DEFAULT_NPC_TOKEN_SESSION_SCALE,
      spawned,
    };
  }
  if (event.kind === 'setSize') {
    const placementId = String(event.placementId ?? '');
    if (!placementId) return prev;
    const sizeN = clampSceneNpcTokenSizeN(event.sizeN);
    const cur = prev.byPlacementId[placementId];
    const spawn = spawned[placementId];
    if (cur?.sizeN === sizeN && (!spawn || spawn.sizeN === sizeN)) return prev;
    return {
      revision: prev.revision + 1,
      byPlacementId: {
        ...prev.byPlacementId,
        [placementId]: { ...restOf(cur), sizeN },
      },
      scale: prev.scale ?? DEFAULT_NPC_TOKEN_SESSION_SCALE,
      spawned: spawn ? { ...spawned, [placementId]: { ...spawn, sizeN } } : spawned,
    };
  }
  // move
  const placementId = String(event.placementId ?? '');
  if (!placementId) return prev;
  const cur = prev.byPlacementId[placementId];
  const spawn = spawned[placementId];
  return {
    revision: prev.revision + 1,
    byPlacementId: {
      ...prev.byPlacementId,
      [placementId]: {
        nx: event.nx,
        ny: event.ny,
        ...(cur?.sizeN !== undefined ? { sizeN: cur.sizeN } : {}),
        ...(cur?.disposition ? { disposition: cur.disposition } : {}),
        ...(cur?.inactive ? { inactive: true } : {}),
      },
    },
    scale: prev.scale ?? DEFAULT_NPC_TOKEN_SESSION_SCALE,
    spawned: spawn ? { ...spawned, [placementId]: { ...spawn, nx: event.nx, ny: event.ny } } : spawned,
  };
}

export function useSceneNpcTokensSession(): [
  SceneNpcTokensSessionState,
  { dispatch: (event: SceneNpcTokensSessionEvent) => void },
] {
  const api = getDndApi();
  const [state, setState] = useState<SceneNpcTokensSessionState>(() =>
    emptySceneNpcTokensSessionState(0),
  );
  const localRevRef = useRef(0);

  useEffect(() => {
    void api.invoke(ipcChannels.sceneNpcTokensSession.getState, {}).then((res) => {
      const next = withScale(res.state);
      if (next.revision < localRevRef.current) return;
      localRevRef.current = next.revision;
      setState(next);
    });
    return api.on(ipcChannels.sceneNpcTokensSession.stateChanged, ({ state: incoming }) => {
      const next = withScale(incoming);
      if (next.revision < localRevRef.current) return;
      localRevRef.current = next.revision;
      setState(next);
    });
  }, [api]);

  const dispatch = useCallback(
    (event: SceneNpcTokensSessionEvent) => {
      setState((prev) => {
        const next = applyEvent(prev, event);
        localRevRef.current = Math.max(localRevRef.current, next.revision);
        return next;
      });
      void api.invoke(ipcChannels.sceneNpcTokensSession.dispatch, { event });
    },
    [api],
  );

  return [state, { dispatch }];
}
