import type {
  GameState,
  PreparedWorldDefinition,
  TimelineEntry,
  ValueDefinition,
} from "./types";
import { clone, clamp, uniqueStrings } from "./utils";

function applyValuePatch(
  target: Record<string, number>,
  patch: Record<string, number>,
  definitions: ValueDefinition[],
  defaultMin?: number,
): void {
  const definitionLookup = new Map(
    definitions.map((definition) => [definition.id, definition]),
  );

  for (const [key, delta] of Object.entries(patch)) {
    const definition = definitionLookup.get(key);
    const current = target[key] ?? 0;
    target[key] = clamp(
      current + delta,
      definition?.min ?? defaultMin,
      definition?.max,
    );
  }
}

export function applyEffects(
  world: PreparedWorldDefinition,
  state: GameState,
  effects?: {
    stats?: Record<string, number>;
    resources?: Record<string, number>;
    inventory?: Record<string, number>;
    flags?: Record<string, boolean>;
    addTags?: string[];
    removeTags?: string[];
  },
): GameState {
  if (!effects) {
    return clone(state);
  }

  const next = clone(state);
  applyValuePatch(next.stats, effects.stats ?? {}, world.stats);
  applyValuePatch(next.resources, effects.resources ?? {}, world.resources, 0);
  applyValuePatch(next.inventory, effects.inventory ?? {}, world.inventory, 0);

  if (effects.flags) {
    next.flags = {
      ...next.flags,
      ...effects.flags,
    };
  }

  if (effects.addTags?.length || effects.removeTags?.length) {
    const tagSet = new Set(next.tags);

    for (const tag of effects.addTags ?? []) {
      tagSet.add(tag);
    }

    for (const tag of effects.removeTags ?? []) {
      tagSet.delete(tag);
    }

    next.tags = uniqueStrings(Array.from(tagSet));
  }

  return next;
}

function formatDelta(label: string, delta: number): string {
  const prefix = delta > 0 ? "+" : "";
  return `${label} ${prefix}${delta}`;
}

function describeValueDelta(
  changes: string[],
  definitions: ValueDefinition[],
  before: Record<string, number>,
  after: Record<string, number>,
): void {
  const labelLookup = new Map(
    definitions.map((definition) => [definition.id, definition.label]),
  );
  const keys = new Set([
    ...definitions.map((definition) => definition.id),
    ...Object.keys(before),
    ...Object.keys(after),
  ]);

  for (const key of keys) {
    const delta = (after[key] ?? 0) - (before[key] ?? 0);
    if (delta !== 0) {
      changes.push(formatDelta(labelLookup.get(key) ?? key, delta));
    }
  }
}

export function describeStateDelta(
  world: PreparedWorldDefinition,
  before: GameState,
  after: GameState,
): string[] {
  const changes: string[] = [];
  describeValueDelta(changes, world.stats, before.stats, after.stats);
  describeValueDelta(
    changes,
    world.resources,
    before.resources,
    after.resources,
  );
  describeValueDelta(
    changes,
    world.inventory,
    before.inventory,
    after.inventory,
  );

  for (const key of new Set([
    ...Object.keys(before.flags),
    ...Object.keys(after.flags),
  ])) {
    if ((before.flags[key] ?? false) !== (after.flags[key] ?? false)) {
      changes.push(`${key}: ${after.flags[key] ? "on" : "off"}`);
    }
  }

  for (const tag of after.tags) {
    if (!before.tags.includes(tag)) {
      changes.push(`tag +${tag}`);
    }
  }

  for (const tag of before.tags) {
    if (!after.tags.includes(tag)) {
      changes.push(`tag -${tag}`);
    }
  }

  return changes;
}

export function appendLog(
  state: GameState,
  entries: TimelineEntry[],
  maxEntries = 40,
): GameState {
  const next = clone(state);
  next.log = [...next.log, ...entries].slice(-maxEntries);
  return next;
}
