// 레이트레이서. 바닥 평면과 구 하나와 점광원으로 된 고정 장면을 화소마다 광선으로 추적해 그립니다.
// 입력은 해상도와 생략할 수 있는 광원각 두 정수이고, 출력은 요약 한 줄과 회색조 칸으로 된 SVG 를 담은 HTML 조각입니다.
// 같은 입력이면 인터프리터와 웹 빌드에서 바이트까지 같은 출력을 냅니다.
// 메시, 가속 구조, 반사, 굴절, 안티에일리어싱, 고해상도, 실제 이미지 파일은 다루지 않는 저해상 미리보기입니다.
// Raytracer: traces a fixed floor, sphere and point light scene into a grayscale SVG grid.

record 벡터 { x: float, y: float, z: float }
record 구 { 중심: 벡터, 반지름: float }
// 명중 거리를 태그 안에 함께 싣습니다.
enum 명중 { 없음, 구(float), 바닥(float) }
record 장면설정 { 해상도: int, 광원각: int }

let 엡실론 = 0.0001
let 최대해상도 = 64
let 칸크기 = 6
let 바닥법선 = 벡터 { x: 0.0, y: 1.0, z: 0.0 }
let 장면구 = 구 { 중심: 벡터 { x: 0.0, y: 1.0, z: 0.0 }, 반지름: 1.0 }
let 시점 = 벡터 { x: 0.0, y: 1.6, z: 5.0 }
let 화면거리 = 4.0
let 반화면 = 1.7

// 공용 도우미 시작 (이스케이프 숫자인가 모두숫자인가 오류알림 정수오류 정수인가 정수읽기 결과풀기 오류바꾸기)
// 여러 도구가 같은 원본을 씁니다. 여기서 고치지 않고 원본에서 고친 뒤 다시 생성합니다.

// 사용자 입력을 HTML 에 넣기 전에 반드시 거칩니다.
function 이스케이프(원문: string) -> string {
  원문.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;")
}

function 숫자인가(글자: string) -> bool {
  let 코드 = 글자.codePointAt(0) ?? 0
  코드 >= 48 && 코드 <= 57
}

// 빈 문자열은 참입니다. 부호와 소수점을 받지 않으려고 toInt 앞에서 씁니다.
function 모두숫자인가(글: string) -> bool {
  글.scalars().reduce(true, (모두, 글자) => 모두 && 숫자인가(글자))
}

// 문구는 고정 문장이거나 이미 이스케이프한 값이어야 합니다.
function 오류알림(문구: string) -> string {
  "<div class=\"err\">{문구}</div>"
}

// 정수 읽기가 실패한 까닭입니다. 보여 줄 문구는 앱이 정합니다.
type 정수오류 = "숫자아님" | "너무큼"

// 맨 앞의 빼기 부호 하나만 받습니다. 더하기 부호와 소수점은 받지 않습니다.
function 정수인가(토큰: string) -> bool {
  let 글자들 = 토큰.scalars()
  let 시작 = if 토큰.startsWith("-") { 1 } else { 0 }
  글자들.length > 시작 && 모두숫자인가(글자들.slice(시작, 글자들.length).join(""))
}

// 부호까지 센 글자 수를 최대자릿수로 막아 toInt 와 뒤의 계산이 넘치지 않게 합니다.
function 정수읽기(토큰: string, 최대자릿수: int) -> Result<int, 정수오류> {
  if !정수인가(토큰) { return Err("숫자아님") }
  if 토큰.scalars().length > 최대자릿수 { return Err("너무큼") }
  Ok(toInt(토큰) ?? 0)
}

// 성공 값과 안내가 모두 바로 보여 줄 HTML 조각일 때 진입점에서 한 번 풉니다. 토파즈 과제 'Result 오류 쪽 변환과 풀기' 의 우회입니다.
function 결과풀기(값: Result<string, string>) -> string {
  match 값 {
    case Ok(결과) => 결과
    case Err(안내) => 안내
  }
}

// 성공 값은 그대로 두고 오류만 문구로 바꿉니다. 토파즈 과제 'Result 오류 쪽 변환과 풀기' 의 우회입니다.
function 오류바꾸기<T, E>(결과: Result<T, E>, 바꾸기: (E) -> string) -> Result<T, string> {
  match 결과 {
    case Ok(값) => Ok(값)
    case Err(오류) => Err(바꾸기(오류))
  }
}

// 공용 도우미 끝

// Math.sqrt 대신 앱에서 계산하는 것이 이 도구의 주제이고, 반복 횟수가 출력 소수를 정하므로 고정합니다.
function 제곱근(값: float) -> float {
  if 값 <= 0.0 { return 0.0 }
  let mut 추정 = if 값 > 1.0 { 값 / 2.0 } else { 1.0 }
  for _ in 0..<22 {
    추정 = (추정 + 값 / 추정) / 2.0
  }
  추정
}

