/*
Copyright 2023-2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>

This file is part of FitnessTrax.

FitnessTrax is free software: you can redistribute it and/or modify it under the terms of the GNU
General Public License as published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.

FitnessTrax is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.

You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>.
*/

use crate::types::{
    DistanceFormatter, DurationFormatter, FormatOption, ParseError, TimeFormatter, WeightFormatter,
};
use gtk::prelude::*;
use std::{cell::RefCell, rc::Rc};

pub type Parser<T> = dyn Fn(&str) -> Result<T, ParseError>;
pub type OnUpdate<T> = dyn Fn(Option<T>);

// TextEntry is not a proper widget because I was never able to figure out how to do a type parameterization on a GTK widget.
#[derive(Clone)]
pub struct TextEntry<T: Clone + std::fmt::Debug> {
    value: Rc<RefCell<Option<T>>>,

    widget: gtk::Entry,
    renderer: Rc<dyn Fn(&T) -> String>,
    parser: Rc<Parser<T>>,
    on_update: Rc<OnUpdate<T>>,
}

impl<T: Clone + std::fmt::Debug> std::fmt::Debug for TextEntry<T> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
        write!(
            f,
            "{{ value: {:?}, widget: {:?} }}",
            self.value, self.widget
        )
    }
}

// I do not understand why the data should be 'static.
impl<T: Clone + std::fmt::Debug + 'static> TextEntry<T> {
    fn from_builder(builder: TextEntryBuilder<T>) -> TextEntry<T> {
        let widget = gtk::Entry::builder()
            .placeholder_text(builder.placeholder)
            .build();
        if let Some(ref v) = builder.value {
            widget.set_text(&(builder.renderer)(v))
        }

        let s = Self {
            value: Rc::new(RefCell::new(builder.value)),
            widget,
            renderer: Rc::new(builder.renderer),
            parser: Rc::new(builder.parser),
            on_update: Rc::new(builder.on_update),
        };

        s.widget.buffer().connect_text_notify({
            let s = s.clone();
            move |buffer| s.handle_text_change(buffer)
        });

        if let Some(length) = builder.length {
            s.widget.set_max_length(length.try_into().unwrap());
        }

        // let classes: Vec<&str> = builder.css_classes.iter(|v| v.as_ref()).collect();
        let classes: Vec<&str> = builder.css_classes.iter().map(AsRef::as_ref).collect();
        s.widget.set_css_classes(&classes);

        s
    }

    pub fn builder() -> TextEntryBuilder<T> {
        TextEntryBuilder::default()
    }

    pub fn set_value(&self, val: Option<T>) {
        if let Some(ref v) = val {
            self.widget.set_text(&(self.renderer)(v));
        }
    }

    fn handle_text_change(&self, buffer: &gtk::EntryBuffer) {
        if buffer.text().is_empty() {
            *self.value.borrow_mut() = None;
            self.widget.remove_css_class("error");
            (self.on_update)(None);
            return;
        }
        match (self.parser)(buffer.text().as_str()) {
            Ok(v) => {
                *self.value.borrow_mut() = Some(v.clone());
                self.widget.remove_css_class("error");
                (self.on_update)(Some(v));
            }
            // need to change the border to provide a visual indicator of an error
            Err(_) => {
                self.widget.add_css_class("error");
            }
        }
    }

    pub fn widget(&self) -> gtk::Widget {
        self.widget.clone().upcast::<gtk::Widget>()
    }

    #[cfg(test)]
    fn has_parse_error(&self) -> bool {
        self.widget.has_css_class("error")
    }
}

pub struct TextEntryBuilder<T: Clone + std::fmt::Debug + 'static> {
    placeholder: String,
    value: Option<T>,
    length: Option<usize>,
    css_classes: Vec<String>,
    renderer: Box<dyn Fn(&T) -> String>,
    parser: Box<Parser<T>>,
    on_update: Box<OnUpdate<T>>,
}

impl<T: Clone + std::fmt::Debug + 'static> Default for TextEntryBuilder<T> {
    fn default() -> TextEntryBuilder<T> {
        TextEntryBuilder {
            placeholder: "".to_owned(),
            value: None,
            length: None,
            css_classes: vec![],
            renderer: Box::new(|_| "".to_owned()),
            parser: Box::new(|_| Err(ParseError)),
            on_update: Box::new(|_| {}),
        }
    }
}

impl<T: Clone + std::fmt::Debug + 'static> TextEntryBuilder<T> {
    pub fn build(self) -> TextEntry<T> {
        TextEntry::from_builder(self)
    }

    pub fn with_placeholder(self, placeholder: String) -> Self {
        Self {
            placeholder,
            ..self
        }
    }

    pub fn with_value(self, value: T) -> Self {
        Self {
            value: Some(value),
            ..self
        }
    }

    pub fn with_length(self, length: usize) -> Self {
        Self {
            length: Some(length),
            ..self
        }
    }

    pub fn with_css_classes(self, classes: Vec<String>) -> Self {
        Self {
            css_classes: classes,
            ..self
        }
    }

    pub fn with_renderer(self, renderer: impl Fn(&T) -> String + 'static) -> Self {
        Self {
            renderer: Box::new(renderer),
            ..self
        }
    }

    pub fn with_parser(self, parser: impl Fn(&str) -> Result<T, ParseError> + 'static) -> Self {
        Self {
            parser: Box::new(parser),
            ..self
        }
    }

    pub fn with_on_update(self, on_update: impl Fn(Option<T>) + 'static) -> Self {
        Self {
            on_update: Box::new(on_update),
            ..self
        }
    }
}

