///|
/// A source of Gherkin text, bundling content with metadata.
///
/// The internal representation is opaque — callers use factory functions
/// to create and accessor methods to read. This allows the internal
/// representation to evolve without breaking consumers.
pub struct Source {
  priv lines : Array[String]
  priv uri : String?
} derive(Debug, Eq)

///|
/// Create a `Source` from a string, splitting into lines.
///
/// Handles both Unix (`\n`) and Windows (`\r\n`) line endings.
/// The optional `uri` identifies where the content came from
/// (file path, URL, etc.).
pub fn Source::from_string(content : String, uri? : String) -> Source {
  let lines : Array[String] = []
  let mut current : Array[Char] = []
  for c in content.iter() {
    if c == '\n' {
      lines.push(String::from_array(current))
      current = []
    } else if c != '\r' {
      current.push(c)
    }
  }
  lines.push(String::from_array(current))
  { lines, uri, }
}

///|
/// Create a `Source` from UTF-8 encoded bytes.
///
/// Decodes the bytes to a string, then splits into lines.
/// Invalid UTF-8 sequences are replaced with the Unicode replacement character.
pub fn Source::from_bytes(bytes : Bytes, uri? : String) -> Source {
  let content = @utf8.decode_lossy(bytes[:])
  Source::from_string(content, uri?)
}

///|
/// Returns the URI identifying this source, if any.
pub fn Source::uri(self : Source) -> String? {
  self.uri
}

///|
/// Returns the full content as a single string, with lines joined by `\n`.
///
/// This allocates a new string. For line-by-line access, prefer `line()`.
pub fn Source::content(self : Source) -> String {
  self.lines.join("\n")
}

///|
/// Returns the line at the given 1-based line number, matching `Location.line`.
///
/// Returns `None` if the line number is out of range.
pub fn Source::line(self : Source, n : Int) -> String? {
  if n >= 1 && n <= self.lines.length() {
    Some(self.lines[n - 1])
  } else {
    None
  }
}

///|
/// Returns the number of lines in this source.
pub fn Source::line_count(self : Source) -> Int {
  self.lines.length()
}

///|
/// Serialize a Source to JSON with `uri` and `data` fields.
///
/// `uri` is written as a JSON string or `null` (matching what `from_json`
/// accepts), not as the array shape that `Option::to_json` produces.
pub impl ToJson for Source with fn to_json(self) {
  {
    "uri": match self.uri {
      Some(uri) => Json(uri)
      None => Json::null()
    },
    "data": self.content().to_json(),
  }
}

///|
/// Deserialize a Source from JSON with `uri` and `data` fields.
pub impl @json.FromJson for Source with fn from_json(json, path) {
  guard json is Object(obj) else {
    raise @json.JsonDecodeError((path, "expected object for Source"))
  }
  let data = match obj.get("data") {
    Some(String(s)) => s
    _ =>
      raise @json.JsonDecodeError(
        (path, "expected string field 'data' in Source"),
      )
  }
  let uri = match obj.get("uri") {
    Some(String(s)) => Some(s)
    Some(Null) | None => None
    _ =>
      raise @json.JsonDecodeError(
        (path, "expected string or null field 'uri' in Source"),
      )
  }
  Source::from_string(data, uri?)
}

///|
pub extend Source with @moonbitlang/core/debug.Debug::{to_repr}

///|
pub extend Source with Eq::{not_equal, equal}

///|
pub extend Source with ToJson::{to_json}

///|
pub extend Source with @moonbitlang/core/json.FromJson::{from_json}