///|
priv enum PlannedInstruction {
  PlannedAdd(target_offset~ : Int, size~ : Int)
  PlannedRun(byte~ : Byte, size~ : Int)
  PlannedCopy(address~ : Int, size~ : Int)
}

///|
priv struct WindowPlan {
  target_offset : Int
  target_size : Int
  source_size : Int
  source_position : Int
  instructions : Array[PlannedInstruction]
}

///|
fn basic_window_plan(target_offset : Int, target_size : Int) -> WindowPlan {
  let instructions : Array[PlannedInstruction] = []
  if target_size > 0 {
    instructions.push(PlannedAdd(target_offset~, size=target_size))
  }
  {
    target_offset,
    target_size,
    source_size: 0,
    source_position: 0,
    instructions,
  }
}

///|
fn flush_pending_add(
  instructions : Array[PlannedInstruction],
  target_offset : Int,
  literal_start : Int,
  literal_end : Int,
) -> Unit {
  if literal_start >= 0 && literal_end > literal_start {
    instructions.push(
      PlannedAdd(
        target_offset=target_offset + literal_start,
        size=literal_end - literal_start,
      ),
    )
  }
}

///|
fn index_consumed_target(finder : MatchFinder, start : Int, size : Int) -> Unit {
  for position = start; position < start + size; position = position + 1 {
    finder.insert_target(position)
  }
}

///|
fn matched_window_plan(
  source : Bytes,
  target : Bytes,
  target_offset : Int,
  target_size : Int,
  options : EncodeOptions,
) -> WindowPlan raise VcdiffError {
  let instructions : Array[PlannedInstruction] = []
  let finder = MatchFinder::new(source, target, target_offset, target_size)
  let mut position = 0
  let mut literal_start = -1
  while position < target_size {
    let absolute = target_offset + position
    let run = run_length(target, absolute, target_offset + target_size)
    if run >= options.run_threshold {
      flush_pending_add(instructions, target_offset, literal_start, position)
      literal_start = -1
      instructions.push(PlannedRun(byte=target[absolute], size=run))
      index_consumed_target(finder, position, run)
      position += run
    } else {
      let best = finder.find_best(
        position,
        options.minimum_match,
        options.max_candidate_chain,
        options.enable_target_matches,
      )
      if best.size > 0 {
        flush_pending_add(instructions, target_offset, literal_start, position)
        literal_start = -1
        instructions.push(PlannedCopy(address=best.address, size=best.size))
        index_consumed_target(finder, position, best.size)
        position += best.size
      } else {
        if literal_start < 0 {
          literal_start = position
        }
        finder.insert_target(position)
        position += 1
      }
    }
  }
  flush_pending_add(instructions, target_offset, literal_start, target_size)
  {
    target_offset,
    target_size,
    source_size: source.length(),
    source_position: 0,
    instructions,
  }
}

///|
fn plan_windows(
  source : Bytes,
  target : Bytes,
  options : EncodeOptions,
) -> Array[WindowPlan] raise VcdiffError {
  let plans : Array[WindowPlan] = []
  let mut offset = 0
  while offset < target.length() {
    let remaining = target.length() - offset
    let size = if remaining < options.window_size {
      remaining
    } else {
      options.window_size
    }
    if source.length() == 0 &&
      !options.enable_target_matches &&
      options.run_threshold > size {
      plans.push(basic_window_plan(offset, size))
    } else {
      plans.push(matched_window_plan(source, target, offset, size, options))
    }
    offset += size
  }
  plans
}

///|
fn plan_output_size(plan : WindowPlan) -> Int {
  let mut output_size = 0
  for instruction in plan.instructions {
    match instruction {
      PlannedAdd(size~, ..) | PlannedRun(size~, ..) | PlannedCopy(size~, ..) =>
        output_size += size
    }
  }
  output_size
}