pub fn time_field<OnUpdate>(
    value: Option<TimeFormatter>,
    on_update: OnUpdate,
) -> TextEntry<TimeFormatter>
where
    OnUpdate: Fn(Option<TimeFormatter>) + 'static,
{
    let text_entry = TextEntry::builder()
        .with_placeholder("HH:MM".to_owned())
        .with_renderer(|val: &TimeFormatter| val.format(FormatOption::Abbreviated))
        .with_parser(TimeFormatter::parse)
        .with_on_update(on_update);

    if let Some(time) = value {
        text_entry.with_value(time)
    } else {
        text_entry
    }
    .build()
}

pub fn distance_field<OnUpdate>(
    value: Option<DistanceFormatter>,
    on_update: OnUpdate,
) -> TextEntry<DistanceFormatter>
where
    OnUpdate: Fn(Option<DistanceFormatter>) + 'static,
{
    let text_entry = TextEntry::builder()
        .with_placeholder("0 km".to_owned())
        .with_renderer(|val: &DistanceFormatter| val.format(FormatOption::Abbreviated))
        .with_parser(DistanceFormatter::parse)
        .with_on_update(on_update);

    if let Some(distance) = value {
        text_entry.with_value(distance)
    } else {
        text_entry
    }
    .build()
}

pub fn duration_field<OnUpdate>(
    value: Option<DurationFormatter>,
    on_update: OnUpdate,
) -> TextEntry<DurationFormatter>
where
    OnUpdate: Fn(Option<DurationFormatter>) + 'static,
{
    let text_entry = TextEntry::builder()
        .with_placeholder("0 m".to_owned())
        .with_renderer(|val: &DurationFormatter| val.format(FormatOption::Abbreviated))
        .with_parser(DurationFormatter::parse)
        .with_on_update(on_update);

    if let Some(duration) = value {
        text_entry.with_value(duration)
    } else {
        text_entry
    }
    .build()
}
pub fn weight_field<OnUpdate>(
    weight: Option<WeightFormatter>,
    on_update: OnUpdate,
) -> TextEntry<WeightFormatter>
where
    OnUpdate: Fn(Option<WeightFormatter>) + 'static,
{
    let text_entry = TextEntry::builder()
        .with_placeholder("0 kg".to_owned())
        .with_renderer(|val: &WeightFormatter| val.format(FormatOption::Abbreviated))
        .with_parser(WeightFormatter::parse)
        .with_on_update(on_update);
    if let Some(weight) = weight {
        text_entry.with_value(weight)
    } else {
        text_entry
    }
    .build()
}

pub fn i32_field_builder() -> TextEntryBuilder<i32>
{
    TextEntry::builder()
        .with_placeholder("0".to_owned())
        .with_renderer(|val| format!("{}", val))
        .with_parser(|v| v.parse::<i32>().map_err(|_| ParseError))
}

pub fn month_field_builder() -> TextEntryBuilder<u32>
{
    TextEntry::builder()
        .with_placeholder("0".to_owned())
        .with_renderer(|val| format!("{}", val))
        .with_parser(|v| {
            let val = v.parse::<u32>().map_err(|_| ParseError)?;
            if val == 0 || val > 12 {
                return Err(ParseError);
            }
            Ok(val)
        })
}

#[cfg(test)]
mod test {
    use super::*;
    use crate::gtk_init::gtk_init;

    fn setup_u32_entry() -> (Rc<RefCell<Option<u32>>>, TextEntry<u32>) {
        let current_value = Rc::new(RefCell::new(None));

        let entry = TextEntry::builder()
            .with_placeholder("step count".to_owned())
            .with_renderer(|steps| format!("{}", steps))
            .with_parser(|v| v.parse::<u32>().map_err(|_| ParseError))
            .with_on_update({
                let current_value = current_value.clone();
                move |v| *current_value.borrow_mut() = v
            })
            .build();

        (current_value, entry)
    }

    #[test]
    fn it_responds_to_field_changes() {
        gtk_init();
        let (current_value, entry) = setup_u32_entry();
        let buffer = entry.widget.buffer();

        buffer.set_text("1");
        assert_eq!(*current_value.borrow(), Some(1));

        buffer.set_text("15");
        assert_eq!(*current_value.borrow(), Some(15));
    }

    #[test]
    fn it_preserves_last_value_in_parse_error() {
        crate::gtk_init::gtk_init();
        let (current_value, entry) = setup_u32_entry();
        let buffer = entry.widget.buffer();

        buffer.set_text("1");
        assert_eq!(*current_value.borrow(), Some(1));

        buffer.set_text("a5");
        assert_eq!(*current_value.borrow(), Some(1));
        assert!(entry.has_parse_error());
    }

    #[test]
    fn it_update_on_empty_strings() {
        gtk_init();
        let (current_value, entry) = setup_u32_entry();
        let buffer = entry.widget.buffer();

        buffer.set_text("1");
        assert_eq!(*current_value.borrow(), Some(1));
        buffer.set_text("");
        assert_eq!(*current_value.borrow(), None);

        buffer.set_text("1");
        assert_eq!(*current_value.borrow(), Some(1));
        buffer.set_text("1a");
        assert_eq!(*current_value.borrow(), Some(1));
        assert!(entry.has_parse_error());

        buffer.set_text("");
        assert_eq!(*current_value.borrow(), None);
        assert!(!entry.has_parse_error());
    }
}