///|
priv struct ToolingDocument {
  source : String
  filename : String
  chars : Array[Char]
}

///|
fn ToolingDocument::new(source : String, filename : String) -> ToolingDocument {
  { source, filename, chars: source.to_array() }
}

///|
fn ToolingDocument::position_to_offset(
  self : ToolingDocument,
  line : Int,
  character : Int,
) -> Int {
  let chars = self.chars
  let mut cursor = 0
  let mut current_line = 0
  let mut current_character = 0
  while cursor < chars.length() {
    if current_line == line && character <= current_character {
      return cursor
    }
    let char_ = chars[cursor]
    if char_ == '\n' {
      if current_line == line {
        return cursor
      }
      current_line += 1
      current_character = 0
    } else {
      let width = utf16_width(char_)
      if current_line == line && character < current_character + width {
        return cursor
      }
      current_character += width
    }
    cursor += 1
  }
  chars.length()
}

///|
fn utf16_width(c : Char) -> Int {
  if c.to_int() > 0xFFFF {
    2
  } else {
    1
  }
}

///|
fn ToolingDocument::offset_to_position(
  self : ToolingDocument,
  offset : Int,
) -> ToolingPosition {
  let chars = self.chars
  let limit = if offset < chars.length() { offset } else { chars.length() }
  let mut cursor = 0
  let mut line = 0
  let mut character = 0
  while cursor < limit {
    if chars[cursor] == '\n' {
      line += 1
      character = 0
    } else {
      character += utf16_width(chars[cursor])
    }
    cursor += 1
  }
  { line, character }
}

///|
fn ToolingDocument::location_from_span(
  self : ToolingDocument,
  span : @compiler.SourceSpan,
) -> ToolingLocation {
  {
    filename: self.filename,
    span,
    range: {
      start: self.offset_to_position(span.start),
      end_: self.offset_to_position(span.end),
    },
  }
}

///|
pub fn full_document_range(source : String) -> ToolingRange {
  let document = ToolingDocument::new(source, "")
  {
    start: { line: 0, character: 0 },
    end_: document.offset_to_position(source.length()),
  }
}

///|
fn contains_offset(span : @compiler.SourceSpan, offset : Int) -> Bool {
  offset >= span.start && offset < span.end
}

///|
fn spans_overlap(a : @compiler.SourceSpan, b : @compiler.SourceSpan) -> Bool {
  a.start < b.end && b.start < a.end
}

///|
fn is_identifier_char(c : Char) -> Bool {
  (c >= 'a' && c <= 'z') ||
  (c >= 'A' && c <= 'Z') ||
  (c >= '0' && c <= '9') ||
  c == '_' ||
  c == '$'
}

///|
fn token_boundary(c : Char) -> Bool {
  c == ' ' ||
  c == '\n' ||
  c == '\t' ||
  c == '\r' ||
  c == '<' ||
  c == '>' ||
  c == '"' ||
  c == '\'' ||
  c == '='
}

///|
fn ToolingDocument::span_text(
  self : ToolingDocument,
  span : @compiler.SourceSpan,
) -> String {
  String::unsafe_substring(self.source, start=span.start, end=span.end)
}

///|
fn ToolingDocument::current_token_prefix(
  self : ToolingDocument,
  offset : Int,
) -> String {
  let chars = self.chars
  let limit = if offset < chars.length() { offset } else { chars.length() }
  let mut start = limit
  while start > 0 && not(token_boundary(chars[start - 1])) {
    start -= 1
  }
  String::unsafe_substring(self.source, start~, end=limit)
}