use crate::{
    MathLang, elements::{Image, Margins, Math}, widget_factory::LabelWithWidgets,
};
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::collections::HashSet;

#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
pub enum LabelElement {
    TagStart { pos: i32, text: String },
    TagStop { pos: i32, start: i32, text: String },
    WidgetPlaceHolder { pos: i32 },
}

impl PartialOrd for LabelElement {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for LabelElement {
    fn cmp(&self, other: &Self) -> Ordering {
        let by_pos = self.pos().cmp(&other.pos());
        if by_pos == Ordering::Equal {
            let by_text = self.text().cmp(other.text());
            if let LabelElement::TagStop {
                start: self_start, ..
            } = self
                && let LabelElement::TagStop {
                    start: other_start, ..
                } = other
            {
                let by_start = self_start.cmp(other_start).reverse();
                if by_start == Ordering::Equal {
                    by_text.reverse()
                } else {
                    by_start
                }
            } else if matches!(self, Self::TagStart { .. })
                && matches!(other, Self::TagStart { .. })
            {
                by_text
            } else if matches!(self, Self::TagStart { .. }) && matches!(other, Self::TagStop { .. })
            {
                Ordering::Greater
            } else if matches!(self, Self::TagStop { .. }) && matches!(other, Self::TagStart { .. })
            {
                Ordering::Less
            } else {
                by_text
            }
        } else {
            by_pos
        }
    }
}

impl LabelElement {
    pub fn pos(&self) -> i32 {
        match self {
            Self::TagStart { pos, text: _ } => *pos,
            Self::TagStop {
                pos,
                start: _,
                text: _,
            } => *pos,
            Self::WidgetPlaceHolder { pos } => *pos,
        }
    }

    pub fn text(&self) -> &str {
        match self {
            Self::TagStart { pos: _, text } => text,
            Self::TagStop {
                pos: _,
                start: _,
                text,
            } => text,
            Self::WidgetPlaceHolder { pos: _ } => LabelWithWidgets::PLACEHOLDER,
        }
    }

    pub fn from_text_tag(tags: &[TextTag]) -> Vec<LabelElement> {
        if tags.is_empty() {
            return Vec::new();
        }

        let len = tags.len();
        let mut res = Vec::with_capacity(2 * len);
        for tag in tags {
            if tag.start == tag.stop {
                continue;
            }
            if let Some(start_text) = tag.tag_type.start() {
                res.push(LabelElement::TagStart {
                    pos: tag.start,
                    text: start_text,
                });
            }

            if let Some(stop_text) = tag.tag_type.stop() {
                res.push(LabelElement::TagStop {
                    pos: tag.stop,
                    start: tag.start,
                    text: stop_text.to_string(),
                });
            }
        }
        res
    }

    pub fn from_inline_widgets(widgets: &[InlineWidget]) -> Vec<LabelElement> {
        if widgets.is_empty() {
            return Vec::new();
        }

        let len = widgets.len();
        let mut res = Vec::with_capacity(len);
        for widget in widgets {
            res.push(LabelElement::WidgetPlaceHolder { pos: widget.offset });
        }
        res
    }
}

#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Serialize)]
pub struct Label {
    #[serde(default)]
    #[serde(skip_serializing_if = "HashSet::is_empty")]
    pub ids: HashSet<String>,
    pub text: String,
    #[serde(default)]
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub styles: Vec<String>,
    #[serde(default)]
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub widgets: Vec<InlineWidget>,
    #[serde(default)]
    #[serde(skip_serializing_if = "Margins::is_default")]
    pub margins: Margins,
}

impl From<Paragraph> for Label {
    fn from(mut value: Paragraph) -> Self {
        value.escape_markup();
        
        let mut text = value.text;
        let mut styles = Vec::new();
        let mut margins = Margins::Default;
        let widgets = value.widgets;

        for tag in value.tags.iter() {
            let tag_margins = tag.tag_type.margin();
            if tag_margins > margins {
                margins = tag_margins;
            }

            if let Some(css_class) = tag.tag_type.to_css_class() {
                styles.push(css_class.to_string());
            }
        }

        let mut label_elements = LabelElement::from_text_tag(&value.tags);
        let mut widget_elements = LabelElement::from_inline_widgets(&widgets);
        label_elements.append(&mut widget_elements);
        label_elements.sort();

        for tag_element in label_elements.into_iter().rev() {
            let mut pos = tag_element.pos() as usize;

            if pos > 0 {
                let mut char_iter = text.char_indices();
                _ = char_iter.nth(pos - 1);
                pos = char_iter.offset();
            }

            let element_string = tag_element.text();
            text.insert_str(pos, element_string);
        }

        Label {
            ids: value.ids,
            text: text.trim_end().to_string(),
            styles,
            widgets,
            margins,
        }
    }
}

#[derive(Clone, Debug, Deserialize, Hash, PartialEq, Eq, Serialize)]
pub enum TagType {
    Bold,
    Italic,
    Underscore,
    Cite,
    Big,
    Small,
    Strikethrough,
    Subscript,
    Superscript,
    Code,
    Title1,
    Title2,
    Title3,
    Title4,
    Title5,
    Title6,
    Link { href: String },
}

impl TagType {
    pub fn to_css_class(&self) -> Option<&'static str> {
        match self {
            Self::Title1 => Some("title-1"),
            Self::Title2 => Some("title-2"),
            Self::Title3 => Some("title-3"),
            Self::Title4 => Some("title-4"),
            Self::Title5 => Some("title-5"),
            Self::Title6 => Some("title-6"),
            _ => None,
        }
    }

