use std::{
    cmp::Ordering,
    fmt::{self, Display},
    fs::{self, File},
    io::{Read as _, Write},
    ops::{Div as _, Sub as _},
    path::{Path, PathBuf},
    str::FromStr,
    sync::mpsc,
};

use chrono::{DateTime, Local};
use egui::{
    Align, Button, Context, DragAndDrop, Frame, Layout, ScrollArea, Ui, UiBuilder, Vec2, vec2,
};
use eyre::{Context as _, eyre};
use notify::{EventKind, Watcher};

use crate::{
    app::Jobs,
    custom_code_block::{MdItem, iter_lines_and_code_blocks},
    handwriting::{self, Handwriting, HandwritingStyle},
    preferences::Preferences,
    text_editor::MdTextEdit,
    util::{file_mtime, log_error},
};

#[derive(serde::Deserialize, serde::Serialize)]
pub struct FileEditor {
    title: String,

    path: Option<PathBuf>,
    pub buffer: Vec<BufferItem>,

    pub file_mtime: Option<DateTime<Local>>,
    pub buffer_mtime: DateTime<Local>,

    // TODO: instantiate these on load
    #[serde(skip)]
    inner: Option<Inner>,

    /// The distance to scroll when paging up or down.
    ///
    /// This is calculated when the view is rendered.
    #[serde(skip)]
    scroll_delta: f32,

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

struct Inner {
    file_events: mpsc::Receiver<FileEvent>,
    file_events_tx: mpsc::Sender<FileEvent>,
    _file_watcher: notify::RecommendedWatcher,
}

#[derive(Debug)]
pub enum SaveStatus {
    /// The contents of the buffer is the same as on disk.
    Synced,

    /// The contents exits only in memory and has never been saved to disk.
    NoFile,

    /// The buffer has been edited but not saved to disk.
    FileOutdated,

    /// The contents on disk has changes that are newer than the buffer.
    BufferOutdated,

    /// The contents on disk and in the buffer has diverged.
    Desynced,
}

#[derive(Debug)]
enum FileEvent {
    NewFileMTime(DateTime<Local>),
    NewBufferMTime(DateTime<Local>),
}

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

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

