export interface StatCounterV1 {
  agi: number;
  int: number;
  cha: number;
  psi: number;
}

export interface SaveFileV1 {
  version: "fledgling_save_v1";
  revision: number;

  turn: number;

  name: string;
  thrallTemplateId: number;
  nImprovements: number;
  stats: StatCounterV1;
  talents: StatCounterV1;
  isInPenance: boolean;
  wishId: number; // negative: Wish is absent
  exp: number;
  blood: number;
  itemsPurloined: number;
  skillsLearned: number[];
  untrimmedSkillsAvailableIds: number[];
  thrallsUnlocked: number[];
  thrallDamage: number[]; // 0: thrall is absent or undamaged
  thrallsObtainedItem: number[];
  thrallsDeliveredItem: number[];
  depth: number;
}

/// Checks whether obj is a valid save file, as far as we can tell, and returns
/// it unchanged if it is, or throws an error if it's not valid.
export function mustBeSaveFileV1(obj: unknown): SaveFileV1 {
  if (obj === undefined || obj === null) {
    throw new Error("nonexistent");
  }
  if (typeof obj !== "object") {
    throw new Error(`not an object; was ${typeof obj}`);
  }

  if (!("version" in obj)) {
    throw new Error("no magic number");
  }
  if (obj.version !== "fledgling_save_v1") {
    throw new Error(`bad magic number: ${obj.version}`);
  }

  return {
    version: "fledgling_save_v1",
    revision: mustGetNumber(obj, "revision"),
    turn: mustGetNumber(obj, "turn"),
    name: mustGetString(obj, "name"),
    thrallTemplateId: mustGetNumber(obj, "thrallTemplateId"),
    nImprovements: mustGetNumber(obj, "nImprovements"),
    stats: mustGetStatCounterV1(obj, "stats"),
    talents: mustGetStatCounterV1(obj, "talents"),
    isInPenance: mustGetBoolean(obj, "isInPenance"),
    wishId: mustGetNumber(obj, "wishId"),
    exp: mustGetNumber(obj, "exp"),
    blood: mustGetNumber(obj, "blood"),
    itemsPurloined: mustGetNumber(obj, "itemsPurloined"),
    skillsLearned: mustGetNumberArray(obj, "skillsLearned"),
    untrimmedSkillsAvailableIds: mustGetNumberArray(
      obj,
      "untrimmedSkillsAvailableIds",
    ),
    thrallsUnlocked: mustGetNumberArray(obj, "thrallsUnlocked"),
    thrallDamage: mustGetNumberArray(obj, "thrallDamage"),
    thrallsObtainedItem: mustGetNumberArray(obj, "thrallsObtainedItem"),
    thrallsDeliveredItem: mustGetNumberArray(obj, "thrallsDeliveredItem"),
    depth: mustGetNumber(obj, "depth"),
  };
}

function mustGetNumber(obj: object, key: string): number {
  if (obj === null || obj === undefined) {
    throw new Error("container absent");
  }
  if (typeof obj !== "object") {
    throw new Error(`container was not an object; was ${typeof obj}`);
  }
  if (!(key in obj)) {
    throw new Error(`missing number: ${key}`);
  }
  const dict = obj as { [key: string]: any };
  const val = dict[key];
  if (typeof val !== "number") {
    throw new Error(`not a number: ${key}: ${val}`);
  }
  return val;
}

function mustGetString(obj: object, key: string): string {
  if (obj === null || obj === undefined) {
    throw new Error("container absent");
  }
  if (typeof obj !== "object") {
    throw new Error(`container was not an object; was ${typeof obj}`);
  }
  if (!(key in obj)) {
    throw new Error(`missing number: ${key}`);
  }
  const dict = obj as { [key: string]: any };
  const val = dict[key];
  if (typeof val !== "string") {
    throw new Error(`not a string: ${key}: ${val}`);
  }
  return val;
}

function mustGetStatCounterV1(obj: object, key: string): StatCounterV1 {
  if (obj === null || obj === undefined) {
    throw new Error("container absent");
  }
  if (typeof obj !== "object") {
    throw new Error(`container was not an object; was ${typeof obj}`);
  }
  if (!(key in obj)) {
    throw new Error(`missing number: ${key}`);
  }
  const dict = obj as { [key: string]: any };
  const val = dict[key];
  if (typeof val !== "object") {
    throw new Error(`not an object: ${key}: ${val}`);
  }

  try {
    return {
      agi: mustGetNumber(val, "agi"),
      int: mustGetNumber(val, "int"),
      cha: mustGetNumber(val, "cha"),
      psi: mustGetNumber(val, "psi"),
    };
  } catch (e) {
    let message = "unrecognizable error";
    if (e instanceof Error) {
      message = e.message;
    }
    throw new Error(`reading ${key}: ${message}`);
  }
}

function mustGetBoolean(obj: object, key: string): boolean {
  if (obj === null || obj === undefined) {
    throw new Error("container absent");
  }
  if (typeof obj !== "object") {
    throw new Error(`container was not an object; was ${typeof obj}`);
  }
  if (!(key in obj)) {
    throw new Error(`missing number: ${key}`);
  }
  const dict = obj as { [key: string]: any };
  const val = dict[key];
  if (typeof val !== "boolean") {
    throw new Error(`not boolean: ${key}: ${val}`);
  }
  return val;
}

function mustGetNumberArray(obj: object, key: string): number[] {
  if (obj === null || obj === undefined) {
    throw new Error("container absent");
  }
  if (typeof obj !== "object") {
    throw new Error(`container was not an object; was ${typeof obj}`);
  }
  if (!(key in obj)) {
    throw new Error(`missing number: ${key}`);
  }
  const dict = obj as { [key: string]: any };
  const val = dict[key];
  if (typeof val !== "object") {
    throw new Error(`not an object: ${key}: ${val}`);
  }

  for (const x of val) {
    if (typeof x !== "number") {
      throw new Error(`contained non-number item in ${key}: ${val}`);
    }
  }
  return val;
}