use std::fmt::{Debug, Display, Formatter};
use std::sync::atomic::{AtomicUsize, Ordering};

use crate::app::context_clues::ContextClue;
use crate::app::ServarrConfig;
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use radarr_models::RadarrSerdeable;
use regex::Regex;
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use serde_json::Number;
use servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
use sonarr_models::SonarrSerdeable;

pub mod radarr_models;
pub mod servarr_data;
pub mod servarr_models;
pub mod sonarr_models;
pub mod stateful_list;
pub mod stateful_table;
pub mod stateful_tree;

#[cfg(test)]
#[path = "model_tests.rs"]
mod model_tests;

// Allowing dead code for now since we'll eventually be implementing additional Servarr support, and we'll need it then
#[allow(dead_code)]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Route {
  Radarr(ActiveRadarrBlock, Option<ActiveRadarrBlock>),
  Sonarr(ActiveSonarrBlock, Option<ActiveSonarrBlock>),
  Readarr,
  Lidarr,
  Whisparr,
  Bazarr,
  Prowlarr,
  Tautulli,
  Keybindings,
}

#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
#[serde(untagged)]
pub enum Serdeable {
  Radarr(RadarrSerdeable),
  Sonarr(SonarrSerdeable),
}

pub trait Scrollable {
  fn scroll_down(&mut self);
  fn scroll_up(&mut self);
  fn scroll_to_top(&mut self);
  fn scroll_to_bottom(&mut self);
}

pub trait Paginated {
  fn page_down(&mut self);
  fn page_up(&mut self);
}

#[derive(Default)]
pub struct ScrollableText {
  pub items: Vec<String>,
  pub offset: u16,
}

impl ScrollableText {
  pub fn with_string(item: String) -> ScrollableText {
    let items: Vec<&str> = item.split('\n').collect();
    let items: Vec<String> = items.iter().map(|it| it.to_string()).collect();
    ScrollableText { items, offset: 0 }
  }

  pub fn get_text(&self) -> String {
    self.items.join("\n")
  }

  pub fn is_empty(&self) -> bool {
    self.items.is_empty()
  }
}

impl Scrollable for ScrollableText {
  fn scroll_down(&mut self) {
    if self.items.is_empty() {
      return;
    }

    if self.offset < (self.items.len() - 1) as u16 {
      self.offset += 1;
    }
  }

  fn scroll_up(&mut self) {
    if self.items.is_empty() {
      return;
    }

    if self.offset > 0 {
      self.offset -= 1;
    }
  }

  fn scroll_to_top(&mut self) {
    self.offset = 0;
  }

  fn scroll_to_bottom(&mut self) {
    self.offset = (self.items.len() - 1) as u16;
  }
}

#[derive(Default, Deserialize, Debug)]
#[serde(from = "String")]
pub struct HorizontallyScrollableText {
  pub text: String,
  pub offset: AtomicUsize,
}

impl Clone for HorizontallyScrollableText {
  fn clone(&self) -> Self {
    HorizontallyScrollableText {
      text: self.text.clone(),
      offset: AtomicUsize::new(self.offset.load(Ordering::SeqCst)),
    }
  }
}

impl PartialEq for HorizontallyScrollableText {
  fn eq(&self, other: &Self) -> bool {
    self.text == other.text
  }
}

impl Eq for HorizontallyScrollableText {}

impl From<String> for HorizontallyScrollableText {
  fn from(text: String) -> HorizontallyScrollableText {
    HorizontallyScrollableText::new(text)
  }
}

impl From<&String> for HorizontallyScrollableText {
  fn from(text: &String) -> HorizontallyScrollableText {
    HorizontallyScrollableText::new(text.clone())
  }
}

impl From<&str> for HorizontallyScrollableText {
  fn from(text: &str) -> HorizontallyScrollableText {
    HorizontallyScrollableText::new(text.to_owned())
  }
}

