///|
struct Replacement {
  start_line : Int
  start_column : Int
  end_line : Int
  end_column : Int
  replacement : String
  unit : PosUnit
} derive(Eq)

///|
pub impl Compare for Replacement with compare(self, other) {
  match self.start_line.compare(other.start_line) {
    0 =>
      match self.start_column.compare(other.start_column) {
        0 => 0
        x => x
      }
    x => x
  }
}

///|
enum PosUnit {
  Unicode
} derive(Eq, Compare)

///|
priv struct OffsetReplacement {
  start_offset : Int
  end_offset : Int
  replacement : String
}

///|
/// Creates a `Replacement` for a single point in the text, referring to the left side of the character.
/// 
/// # Parameters
/// - `line`: The line number (0-based) where the replacement occurs.
/// - `column`: The column number (0-based) where the replacement occurs.
/// - `replacement`: The string to insert at the specified point.
pub fn point(
  line : Int,
  column : Int,
  replacement : String,
  unit? : PosUnit = Unicode,
) -> Replacement {
  {
    start_line: line,
    end_line: line,
    start_column: column,
    end_column: column - 1,
    replacement,
    unit,
  }
}

///|
/// Creates a `Replacement` for a char in the text.
/// 
/// # Parameters
/// - `line`: The line number (0-based) where the replacement occurs.
/// - `column`: The column number (0-based) where the replacement occurs.
/// - `replacement`: The string to insert at the specified point.
pub fn char_at(
  line : Int,
  column : Int,
  replacement : String,
  unit? : PosUnit = Unicode,
) -> Replacement {
  {
    start_line: line,
    end_line: line,
    start_column: column,
    end_column: column,
    replacement,
    unit,
  }
}

///|
/// Creates a `Replacement` for a range in a single line.
/// 
/// # Parameters
/// - `line`: The line number (0-based) where the replacement occurs.
/// - `start_column`: The starting column number (0-based, in UTF-16 code units) of the range.
/// - `end_column`: The ending column number (0-based, in UTF-16 code units, inclusive) of the range.
/// - `replacement`: The string to replace the specified range with.
pub fn range(
  line : Int,
  start_column : Int,
  end_column : Int,
  replacement : String,
  unit? : PosUnit = PosUnit::Unicode,
) -> Replacement {
  {
    start_line: line,
    end_line: line,
    start_column,
    end_column,
    replacement,
    unit,
  }
}

///|
/// Creates a `Replacement` for a range that spans multiple lines.
/// 
/// # Parameters
/// - `start`: A tuple `(line, column)` representing the start of the range (0-based, in UTF-16 code units).
/// - `end`: A tuple `(line, column)` representing the end of the range (0-based, in UTF-16 code units, inclusive).
/// - `replacement`: The string to replace the specified range.
/// 
pub fn multiline(
  start : (Int, Int),
  end : (Int, Int),
  replacement : String,
  unit? : PosUnit = PosUnit::Unicode,
) -> Replacement {
  {
    start_line: start.0,
    end_line: end.0,
    start_column: start.1,
    end_column: end.1,
    replacement,
    unit,
  }
}

///|
test {
  let template =
    #|fn get(🐰){
    #| ...
    #| ...
    #| h($)
    #|}
  inspect(
    splice(template, [
      char_at(3, 3, "1001"),
      range(0, 7, 7, "x : Int"),
      point(0, 9, " -> Int "),
      multiline((1, 1), (2, 3), "f(x)\n g()"),
    ]),
    content=(
      #|fn get(x : Int) -> Int {
      #| f(x)
      #| g()
      #| h(1001)
      #|}
    ),
  )
}

///|
/// Splices substrings in `template` at the specified ranges with the given replacements.
/// 
/// The replacement ranges must not overlap.
/// 
/// # Parameters
/// 
/// - `template`: The original string to perform splicing on.
/// - `replacements`: An array of `Replacement` structs specifying the ranges and their replacements.
///           
/// # Returns
/// - The modified string after applying all replacements.
/// 
/// # Exceptions
/// - Raises an Error if any of the specified ranges are invalid or out of bounds.
/// 
/// # Examples
/// ```
/// let template =
///   #|fn get(🐰){
///   #| ...
///   #| ...
///   #| h($)
///   #|}
/// inspect(
///   splice(template, [
///     multiline((1, 1), (2, 3), "f(x)\n g()"),
///     range(0, 7, 7, "x : Int"),
///     point(0, 9, " -> Int "),
///     char_at(3, 3, "1001"),
///   ]),
///   content=(
///     #|fn get(x : Int) -> Int {
///     #| f(x)
///     #| g()
///     #| h(1001)
///     #|}
///   ),
/// )
/// ```
pub fn splice(
  template : StringView,
  replacements : Array[Replacement],
) -> String {
  replacements.sort()
  let mut last_offset = 0
  let buf = StringBuilder::new()
  let (chars, replacements) = get_utf16_codeunit_index(template, replacements)
  for subst in replacements {
    buf
    ..write_iter(chars[last_offset:subst.start_offset].iter())
    ..write_string(subst.replacement)
    last_offset = subst.end_offset + 1
  }
  buf.write_iter(chars[last_offset:].iter())
  buf.to_string()
}

///|
fn get_utf16_codeunit_index(
  s : StringView,
  xs : Array[Replacement],
) -> (Array[Char], Array[OffsetReplacement]) {
  let line_offsets = [0]
  let chars = s.to_array()
  for i, c in s {
    if c == '\n' {
      line_offsets.push(i + 1)
    }
  }
  let xs = xs.map(fn(x) {
    OffsetReplacement::{
      start_offset: line_offsets[x.start_line] + x.start_column,
      end_offset: line_offsets[x.end_line] + x.end_column,
      replacement: x.replacement,
    }
  })
  (chars, xs)
}