use std::{
    cmp::Ordering,
    fmt::{self, Display},
    ops::{Div as _, Sub as _},
    path::{Path, PathBuf},
    str::FromStr,
};

use egui::{
    Align, Button, DragAndDrop, Frame, Layout, ScrollArea, Ui, UiBuilder, Vec2, Widget as _, vec2,
};

use crate::{
    custom_code_block::{MdItem, iter_lines_and_code_blocks},
    handwriting::{self, Handwriting, HandwritingStyle},
    preferences::Preferences,
    text_editor::MdTextEdit,
};

#[derive(serde::Deserialize, serde::Serialize)]
pub struct FileEditor {
    title: String,
    pub path: Option<PathBuf>,
    pub buffer: Vec<BufferItem>,

    /// Whether the file has been edited since it was laste saved to disk.
    pub is_dirty: bool,
}

#[derive(serde::Deserialize, serde::Serialize)]
pub enum BufferItem {
    Text(MdTextEdit),
    Handwriting(Box<Handwriting>),
}

impl FileEditor {
    pub fn new(title: impl Into<String>) -> Self {
        let buffer = vec![BufferItem::Text(MdTextEdit::new())];
        Self {
            title: title.into(),
            path: None,
            buffer,
            is_dirty: false,
        }
    }

    pub fn from_file(file_path: PathBuf, contents: &str) -> Self {
        let file_title = file_path
            .file_name()
            .map(|name| name.to_string_lossy().to_string())
            .unwrap_or_else(|| String::from("untitled.md"));
        Self {
            title: file_title,
            path: Some(file_path),
            ..FileEditor::from(contents)
        }
    }

    pub fn title(&self) -> &str {
        &self.title
    }

    pub fn path(&self) -> Option<&Path> {
        self.path.as_deref()
    }

    pub fn show(&mut self, ui: &mut Ui, preferences: &Preferences) {
        ui.vertical_centered_justified(|ui| {
            ui.heading(&self.title);

            const MAX_NOTE_WIDTH: f32 = 600.0;

            ui.horizontal(|ui| {
                ui.label("new");
                if ui.button("text").clicked() {
                    self.is_dirty = true;
                    self.buffer.push(BufferItem::Text(Default::default()));
                }
                if ui.button("writing").clicked() {
                    self.is_dirty = true;
                    self.buffer
                        .push(BufferItem::Handwriting(Default::default()));
                }
            });

            ScrollArea::vertical().show(ui, |ui| {
                ui.horizontal(|ui| {
                    let side_padding = ui.available_width().sub(MAX_NOTE_WIDTH).max(0.0).div(2.0);
                    ui.add_space(side_padding);
                    ui.vertical(|ui| {
                        ui.set_max_width(MAX_NOTE_WIDTH);
                        self.show_contents(ui, preferences);
                    });
                    ui.add_space(side_padding);
                });
            });
        });
    }

    fn show_contents(&mut self, ui: &mut Ui, preferences: &Preferences) {
        if self.buffer.is_empty() {
            self.buffer.push(BufferItem::Text(Default::default()));
        }

        struct DraggingItem {
            index: usize,
        }

        let mut drop_from_to: Option<(usize, usize)> = None;
        let is_dragging = DragAndDrop::has_payload_of_type::<DraggingItem>(ui.ctx());
        let drag_zone_height = 10.0;

        // Iterate over buffer items using `retain` so that we can handle deletions
        let mut i = 0usize..;
        let len = self.buffer.len();
        self.buffer.retain_mut(|item| {
            let i = i.next().unwrap();
            let is_first = i == 0;
            let is_last = i == len - 1;

            let mut retain = true;

            if is_dragging {
                let (_, drop) = ui.dnd_drop_zone::<DraggingItem, _>(Frame::NONE, |ui| {
                    ui.set_min_size(vec2(ui.available_width(), drag_zone_height));
                });
                if let Some(drop) = drop {
                    drop_from_to = Some((drop.index, i));
                }
            } else {
                // the dnd_drop_zone adds 3pts work of extra space
                ui.add_space(drag_zone_height + 3.0);
            }

            ui.horizontal(|ui| {
                // We don't know how tall the buffer item will be, so we'll reserve
                // some horizontal space here and come back to drawing the dragger
                // later.
                let (dragger_id, mut dragger_rect) = ui.allocate_space(Vec2::new(20.0, 1.0));

                // Leave some space at the end for the delete button..
                let w = ui.available_width();
                let item_size = Vec2::new(w - 20.0, 0.0);

                let item_response = ui.allocate_ui(item_size, |ui| match item {
                    BufferItem::Text(text_edit) => {
                        if text_edit.ui(ui).changed {
                            self.is_dirty = true;
                        }
                    }
                    BufferItem::Handwriting(handwriting) => {
                        let style = HandwritingStyle {
                            animate: preferences.animations,
                            hide_cursor: preferences.hide_handwriting_cursor,
                            ..HandwritingStyle::from_theme(ui.ctx().theme())
                        };
                        if handwriting.ui(&style, ui).changed {
                            self.is_dirty = true;
                        }
                    }
                });

                // Delete-button
                if ui.button("x").clicked() {
                    retain = false;
                    ui.ctx().request_repaint();
                }

                // Draw the dragger using the height from the buffer item
                dragger_rect.set_height(item_response.response.rect.height());

                // Controls for moving the buffer item
                ui.scope_builder(
                    UiBuilder::new()
                        .max_rect(dragger_rect)
                        .layout(Layout::top_down(Align::Center)),
                    |ui| {
                        let up_button_response = ui.add_enabled(!is_first, Button::new("⇡"));
                        if up_button_response.clicked() {
                            drop_from_to = Some((i, i - 1));
                        }

                        ui.dnd_drag_source(dragger_id, DraggingItem { index: i }, |ui| {
                            Button::new("≡")
                                .min_size(
                                    // Use all available height, save for the height taken up by
                                    // the up/down buttons + padding. Assume down-button is the
                                    // equally tall as the up-button.
                                    dragger_rect.size()
                                        - Vec2::Y * (up_button_response.rect.height() * 2.0 + 4.0),
                                )
                                .ui(ui);
                        });

                        if ui.add_enabled(!is_last, Button::new("⇣")).clicked() {
                            drop_from_to = Some((i, i + 2));
                        }
                    },
                );
            });

            retain
        });

        if is_dragging {
            let (_, drop) = ui.dnd_drop_zone::<DraggingItem, _>(Frame::NONE, |ui| {
                ui.set_min_size(vec2(ui.available_width(), drag_zone_height));
            });
            if let Some(drop) = drop {
                drop_from_to = Some((drop.index, self.buffer.len()));
            }
        } else {
            // the dnd_drop_zone adds 3.0pts work of extra space
            ui.add_space(drag_zone_height + 3.0);
        }

        // Handle drag-and-dropping buffer items
        // TODO: make sure nothing was removed from self.buffer this frame
        if let Some((from, to)) = drop_from_to {
            if from < self.buffer.len() {
                match from.cmp(&to) {
                    Ordering::Greater => {
                        let item = self.buffer.remove(from);
                        self.buffer.insert(to, item);
                        self.is_dirty = true;
                    }
                    Ordering::Less => {
                        let item = self.buffer.remove(from);
                        self.buffer.insert(to - 1, item);
                        self.is_dirty = true;
                    }
                    Ordering::Equal => {}
                }
            }
        }
    }

