import {
  clampSceneNpcTokenSizeN,
  DEFAULT_SCENE_NPC_TOKEN_SIZE_N,
  layoutPlayerTokensBottom,
  type ScenePlayerTokensSessionEvent,
  type ScenePlayerTokensSessionState,
} from '../../shared/types/appPlayers';
import { rotateMapNormPointByCwSteps } from '../../shared/types/scenePreviewRotation';

function emptyState(revision = 1): ScenePlayerTokensSessionState {
  return {
    revision,
    selectedPlayerIds: [],
    visible: false,
    byPlayerId: {},
  };
}

function normalizePlayerIds(raw: readonly string[]): string[] {
  const seen = new Set<string>();
  const out: string[] = [];
  for (const id of raw) {
    const s = String(id ?? '').trim();
    if (!s || seen.has(s)) continue;
    seen.add(s);
    out.push(s);
  }
  return out;
}

export class ScenePlayerTokensSessionStore {
  private state: ScenePlayerTokensSessionState = emptyState();

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

  reset(): ScenePlayerTokensSessionState {
    if (
      this.state.selectedPlayerIds.length === 0 &&
      !this.state.visible &&
      Object.keys(this.state.byPlayerId).length === 0
    ) {
      return this.state;
    }
    this.state = emptyState(this.state.revision + 1);
    return this.state;
  }

  /** Смена сцены: спрятать токены, сбросить позиции; выбор игроков сохранить. */
  clearPlacements(): ScenePlayerTokensSessionState {
    if (!this.state.visible && Object.keys(this.state.byPlayerId).length === 0) {
      return this.state;
    }
    this.state = {
      ...this.state,
      revision: this.state.revision + 1,
      visible: false,
      byPlayerId: {},
    };
    return this.state;
  }

  /** Rotate live player tokens with the scene preview (CSS rotate steps). */
  rotateMapCwSteps(steps: number): ScenePlayerTokensSessionState {
    const n = ((steps % 4) + 4) % 4;
    if (n === 0) return this.state;
    const ids = Object.keys(this.state.byPlayerId);
    if (ids.length === 0) return this.state;
    const byPlayerId: ScenePlayerTokensSessionState['byPlayerId'] = {};
    for (const id of ids) {
      const prev = this.state.byPlayerId[id];
      if (!prev) continue;
      const p = rotateMapNormPointByCwSteps(prev.nx, prev.ny, n);
      byPlayerId[id] = { ...prev, nx: p.nx, ny: p.ny };
    }
    this.state = {
      ...this.state,
      revision: this.state.revision + 1,
      byPlayerId,
    };
    return this.state;
  }

  dispatch(event: ScenePlayerTokensSessionEvent): ScenePlayerTokensSessionState {
    switch (event.kind) {
      case 'clear':
        return this.reset();
      case 'clearPlacements':
        return this.clearPlacements();
      case 'setSelection': {
        const selectedPlayerIds = normalizePlayerIds(event.playerIds);
        const same =
          selectedPlayerIds.length === this.state.selectedPlayerIds.length &&
          selectedPlayerIds.every((id, i) => id === this.state.selectedPlayerIds[i]);
        if (same) return this.state;
        this.state = {
          revision: this.state.revision + 1,
          selectedPlayerIds,
          visible: false,
          byPlayerId: {},
        };
        return this.state;
      }
      case 'setVisible': {
        const visible = Boolean(event.visible);
        if (this.state.visible === visible) return this.state;
        if (visible && this.state.selectedPlayerIds.length === 0) return this.state;
        this.state = { ...this.state, revision: this.state.revision + 1, visible };
        return this.state;
      }
      case 'show': {
        if (this.state.selectedPlayerIds.length === 0) return this.state;
        const sizeN = clampSceneNpcTokenSizeN(event.sizeN);
        const byPlayerId =
          Object.keys(this.state.byPlayerId).length > 0
            ? this.state.byPlayerId
            : layoutPlayerTokensBottom(this.state.selectedPlayerIds, sizeN);
        if (this.state.visible && byPlayerId === this.state.byPlayerId) return this.state;
        this.state = {
          ...this.state,
          revision: this.state.revision + 1,
          visible: true,
          byPlayerId,
        };
        return this.state;
      }
      case 'seedBottom': {
        if (this.state.selectedPlayerIds.length === 0) return this.state;
        if (Object.keys(this.state.byPlayerId).length > 0) return this.state;
        const sizeN = clampSceneNpcTokenSizeN(event.sizeN);
        this.state = {
          ...this.state,
          revision: this.state.revision + 1,
          byPlayerId: layoutPlayerTokensBottom(this.state.selectedPlayerIds, sizeN),
        };
        return this.state;
      }
      case 'move': {
        const playerId = String(event.playerId ?? '');
        if (!playerId || !this.state.selectedPlayerIds.includes(playerId)) 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.byPlayerId[playerId];
        if (prev && prev.nx === nx && prev.ny === ny) return this.state;
        const sizeN = clampSceneNpcTokenSizeN(prev?.sizeN ?? DEFAULT_SCENE_NPC_TOKEN_SIZE_N);
        this.state = {
          ...this.state,
          revision: this.state.revision + 1,
          byPlayerId: {
            ...this.state.byPlayerId,
            [playerId]: { nx, ny, sizeN },
          },
        };
        return this.state;
      }
      case 'setSize': {
        const playerId = String(event.playerId ?? '');
        if (!playerId || !this.state.selectedPlayerIds.includes(playerId)) return this.state;
        const prev = this.state.byPlayerId[playerId];
        if (!prev) return this.state;
        const sizeN = clampSceneNpcTokenSizeN(event.sizeN);
        if (prev.sizeN === sizeN) return this.state;
        this.state = {
          ...this.state,
          revision: this.state.revision + 1,
          byPlayerId: {
            ...this.state.byPlayerId,
            [playerId]: { ...prev, sizeN },
          },
        };
        return this.state;
      }
      default: {
        const _exhaustive: never = event;
        void _exhaustive;
        return this.state;
      }
    }
  }
}
