use crate::macros::is_script_tamil_ext;
use crate::script_data::{List, ScriptData};
use crate::utils::binary_search::binary_search_lower_with_index;
use crate::utils::strings::char_substring;
// pub fn krama_index_of_text()

impl ScriptData {
  #[allow(dead_code)]
  pub fn krama_text_or_null(&self, idx: i16) -> Option<&str> {
    if idx < 0 {
      return None;
    }
    match &self.get_common_attr().krama_text_arr.get(idx as usize) {
      Some(item) => Some(&item.0),
      None => None,
    }
  }

  pub fn krama_text_or_empty(&self, idx: i16) -> &str {
    if idx < 0 {
      return &"";
    }
    match &self.get_common_attr().krama_text_arr.get(idx as usize) {
      Some(item) => &item.0,
      None => &"",
    }
  }

  pub fn krama_index_of_text(&self, text: &str) -> Option<usize> {
    binary_search_lower_with_index(
      &self.get_common_attr().krama_text_arr,
      &self.get_common_attr().krama_text_arr_index,
      &text,
      |arr, i| &arr[i].0,
      |a, b| a.cmp(b),
    )
  }
}

/// custom struct to construct output string
pub struct ResultStringBuilder {
  result: Vec<String>,
}

impl ResultStringBuilder {
  pub fn new() -> ResultStringBuilder {
    ResultStringBuilder { result: Vec::new() }
  }
  pub fn emit(&mut self, text: String) {
    if text.is_empty() {
      return;
    }
    self.result.push(text);
  }
  pub fn emit_pieces(&mut self, pieces: &[String]) {
    for p in pieces {
      self.emit(p.to_owned());
    }
  }
  pub fn last_piece(&self) -> Option<String> {
    let last = self.result.last().cloned();
    return last;
  }
  pub fn last_char(&self) -> Option<char> {
    match self.last_piece() {
      Some(v) => v.chars().last(),
      None => None,
    }
  }
  pub fn pop_last_char(&mut self) -> Option<char> {
    let result_len = self.result.len();
    let lp = self.last_piece();
    match lp {
      Some(mut lp) => {
        if lp.len() == 0 {
          // handling the panic case
          return None;
        }
        let ch = lp.pop();
        // ^ slice(0, -1) done with pop inself
        if let Some(ch) = ch {
          self.result[result_len - 1] = lp;
          return Some(ch);
        }
        return None;
      }
      None => None,
    }
  }
  pub fn rewrite_tail_pieces(&mut self, count: usize, new_pieces: &[String]) {
    let len = self.result.len();
    let start = len.saturating_sub(count); // -count but safe
    self.result.truncate(start);
    for p in new_pieces {
      if !p.is_empty() {
        self.result.push(p.to_owned());
      }
    }
  }

  pub fn with_last_char_moved_after(&mut self, before_pieces: &[String], after_pieces: &[String]) {
    let ch = self.pop_last_char();
    match ch {
      None => {
        self.emit_pieces(before_pieces);
        self.emit_pieces(after_pieces);
      }
      Some(c) => {
        self.emit_pieces(before_pieces);
        self.emit(c.to_string());
        self.emit_pieces(after_pieces);
      }
    }
  }

  /// index can be -ve
  pub fn peek_at(&self, index: isize) -> Option<InputCursor> {
    let len = self.result.len() as isize;
    if len == 0 {
      return None;
    }

    let mut i = index;
    if i < 0 {
      // handle negative indexes
      i = len + i;
    } else if i < 0 || i >= len {
      return None;
    }

    let item = self.result.get(i as usize);
    match item {
      Some(item) => {
        return Some(InputCursor {
          ch: item.to_owned(),
        });
      }
      None => None,
    }
  }

  pub fn rewrite_at(&mut self, index: isize, new_piece: String) {
    let len = self.result.len() as isize;
    if len == 0 {
      return;
    }

    let mut i = index;
    if i < 0 {
      i = len + i;
    } else if i < 0 || i >= len {
      return;
    }

    self.result[i as usize] = new_piece;
  }

  pub fn to_string(&self) -> String {
    self.result.concat()
  }
}

