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

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 withMove(
  prev: SceneNpcTokensSessionPlacement | undefined,
  nx: number,
  ny: number,
): SceneNpcTokensSessionPlacement {
  return {
    ...restOf(prev),
    nx,
    ny,
  };
}

function withoutPlacement(
  state: SceneNpcTokensSessionState,
  placementId: string,
): SceneNpcTokensSessionState {
  if (!(placementId in state.byPlacementId)) return state;
  const { [placementId]: _removed, ...rest } = state.byPlacementId;
  return {
    revision: state.revision + 1,
    byPlacementId: rest,
    scale: state.scale,
    spawned: state.spawned,
  };
}

export class SceneNpcTokensSessionStore {
  private state: SceneNpcTokensSessionState = emptySceneNpcTokensSessionState();

  getState(): SceneNpcTokensSessionState {
    return this.state;
  }

  reset(): SceneNpcTokensSessionState {
    if (
      Object.keys(this.state.byPlacementId).length === 0 &&
      Object.keys(this.state.spawned).length === 0 &&
      this.state.scale === DEFAULT_NPC_TOKEN_SESSION_SCALE
    ) {
      return this.state;
    }
    this.state = emptySceneNpcTokensSessionState(this.state.revision + 1);
    return this.state;
  }

  /** Rotate session position overrides with the scene preview (CSS rotate steps). */
  rotateMapCwSteps(steps: number): SceneNpcTokensSessionState {
    const n = ((steps % 4) + 4) % 4;
    if (n === 0) return this.state;
    const ids = Object.keys(this.state.byPlacementId);
    const spawnIds = Object.keys(this.state.spawned);
    if (ids.length === 0 && spawnIds.length === 0) return this.state;
    let changed = false;
    const byPlacementId: SceneNpcTokensSessionState['byPlacementId'] = {};
    for (const id of ids) {
      const prev = this.state.byPlacementId[id];
      if (!prev) continue;
      if (typeof prev.nx === 'number' && typeof prev.ny === 'number') {
        const p = rotateMapNormPointByCwSteps(prev.nx, prev.ny, n);
        byPlacementId[id] = { ...prev, nx: p.nx, ny: p.ny };
        changed = true;
      } else {
        byPlacementId[id] = prev;
      }
    }
    const spawned: SceneNpcTokensSessionState['spawned'] = {};
    for (const id of spawnIds) {
      const prev = this.state.spawned[id];
      if (!prev) continue;
      const p = rotateMapNormPointByCwSteps(prev.nx, prev.ny, n);
      spawned[id] = { ...prev, nx: p.nx, ny: p.ny };
      changed = true;
    }
    if (!changed) return this.state;
    this.state = {
      revision: this.state.revision + 1,
      byPlacementId,
      scale: this.state.scale,
      spawned,
    };
    return this.state;
  }

