// 달시장. 시드로 정해진 손님 줄을 다섯 밤 동안 응대하는 야시장 경제를 한 스텝씩 진행합니다.
// 입력은 DS1 패킷의 STATE, EVENT, SHELL 줄이고 출력은 다음 STATE 줄과 화면을 그릴 DRAW 목록입니다.
// 같은 시드와 같은 조작이면 늘 같은 밤이 나옵니다. 시간과 외부 난수를 쓰지 않습니다.
// 화면을 직접 그리거나 저장하지 않습니다. 저장 문자열은 페이지가 들고 있다가 다음 스텝에 돌려줍니다.
// moon-market: a deterministic night-market economy stepper over seeded customer queues.

import core.state { 상태, 기본상태, 역직렬화, 직렬화, 품목표, 표갱신, 마지막밤, 알림번호, 단계토큰 }
import data.market

let 기본시드 = "DS-0001"
let 사입원가 = 5

// 공용 도우미 시작 (줄값 셸값)
// 여러 도구가 같은 원본을 씁니다. 여기서 고치지 않고 원본에서 고친 뒤 다시 생성합니다.

// 같은 접두의 줄이 여럿이면 마지막 줄을 씁니다.
function 줄값(입력: string, 접두: string) -> string {
  let 맞는줄들 = 입력.split("\n").map(줄 => 줄.trim()).filter(줄 => 줄.startsWith(접두))
  if 맞는줄들.length == 0 { return "" }
  let 끝줄 = 맞는줄들[맞는줄들.length - 1]
  끝줄.slice(접두.scalars().length, 끝줄.scalars().length).trim()
}

// "키=값,키=값" 에서 = 로 정확히 두 조각인 항만 읽고 키와 값의 앞뒤 공백을 지웁니다. 같은 키가 여럿이면 마지막 값을 씁니다.
function 셸값(셸: string, 키: string) -> string {
  let 값들 = 셸.split(",").map(항 => 항.split("=")).filter(조각들 => 조각들.length == 2 && 조각들[0].trim() == 키)
  if 값들.length == 0 { "" } else { 값들[값들.length - 1][1].trim() }
}

// 공용 도우미 끝

type 이벤트 = "init" | "resume" | "up" | "down" | "left" | "right" | "a" | "b" | "unknown"

function 이벤트파싱(토큰: string) -> 이벤트 {
  match 토큰 {
    case "init" => "init"
    case "resume" => "resume"
    case "up" => "up"
    case "down" => "down"
    case "left" => "left"
    case "right" => "right"
    case "a" => "a"
    case "b" => "b"
    case _ => "unknown"
  }
}

// 정산 화면의 A 는 다음 밤으로 넘어갑니다. 현금, 평판, 가격은 이어 가고 재고는 하룻밤에 하나씩 상합니다.
function 다음밤(직전: 상태) -> 상태 {
  if 직전.밤 >= 마지막밤 {
    상태 { ...직전, 단계: "끝", 알림: "장사끝" }
  } else {
    상태 {
      ...직전,
      밤: 직전.밤 + 1,
      단계: "영업",
      재고: 품목표(market.품목목록().map(품 => (직전.재고.getOr(품, 0) - 1).atLeast(0))),
      손님차례: 0,
      알림: "다음밤",
    }
  }
}

// 현금이 모자라면 사지 않고 알림만 지웁니다.
function 사입(직전: 상태, 품: market.품목) -> 상태 {
  if 직전.현금 >= 사입원가 {
    상태 {
      ...직전,
      현금: 직전.현금 - 사입원가,
      재고: 표갱신(직전.재고, 품, 직전.재고.getOr(품, 0) + 1),
      알림: "사입",
    }
  } else {
    상태 { ...직전, 알림: "없음" }
  }
}

// 알뜰한 손님은 흥정해서 예산의 80% 까지만 냅니다.
function 지불한도(손님값: market.손님) -> int {
  match 손님값.성향 {
    case "알뜰" => 손님값.예산 * 8 / 10
    case "급함" | "묶음" => 손님값.예산
  }
}

// 묶음을 좋아하는 손님은 좋은 거래에 평판을 하나 더 줍니다.
function 평판보상(손님값: market.손님) -> int {
  match 손님값.성향 {
    case "묶음" => 2
    case "알뜰" | "급함" => 1
  }
}

