use std::{
    fs,
    path::PathBuf,
    sync::{Arc, mpsc},
    thread::JoinHandle,
    time::{Duration, Instant},
};

use crate::{
    command_palette::{self, CommandPalette},
    file_editor::{FileEditor, SaveStatus},
    folder::Folder,
    preferences::Preferences,
    text_styles::{H1, H1_MONO, H2, H2_MONO, H3, H3_MONO, H4, H4_MONO, H5, H5_MONO, H6, H6_MONO},
    util::{GuiSender, log_error},
};
use egui::{
    Align, Button, Context, FontData, FontDefinitions, FontFamily, FontId, Frame, Image, Key,
    Modifiers, PointerButton, RichText, ScrollArea, Theme, Widget, include_image,
};
use eyre::eyre;

#[derive(serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct App {
    preferences: Preferences,

    #[serde(skip)]
    actions_tx: mpsc::Sender<Action>,
    #[serde(skip)]
    actions_rx: mpsc::Receiver<Action>,
    #[serde(skip)]
    jobs: Jobs,

    tabs: Vec<(TabId, Tab)>,

    show_folders: bool,
    folders: Vec<Folder>,

    open_tab_index: Option<usize>,

    next_tab_id: TabId,

    #[serde(skip)]
    command_palette: Option<CommandPalette>,
}

pub struct Jobs {
    handles: Vec<JoinHandle<()>>,
    actions_tx: mpsc::Sender<Action>,
}

impl Jobs {
    pub fn start(&mut self, ctx: &Context, job: impl FnOnce() -> Option<Action> + Send + 'static) {
        let ctx = ctx.clone();
        let actions_tx = self.actions_tx.clone();
        self.handles.push(std::thread::spawn(move || {
            // start rendering the spinner thingy
            ctx.request_repaint();

            let start = Instant::now();

            if let Some(action) = job() {
                let _ = actions_tx.send(action);
                ctx.request_repaint();
            };

            // Make sure that task takes at least 250ms to run, so that the spinner won't blink
            let sleep_for = Duration::from_millis(250).saturating_sub(start.elapsed());
            std::thread::sleep(sleep_for);
        }));
    }
}

#[derive(serde::Deserialize, serde::Serialize)]
enum Tab {
    File(FileEditor),
}

impl Tab {
    pub fn title(&self) -> &str {
        match self {
            Tab::File(file_editor) => file_editor.title(),
        }
    }

    pub fn notice_symbol(&self) -> Option<&'static str> {
        match self {
            Tab::File(file_editor) => match file_editor.save_status() {
                SaveStatus::Synced => None,
                SaveStatus::NoFile => Some("?"),
                SaveStatus::FileOutdated => Some("*"),
                SaveStatus::BufferOutdated => Some("!"),
                SaveStatus::Desynced => Some("!!"),
            },
        }
    }

    pub fn save(&mut self, ctx: &Context, jobs: &mut Jobs) {
        match self {
            Tab::File(file_editor) => file_editor.save(ctx, jobs),
        }
    }
}

pub type TabId = usize;

pub enum Action {
    OpenFile(FileEditor),
    OpenFolder(Folder),
    MoveFile(TabId, PathBuf),
    CloseTab(TabId),
    // TODO
    //ShowError {
    //    error: RichText
    //},
}

impl Default for App {
    fn default() -> Self {
        let (actions_tx, actions_rx) = mpsc::channel();
        Self {
            preferences: Preferences::default(),
            actions_tx: actions_tx.clone(/* this is silly, i know */),
            actions_rx,
            jobs: Jobs {
                handles: Default::default(),
                actions_tx,
            },
            tabs: vec![(1, Tab::File(FileEditor::new("note.md")))],
            open_tab_index: None,
            next_tab_id: 2,
            show_folders: false,
            folders: vec![],
            command_palette: None,
        }
    }
}