function 더하기(가: 벡터, 나: 벡터) -> 벡터 { 벡터 { x: 가.x + 나.x, y: 가.y + 나.y, z: 가.z + 나.z } }

function 빼기(가: 벡터, 나: 벡터) -> 벡터 { 벡터 { x: 가.x - 나.x, y: 가.y - 나.y, z: 가.z - 나.z } }

function 곱하기(가: 벡터, 배율: float) -> 벡터 { 벡터 { x: 가.x * 배율, y: 가.y * 배율, z: 가.z * 배율 } }

function 내적(가: 벡터, 나: 벡터) -> float { 가.x * 나.x + 가.y * 나.y + 가.z * 나.z }

function 길이(가: 벡터) -> float { 제곱근(내적(가, 가)) }

function 정규화(가: 벡터) -> 벡터 {
  let 크기 = 길이(가)
  if 크기 < 엡실론 { 가 } else { 곱하기(가, 1.0 / 크기) }
}

// 방향은 단위 벡터여야 합니다. 시점 바로 앞의 교차는 자기 표면이므로 엡실론보다 먼 것만 셉니다.
function 구교차(시점: 벡터, 방향: 벡터, 공: 구) -> Option<float> {
  let 상대 = 빼기(시점, 공.중심)
  let 투영 = 내적(상대, 방향)
  let 바깥정도 = 내적(상대, 상대) - 공.반지름 * 공.반지름
  let 판별식 = 투영 * 투영 - 바깥정도
  if 판별식 < 0.0 { return None }
  let 뿌리 = 제곱근(판별식)
  let 가까운거리 = -투영 - 뿌리
  if 가까운거리 > 엡실론 { return Some(가까운거리) }
  let 먼거리 = -투영 + 뿌리
  if 먼거리 > 엡실론 { Some(먼거리) } else { None }
}

// 바닥은 y = 0 평면입니다.
function 바닥교차(시점: 벡터, 방향: 벡터) -> Option<float> {
  if Math.abs(방향.y) < 엡실론 { return None }
  let t = -시점.y / 방향.y
  if t > 엡실론 { Some(t) } else { None }
}

// 거리가 같으면 구를 고릅니다.
function 가까운명중(구까지: Option<float>, 바닥까지: Option<float>) -> 명중 {
  match 구까지 {
    case Some(구거리) => match 바닥까지 {
      case Some(바닥거리) => if 바닥거리 < 구거리 { 명중.바닥(바닥거리) } else { 명중.구(구거리) }
      case None => 명중.구(구거리)
    }
    case None => match 바닥까지 {
      case Some(바닥거리) => 명중.바닥(바닥거리)
      case None => 명중.없음
    }
  }
}

// 한 변 8 인 정사각형을 네 칸으로 나눈 체커무늬입니다.
function 바닥밝기(점: 벡터) -> float {
  let 칸x = 점.x - 8.0 * Math.floor(점.x / 8.0)
  let 칸z = 점.z - 8.0 * Math.floor(점.z / 8.0)
  if (칸x < 4.0) != (칸z < 4.0) { 0.35 } else { 0.6 }
}

// 그림자 광선은 표면에서 법선 쪽으로 조금 띄워 쏴야 자기 표면에 걸리지 않습니다.
function 비추기(점: 벡터, 법선: 벡터, 기본밝기: float, 공: 구, 광원: 벡터) -> float {
  let 빛방향 = 빼기(광원, 점) |> 정규화
  let 확산 = Math.max(0.0, 내적(법선, 빛방향))
  let 그늘시점 = 더하기(점, 곱하기(법선, 엡실론 * 10.0))
  let 광원거리 = 길이(빼기(광원, 점))
  let 그림자 = match 구교차(그늘시점, 빛방향, 공) {
    case Some(막힌거리) => if 막힌거리 < 광원거리 { 0.3 } else { 1.0 }
    case None => 1.0
  }
  let 명암 = 0.15 * 기본밝기 + 0.85 * 기본밝기 * 확산 * 그림자
  Math.min(1.0, Math.max(0.0, 명암))
}

// 0 에서 1 사이 명암을 돌려줍니다. 아무것도 맞지 않으면 위로 갈수록 밝은 하늘입니다.
function 추적(시점: 벡터, 방향: 벡터, 공: 구, 광원: 벡터) -> float {
  match 가까운명중(구교차(시점, 방향, 공), 바닥교차(시점, 방향)) {
    case 없음 => 0.75 + 0.2 * Math.max(0.0, 방향.y)
    case 구(거리) => {
      let 점 = 더하기(시점, 곱하기(방향, 거리))
      비추기(점, 빼기(점, 공.중심) |> 정규화, 0.85, 공, 광원)
    }
    case 바닥(거리) => {
      let 점 = 더하기(시점, 곱하기(방향, 거리))
      비추기(점, 바닥법선, 바닥밝기(점), 공, 광원)
    }
  }
}