function 응대(직전: 상태) -> 상태 {
  if 직전.손님차례 >= 직전.손님수 { return 상태 { ...직전, 알림: "없음" } }
  let 손님값 = market.손님생성(market.djb2해시(직전.시드), 직전.밤, 직전.손님차례)
  let 판매가 = 직전.가격.getOr(손님값.원함, 0)
  let 재고수 = 직전.재고.getOr(손님값.원함, 0)
  let 응대뒤 = if 판매가 <= 지불한도(손님값) && 재고수 > 0 {
    상태 {
      ...직전,
      현금: 직전.현금 + 판매가,
      재고: 표갱신(직전.재고, 손님값.원함, 재고수 - 1),
      판매: 직전.판매 + 1,
      평판: 직전.평판 + 평판보상(손님값),
      알림: "판매",
    }
  } else {
    상태 { ...직전, 놓침: 직전.놓침 + 1, 평판: (직전.평판 - 1).atLeast(0), 알림: "놓침" }
  }
  let 차례 = 직전.손님차례 + 1
  if 차례 >= 직전.손님수 {
    상태 { ...응대뒤, 손님차례: 차례, 단계: "정산", 알림: "정산" }
  } else {
    상태 { ...응대뒤, 손님차례: 차례 }
  }
}

// 영업 중에는 조작마다 알림을 새로 정하므로 아무것도 바꾸지 않는 조작도 알림을 지웁니다.
function 영업(이벤트값: 이벤트, 직전: 상태) -> 상태 {
  let 품목들 = market.품목목록()
  match 이벤트값 {
    case "left" => 상태 { ...직전, 선택: (직전.선택 + 품목들.length - 1) % 품목들.length, 알림: "없음" }
    case "right" => 상태 { ...직전, 선택: (직전.선택 + 1) % 품목들.length, 알림: "없음" }
    case "up" => {
      let 품 = 품목들[직전.선택]
      상태 { ...직전, 가격: 표갱신(직전.가격, 품, 직전.가격.getOr(품, 0) + 1), 알림: "가격올림" }
    }
    case "down" => {
      let 품 = 품목들[직전.선택]
      상태 { ...직전, 가격: 표갱신(직전.가격, 품, (직전.가격.getOr(품, 0) - 1).atLeast(1)), 알림: "가격내림" }
    }
    case "b" => 사입(직전, 품목들[직전.선택])
    case "a" => 응대(직전)
    case "init" | "resume" | "unknown" => 상태 { ...직전, 알림: "없음" }
  }
}

// 끝난 판과 정산 화면은 A 말고는 아무 조작도 받지 않으며 알림도 그대로 둡니다.
function 적용(이벤트값: 이벤트, 직전: 상태) -> 상태 {
  match 직전.단계 {
    case "끝" => 직전
    case "정산" => if 이벤트값 == "a" { 다음밤(직전) } else { 직전 }
    case "영업" => 영업(이벤트값, 직전)
  }
}

function 품목그리기(현재: 상태, i: int) -> string {
  let 품 = market.품목목록()[i]
  let 골랐음 = if i == 현재.선택 { 1 } else { 0 }
  "\nGOOD|{i}|{품}|{현재.재고.getOr(품, 0)}|{현재.가격.getOr(품, 0)}|{골랐음}"
}

function 그리기목록(현재: 상태) -> string {
  let 품목줄 = map(0..<market.품목목록().length, i => 품목그리기(현재, i)).join("")
  let 손님줄 = if 현재.단계 == "영업" && 현재.손님차례 < 현재.손님수 {
    let 손님값 = market.손님생성(market.djb2해시(현재.시드), 현재.밤, 현재.손님차례)
    "\nCUST|{손님값.종류}|{손님값.원함}|{손님값.예산}|{손님값.성향}"
  } else {
    ""
  }
  "DD1{품목줄}{손님줄}\nHUD|{현재.밤}|{현재.현금}|{현재.평판}|{현재.손님차례}|{현재.손님수}|{현재.판매}|{현재.놓침}|{단계토큰(현재.단계)}|{알림번호(현재.알림)}"
}

function 처리(입력: string) -> string {
  let 이벤트값 = 줄값(입력, "EVENT:") |> 이벤트파싱
  let 설정시드 = 셸값(줄값(입력, "SHELL:"), "seed")
  let 시드 = if 설정시드 != "" { 설정시드 } else { 기본시드 }
  let 새상태 = match 이벤트값 {
    case "init" => 기본상태(시드)
    case "resume" => 역직렬화(줄값(입력, "STATE:"), 시드)
    case "up" | "down" | "left" | "right" | "a" | "b" | "unknown" => 적용(이벤트값, 역직렬화(줄값(입력, "STATE:"), 시드))
  }
  "DS1\nSTATE:{직렬화(새상태)}\nDRAW:{그리기목록(새상태)}"
}

input() |> 처리 |> print