    pub fn set_path(&mut self, new_path: PathBuf) {
        let Some(title) = new_path.file_name() else {
            log::error!("No filename in path {new_path:?}");
            return;
        };
        self.title = title.to_string_lossy().to_string();
        self.path = Some(new_path);
    }
}

impl Display for BufferItem {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            BufferItem::Text(md_text_edit) => Display::fmt(md_text_edit, f),
            BufferItem::Handwriting(handwriting) => Display::fmt(handwriting, f),
        }
    }
}

impl Display for FileEditor {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let mut first = true;
        for item in &self.buffer {
            if !first {
                writeln!(f)?;
            }

            first = false;

            write!(f, "{item}")?;
        }
        Ok(())
    }
}

impl From<&str> for FileEditor {
    fn from(s: &str) -> Self {
        let mut editor = FileEditor::new("note.md");
        let buffer = &mut editor.buffer;

        let push_text = |buffer: &mut Vec<BufferItem>, text| match buffer.last_mut() {
            Some(BufferItem::Text(text_edit)) => text_edit.text.push_str(text),
            _ => {
                let mut text_edit = MdTextEdit::new();
                text_edit.text.push_str(text);
                buffer.push(BufferItem::Text(text_edit));
            }
        };

        for item in iter_lines_and_code_blocks(s) {
            match item {
                MdItem::Line(line) => push_text(buffer, line),
                MdItem::CodeBlock { key, content, span } => match key {
                    handwriting::CODE_BLOCK_KEY => match Handwriting::from_str(span) {
                        Ok(handwriting) => {
                            if let Some(BufferItem::Text(text_edit)) = buffer.last_mut() {
                                if text_edit.text.ends_with('\n') {
                                    text_edit.text.pop();
                                    if text_edit.text.is_empty() {
                                        buffer.pop();
                                    }
                                }
                            };
                            buffer.push(BufferItem::Handwriting(Box::new(handwriting)))
                        }
                        Err(e) => {
                            log::error!("Failed to decode handwriting {content:?}: {e}");
                            push_text(buffer, span);
                        }
                    },
                    _ => push_text(buffer, span),
                },
            }
        }

        editor
    }
}

#[cfg(test)]
mod test {

    use crate::file_editor::BufferItem;

    use super::FileEditor;

    #[test]
    fn from_str_and_back_1() {
        let markdown = r#"
# Hello world!

This is some text.
Here's some handwriting:
```handwriting
DgB0UUlNeFFJTX9RUE2pUYZNDlIATotSjk4AUwxPaFODT89T608UVBtQL1QqUDtULlBDVDFQSVQuUA==
```

And here's some more text :D
```
with a regular code-block!
```"#;

        println!("{markdown}");
        println!();
        println!();
        println!("{markdown:?}");
        println!();
        println!();

        let file_editor = FileEditor::from(markdown);

        for item in &file_editor.buffer {
            match item {
                BufferItem::Text(md_text_edit) => {
                    println!("{:?}", md_text_edit.text);
                }
                BufferItem::Handwriting(_) => {
                    println!("<handwriting>");
                }
            }
        }

        println!();
        println!();

        let serialized = file_editor.to_string();
        assert_eq!(
            markdown, serialized,
            "FileEditor should preserve formatting"
        );
    }
}
