///|
pub struct HistoryConfiguration {
  persistent_history_filepath : String?
  max_entries : Int
}

///|
pub fn HistoryConfiguration::new(
  persistent_history_filepath? : String,
  max_entries? : Int = 500,
) -> HistoryConfiguration {
  let max_entries = if max_entries < 1 { 1 } else { max_entries }
  { persistent_history_filepath, max_entries }
}

///|
pub impl Default for HistoryConfiguration with default() {
  HistoryConfiguration::new()
}

///|
pub struct HistoryLog {
  entries : Array[String]
  config : HistoryConfiguration
  mut navigation_index : Int?
  mut unsubmitted_buffer : String
  mut submissions_since_last_trim : Int
}

///|
pub fn HistoryLog::new(config : HistoryConfiguration) -> HistoryLog {
  {
    entries: [],
    config,
    navigation_index: None,
    unsubmitted_buffer: "",
    submissions_since_last_trim: 0,
  }
}

///|
pub async fn HistoryLog::load_persistent_history(self : HistoryLog) -> Unit {
  let filepath = match self.config.persistent_history_filepath {
    Some(filepath) => filepath
    None => return
  }

  if !@fs.exists(filepath) {
    return
  }

  let contents = @fs.read_file(filepath).text() catch {
    e => {
      if @async.is_being_cancelled() {
        raise e
      }
      return
    }
  }

  self.entries.clear()
  for line in contents.split("\n") {
    if line.length() == 0 {
      continue
    }
    let decoded = @base64.decode(line) catch { _ => continue }
    let text = @utf8.decode(decoded) catch { _ => continue }
    self.entries.push(text)
  }
  trim_entries(self.entries, self.config.max_entries)
}

///|
pub fn HistoryLog::navigate_previous(
  self : HistoryLog,
  current_buffer : String,
) -> String? {
  if self.entries.length() == 0 {
    return None
  }

  if self.navigation_index is None {
    self.unsubmitted_buffer = current_buffer
  }
  let pattern = self.unsubmitted_buffer

  let start = match self.navigation_index {
    Some(index) => if index > 0 { index - 1 } else { 0 }
    None => self.entries.length() - 1
  }

  find_previous_matching_index(self.entries, pattern, start).bind(index => {
    self.navigation_index = Some(index)
    Some(self.entries[index])
  })
}

///|
pub fn HistoryLog::navigate_next(self : HistoryLog) -> String? {
  let current_index = match self.navigation_index {
    Some(index) => index
    None => return None
  }

  let pattern = self.unsubmitted_buffer

  if current_index + 1 >= self.entries.length() {
    self.navigation_index = None
    return Some(self.unsubmitted_buffer)
  }

  match find_next_matching_index(self.entries, pattern, current_index + 1) {
    Some(index) => {
      self.navigation_index = Some(index)
      Some(self.entries[index])
    }
    None => {
      self.navigation_index = None
      Some(self.unsubmitted_buffer)
    }
  }
}

///|
pub fn HistoryLog::reset_navigation(self : HistoryLog) -> Unit {
  self.navigation_index = None
}

///|
pub async fn HistoryLog::track_submission(
  self : HistoryLog,
  text : String,
) -> Unit {
  self.reset_navigation()
  if text.length() == 0 {
    return
  }
  if self.entries.length() > 0 &&
    self.entries[self.entries.length() - 1] == text {
    return
  }

  self.entries.push(text)
  trim_entries(self.entries, self.config.max_entries)
  self.append_persistent_history_entry(text)

  self.submissions_since_last_trim = self.submissions_since_last_trim + 1
  if self.submissions_since_last_trim >= 100 {
    self.save_persistent_history_full()
    self.submissions_since_last_trim = 0
  }
}

///|
/// 在历史记录中搜索匹配项
pub fn HistoryLog::navigate_search(
  self : HistoryLog,
  query : String,
  start_index : Int,
) -> (Int, String)? {
  if self.entries.length() == 0 {
    return None
  }
  let start = if start_index < 0 {
    self.entries.length() - 1
  } else if start_index >= self.entries.length() {
    self.entries.length() - 1
  } else {
    start_index
  }
  for i in start>=..0 {
    let entry = self.entries[i]
    if matches_history_pattern(entry, query) {
      return Some((i, entry))
    }
  }
  None
}

///|
fn trim_entries(entries : Array[String], max_entries : Int) -> Unit {
  while entries.length() > max_entries {
    ignore(entries.remove(0))
  }
}

///|
fn find_previous_matching_index(
  entries : Array[String],
  pattern : String,
  start : Int,
) -> Int? {
  if entries.length() == 0 {
    return None
  }
  let mut i = start
  while i >= 0 {
    let entry = entries[i]
    if matches_history_pattern(entry, pattern) && entry != pattern {
      return Some(i)
    }
    i = i - 1
  }
  None
}

///|
fn find_next_matching_index(
  entries : Array[String],
  pattern : String,
  start : Int,
) -> Int? {
  let mut i = start
  while i < entries.length() {
    let entry = entries[i]
    if matches_history_pattern(entry, pattern) && entry != pattern {
      return Some(i)
    }
    i = i + 1
  }
  None
}

///|
fn matches_history_pattern(entry : String, pattern : String) -> Bool {
  if pattern.length() == 0 {
    return true
  }

  if entry.has_prefix(pattern) {
    return true
  }

  let entry_lower = entry.to_lower()
  let pattern_lower = pattern.to_lower()
  entry_lower.has_prefix(pattern_lower) ||
  entry.contains(pattern) ||
  entry_lower.contains(pattern_lower)
}

///|
async fn HistoryLog::append_persistent_history_entry(
  self : HistoryLog,
  text : String,
) -> Unit {
  let filepath = match self.config.persistent_history_filepath {
    Some(filepath) => filepath
    None => return
  }

  let entry = "\{text |> @utf8.encode |> @base64.encode}\n"
  let file = @fs.open(
    filepath,
    mode=WriteOnly,
    sync=Data,
    append=true,
    create=0o644,
  ) catch {
    _ => return
  }
  defer file.close()
  file.write(entry) catch {
    _ => ()
  }
}

///|
pub async fn HistoryLog::save_persistent_history_full(
  self : HistoryLog,
) -> Unit {
  let filepath = match self.config.persistent_history_filepath {
    Some(filepath) => filepath
    None => return
  }

  let lines = StringBuilder::new()
  for entry in self.entries {
    let encoded = entry |> @utf8.encode |> @base64.encode
    lines..write_string(encoded).write_char('\n')
  }

  @fs.write_file(filepath, lines.to_string(), create=0o644) catch {
    _ => ()
  }
}