// Word-at-position ported from
// vscode/src/vs/editor/common/core/wordHelper.ts
// and TextModel.getWordAtPosition (textModel.ts), reduced to the default word
// definition. The performance windowing (windowSize/timeBudget) and
// language-specific word definitions are omitted; see the marker-render plan
// Deviations.

///|
/// Default word separators, `USUAL_WORD_SEPARATORS` (`wordHelper.ts:11`).
let usual_word_separators : String = "`~!@#$%^&*()-=+[{]}\\|;:'\",.<>/?"

///|
/// A word inside a model, `IWordAtPosition` (`wordHelper.ts:16`). Columns are
/// 1-based.
pub struct WordAtPosition {
  word : String
  start_column : Int32
  end_column : Int32
} derive(Eq, Debug)

///|
fn is_ascii_digit(code : UInt16) -> Bool {
  code is ('0'..<='9')
}

///|
/// `\w` = `[A-Za-z0-9_]` (ASCII, no `u` flag).
fn is_regex_word_char(code : UInt16) -> Bool {
  code is ('A'..<='Z' | 'a'..<='z' | '0'..<='9' | '_')
}

///|
/// Whether `code` is a default word separator or whitespace, i.e. excluded from
/// the `[^...\s]+` character class (`wordHelper.ts:39`). Whitespace follows the
/// repo convention (space/tab), the only whitespace present in line content.
fn is_word_break(code : UInt16) -> Bool {
  code is (' ' | '\t') || usual_word_separators.code_units().contains(code)
}

///|
/// Matches the number alternative `-?\d*\.\d\w*` (`wordHelper.ts:39`) anchored at
/// `i`, returning the exclusive end index or `None`. No backtracking is needed:
/// the dot must immediately follow the greedy `\d*`, so fewer digits can never
/// recover a failed match.
fn match_number(text : String, i : Int32) -> Int32? {
  let len = text.length()
  let j = if i < len && text[i] == '-' { i + 1 } else { i }
  let j = for j = j; j < len && is_ascii_digit(text[j]); {
    continue j + 1
  } nobreak {
    j
  }
  if j >= len || text[j] != '.' {
    return None
  }
  let j = j + 1
  if j >= len || !is_ascii_digit(text[j]) {
    return None
  }
  for j = j + 1; j < len && is_regex_word_char(text[j]); {
    continue j + 1
  } nobreak {
    Some(j)
  }
}

///|
/// Matches the word alternative `[^\s]+` anchored at `i`, returning the
/// exclusive end index or `None`.
fn match_word_run(text : String, i : Int32) -> Int32? {
  let len = text.length()
  let j = for j = i; j < len && !is_word_break(text[j]); {
    continue j + 1
  } nobreak {
    j
  }
  if j > i {
    Some(j)
  } else {
    None
  }
}

///|
/// Next regex match at or after `from`, `(start, end_exclusive)`, trying the
/// number alternative first at each candidate start (alternation priority), then
/// the word run; advances one character when neither matches.
fn next_word_match(text : String, from : Int32) -> (Int32, Int32)? {
  let len = text.length()
  for i in from.. return Some((i, end))
      None =>
        match match_word_run(text, i) {
          Some(end) => return Some((i, end))
          None => continue
        }
    }
  }
  None
}

///|
/// `getWordAtText` (`wordHelper.ts:96`), default definition: the word enclosing
/// `column` in `text`. A position is enclosed when `start <= pos <= end`
/// (`_findRegexMatchEnclosingPosition`: `matchIndex <= pos && lastIndex >= pos`).
fn get_word_at_text(
  column : Int32,
  text : String,
  text_offset : Int32,
) -> WordAtPosition? {
  let pos = column - 1 - text_offset
  for from = 0 {
    match next_word_match(text, from) {
      None => break None
      Some((start, end)) =>
        if start <= pos && end >= pos {
          break Some({
            word: text[start:end].to_owned(),
            start_column: text_offset + 1 + start,
            end_column: text_offset + 1 + end,
          })
        } else if start > pos {
          // matches are left-to-right and non-overlapping; nothing further right
          // can enclose pos.
          break None
        } else {
          continue end
        }
    }
  }
}

///|
/// `TextModel.getWordAtPosition` (`textModel.ts:2108`), default word
/// definition: the word at `position` on its line, or `None`.
pub fn TextModel::get_word_at_position(
  self : TextModel,
  position : @base_common.Position,
) -> WordAtPosition? {
  let position = self.validate_position(position)
  let line_content = self.get_line_content(position.line_number)
  get_word_at_text(position.column, line_content, 0)
}

///|
pub extend WordAtPosition with Debug::{to_repr}

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