if (window) window.ludus = {run, kill, flush_stdout, stdout, p5, svg, flush_commands, commands, result, input, is_running}

const worker_url = new URL("worker.js", import.meta.url)
const worker = new Worker(worker_url, {type: "module"})

let outbox = []
let ludus_console = ""
let ludus_commands = []
let ludus_result = null
let code = null
let running = false
let ready = false
let io_interval_id = null

worker.onmessage = handle_messages

async function handle_messages (e) {
  let msgs
  try {
    msgs = JSON.parse(e.data)    
  } catch {
    console.log(e.data)
    throw Error("Main: bad json from Ludus")
  }
  for (const msg of msgs) {
    switch (msg.verb) {
      case "Complete": {
        console.log("Main: ludus completed with => ", msg.data)
        ludus_result = msg.data
        running = false
        ready = false
        outbox = []
        break
      }
      case "Error": {
        console.log("Main: ludus errored with => ", msg.data)
        ludus_result = msg.data
        running = false
        ready = false
        outbox = []
        break
      }
      // TODO: do more than report these
      case "Console": {
        let new_lines = msg.data.join("\n");
        console.log("Main: ludus says => ", new_lines)
        ludus_console = ludus_console + new_lines
        break
      }
      case "Commands": {
        console.log("Main: ludus commands => ", msg.data)
        for (const command of msg.data) {
          // attempt to solve out-of-order command bug
          ludus_commands[command[1]] = command
        }
        break
      }
      case "Fetch": {
        console.log("Main: ludus requests => ", msg.data)
        const res = await fetch(msg.data, {mode: "cors"})
        const text = await res.text()
        console.log("Main: js responds => ", text)
        outbox.push({verb: "Fetch", data: [msg.data, res.status, text]})
      }
      case "Ready": {
        console.log("Main: ludus is ready")
        ready = true
      }
    }
  }
}

function io_poller () {
  if (io_interval_id && !running) {
    // flush the outbox one last time
    // (presumably, with the kill message)
    worker.postMessage(outbox)
    // cancel the poller
    clearInterval(io_interval_id)
    outbox = []
  }
  if (ready && running) {
    worker.postMessage(outbox)
    outbox = []
  }
}

function start_io_polling () {
  io_interval_id = setInterval(io_poller, 10)
}

// runs a ludus script; does not return the result
// the result must be explicitly polled with `result`
export function run (source) {
  if (running || ready) {
    return "TODO: handle this? should not be running"
  }
  running = true
  ready = false
  result = null
  code = source
  worker.postMessage([{verb: "Run", data: source}])
  outbox = []
  start_io_polling()
}

// tells if the ludus script is still running
export function is_running() {
  return running && ready
}

// kills a ludus script
export function kill () {
  running = false
  outbox.push({verb: "Kill"})
  console.log("Main: Killed Ludus")
}

// sends text into ludus (status: not working)
export function input (text) {
  console.log("Main: calling `input` with ", text)
  outbox.push({verb: "Input", data: text})
}

// returns the contents of the ludus console and resets the console
export function flush_stdout () {
  let out = ludus_console
  ludus_console = ""
  out
}

// returns the contents of the ludus console, retaining them
export function stdout () {
  return ludus_console
}

// returns the array of turtle commands
export function commands () {
  return ludus_commands
}

// returns the array of turtle commands and clears it
export function flush_commands () {
  let out = ludus_commands
  ludus_commands = []
  out
}

// returns the ludus result
// this is effectively Option<String>:
// null if no result has been returned, or
// a string representation of the result
export function result () {
  return ludus_result
}

//////////// turtle plumbing below
// TODO: refactor this out into modules
const turtle_init = {
  position: [0, 0],
  heading: 0,
  pendown: true,
  pencolor: "white",
  penwidth: 1,
  visible: true
}

const colors = {
	black: [0, 0, 0, 255],
	silver: [192, 192, 192, 255],
	gray: [128, 128, 128, 255],
	white: [255, 255, 255, 255],
	maroon: [128, 0, 0, 255],
	red: [255, 0, 0, 255],
	purple: [128, 0, 128, 255],
	fuchsia: [255, 0, 255, 255],
	green: [0, 128, 0, 255],
	lime: [0, 255, 0, 255],
	olive: [128, 128, 0, 255],
	yellow: [255, 255, 0, 255],
	navy: [0, 0, 128, 255],
	blue: [0, 0, 255, 255],
	teal: [0, 128, 128, 255],
	aqua: [0, 255, 255, 255],
}

function resolve_color (color) {
  if (typeof color === 'string') return colors[color]
  if (typeof color === 'number') return [color, color, color, 255]
  if (Array.isArray(color)) return color
  return [0, 0, 0, 255] // default to black?
}

let background_color = "black"

function add (v1, v2) {
  const [x1, y1] = v1
  const [x2, y2] = v2
  return [x1 + x2, y1 + y2]
}

function mult (vector, scalar) {
  const [x, y] = vector
  return [x * scalar, y * scalar]
}

