use std::{
    fs::read_dir,
    mem,
    ops::Deref,
    path::{Path, PathBuf},
    sync::mpsc,
    thread,
};

use egui::{Button, Color32, Response, Stroke, TextWrapMode, Ui, Vec2, Widget};
use eyre::{Context, OptionExt, eyre};
use serde::{Deserialize, Serialize};

pub enum Folder {
    NotLoaded {
        name: String,
        path: PathBuf,
    },
    Loading {
        name: String,
        path: PathBuf,
        recv: mpsc::Receiver<LoadedFolder>,
    },
    Loaded(LoadedFolder),
}

pub struct LoadedFolder {
    pub name: String,
    pub path: PathBuf,
    pub child_folders: Vec<Folder>,
    pub child_files: Vec<File>,
}

pub struct File {
    pub name: String,
    pub path: PathBuf,
}

pub struct FolderResponse<'a> {
    inner: Response,
    pub open_file: Option<&'a Path>,
}

impl Deref for FolderResponse<'_> {
    type Target = Response;

    fn deref(&self) -> &Self::Target {
        &self.inner
    }
}

impl LoadedFolder {
    pub fn show<'a>(&'a mut self, ui: &mut Ui) -> FolderResponse<'a> {
        let mut open_file = None;

        let draw_highlight = |ui: &mut Ui, rect| {
            ui.painter().rect(
                rect,
                2.0,
                if ui.visuals().dark_mode {
                    Color32::from_white_alpha(16)
                } else {
                    Color32::from_black_alpha(16)
                },
                Stroke::NONE,
                egui::StrokeKind::Outside,
            );
        };

        let inner = ui
            .collapsing(&self.name, |ui| {
                for folder in &mut self.child_folders {
                    open_file = open_file.or(folder.show(ui).open_file);
                }

                let w = ui.available_width();

                let mut first = true;

                for file in &mut self.child_files {
                    if !first {
                        ui.add_space(2.0);
                    }
                    first = false;

                    let button = Button::new(&file.name)
                        .min_size(Vec2::new(w, 0.0))
                        .wrap_mode(TextWrapMode::Truncate)
                        .frame(false)
                        .corner_radius(0.0)
                        .ui(ui);

                    if button.hovered() {
                        draw_highlight(ui, button.rect);
                    }

                    if button.clicked() {
                        open_file = Some(file.path.as_path())
                    };
                }
            })
            .header_response;

        if inner.hovered() {
            draw_highlight(ui, inner.rect);
        }

        FolderResponse { inner, open_file }
    }

    fn load(path: PathBuf) -> eyre::Result<Self> {
        let name = path
            .file_name()
            .ok_or_eyre("Path is missing a file-name")?
            .to_string_lossy()
            .to_string();

        let mut child_folders = vec![];
        let mut child_files = vec![];

        for entry in read_dir(&path).with_context(|| eyre!("Couldn't read dir {path:?}"))? {
            let entry = entry.with_context(|| eyre!("Couldn't read dir {path:?}"))?;
            let path = entry.path();
            let name = path
                .file_name()
                .ok_or_eyre("Path is missing a file-name")?
                .to_string_lossy()
                .to_string();

            let file_type = entry.file_type()?;

            if file_type.is_symlink() {
                log::error!("Symlinks not yet supported, skipping {path:?}");
                continue;
            } else if file_type.is_file() {
                if filter_file(&name) {
                    child_files.push(File { name, path });
                }
            } else if file_type.is_dir() {
                if filter_folder(&name) {
                    child_folders.push(Folder::NotLoaded { name, path });
                }
            }
        }

        child_folders.sort_by_key(|folder| folder.name().to_owned());
        child_files.sort_by_key(|file| (!file.name.ends_with(".md"), file.name.clone()));

        let folder = LoadedFolder {
            name,
            path,
            child_folders,
            child_files,
        };

        Ok(folder)
    }
}

impl Folder {
    pub fn load(&mut self, ui: &mut Ui) -> Option<&mut LoadedFolder> {
        if let Folder::NotLoaded { name, path } = self {
            let (tx, rx) = mpsc::channel();

            {
                let path = path.clone();
                let ctx = ui.ctx().clone();
                thread::spawn(move || match LoadedFolder::load(path) {
                    Err(e) => log::error!("Failed to load folder: {e}"),
                    Ok(folder) => {
                        let _ = tx.send(folder);
                        ctx.request_repaint();
                    }
                });
            }

            *self = Folder::Loading {
                name: mem::take(name),
                path: mem::take(path),
                recv: rx,
            };
        }

        if let Folder::Loading { recv, .. } = self {
            match recv.try_recv() {
                Ok(folder) => *self = Folder::Loaded(folder),
                Err(_) => return None,
            }
        }

        let Folder::Loaded(folder) = self else {
            unreachable!()
        };

        Some(folder)
    }

    pub fn show<'a>(&'a mut self, ui: &mut Ui) -> FolderResponse<'a> {
        self.load(ui);

        if let Folder::Loaded(folder) = self {
            return folder.show(ui);
        }

        FolderResponse {
            inner: ui.label(self.name()),
            open_file: None,
        }
    }

    pub fn path(&self) -> &Path {
        match self {
            Folder::NotLoaded { path, .. } => path,
            Folder::Loading { path, .. } => path,
            Folder::Loaded(folder) => &folder.path,
        }
    }

    pub fn name(&self) -> &str {
        match self {
            Folder::NotLoaded { name, .. } => name,
            Folder::Loading { name, .. } => name,
            Folder::Loaded(folder) => &folder.name,
        }
    }

    pub fn unload(&mut self) {
        let (name, path) = match self {
            Folder::NotLoaded { .. } => return,
            Folder::Loading { name, path, .. } => (name, path),
            Folder::Loaded(folder) => (&mut folder.name, &mut folder.path),
        };

        *self = Folder::NotLoaded {
            name: mem::take(name),
            path: mem::take(path),
        }
    }
}

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

impl<'de> Deserialize<'de> for Folder {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        use serde::de::Error;
        let path = PathBuf::deserialize(deserializer)?;
        let name = path
            .file_name()
            .ok_or(D::Error::custom("Path is missing a file-name"))?
            .to_string_lossy()
            .to_string();
        Ok(Folder::NotLoaded { name, path })
    }
}

fn filter_folder(_folder_name: &str) -> bool {
    true
}

fn filter_file(file_name: &str) -> bool {
    file_name != ".DS_Store"
}
