///|
/// Built-in source for Wren 0.4's optional `random` module. The public API and
/// algorithms follow the upstream optional module; host state is kept behind
/// MoonWren foreign-class handles so each VM owns an isolated generator.
let optional_random_source =
  #|foreign class Random {
  #|  construct new() { seed_() }
  #|  construct new(seed) {
  #|    if (seed is Num) {
  #|      seed_(seed)
  #|    } else if (seed is Sequence) {
  #|      if (seed.isEmpty) Fiber.abort("Sequence cannot be empty.")
  #|      var seeds = []
  #|      for (element in seed) {
  #|        if (!(element is Num)) Fiber.abort("Sequence elements must all be numbers.")
  #|        seeds.add(element)
  #|        if (seeds.count == 16) break
  #|      }
  #|      var i = 0
  #|      while (seeds.count < 16) {
  #|        seeds.add(seeds[i])
  #|        i = i + 1
  #|      }
  #|      seed_(seeds[0], seeds[1], seeds[2], seeds[3], seeds[4], seeds[5], seeds[6], seeds[7], seeds[8], seeds[9], seeds[10], seeds[11], seeds[12], seeds[13], seeds[14], seeds[15])
  #|    } else {
  #|      Fiber.abort("Seed must be a number or a sequence of numbers.")
  #|    }
  #|  }
  #|  foreign seed_()
  #|  foreign seed_(seed)
  #|  foreign seed_(n1, n2, n3, n4, n5, n6, n7, n8, n9, n10, n11, n12, n13, n14, n15, n16)
  #|  foreign float()
  #|  float(end) { float() * end }
  #|  float(start, end) { float() * (end - start) + start }
  #|  foreign int()
  #|  int(end) { (float() * end).floor }
  #|  int(start, end) { (float() * (end - start)).floor + start }
  #|  sample(list) {
  #|    if (list.count == 0) Fiber.abort("Not enough elements to sample.")
  #|    return list[int(list.count)]
  #|  }
  #|  sample(list, count) {
  #|    if (count > list.count) Fiber.abort("Not enough elements to sample.")
  #|    var result = []
  #|    if (count * 4 < list.count) {
  #|      var picked = {}
  #|      for (i in list.count - count...list.count) {
  #|        var index = int(i + 1)
  #|        if (picked.containsKey(index)) index = i
  #|        picked[index] = true
  #|        result.add(list[index])
  #|      }
  #|    } else {
  #|      var picked = List.filled(list.count, false)
  #|      for (i in list.count - count...list.count) {
  #|        var index = int(i + 1)
  #|        if (picked[index]) index = i
  #|        picked[index] = true
  #|        result.add(list[index])
  #|      }
  #|    }
  #|    return result
  #|  }
  #|  shuffle(list) {
  #|    if (list.isEmpty) return
  #|    for (i in 0...list.count - 1) {
  #|      var from = int(i, list.count)
  #|      var temp = list[from]
  #|      list[from] = list[i]
  #|      list[i] = temp
  #|    }
  #|  }
  #|}

///|
let optional_meta_source =
  #|class Meta {
  #|  foreign static getModuleVariables(module)
  #|  foreign static eval(source)
  #|  foreign static compileExpression(source)
  #|  foreign static compile(source)
  #|}

///|
let optional_timer_source =
  #|class Timer {
  #|  static sleep(milliseconds) {
  #|    if (!(milliseconds is Num)) Fiber.abort("Sleep duration must be a number.")
  #|    if (milliseconds < 0) Fiber.abort("Sleep duration cannot be negative.")
  #|    return Fiber.yield({"$moonwren": "sleep", "milliseconds": milliseconds})
  #|  }
  #|}

///|
let optional_task_source =
  #|class Task {
  #|  construct new(id) { _id = id }
  #|  id { _id }
  #|  static run(callable) {
  #|    if (!(callable is Fn)) Fiber.abort("Task callable must be a function.")
  #|    return Task.new(Fiber.yield({"$moonwren": "spawn", "callable": callable}))
  #|  }
  #|  await { return Fiber.yield({"$moonwren": "join", "task": _id}) }
  #|  static all(tasks) {
  #|    var ids = []
  #|    for (task in tasks) {
  #|      if (!(task is Task)) Fiber.abort("Task.all expects Task values.")
  #|      ids.add(task.id)
  #|    }
  #|    return Fiber.yield({"$moonwren": "joinAll", "tasks": ids})
  #|  }
  #|  static race(tasks) {
  #|    var ids = []
  #|    for (task in tasks) {
  #|      if (!(task is Task)) Fiber.abort("Task.race expects Task values.")
  #|      ids.add(task.id)
  #|    }
  #|    return Fiber.yield({"$moonwren": "race", "tasks": ids})
  #|  }
  #|}