function unit_of (heading) {
  const turns = -heading + 0.25
  const radians = turn_to_rad(turns)
  return [Math.cos(radians), Math.sin(radians)]
}

function command_to_state (prev_state, command) {
  const [_target, _id, curr_command] = command
  const [verb] = curr_command
  switch (verb) {
    case "goto": {
      const [_, x, y] = curr_command
      return {...prev_state, position: [x, y]}
    }
    case "home": {
      return {...prev_state, position: [0, 0], heading: 0}
    }
    case "right": {
      const [_, angle] = curr_command
      const {heading} = prev_state
      return {...prev_state, heading: heading + angle}
    }
    case "left": {
      const [_, angle] = curr_command
      const {heading} = prev_state
      return {...prev_state, heading: heading - angle}
    }
    case "forward": {
      const [_, steps] = curr_command
      const {heading, position} = prev_state
      const unit = unit_of(heading)
      const move = mult(unit, steps)
      return {...prev_state, position: add(position, move)}
    }
    case "back": {
      const [_, steps] = curr_command
      const {heading, position} = prev_state
      const unit = unit_of(heading)
      const move = mult(unit, -steps)
      return {...prev_state, position: add(position, move)}
    }
    case "penup": {
      return {...prev_state, pendown: false}
    }
    case "pendown": {
      return {...prev_state, pendown: true}
    }
    case "penwidth": {
      const [_, width] = curr_command
      return {...prev_state, penwidth: width}
    }
    case "pencolor": {
      const [_, color] = curr_command
      return {...prev_state, pencolor: color}
    }
    case "setheading": {
      const [_, heading] = curr_command
      return {...prev_state, heading: heading}
    }
    case "loadstate": {
      // console.log("LOADSTATE: ", curr_command)
      const [_, [x, y], heading, visible, pendown, penwidth, pencolor] = curr_command
      return {position: [x, y], heading, visible, pendown, penwidth, pencolor}
    }
    case "show": {
      return {...prev_state, visible: true}
    }
    case "hide": {
      return {...prev_state, visible: false}
    }
    case "background": {
      background_color = curr_command[1]
      return prev_state
    }
  }
}

function eq_vect (v1, v2) {
  const [x1, y1] = v1
  const [x2, y2] = v2
  return (x1 === x2) && (y1 === y2)
}

function eq_color (c1, c2) {
  if (c1 === c2) return true
  const res1 = resolve_color(c1)
  const res2 = resolve_color(c2)
  for (let i = 0; i < res1.length; ++i) {
    if (res1[i] !== res2[i]) return false
  }
  return true
}

function states_to_call (prev, curr) {
  const calls = []
  // whose state should we use?
  // pen states will only differ on more than one property
  // if we use `loadstate`
  // my sense is `prev`, but that may change
  if (prev.pendown && !eq_vect(prev.position, curr.position)) {
    calls.push(["line", prev.position[0], prev.position[1], curr.position[0], curr.position[1]])
  }
  if (!eq_color(curr.pencolor, prev.pencolor)) {
    calls.push(["stroke", ...resolve_color(curr.pencolor)])
  }
  if (curr.penwidth !== prev.penwidth) {
    calls.push(["strokeWeight", curr.penwidth])
  }
  return calls
}

const turtle_radius = 20

const turtle_angle = 0.385

let turtle_color = [255, 255, 255, 150]

function p5_call_root () {
  return [
    ["background", ...resolve_color(background_color)],
    ["push"],
    ["rotate", Math.PI],
    ["scale", -1, 1],
    ["stroke", ...resolve_color(turtle_init.pencolor)],
  ]
}

function rotate (vector, heading) {
  const radians = turn_to_rad(heading)
  const [x, y] = vector
  return [
    (x * Math.cos (radians)) - (y * Math.sin (radians)),
    (x * Math.sin (radians)) + (y * Math.cos (radians))
  ]
}

function turn_to_rad (heading) {
  return (heading % 1) * 2 * Math.PI
}

function turn_to_deg (heading) {
  return (heading % 1) * 360
}

function hex (n) {
  return n.toString(16).padStart(2, "0")
}

function svg_render_line (prev, curr) {
  if (!prev.pendown) return ""
  if (eq_vect(prev.position, curr.position)) return ""
  const {position: [x1, y1], pencolor, penwidth} = prev
  const {position: [x2, y2]} = curr
  const [r, g, b, a] = resolve_color(pencolor)
  return `
    <line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="#${hex(r)}${hex(g)}${hex(b)}" stroke-linecap="square" stroke-opacity="${a/255}" stroke-width="${penwidth}"/>
    `
}

function escape_svg (svg) {
  return svg
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&apos;")
}

export function extract_ludus (svg) {
  const code = svg.split("<ludus>")[1]?.split("</ludus>")[0] ?? ""
  return code 
    .replace(/&amp;/g, "&")
    .replace(/&lt;/g, "<")
    .replace(/&gt;/g, ">")
    .replace(/&quot;/g, `"`)
    .replace(/&apos;/g, `'`)
}