  dispatch(event: SceneNpcTokensSessionEvent): SceneNpcTokensSessionState {
    switch (event.kind) {
      case 'clear':
        return this.reset();
      case 'setScale': {
        const scale = clampNpcTokenSessionScale(event.scale);
        if (this.state.scale === scale) return this.state;
        this.state = {
          ...this.state,
          revision: this.state.revision + 1,
          scale,
        };
        return this.state;
      }
      case 'spawn': {
        const npcId = String(event.npcId ?? '');
        const sceneId = String(event.sceneId ?? '');
        if (!npcId || !sceneId) return this.state;
        const nx = Math.max(0, Math.min(1, event.nx));
        const ny = Math.max(0, Math.min(1, event.ny));
        if (!Number.isFinite(nx) || !Number.isFinite(ny)) return this.state;
        const id = event.placementId
          ? asSceneNpcTokenId(String(event.placementId))
          : newSceneNpcSessionSpawnId();
        const spawn: SceneNpcSessionSpawn = {
          id,
          sceneId,
          npcId: asNpcId(npcId),
          nx,
          ny,
          sizeN: clampSceneNpcTokenSizeN(
            typeof event.sizeN === 'number' ? event.sizeN : DEFAULT_SCENE_NPC_TOKEN_SIZE_N,
          ),
          disposition: normalizeNpcDisposition(event.disposition),
        };
        this.state = {
          revision: this.state.revision + 1,
          byPlacementId: this.state.byPlacementId,
          scale: this.state.scale,
          spawned: { ...this.state.spawned, [String(spawn.id)]: spawn },
        };
        return this.state;
      }
      case 'despawn': {
        const placementId = String(event.placementId ?? '');
        if (!placementId) return this.state;
        if (!(placementId in this.state.spawned) && !(placementId in this.state.byPlacementId)) {
          return this.state;
        }
        const { [placementId]: _s, ...spawned } = this.state.spawned;
        const { [placementId]: _p, ...byPlacementId } = this.state.byPlacementId;
        this.state = {
          revision: this.state.revision + 1,
          byPlacementId,
          scale: this.state.scale,
          spawned,
        };
        return this.state;
      }
      case 'move': {
        const placementId = String(event.placementId ?? '');
        if (!placementId) return this.state;
        const nx = Math.max(0, Math.min(1, event.nx));
        const ny = Math.max(0, Math.min(1, event.ny));
        if (!Number.isFinite(nx) || !Number.isFinite(ny)) return this.state;
        const prev = this.state.byPlacementId[placementId];
        const spawn = this.state.spawned[placementId];
        if (prev && prev.nx === nx && prev.ny === ny) return this.state;
        const spawned = spawn
          ? { ...this.state.spawned, [placementId]: { ...spawn, nx, ny } }
          : this.state.spawned;
        this.state = {
          revision: this.state.revision + 1,
          byPlacementId: {
            ...this.state.byPlacementId,
            [placementId]: withMove(prev, nx, ny),
          },
          scale: this.state.scale,
          spawned,
        };
        return this.state;
      }
      case 'setDisposition': {
        const placementId = String(event.placementId ?? '');
        if (!placementId) return this.state;
        const disposition = normalizeNpcDisposition(event.disposition);
        const prev = this.state.byPlacementId[placementId];
        const spawn = this.state.spawned[placementId];
        if (prev?.disposition === disposition && (!spawn || spawn.disposition === disposition)) {
          return this.state;
        }
        const next: SceneNpcTokensSessionPlacement = {
          ...restOf(prev),
          disposition,
        };
        this.state = {
          revision: this.state.revision + 1,
          byPlacementId: {
            ...this.state.byPlacementId,
            [placementId]: next,
          },
          scale: this.state.scale,
          spawned: spawn
            ? { ...this.state.spawned, [placementId]: { ...spawn, disposition } }
            : this.state.spawned,
        };
        return this.state;
      }
      case 'setInactive': {
        const placementId = String(event.placementId ?? '');
        if (!placementId) return this.state;
        const inactive = Boolean(event.inactive);
        const prev = this.state.byPlacementId[placementId];
        if (Boolean(prev?.inactive) === inactive) return this.state;
        const next: SceneNpcTokensSessionPlacement = {
          ...(prev?.nx !== undefined ? { nx: prev.nx } : {}),
          ...(prev?.ny !== undefined ? { ny: prev.ny } : {}),
          ...(prev?.sizeN !== undefined ? { sizeN: prev.sizeN } : {}),
          ...(prev?.disposition ? { disposition: prev.disposition } : {}),
          ...(inactive ? { inactive: true } : {}),
        };
        if (isEmptyPlacement(next)) {
          this.state = withoutPlacement(this.state, placementId);
          return this.state;
        }
        this.state = {
          revision: this.state.revision + 1,
          byPlacementId: {
            ...this.state.byPlacementId,
            [placementId]: next,
          },
          scale: this.state.scale,
          spawned: this.state.spawned,
        };
        return this.state;
      }
      case 'setSize': {
        const placementId = String(event.placementId ?? '');
        if (!placementId) return this.state;
        const sizeN = clampSceneNpcTokenSizeN(event.sizeN);
        const prev = this.state.byPlacementId[placementId];
        const spawn = this.state.spawned[placementId];
        if (prev?.sizeN === sizeN && (!spawn || spawn.sizeN === sizeN)) return this.state;
        this.state = {
          revision: this.state.revision + 1,
          byPlacementId: {
            ...this.state.byPlacementId,
            [placementId]: { ...restOf(prev), sizeN },
          },
          scale: this.state.scale,
          spawned: spawn
            ? { ...this.state.spawned, [placementId]: { ...spawn, sizeN } }
            : this.state.spawned,
        };
        return this.state;
      }
      default: {
        const _exhaustive: never = event;
        void _exhaustive;
        return this.state;
      }
    }
  }
}