///|
let optional_scheduler_source =
  #|import "task" for Task
  #|class Scheduler {
  #|  static spawn(callable) { Task.run(callable) }
  #|  static all(tasks) { Task.all(tasks) }
  #|  static race(tasks) { Task.race(tasks) }
  #|}

///|
fn optional_module_source(name : String) -> String? {
  match name {
    "random" => Some(optional_random_source)
    "meta" => Some(optional_meta_source)
    "timer" => Some(optional_timer_source)
    "task" => Some(optional_task_source)
    "scheduler" => Some(optional_scheduler_source)
    _ => standard_module_source(name)
  }
}

///|
priv struct BuiltinRandomState {
  state : Array[UInt]
  mut index : Int
  mut alive : Bool
}

///|
fn random_state_for_handle(
  states : Array[BuiltinRandomState],
  handle : Int,
) -> BuiltinRandomState? {
  if handle < 0 || handle >= states.length() || !states[handle].alive {
    None
  } else {
    Some(states[handle])
  }
}

///|
fn seed_random_number(state : BuiltinRandomState, seed : Double) -> Unit {
  let mut value = wren_u32(seed)
  for index = 0; index < 16; index = index + 1 {
    value = value * 1664525U + 1013904223U
    state.state[index] = value
  }
  state.index = 0
}

///|
fn seed_random_values(
  state : BuiltinRandomState,
  arguments : Array[Value],
) -> Bool {
  if arguments.length() != 16 {
    return false
  }
  for index = 0; index < 16; index = index + 1 {
    match arguments[index] {
      NumberValue(number) => state.state[index] = wren_u32(number)
      _ => return false
    }
  }
  state.index = 0
  true
}

///|
fn advance_random(state : BuiltinRandomState) -> UInt {
  let a = state.state[state.index]
  let mut c = state.state[(state.index + 13) & 15]
  let b = a ^ c ^ (a << 16) ^ (c << 15)
  c = state.state[(state.index + 9) & 15]
  c = c ^ (c >> 11)
  let next = b ^ c
  state.state[state.index] = next
  let d = next ^ ((next << 5) & 0xda442d24U)
  state.index = (state.index + 15) & 15
  let tail = state.state[state.index]
  state.state[state.index] = tail ^ b ^ d ^ (tail << 2) ^ (b << 18) ^ (c << 28)
  state.state[state.index]
}

///|
fn builtin_random_float(state : BuiltinRandomState) -> Double {
  let high = advance_random(state).to_double() * 2097152.0
  let low = (advance_random(state) & 0x1fffffU).to_double()
  (high + low) / 9007199254740992.0
}

///|
fn invoke_builtin_random_collection_method(
  receiver : Int,
  name : String,
  arguments : Array[Value],
  span : SourceSpan,
  context : RuntimeContext,
) -> EvaluationResult? {
  if name != "sample" && name != "shuffle" {
    return None
  }
  let handle = if receiver >= 0 && receiver < context.instances.length() {
    context.instances[receiver].foreign_handle
  } else {
    None
  }
  let state = match handle {
    Some(identifier) =>
      match random_state_for_handle(context.builtin_random_states, identifier) {
        Some(value) => value
        None =>
          return Some(
            evaluation_error(
              InvalidOperand,
              "random generator is no longer available",
              span,
            ),
          )
      }
    None =>
      return Some(
        evaluation_error(
          InvalidOperand,
          "random generator has no host state",
          span,
        ),
      )
  }
  let list_identifier = match arguments[0] {
    ListValue(identifier) => identifier
    value =>
      return Some(
        evaluation_error(
          InvalidOperand,
          "expected List, received \{value.type_name()}",
          span,
        ),
      )
  }
  if list_identifier < 0 || list_identifier >= context.lists.length() {
    return Some(evaluation_error(InvalidOperand, "list is unavailable", span))
  }
  let items = context.lists[list_identifier].items
  if name == "shuffle" {
    for index = 0; index + 1 < items.length(); index = index + 1 {
      let remaining = items.length() - index
      let offset = (builtin_random_float(state) * remaining.to_double())
        .floor()
        .to_int()
      let selected = index + offset
      let temporary = items[selected]
      items[selected] = items[index]
      items[index] = temporary
    }
    return Some(EvaluationSucceeded(Value::null()))
  }
  if arguments.length() == 1 {
    if items.is_empty() {
      return Some(
        evaluation_error(FiberAborted, "Not enough elements to sample.", span),
      )
    }
    let index = (builtin_random_float(state) * items.length().to_double())
      .floor()
      .to_int()
    return Some(EvaluationSucceeded(items[index]))
  }
  let count = match arguments[1] {
    NumberValue(number) => number.to_int()
    value =>
      return Some(
        evaluation_error(
          InvalidOperand,
          "sample count must be Num, received \{value.type_name()}",
          span,
        ),
      )
  }
  if count > items.length() {
    return Some(
      evaluation_error(FiberAborted, "Not enough elements to sample.", span),
    )
  }
  if count < 0 {
    return Some(
      evaluation_error(InvalidOperand, "sample count is negative", span),
    )
  }
  let pool = items.copy()
  let result : Array[Value] = []
  for index = 0; index < count; index = index + 1 {
    let remaining = pool.length() - index
    let offset = (builtin_random_float(state) * remaining.to_double())
      .floor()
      .to_int()
    let selected = index + offset
    let temporary = pool[selected]
    pool[selected] = pool[index]
    pool[index] = temporary
    result.push(pool[index])
  }
  Some(EvaluationSucceeded(context.new_list(result)))
}