function svg_render_path (states) {
  const path = []
  for (let i = 1; i < states.length; ++i) {
    const prev = states[i - 1]
    const curr = states[i]
    path.push(svg_render_line(prev, curr))
  }
  return path.join("")
}

function svg_render_turtle (state) {
  if (!state.visible) return ""
  const [fr, fg, fb, fa] = turtle_color
  const fill_alpha = fa/255
  const {heading, pencolor, position: [x, y], pendown, penwidth} = state
  const origin = [0, turtle_radius]
  const [x1, y1] = origin
  const [x2, y2] = rotate(origin, turtle_angle)
  const [x3, y3] = rotate(origin, -turtle_angle)
  const [pr, pg, pb, pa] = resolve_color(pencolor)
  const pen_alpha = pa/255
  const ink = pendown ? `<line x1="${x1}" y1="${y1}" x2="0" y2="0" stroke="#${hex(pr)}${hex(pg)}${hex(pb)}" stroke-linecap="round" stroke-opacity="${pen_alpha}" stroke-width="${penwidth}" />` : ""
  return `
    <g transform="translate(${x}, ${y})rotate(${-turn_to_deg(heading)})">
      <polygon points="${x1} ${y1} ${x2} ${y2} ${x3} ${y3}" stroke="none" fill="#${hex(fr)}${hex(fg)}${hex(fb)})" fill-opacity="${fill_alpha}"/>
      ${ink}
    </g>
    `
}

export function svg (commands) {
  // console.log(commands)
  const states = [turtle_init]
  commands.reduce((prev_state, command) => {
    const new_state = command_to_state(prev_state, command)
    states.push(new_state)
    return new_state
  }, turtle_init)
  // console.log(states)
  const {maxX, maxY, minX, minY} = states.reduce((accum, {position: [x, y]}) => {
    accum.maxX = Math.max(accum.maxX, x)
    accum.maxY = Math.max(accum.maxY, y)
    accum.minX = Math.min(accum.minX, x)
    accum.minY = Math.min(accum.minY, y)
    return accum
    
  }, {maxX: 0, maxY: 0, minX: 0, minY: 0})
  const [r, g, b] = resolve_color(background_color)  
  if ((r+g+b)/3 > 128) turtle_color = [0, 0, 0, 150]
  const view_width = (maxX - minX) * 1.2
  const view_height = (maxY - minY) * 1.2
  const margin = Math.max(view_width, view_height) * 0.1
  const x_origin = minX - margin
  const y_origin = -maxY - margin
  const path = svg_render_path(states)
  const turtle = svg_render_turtle(states[states.length - 1])
  return `<?xml version="1.0" standalone="no"?>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="${x_origin} ${y_origin} ${view_width} ${view_height}" width="10in" height="8in">

  <rect x="${x_origin - 5}" y="${y_origin - 5}" width="${view_width + 10}" height="${view_height + 10}" fill="#${hex(r)}${hex(g)}${hex(b)}" stroke-width="0" paint-order="fill" />

  <g transform="scale(-1, 1) rotate(180)">
    ${path}
    ${turtle}
  </g>

<ludus>
${escape_svg(code)}
</ludus>
</svg>
  `
}

function p5_render_turtle (state, calls) {
  if (!state.visible) return
  calls.push(["push"])
  const [r, g, b, a] = turtle_color
  calls.push(["fill", r, g, b, a])
  const {heading, pencolor, position: [x, y], pendown, penwidth} = state
  const origin = [0, turtle_radius]
  const [x1, y1] = origin
  const [x2, y2] = rotate(origin, turtle_angle)
  const [x3, y3] = rotate(origin, -turtle_angle)
  calls.push(["translate", x, y])
  // need negative turtle rotation with the other p5 translations
  calls.push(["rotate", -turn_to_rad(heading)])
  calls.push(["noStroke"])
  calls.push(["beginShape"])
  calls.push(["vertex", x1, y1])
  calls.push(["vertex", x2, y2])
  calls.push(["vertex", x3, y3])
  calls.push(["endShape"])
  calls.push(["strokeWeight", penwidth])
  calls.push(["stroke", ...resolve_color(pencolor)])
  if (pendown) calls.push(["line", 0, 0, x1, y1])
  calls.push(["pop"])
  return calls
}

export function p5 (commands) {
  const states = [turtle_init]
  commands.reduce((prev_state, command) => {
    const new_state = command_to_state(prev_state, command)
    states.push(new_state)
    return new_state
  }, turtle_init)
  // console.log(states)
  const [r, g, b, _] = resolve_color(background_color)  
  if ((r + g + b)/3 > 128) turtle_color = [0, 0, 0, 150]
  const p5_calls = [...p5_call_root()]
  for (let i = 1; i < states.length; ++i) {
    const prev = states[i - 1]
    const curr = states[i]
    const calls = states_to_call(prev, curr)
    for (const call of calls) {
      p5_calls.push(call)
    }
  }
  p5_calls[0] = ["background", ...resolve_color(background_color)]
  p5_render_turtle(states[states.length - 1], p5_calls)
  p5_calls.push(["pop"])
  return p5_calls
}