    pub fn margin(&self) -> Margins {
        match self {
            Self::Title1
            | Self::Title2
            | Self::Title3
            | Self::Title4
            | Self::Title5
            | Self::Title6 => Margins::Title,
            _ => Margins::Default,
        }
    }

    pub fn start(&self) -> Option<String> {
        match self {
            TagType::Bold => Some("<b>".to_string()),
            TagType::Italic => Some("<i>".to_string()),
            TagType::Underscore => Some("<u>".to_string()),
            TagType::Cite => Some("<tt>".to_string()),
            TagType::Big => Some("<big>".to_string()),
            TagType::Small => Some("<small>".to_string()),
            TagType::Strikethrough => Some("<s>".to_string()),
            TagType::Subscript => Some("<sub>".to_string()),
            TagType::Superscript => Some("<sup>".to_string()),
            TagType::Code => {
                Some("<span foreground=\"black\" background=\"#dbdbdb\"><tt>".to_string())
            }
            TagType::Title1 => None,
            TagType::Title2 => None,
            TagType::Title3 => None,
            TagType::Title4 => None,
            TagType::Title5 => None,
            TagType::Title6 => None,
            TagType::Link { href } => Some(format!(
                "<a href=\"{}\">",
                gtk4::glib::markup_escape_text(href.as_str())
            )),
        }
    }

    pub fn stop(&self) -> Option<&'static str> {
        match self {
            TagType::Bold => Some("</b>"),
            TagType::Italic => Some("</i>"),
            TagType::Underscore => Some("</u>"),
            TagType::Cite => Some("</tt>"),
            TagType::Big => Some("</big>"),
            TagType::Small => Some("</small>"),
            TagType::Strikethrough => Some("</s>"),
            TagType::Subscript => Some("</sub>"),
            TagType::Superscript => Some("</sup>"),
            TagType::Code => Some("</tt></span>"),
            TagType::Title1 => None,
            TagType::Title2 => None,
            TagType::Title3 => None,
            TagType::Title4 => None,
            TagType::Title5 => None,
            TagType::Title6 => None,
            TagType::Link { href: _ } => Some("</a>"),
        }
    }
}

#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
pub struct TextTag {
    pub start: i32,
    pub stop: i32,
    pub tag_type: TagType,
}

#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
pub struct InlineWidget {
    pub offset: i32,
    pub widget_type: InlineWidgetType,
}

#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
pub enum InlineWidgetType {
    Image(Image),
    Math(Math),
}

#[derive(Clone, Default, Debug, Deserialize, PartialEq, Eq, Serialize)]
pub struct Paragraph {
    #[serde(default)]
    #[serde(skip_serializing_if = "HashSet::is_empty")]
    pub ids: HashSet<String>,
    pub text: String,
    #[serde(default)]
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub tags: Vec<TextTag>,
    #[serde(default)]
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub widgets: Vec<InlineWidget>,
}

