// 달시장 상태. 한 밤의 진행을 레코드와 품목별 표로 들고, 스텝 경계에서만 저장 문자열로 바꿉니다.
// 저장 문자열은 DS1|시드|밤|단계|현금|평판|재고|가격|선택|손님차례|손님수|판매|놓침|알림 이고 재고와 가격은 품목 순서대로 ; 로 잇습니다.
// 같은 상태는 늘 같은 문자열이 됩니다. 시간과 외부 난수를 쓰지 않습니다.
// 칸이 열넷보다 적으면 새로 시작하고, 숫자가 아닌 칸은 칸마다 정한 기본값으로 읽은 뒤 모든 값을 범위 안으로 가둡니다.
// moon-market state: the night record, per-good tables and the pipe-separated save string.

import data.market

export type 진행단계 = "영업" | "정산" | "끝"
export type 알림종류 = "없음" | "가격올림" | "가격내림" | "사입" | "판매" | "놓침" | "정산" | "장사끝" | "다음밤"

export let 마지막밤 = 5
// 아래 상한은 정상 플레이가 닿지 않으므로 정상 저장의 값은 그대로 지나갑니다.
// 현금, 평판, 판매, 놓침, 재고, 가격의 상한입니다. 한 스텝 뒤의 + 판매가 와 + 1 이 64비트를 넘지 않습니다.
let 값상한 = 1000000000
// 한 밤의 손님 수 상한입니다. 정상 플레이는 여섯 명입니다.
let 손님수상한 = 64

export record 상태 {
  시드: string,
  밤: int,
  단계: 진행단계,
  현금: int,
  평판: int,
  재고: Map<market.품목, int>,
  가격: Map<market.품목, int>,
  선택: int,
  손님차례: int,
  손님수: int,
  판매: int,
  놓침: int,
  알림: 알림종류,
}

// 표는 만든 뒤 바꾸지 않습니다. 바뀐 값은 새 표로 만들므로 이전 상태와 표를 함께 써도 안전합니다.
export function 품목표(값들: Array<int>) -> Map<market.품목, int> {
  let 품목들 = market.품목목록()
  let mut 표: Map<market.품목, int> = Map.new()
  for i in 0..<품목들.length { 표.insert(품목들[i], 값들[i]) }
  표
}

export function 표갱신(표: Map<market.품목, int>, 바꿀품목: market.품목, 새값: int) -> Map<market.품목, int> {
  품목표(market.품목목록().map(품 => if 품 == 바꿀품목 { 새값 } else { 표.getOr(품, 0) }))
}

export function 기본상태(시드: string) -> 상태 {
  상태 {
    시드: 시드,
    밤: 1,
    단계: "영업",
    현금: 20,
    평판: 50,
    재고: 품목표(market.품목목록().map(품 => 5)),
    가격: 품목표(market.품목목록().map(품 => 10)),
    선택: 0,
    손님차례: 0,
    손님수: 6,
    판매: 0,
    놓침: 0,
    알림: "없음",
  }
}

function 표직렬화(표: Map<market.품목, int>) -> string {
  market.품목목록().map(품 => "{표.getOr(품, 0)}").join(";")
}

function 값가두기(값: int, 최소: int) -> int {
  값.atLeast(최소).atMost(값상한)
}

// 모자라거나 숫자가 아닌 값은 0 으로 읽은 뒤 최소와 값상한 사이로 가둡니다.
function 표역직렬화(칸: string, 최소: int) -> Map<market.품목, int> {
  let 값들 = 칸.split(";")
  품목표(map(0..<market.품목목록().length, i => 값가두기(if i < 값들.length { toInt(값들[i]) ?? 0 } else { 0 }, 최소)))
}

// 알림은 셸이 읽는 번호라 저장 문자열과 HUD 줄에서만 숫자로 씁니다. 모르는 번호는 없음으로 읽습니다.
export function 알림번호(알림값: 알림종류) -> int {
  match 알림값 {
    case "없음" => 0
    case "가격올림" => 1
    case "가격내림" => 2
    case "사입" => 3
    case "판매" => 4
    case "놓침" => 5
    case "정산" => 6
    case "장사끝" => 7
    case "다음밤" => 8
  }
}

function 알림읽기(번호: int) -> 알림종류 {
  match 번호 {
    case 1 => "가격올림"
    case 2 => "가격내림"
    case 3 => "사입"
    case 4 => "판매"
    case 5 => "놓침"
    case 6 => "정산"
    case 7 => "장사끝"
    case 8 => "다음밤"
    case _ => "없음"
  }
}

// 저장 문자열과 HUD 줄의 단계 토큰은 serve, summary, 끝 입니다. 모르는 토큰은 영업으로 읽습니다.
export function 단계토큰(단계값: 진행단계) -> string {
  match 단계값 {
    case "영업" => "serve"
    case "정산" => "summary"
    case "끝" => "끝"
  }
}

function 단계읽기(토큰: string) -> 진행단계 {
  match 토큰 {
    case "summary" => "정산"
    case "끝" => "끝"
    case _ => "영업"
  }
}

export function 직렬화(현재: 상태) -> string {
  "DS1|{현재.시드}|{현재.밤}|{단계토큰(현재.단계)}|{현재.현금}|{현재.평판}|{표직렬화(현재.재고)}|{표직렬화(현재.가격)}|{현재.선택}|{현재.손님차례}|{현재.손님수}|{현재.판매}|{현재.놓침}|{알림번호(현재.알림)}"
}

// 첫 칸의 판 표시는 보지 않고 열네 칸 뒤의 칸도 버립니다.
export function 역직렬화(저장: string, 대체시드: string) -> 상태 {
  let 칸들 = 저장.split("|")
  if 칸들.length < 14 { return 기본상태(대체시드) }
  let 손님수 = (toInt(칸들[10]) ?? 6).atLeast(1).atMost(손님수상한)
  상태 {
    시드: 칸들[1],
    밤: (toInt(칸들[2]) ?? 1).atLeast(1).atMost(마지막밤),
    단계: 단계읽기(칸들[3]),
    현금: 값가두기(toInt(칸들[4]) ?? 0, 0),
    평판: 값가두기(toInt(칸들[5]) ?? 50, 0),
    재고: 표역직렬화(칸들[6], 0),
    가격: 표역직렬화(칸들[7], 1),
    선택: (toInt(칸들[8]) ?? 0).atLeast(0).atMost(market.품목목록().length - 1),
    손님차례: (toInt(칸들[9]) ?? 0).atLeast(0).atMost(손님수),
    손님수: 손님수,
    판매: 값가두기(toInt(칸들[11]) ?? 0, 0),
    놓침: 값가두기(toInt(칸들[12]) ?? 0, 0),
    알림: 알림읽기(toInt(칸들[13]) ?? 0),
  }
}