type PrevContextItem = (Option<String>, Option<List>);

pub struct PrevContextBuilder {
  arr: Vec<PrevContextItem>,
  max_len: usize,
}

impl PrevContextBuilder {
  pub fn new(max_len: usize) -> PrevContextBuilder {
    PrevContextBuilder {
      arr: Vec::new(),
      max_len,
    }
  }

  pub fn clear(&mut self) {
    self.arr.clear();
  }

  pub fn length(&self) -> usize {
    self.arr.len()
  }

  /// resolves negative index for the `arr`
  fn resolve_arr_index(&self, i: isize) -> Option<usize> {
    if self.arr.is_empty() {
      return None;
    }
    let len = self.arr.len() as isize;
    let mut idx = i;
    if idx < 0 {
      idx = len + idx;
    }
    if idx < 0 || idx >= len {
      None
    } else {
      Some(idx as usize)
    }
  }

  pub fn at(&self, i: isize) -> Option<&PrevContextItem> {
    match self.resolve_arr_index(i) {
      None => None,
      Some(idx) => self.arr.get(idx),
    }
  }
  #[allow(dead_code)]
  pub fn last(&self) -> Option<&PrevContextItem> {
    self.arr.last()
  }
  #[allow(dead_code)]
  pub fn last_text(&self) -> Option<&str> {
    self.last().and_then(|(text_opt, _)| text_opt.as_deref())
  }
  #[allow(dead_code)]
  pub fn last_type(&self) -> Option<&List> {
    self.last().and_then(|(_, list_opt)| list_opt.as_ref())
  }

  pub fn type_at(&self, i: isize) -> Option<&List> {
    self.at(i).and_then(|(_, list_opt)| list_opt.as_ref())
  }

  /// Text at a given index (supports -ve indices).
  pub fn text_at(&self, i: isize) -> Option<&str> {
    self.at(i).and_then(|(text_opt, _)| text_opt.as_deref())
  }

  /// Check if the last context item has the given type.
  #[allow(dead_code)]
  pub fn is_last_type(&self, t: &List) -> bool {
    self.last_type() == Some(t)
  }

  /// Push a new context item, enforcing `max_len` and skipping empty/None text.
  pub fn push(&mut self, item: PrevContextItem) {
    let text_ok = item.0.as_ref().map(|s| !s.is_empty()).unwrap_or(false);
    if !text_ok {
      return;
    }
    self.arr.push(item);
    if self.arr.len() > self.max_len {
      // Remove oldest, similar to shift
      self.arr.remove(0);
    }
  }
}

pub struct InputTextCursor<'a> {
  text: &'a str,
  pos: usize,
}

pub struct InputCursor {
  pub ch: String,
  // no cp(codepoint) or width needed here in rust
}

impl<'a> InputTextCursor<'a> {
  pub fn new(text: &'a str) -> InputTextCursor<'a> {
    InputTextCursor { text, pos: 0 }
  }

  pub fn pos(&self) -> usize {
    self.pos
  }

  pub fn peek_at(&self, index_units: usize) -> Option<InputCursor> {
    self
      .text
      .chars()
      .nth(index_units)
      .and_then(|ch| Some(InputCursor { ch: ch.to_string() }))
  }

  pub fn peek(&self) -> Option<InputCursor> {
    self.peek_at(self.pos)
  }

  #[allow(dead_code)]
  pub fn peek_at_offset_units(&self, offset_units: usize) -> Option<InputCursor> {
    self.peek_at(self.pos + offset_units)
  }

  /// units here is for char (and not bytes)
  /// in TS version `units` is byte index for utf-16 encoding used by js
  /// rust stores as utf-8 but has methods to access nth char or substring (char_substring)
  pub fn advance(&mut self, units: usize) {
    self.pos += units;
  }

  pub fn slice(&self, start: usize, end: usize) -> Option<String> {
    let char_count = self.text.chars().count();
    if start > end || end > char_count {
      return None;
    }
    Some(char_substring(&self.text, start, end).to_owned())
  }
}

/// Result type for `match_prev_krama_sequence`
pub struct MatchPrevKramaSequenceResult {
  pub matched: bool,
  pub matched_len: usize,
}

impl ScriptData {
  pub fn match_prev_krama_sequence<F>(
    &self,
    peek_at: F,
    anchor_index: isize,
    prev: &[usize], // indices(number) array
  ) -> MatchPrevKramaSequenceResult
  where
    F: Fn(isize) -> Option<InputCursor>,
  {
    for i in 0..prev.len() {
      let expected_krama_index = prev[prev.len() - 1 - i];
      let info = match peek_at(anchor_index - i as isize) {
        Some(v) => v,
        None => {
          return MatchPrevKramaSequenceResult {
            matched: false,
            matched_len: 0,
          };
        }
      };

      let got_krama_index = self.krama_index_of_text(&info.ch);
      // a method check for in cases where a nullable is there
      // to check something like !ch || ch.name
      // in rust we cannot check for both nullability(single possible) and non-nullable
      // attribute at the same time. In rust a variable wont change on the same expression
      match got_krama_index {
        Some(got) if got == expected_krama_index => {}
        _ => {
          return MatchPrevKramaSequenceResult {
            matched: false,
            matched_len: 0,
          };
        }
      }
    }

    MatchPrevKramaSequenceResult {
      matched: true,
      matched_len: prev.len(),
    }
  }