    pub fn from_path(file_path: PathBuf, contents: &str, mtime: DateTime<Local>) -> 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),
            file_mtime: Some(mtime),
            buffer_mtime: mtime,
            ..FileEditor::from(contents)
        }
    }

    pub fn open_file(file_path: PathBuf) -> eyre::Result<Self> {
        let mut file =
            fs::File::open(&file_path).wrap_err_with(|| eyre!("Failed to open {file_path:?}"))?;

        let mtime = file_mtime(&file)?;

        let mut text = String::new();
        file.read_to_string(&mut text)
            .wrap_err_with(|| eyre!("Failed to read {file_path:?}"))?;

        Ok(FileEditor::from_path(file_path, &text, mtime))
    }

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

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

    pub fn save_status(&self) -> SaveStatus {
        let Some(file_mtime) = self.file_mtime else {
            return SaveStatus::NoFile;
        };

        let buffer_is_newer = self.buffer_mtime > file_mtime;
        let file_is_newer = self.buffer_mtime < file_mtime;

        if buffer_is_newer || (file_is_newer && self.is_dirty) {
            SaveStatus::Desynced
        } else if file_is_newer {
            SaveStatus::BufferOutdated
        } else if self.is_dirty {
            SaveStatus::FileOutdated
        } else {
            SaveStatus::Synced
        }
    }

    pub fn show(&mut self, ui: &mut Ui, preferences: &Preferences) {
        if let Some(path) = &self.path
            && self.inner.is_none()
        {
            self.inner = Some(spawn_file_watcher(path));
        }

        if let Some(inner) = &mut self.inner {
            while let Ok(event) = inner.file_events.try_recv() {
                match event {
                    FileEvent::NewFileMTime(mtime) => {
                        self.file_mtime = Some(mtime);
                    }
                    FileEvent::NewBufferMTime(mtime) => {
                        self.buffer_mtime = mtime;
                        self.file_mtime = Some(mtime);
                    }
                }
            }
        }

        ui.vertical_centered_justified(|ui| {
            ui.heading(&self.title);

            const MAX_NOTE_WIDTH: f32 = 600.0;

            // distance to scroll when paging up or down.
            let mut scroll_delta = 0.0;

            ui.input_mut(|input| {
                if input.consume_key(egui::Modifiers::NONE, egui::Key::PageUp) {
                    scroll_delta += self.scroll_delta;
                }
                if input.consume_key(egui::Modifiers::NONE, egui::Key::PageDown) {
                    scroll_delta -= self.scroll_delta;
                }
            });

            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()));
                }

                ui.add_space(16.0);

                ui.label("scroll");
                if ui.button("  up  ").clicked() {
                    scroll_delta += self.scroll_delta;
                }
                if ui.button("down").clicked() {
                    scroll_delta -= self.scroll_delta;
                }
            });

            let scroll_area = ScrollArea::vertical().show(ui, |ui| {
                if scroll_delta != 0.0 {
                    ui.scroll_with_delta(Vec2::new(0.0, scroll_delta));
                }

                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);
                });
            });

            self.scroll_delta = scroll_area.inner_rect.height() * 0.5;
        });
    }

    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;

            // Createa horizontal area to draw the buffer item. The three things drawn here are:
            // - The controls that exist at the left-size of the buffer item, i.e. "up"/"down".
            // - The buffer item.
            // - The controls that exist at the right-size of the buffer item, i.e. "delete".
            ui.horizontal(|ui| {
                // At this point, 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 controls later.
                let (_id, mut left_controls_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("⌫").clicked() {
                    retain = false;
                    ui.ctx().request_repaint();
                }

                left_controls_rect.set_height(item_response.response.rect.height());

                // Controls for moving the buffer item
                ui.scope_builder(
                    UiBuilder::new()
                        .max_rect(left_controls_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));
                        }

                        // Add some space so that the next button is drawn
                        // at the bottom of the buffer item.
                        ui.add_space(
                            left_controls_rect.height()
                                - (up_button_response.rect.height() * 2.0 + 4.0),
                        );

                        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);
    }

    pub fn set_dirty(&mut self, value: bool) {
        self.is_dirty = value;
    }

    pub fn save(&mut self, ctx: &Context, jobs: &mut Jobs) {
        let Some(file_path) = self.path.clone() else {
            log::info!("Can't save {}, no path set.", self.title);
            return;
        };

        self.is_dirty = false;
        let text = self.to_string();
        let inner = self
            .inner
            .get_or_insert_with(|| spawn_file_watcher(&file_path));
        let file_event_tx = inner.file_events_tx.clone();

        jobs.start(ctx, move || {
            log_error(eyre!("Failed to save file {file_path:?}"), || {
                let mut file = fs::File::create(file_path)?;
                file.write_all(text.as_bytes())?;
                let mtime = file_mtime(&file)?;

                let _ = file_event_tx.send(FileEvent::NewBufferMTime(mtime));

                Ok(())
            });

            None
        });
    }
}

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(Box::new(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
    }
}

fn spawn_file_watcher(p: &Path) -> Inner {
    let (tx, rx) = mpsc::channel();

    let path = p.to_owned();
    let events_tx = tx.clone();
    let mut watcher = notify::recommended_watcher(move |event: notify::Result<notify::Event>| {
        log_error(eyre!("watch {path:?} error"), || {
            match event?.kind {
                EventKind::Create(..) | EventKind::Modify(..) | EventKind::Remove(..) => {}

                EventKind::Access(..) | EventKind::Any | EventKind::Other => return Ok(()),
            }

            let file = File::open(&path)?;
            let mtime = file_mtime(&file)?;

            let _ = events_tx.send(FileEvent::NewFileMTime(mtime));

            Ok(())
        });
    })
    .unwrap();

    if let Err(e) = watcher.watch(p, notify::RecursiveMode::NonRecursive) {
        log::error!("Failed to watch {p:?}: {e}");
    };

    Inner {
        file_events: rx,
        file_events_tx: tx,
        _file_watcher: watcher,
    }
}

#[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"
        );
    }
}