// float 를 int 로 바꾸는 표준 기능이 없어 0 쪽으로 자른 값을 이분 탐색으로 찾습니다. 절댓값 2^30 근처에서 멈춥니다. 토파즈 과제 '실수를 정수로 자르는 변환' 의 우회입니다.
function 정수로자르기(값: float) -> int {
  let 크기 = Math.abs(값)
  if 크기 < 1.0 { return 0 }
  let mut 상한 = 1
  while toFloat(상한) <= 크기 && 상한 < 1073741824 { 상한 = 상한 * 2 }
  let mut 하한 = 상한 / 2
  while 상한 - 하한 > 1 {
    let 가운데 = (하한 + 상한) / 2
    if toFloat(가운데) <= 크기 { 하한 = 가운데 } else { 상한 = 가운데 }
  }
  if 값 < 0.0 { -하한 } else { 하한 }
}

function 십육진두자리(값: int) -> string {
  let 숫자표 = "0123456789abcdef".scalars()
  let 채널 = 값.atLeast(0).atMost(255)
  숫자표[채널 / 16] + 숫자표[채널 % 16]
}

function 정수문구(종류: 정수오류, 토큰: string) -> string {
  match 종류 {
    case "숫자아님" => 오류알림("숫자만 입력하세요: {이스케이프(토큰)}")
    case "너무큼" => 오류알림("숫자가 너무 큽니다: {이스케이프(토큰)}")
  }
}

// 아홉 자리까지만 받아 toInt 가 넘치지 않게 합니다.
function 인자읽기(토큰들: Array<string>, 자리: int, 기본값: int) -> Result<int, string> {
  if 자리 >= 토큰들.length { return Ok(기본값) }
  let 토큰 = 토큰들[자리]
  오류바꾸기(정수읽기(토큰, 9), 종류 => 정수문구(종류, 토큰))
}

// 인자 개수보다 각 인자의 오류를 먼저 알립니다.
function 설정읽기(입력: string) -> Result<장면설정, string> {
  let 토큰들 = 입력.trim().split(" ").filter(토큰 => 토큰 != "")
  let 해상도 = 인자읽기(토큰들, 0, 40)?
  let 광원각 = 인자읽기(토큰들, 1, 35)?
  if 토큰들.length > 2 { return Err(오류알림("입력은 해상도와 선택 광원각 두 개입니다. 예: 44 35")) }
  Ok(장면설정 { 해상도: 해상도.atLeast(8).atMost(최대해상도), 광원각: 광원각.atLeast(0).atMost(80) })
}

// 화면은 시점 앞 [-1, 1] 정사각형이고 y 는 위가 양수입니다.
function 칸그리기(줄: int, 열: int, 실수해상도: float, 광원: 벡터) -> string {
  let u = ((toFloat(열) + 0.5) / 실수해상도) * 2.0 - 1.0
  let v = 1.0 - ((toFloat(줄) + 0.5) / 실수해상도) * 2.0
  let 화면점 = 벡터 { x: u * 반화면, y: 시점.y + v * 반화면, z: 시점.z - 화면거리 }
  let 방향 = 빼기(화면점, 시점) |> 정규화
  let 회색 = 십육진두자리(정수로자르기(추적(시점, 방향, 장면구, 광원) * 255.0 + 0.5))
  "<rect x=\"{열 * 칸크기}\" y=\"{줄 * 칸크기}\" width=\"{칸크기}\" height=\"{칸크기}\" fill=\"#{회색}{회색}{회색}\"/>"
}

// 광원은 광원각 35 일 때 가운데이고 각이 커지면 오른쪽으로 갑니다.
function 그리기(설정값: 장면설정) -> string {
  let 해상도 = 설정값.해상도
  let 광원 = 벡터 { x: -2.5 + (toFloat(설정값.광원각) - 35.0) / 12.0, y: 5.0, z: 3.0 }
  let 실수해상도 = toFloat(해상도)
  let 칸들 = [for 줄 in 0..<해상도 for 열 in 0..<해상도 => 칸그리기(줄, 열, 실수해상도, 광원)]
  let 변 = 해상도 * 칸크기
  let 요약 = "<div class=\"meta\">해상도 {해상도}x{해상도}, 광원각 {설정값.광원각}. 앱 레벨 제곱근으로 추적했습니다.</div>"
  let 그림 = "<svg viewBox=\"0 0 {변} {변}\" width=\"{변}\" height=\"{변}\" style=\"image-rendering:pixelated;border:1px solid #000\" xmlns=\"http://www.w3.org/2000/svg\">{칸들.join("")}</svg>"
  요약 + 그림
}

function 처리(입력: string) -> string {
  결과풀기(설정읽기(입력).map(그리기))
}

input() |> 처리 |> print