  pub fn replace_with_pieces(&self, replace_with: &[i16]) -> Vec<String> {
    replace_with
      .iter()
      .map(|&k| self.krama_text_or_empty(k))
      .filter(|s| !s.is_empty())
      .map(|s| s.to_owned())
      .collect()
  }
}
pub const TAMIL_EXTENDED_SUPERSCRIPT_NUMBERS: [char; 3] = ['²', '³', '⁴'];

pub fn is_ta_ext_superscript_tail(ch: Option<char>) -> bool {
  match ch {
    Some(c) => TAMIL_EXTENDED_SUPERSCRIPT_NUMBERS.contains(&c),
    None => false,
  }
}

pub const VEDIC_SVARAS: [char; 4] = ['॒', '॑', '᳚', '᳛'];

pub fn is_vedic_svara_tail(ch: Option<char>) -> bool {
  match ch {
    Some(c) => VEDIC_SVARAS.contains(&c),
    None => false,
  }
}

impl ResultStringBuilder {
  pub fn emit_pieces_with_reorder(
    &mut self,
    pieces: &[String],
    halant: &str,
    should_reorder: bool,
  ) {
    if pieces.is_empty() {
      return;
    }
    if !should_reorder {
      self.emit_pieces(pieces);
      return;
    }

    let first_piece = pieces.first().map(|s| s.as_str()).unwrap_or("");
    if first_piece.starts_with(halant) {
      let rest_first = first_piece.strip_prefix(halant).unwrap_or("");
      let mut after_pieces: Vec<String> = Vec::new();
      if !rest_first.is_empty() {
        after_pieces.push(rest_first.to_owned());
      }
      for p in pieces.into_iter().skip(1) {
        after_pieces.push(p.to_owned());
      }
      self.with_last_char_moved_after(&[halant.to_owned()], &after_pieces);
    } else {
      self.with_last_char_moved_after(pieces, &[]);
    }
  }
}

const VEDIC_SVARAS_TYPING_SYMBOLS: [&str; 4] = ["_", "'''", "''", "'"];
const VEDIC_SVARAS_NORMAL_SYMBOLS: [&str; 4] = ["↓", "↑↑↑", "↑↑", "↑"];

pub fn apply_typing_input_aliases(mut text: String, to_script_name: &str) -> String {
  if text.is_empty() {
    return text;
  }

  // ITRANS-style shortcut: x -> kSh (क्ष)
  if text.contains('x') {
    text = text.replace("x", "kSh");
  }

  if is_script_tamil_ext!(to_script_name) {
    for i in 0..VEDIC_SVARAS_TYPING_SYMBOLS.len() {
      let symbol = VEDIC_SVARAS_TYPING_SYMBOLS[i];
      if text.contains(symbol) {
        text = text.replace(symbol, VEDIC_SVARAS_NORMAL_SYMBOLS[i]);
      }
    }
  }

  text
}