///|
fn append_optional_bindings(
  methods : Array[ForeignMethodBinding],
  classes : Array[ForeignClassBinding],
  states : Array[BuiltinRandomState],
) -> Unit {
  append_standard_module_bindings(methods)
  for
    signature in [
      "getModuleVariables(_)", "eval(_)", "compileExpression(_)", "compile(_)",
    ] {
    methods.push(
      ForeignMethodBinding::new("meta", "Meta", true, signature, fn(_, _) {
        ForeignCallFailed("built-in Meta dispatch was not available")
      }),
    )
  }
  let nonce = Ref(0)
  classes.push(
    ForeignClassBinding::new(
      "random",
      "Random",
      fn(_) {
        let values = Array::make(16, 0U)
        let handle = states.length()
        states.push({ state: values, index: 0, alive: true, })
        ForeignAllocationSucceeded(handle)
      },
      fn(handle) {
        match random_state_for_handle(states, handle) {
          Some(state) => state.alive = false
          None => ()
        }
      },
    ),
  )
  methods.push(
    ForeignMethodBinding::new_for_foreign_class("random", "Random", "seed_()", fn(
      handle,
      _,
    ) {
      match random_state_for_handle(states, handle) {
        None => ForeignCallFailed("random generator is no longer available")
        Some(state) => {
          nonce.val = nonce.val + 1
          seed_random_number(state, nonce.val.to_double() * 2654435761.0)
          ForeignCallSucceeded(Value::null())
        }
      }
    }),
  )
  methods.push(
    ForeignMethodBinding::new_for_foreign_class(
      "random",
      "Random",
      "seed_(_)",
      fn(handle, arguments) {
        match random_state_for_handle(states, handle) {
          None => ForeignCallFailed("random generator is no longer available")
          Some(state) =>
            match arguments[0] {
              NumberValue(seed) => {
                seed_random_number(state, seed)
                ForeignCallSucceeded(Value::null())
              }
              _ => ForeignCallFailed("seed must be a number")
            }
        }
      },
    ),
  )
  methods.push(
    ForeignMethodBinding::new_for_foreign_class(
      "random",
      "Random",
      "seed_(_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_)",
      fn(handle, arguments) {
        match random_state_for_handle(states, handle) {
          None => ForeignCallFailed("random generator is no longer available")
          Some(state) =>
            if seed_random_values(state, arguments) {
              ForeignCallSucceeded(Value::null())
            } else {
              ForeignCallFailed("all seed values must be numbers")
            }
        }
      },
    ),
  )
  methods.push(
    ForeignMethodBinding::new_for_foreign_class("random", "Random", "int()", fn(
      handle,
      _,
    ) {
      match random_state_for_handle(states, handle) {
        None => ForeignCallFailed("random generator is no longer available")
        Some(state) =>
          ForeignCallSucceeded(Value::number(advance_random(state).to_double()))
      }
    }),
  )
  methods.push(
    ForeignMethodBinding::new_for_foreign_class("random", "Random", "float()", fn(
      handle,
      _,
    ) {
      match random_state_for_handle(states, handle) {
        None => ForeignCallFailed("random generator is no longer available")
        Some(state) =>
          ForeignCallSucceeded(Value::number(builtin_random_float(state)))
      }
    }),
  )
}