impl Display for HorizontallyScrollableText {
  fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
    if self.offset.load(Ordering::SeqCst) == 0 {
      write!(f, "{}", self.text)
    } else {
      let text_vec = self.text.chars().collect::<Vec<_>>();
      write!(
        f,
        "{}",
        text_vec[self.offset.load(Ordering::SeqCst)..]
          .iter()
          .cloned()
          .collect::<String>()
      )
    }
  }
}

impl Serialize for HorizontallyScrollableText {
  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
  where
    S: Serializer,
  {
    serializer.serialize_str(&self.text)
  }
}

impl HorizontallyScrollableText {
  pub fn new(text: String) -> HorizontallyScrollableText {
    HorizontallyScrollableText {
      text,
      offset: AtomicUsize::new(0),
    }
  }

  pub fn len(&self) -> usize {
    self.text.chars().count()
  }

  pub fn scroll_left(&self) {
    if self.offset.load(Ordering::SeqCst) < self.len() {
      self.offset.fetch_add(1, Ordering::SeqCst);
    }
  }

  pub fn scroll_right(&self) {
    if self.offset.load(Ordering::SeqCst) > 0 {
      self.offset.fetch_sub(1, Ordering::SeqCst);
    }
  }

  pub fn scroll_home(&self) {
    self.offset.store(self.len(), Ordering::SeqCst);
  }

  pub fn reset_offset(&self) {
    self.offset.store(0, Ordering::SeqCst);
  }

  pub fn scroll_left_or_reset(&self, width: usize, is_current_selection: bool, can_scroll: bool) {
    if can_scroll && is_current_selection && self.len() >= width {
      if self.offset.load(Ordering::SeqCst) < self.len() {
        self.scroll_left();
      } else {
        self.reset_offset();
      }
    } else if self.offset.load(Ordering::SeqCst) != 0 && !is_current_selection {
      self.reset_offset();
    }
  }

  pub fn pop(&mut self) {
    if self.offset.load(Ordering::SeqCst) < self.len() {
      let (index, _) = self
        .text
        .chars()
        .enumerate()
        .nth(self.len() - self.offset.load(Ordering::SeqCst) - 1)
        .unwrap();
      self.text = self
        .text
        .chars()
        .enumerate()
        .filter(|(idx, _)| *idx != index)
        .map(|tuple| tuple.1)
        .collect();
    }
  }

  pub fn push(&mut self, character: char) {
    if self.text.is_empty() {
      self.text.push(character);
    } else {
      let index = self.len() - self.offset.load(Ordering::SeqCst);

      if index == self.len() {
        self.text.push(character);
      } else {
        let mut new_text = String::new();
        self
          .text
          .chars()
          .collect::<Vec<char>>()
          .iter()
          .enumerate()
          .for_each(|(idx, &c)| {
            if idx == index {
              new_text.push(character);
            }

            new_text.push(c);
          });

        self.text = new_text;
      }
    }
  }
}

#[derive(Clone, PartialEq, Eq, Debug)]
pub struct TabRoute {
  pub title: String,
  pub route: Route,
  pub contextual_help: Option<&'static [ContextClue]>,
  pub config: Option<ServarrConfig>,
}

pub struct TabState {
  pub tabs: Vec<TabRoute>,
  pub index: usize,
}

impl TabState {
  pub fn new(tabs: Vec<TabRoute>) -> TabState {
    TabState { tabs, index: 0 }
  }

  // Allowing this code for now since we'll eventually be implementing additional Servarr support, and we'll need it then
  #[allow(dead_code)]
  pub fn set_index(&mut self, index: usize) -> &TabRoute {
    self.index = index;
    &self.tabs[self.index]
  }

  pub fn get_active_route(&self) -> Route {
    self.tabs[self.index].route
  }

  pub fn get_active_config(&self) -> &Option<ServarrConfig> {
    if self.tabs.is_empty() {
      return &None;
    }

    &self.tabs[self.index].config
  }