impl App {
    /// Called once before the first frame.
    pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
        let mut fonts = FontDefinitions::empty();
        fonts.font_data = [
            //(
            //    "IosevkaAile-Thin",
            //    include_bytes!("../fonts/IosevkaAile-Thin.ttc").as_slice(),
            //),
            //(
            //    "IosevkaAile-ExtraLight",
            //    include_bytes!("../fonts/IosevkaAile-ExtraLight.ttc").as_slice(),
            //),
            //(
            //    "IosevkaAile-Light",
            //    include_bytes!("../fonts/IosevkaAile-Light.ttc").as_slice(),
            //),
            (
                "IosevkaAile-Regular",
                include_bytes!("../fonts/IosevkaAile-Regular.ttc").as_slice(),
            ),
            //(
            //    "IosevkaAile-Medium",
            //    include_bytes!("../fonts/IosevkaAile-Medium.ttc").as_slice(),
            //),
            //(
            //    "IosevkaAile-Bold",
            //    include_bytes!("../fonts/IosevkaAile-Bold.ttc").as_slice(),
            //),
            (
                "Iosevka-Thin",
                include_bytes!("../fonts/Iosevka-Thin.ttc").as_slice(),
            ),
            //(
            //    "Iosevka-ExtraLight",
            //    include_bytes!("../fonts/Iosevka-ExtraLight.ttc").as_slice(),
            //),
            //(
            //    "Iosevka-Light",
            //    include_bytes!("../fonts/Iosevka-Light.ttc").as_slice(),
            //),
            //(
            //    "Iosevka-Medium",
            //    include_bytes!("../fonts/Iosevka-Medium.ttc").as_slice(),
            //),
            //(
            //    "Iosevka-Regular",
            //    include_bytes!("../fonts/Iosevka-Regular.ttc").as_slice(),
            //),
            //(
            //    "Iosevka-Heavy",
            //    include_bytes!("../fonts/Iosevka-Heavy.ttc").as_slice(),
            //),
        ]
        .into_iter()
        .map(|(name, data)| (name.to_string(), Arc::new(FontData::from_static(data))))
        .collect();

        fonts
            .families
            .insert(FontFamily::Proportional, vec!["IosevkaAile-Regular".into()]);

        fonts
            .families
            .insert(FontFamily::Monospace, vec!["Iosevka-Thin".into()]);

        cc.egui_ctx.set_fonts(fonts);

        // markdown font styles
        for theme in [Theme::Dark, Theme::Light] {
            cc.egui_ctx.style_mut_of(theme, |style| {
                for (name, size, family) in [
                    (H1, 28.0, FontFamily::Proportional),
                    (H2, 26.0, FontFamily::Proportional),
                    (H3, 24.0, FontFamily::Proportional),
                    (H4, 22.0, FontFamily::Proportional),
                    (H5, 20.0, FontFamily::Proportional),
                    (H6, 18.0, FontFamily::Proportional),
                    (H1_MONO, 28.0, FontFamily::Monospace),
                    (H2_MONO, 26.0, FontFamily::Monospace),
                    (H3_MONO, 24.0, FontFamily::Monospace),
                    (H4_MONO, 22.0, FontFamily::Monospace),
                    (H5_MONO, 20.0, FontFamily::Monospace),
                    (H6_MONO, 18.0, FontFamily::Monospace),
                ] {
                    let name = egui::TextStyle::Name(name.into());
                    style.text_styles.insert(name, FontId { size, family });
                }
            });
        }

        // enable features on egui_extras to add more image types
        egui_extras::install_image_loaders(&cc.egui_ctx);

        if let Some(storage) = cc.storage {
            return eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default();
        }

        Default::default()
    }

    fn actions_tx(&self, ctx: &Context) -> GuiSender<Action> {
        GuiSender::new(self.actions_tx.clone(), ctx)
    }

    fn handle_action(&mut self, action: Action) {
        match action {
            Action::OpenFolder(new_folder) => {
                if let Some(folder) = self
                    .folders
                    .iter_mut()
                    .find(|folder| folder.path() == new_folder.path())
                {
                    *folder = new_folder;
                } else {
                    self.folders.push(new_folder);
                    self.folders.sort_by(|a, b| a.name().cmp(b.name()));
                }
            }
            Action::OpenFile(file_editor) => {
                self.open_tab(Tab::File(file_editor));
            }
            Action::MoveFile(tab_id, new_path) => {
                let tab = self.tabs.iter_mut().find(|(id, _)| &tab_id == id);
                let Some((_, tab)) = tab else { return };
                let Tab::File(editor) = tab; // else { return };

                editor.set_path(new_path);
            }
            Action::CloseTab(id) => {
                // TODO: check if the file is dirty and ask to save it first?
                self.tabs.retain(|(tab_id, _)| &id != tab_id);
            }
        }
    }
}

