import init from "./out.mjs"

const mod = await init()

let res = null

let code = null

export function run (source) {
  code = source
  const output = mod.ludus(source).value
  res = JSON.parse(output)
  return res
}

export function stdout () {
  if (!res) return ""
  return res.io.console.data
}

export function turtle_commands () {
  if (!res) return []
  return res.io.turtle.data
}

export function result () {
  return res
}

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, 25, 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, curr_command) {
  const verb = curr_command[0]
  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 "pendwith": {
      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": {
      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

const turtle_color = [255, 255, 255, 150]

const p5_call_root = [
  ["push"],
  ["rotate", Math.PI],
  ["scale", -1, 1],
  ["background", ...resolve_color(background_color)],
  ["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 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="rgb(${r} ${g} ${b})" 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="rgb(${pr} ${pg} ${pb})" 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="rgb(${fr} ${fg} ${fb})" fill-opacity="${fill_alpha}"/>
      ${ink}
    </g>
    `
}

export function svg (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)
  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, a] = resolve_color(background_color)
  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 x1 = minX - margin
  // don't actually need these:
  // const y1 = minY - margin
  // const x2 = maxX + margin
  const y2 = 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" style="background-color:rgb(${r} ${g} ${b}); background-opacity: ${a/255}" viewBox="${x1} ${-y2} ${view_width} ${view_height}">

  <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)
  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.push("pop")
  }
  p5_calls[0] = ["background", ...resolve_color(background_color)]
  p5_render_turtle(states[states.length - 1], p5_calls)
  return p5_calls
}