  pub fn select_tab_by_title(&mut self, name: &str) -> bool {
    if !self.tabs.is_empty() {
      let mut found = false;
      self.tabs.iter().enumerate().for_each(|(idx, tab)| {
        if tab.title == name {
          self.index = idx;
          found = true;
        }
      });

      return found;
    }

    false
  }

  pub fn select_tab_by_config(&mut self, config: &ServarrConfig) -> bool {
    if !self.tabs.is_empty() {
      let mut found = false;
      self.tabs.iter().enumerate().for_each(|(idx, tab)| {
        if tab.config == Some(config.clone()) {
          self.index = idx;
          found = true;
        }
      });

      return found;
    }

    false
  }

  pub fn get_active_route_contextual_help(&self) -> Option<&'static [ContextClue]> {
    self.tabs[self.index].contextual_help
  }

  pub fn next(&mut self) {
    self.index = (self.index + 1) % self.tabs.len();
  }

  pub fn previous(&mut self) {
    if self.index > 0 {
      self.index -= 1;
    } else {
      self.index = self.tabs.len() - 1;
    }
  }
}

#[derive(Default, Eq, PartialEq, Debug)]
pub struct BlockSelectionState<'a, T>
where
  T: Sized + Clone + Copy + Default,
{
  pub blocks: &'a [&'a [T]],
  pub x: usize,
  pub y: usize,
}

impl<'a, T> BlockSelectionState<'a, T>
where
  T: Sized + Clone + Copy + Default,
{
  pub fn new(blocks: &'a [&'a [T]]) -> BlockSelectionState<'a, T> {
    BlockSelectionState { blocks, x: 0, y: 0 }
  }

  pub fn get_active_block(&self) -> T {
    self.blocks[self.y][self.x]
  }

  pub fn left(&mut self) {
    if self.x > 0 {
      self.x -= 1;
    } else {
      self.x = self.blocks[0].len() - 1;
    }
  }

  pub fn right(&mut self) {
    self.x = (self.x + 1) % self.blocks[0].len();
  }

  pub fn up(&mut self) {
    if self.y > 0 {
      self.y -= 1;
    } else {
      self.y = self.blocks.len() - 1;
    }
  }

  pub fn down(&mut self) {
    self.y = (self.y + 1) % self.blocks.len();
  }
}

#[cfg(test)]
impl<T> BlockSelectionState<'_, T>
where
  T: Sized + Clone + Copy + Default,
{
  pub fn set_index(&mut self, x: usize, y: usize) {
    self.x = x;
    self.y = y;
  }
}

pub fn from_i64<'de, D>(deserializer: D) -> Result<i64, D::Error>
where
  D: Deserializer<'de>,
{
  let num: Number = Deserialize::deserialize(deserializer)?;
  num.as_i64().ok_or(de::Error::custom(format!(
    "Unable to convert Number to i64: {num:?}"
  )))
}

pub fn from_f64<'de, D>(deserializer: D) -> Result<f64, D::Error>
where
  D: Deserializer<'de>,
{
  let num: Number = Deserialize::deserialize(deserializer)?;
  num.as_f64().ok_or(de::Error::custom(format!(
    "Unable to convert Number to f64: {num:?}"
  )))
}

pub fn strip_non_search_characters(input: &str) -> String {
  Regex::new(r"[^a-zA-Z0-9.,/'\-:\s]")
    .unwrap()
    .replace_all(&input.to_lowercase(), "")
    .to_string()
}

#[macro_export]
macro_rules! serde_enum_from {
    ($enum_name:ident { $($variant:ident($ty:ty),)* }) => {
      #[derive(Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, Debug)]
      #[serde(untagged)]
      #[allow(clippy::large_enum_variant)]
      pub enum $enum_name {
          $(
              $variant($ty),
          )*
      }

      impl From<()> for $enum_name {
        fn from(_: ()) -> Self {
          $enum_name::Value(serde_json::json!({}))
        }
      }

        $(
            impl From<$ty> for $enum_name {
                fn from(value: $ty) -> Self {
                    $enum_name::$variant(value)
                }
            }
        )*
    }
}