impl eframe::App for App {
    fn save(&mut self, storage: &mut dyn eframe::Storage) {
        eframe::set_value(storage, eframe::APP_KEY, self);
    }

    fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) {
        self.preferences.apply(ctx);

        self.jobs.handles.retain(|job| !job.is_finished());

        while let Ok(action) = self.actions_rx.try_recv() {
            self.handle_action(action);
        }

        if self.open_tab_index >= Some(self.tabs.len()) {
            self.open_tab_index = Some(self.tabs.len().saturating_sub(1));
        }

        ctx.input_mut(|input| {
            if input.consume_key(Modifiers::CTRL, Key::S) {
                self.save_active_tab(ctx);
            }

            if input.consume_key(Modifiers::CTRL, Key::K) {
                self.command_palette = Some(Default::default());
            }
        });

        egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
            egui::MenuBar::new().ui(ui, |ui| {
                // NOTE: no File->Quit on web pages!
                ui.menu_button("Menu ⚙", |ui| {
                    ui.label(RichText::new("Action").weak());

                    if ui.button("New File").clicked() {
                        let file = FileEditor::new("note.md");
                        self.open_tab(Tab::File(file));
                    }

                    #[cfg(not(target_arch = "wasm32"))]
                    if ui.button("Open File").clicked() {
                        self.jobs.start(ui.ctx(), move || {
                            let file_path = rfd::FileDialog::new().pick_file()?;

                            log_error(eyre!("Failed to open file {file_path:?}"), || {
                                FileEditor::open_file(file_path)
                            })
                            .map(Action::OpenFile)
                        });
                    }

                    if ui.button("Open Folder").clicked() {
                        self.jobs.start(ui.ctx(), move || {
                            let path = rfd::FileDialog::new().pick_folder()?;
                            let name = path.file_name()?.to_string_lossy().to_string();
                            let folder = Folder::NotLoaded { name, path };
                            Some(Action::OpenFolder(folder))
                        });
                    }

                    if ui
                        .add_enabled(self.open_tab_index.is_some(), Button::new("Close File"))
                        .clicked()
                    {
                        if let Some(i) = self.open_tab_index.take() {
                            self.tabs.remove(i);
                        }
                    }

                    let can_save_file = self
                        .open_tab_index
                        .and_then(|i| self.tabs.get(i))
                        .map(|(id, tab)| match tab {
                            Tab::File(file_editor) => (*id, file_editor),
                        })
                        .and_then(|(_, file_editor)| file_editor.path().zip(Some(file_editor)))
                        .is_some();

                    if ui.add_enabled(can_save_file, Button::new("Save")).clicked() {
                        self.save_active_tab(ui.ctx());
                    }

                    let open_file = self.open_tab_index.and_then(|i| self.tabs.get(i)).map(
                        |(id, tab)| match tab {
                            Tab::File(file_editor) => (*id, file_editor),
                        },
                    );

                    #[cfg(not(target_arch = "wasm32"))]
                    if ui
                        .add_enabled(open_file.is_some(), Button::new("Save As"))
                        .clicked()
                    {
                        let (tab_id, editor) =
                            open_file.expect("We checked that open_file is_some");
                        let text = editor.to_string();
                        self.jobs.start(ui.ctx(), move || {
                            let file_path = rfd::FileDialog::new().save_file()?;

                            fs::write(&file_path, text.as_bytes())
                                .inspect_err(|e| log::error!("{e}"))
                                .ok()?;

                            Some(Action::MoveFile(tab_id, file_path))
                        });
                    }

                    ui.add_space(8.0);

                    self.preferences.show(ui);

                    ui.add_space(8.0);

                    if cfg!(not(target_arch = "wasm32")) && ui.button("Quit").clicked() {
                        ctx.send_viewport_cmd(egui::ViewportCommand::Close);
                    }
                });

                ui.add_space(8.0);

                let image = Image::new(include_image!("../assets/collapse-icon.svg"));
                let image = image.tint(ui.style().visuals.text_color());
                if Button::image(image).ui(ui).clicked() {
                    self.show_folders = !self.show_folders;
                }

                if !self.jobs.handles.is_empty() {
                    ui.add_space(8.0);
                    ui.spinner();
                }

                ui.add_space(16.0);

                ScrollArea::horizontal().show(ui, |ui| {
                    for (i, (tab_id, tab)) in self.tabs.iter().enumerate() {
                        let selected = self.open_tab_index == Some(i);
                        let mut button = Button::new(tab.title()).selected(selected);

                        if let Some(symbol) = tab.notice_symbol() {
                            button = button.right_text(RichText::new(symbol).strong())
                        }

                        let response = ui.add(button);

                        if response.clicked() {
                            self.open_tab_index = Some(i);
                        } else if response.clicked_by(PointerButton::Secondary) {
                            let _ = self.actions_tx(ui.ctx()).send(Action::CloseTab(*tab_id));
                        }
                    }
                });
            });
        });

        egui::SidePanel::left("file browser")
            .resizable(true)
            .show_animated(ctx, self.show_folders, |ui| {
                if ui.button("refresh").clicked() {
                    for folder in &mut self.folders {
                        folder.unload();
                    }
                }

                ScrollArea::both().auto_shrink(false).show(ui, |ui| {
                    self.folders.retain_mut(|folder| {
                        let response = folder.show(ui);

                        if let Some(file_path) = response.open_file {
                            let file_path = file_path.to_owned();
                            self.jobs.start(ui.ctx(), move || {
                                log_error(eyre!("Failed to open file {file_path:?}"), || {
                                    FileEditor::open_file(file_path)
                                })
                                .map(Action::OpenFile)
                            });
                        }

                        // delete on right-click
                        !response.clicked_by(PointerButton::Secondary)
                    });
                });
            });

        egui::CentralPanel::default()
            .frame(Frame {
                fill: ctx.style().visuals.window_fill(),
                ..Frame::central_panel(&ctx.style())
            })
            .show(ctx, |ui| {
                if let Some(Tab::File(file_editor)) = self
                    .open_tab_index
                    .and_then(|i| self.tabs.get_mut(i))
                    .map(|(_tab_id, tab)| tab)
                {
                    file_editor.show(ui, &self.preferences);
                }

                ui.with_layout(egui::Layout::bottom_up(Align::LEFT), |ui| {
                    egui::warn_if_debug_build(ui);
                });
            });

        if let Some(command_palette) = &mut self.command_palette {
            match command_palette.show(ctx, &mut self.jobs, &mut self.folders) {
                command_palette::Response::None => {}
                command_palette::Response::Close => {
                    self.command_palette = None;
                    ctx.request_repaint();
                }
            };
        }
    }
}

impl App {
    /// Figure out where we should insert the next tab.
    fn insert_tab_at(&self) -> usize {
        match self.open_tab_index {
            None => 0,
            Some(i) => (i + 1).min(self.tabs.len()),
        }
    }

    /// Open a [Tab].
    fn open_tab(&mut self, tab: Tab) {
        let i = self.insert_tab_at();
        let id = self.next_tab_id;
        self.next_tab_id += 1;
        self.tabs.insert(i, (id, tab));
        self.open_tab_index = Some(i);
    }

    fn save_active_tab(&mut self, ctx: &Context) {
        let open_tab = self
            .open_tab_index
            .and_then(|i| self.tabs.get_mut(i))
            .map(|(_id, tab)| tab);

        if let Some(open_tab) = open_tab {
            open_tab.save(ctx, &mut self.jobs);
        }
    }
}