impl Paragraph {
    pub fn escape_markup(&mut self) {
        let char_len = self.text.chars().count() * 2;
        let mut i = 0;
        let mut result = String::with_capacity(char_len);
        let mut offsets = Vec::new();
        
        for c in self.text.chars() {
            match c {
                '&' => {
                    result.push('&');
                    result.push('a');
                    result.push('m');
                    result.push('p');
                    result.push(';');
                    offsets.push((i, 4));
                }
                '<' => {
                    result.push('&');
                    result.push('l');
                    result.push('t');
                    result.push(';');
                    offsets.push((i, 3));
                }
                '>' => {
                    result.push('&');
                    result.push('g');
                    result.push('t');
                    result.push(';');
                    offsets.push((i, 3));
                }
                '\'' => {
                    result.push('&');
                    result.push('a');
                    result.push('p');
                    result.push('o');
                    result.push('s');
                    result.push(';');
                    offsets.push((i, 5));
                }
                '"' => {
                    result.push('&');
                    result.push('q');
                    result.push('u');
                    result.push('o');
                    result.push('t');
                    result.push(';');
                    offsets.push((i, 5));
                }
                _ => {
                    result.push(c);
                }
            }

            i += 1;
        }

        for (start, offset) in offsets {
            self.offset(start, offset);
        }

        self.text = result;
    }

    fn offset(&mut self, start: i32, offset: i32) {
        for tag in self.tags.iter_mut() {
            if tag.start > start {
                tag.start += offset;
                tag.stop += offset;
            } else if tag.stop > start {
                tag.stop += offset;
            }
        }

        for widget in self.widgets.iter_mut() {
            if widget.offset > start {
                widget.offset += offset;
            }
        }
    }

    pub fn convert_inline_math(&mut self, inline_math_delimiter: &str) {
        let mut offset = 0;
        while let Some(new_offset) = self.find_inline_math(inline_math_delimiter, offset) {
            offset = new_offset;
        }
    }

    fn find_inline_math(&mut self, inline_math_delimiter: &str, offset: usize) -> Option<usize> {
        let start = self.find_start_at(offset, inline_math_delimiter)?;
        let end = self.find_start_at(start + 1, inline_math_delimiter)?;

        let delimiter_len = inline_math_delimiter.len();

        let expression = self.text[start + delimiter_len..end - delimiter_len + 1].to_string();

        let math = Math {
            lang: MathLang::TeX,
            expression,
        };
        self.widgets.push(InlineWidget {
            offset: self.text[..start].chars().count() as i32,
            widget_type: InlineWidgetType::Math(math),
        });

        let mut new_text = self.text[..start].to_string();
        new_text.push_str(&self.text[end + 1..]);
        self.text = new_text;

        Some(start + 1)
    }

    fn find_start_at(&self, at: usize, pat: &str) -> Option<usize> {
        self.text
            .get(at..)
            .and_then(|slice| slice.find(pat).map(|i| at + i))
    }
}

#[cfg(test)]
mod tests {
    use crate::{InlineWidgetType, Label, MathLang, Paragraph, TagType, TextTag, elements::Math};
    use std::collections::HashSet;

    #[test]
    fn parse_inline_tex() {
        let mut paragraph = Paragraph {
            ids: HashSet::new(),
            text: "text before $4*4=16$ text between $1+1=2$ text after".to_string(),
            tags: vec![],
            widgets: vec![],
        };

        paragraph.convert_inline_math("$");

        assert_eq!(paragraph.widgets.len(), 2);
        assert_eq!(paragraph.widgets.get(0).unwrap().offset, 12);
        assert_eq!(paragraph.widgets.get(1).unwrap().offset, 26);
        assert_eq!(
            paragraph.widgets.get(0).unwrap().widget_type,
            InlineWidgetType::Math(Math {
                lang: MathLang::TeX,
                expression: "4*4=16".into()
            })
        );
        assert_eq!(
            paragraph.widgets.get(1).unwrap().widget_type,
            InlineWidgetType::Math(Math {
                lang: MathLang::TeX,
                expression: "1+1=2".into()
            })
        );
    }

    #[test]
    fn zero_length_tag() {
        let paragraph = Paragraph {
            ids: HashSet::new(),
            text: "test".to_string(),
            tags: vec![TextTag {
                start: 4,
                stop: 4,
                tag_type: TagType::Small,
            }],
            widgets: vec![],
        };

        let label = Label::from(paragraph);

        assert_eq!(label.text, "test");
    }
}
