// Copyright (C) 2024 Melody Madeline Lyons
//
// This file is part of Luminol.
//
// Luminol is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Luminol is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Luminol.  If not, see <http://www.gnu.org/licenses/>.
//
//     Additional permission under GNU GPL version 3 section 7
//
// If you modify this Program, or any covered work, by linking or combining
// it with Steamworks API by Valve Corporation, containing parts covered by
// terms of the Steamworks API by Valve Corporation, the licensors of this
// Program grant you additional permission to convey the resulting work.

use crate::components::UiExt;
use luminol_core::prelude::*;

pub mod actor;
pub mod animation;
pub mod basic;
pub mod event;
pub mod hue;

#[derive(Default)]
enum Selected {
    #[default]
    None,
    Entry {
        path: camino::Utf8PathBuf,
        sprite: PreviewSprite,
    },
}

struct ButtonSprite {
    sprite: Sprite,
    sprite_size: egui::Vec2,
    viewport: Viewport,
}

struct PreviewSprite {
    sprite: Sprite,
    sprite_size: egui::Vec2,
    viewport: Viewport,
}

#[derive(PartialEq, PartialOrd, Eq, Ord, Clone)]
struct Entry {
    path: camino::Utf8PathBuf,
    invalid: bool,
}

impl ButtonSprite {
    pub fn ui(
        this: Option<&mut Self>,
        ui: &mut egui::Ui,
        update_state: &UpdateState<'_>,
        is_open: bool,
        desired_size: egui::Vec2,
    ) -> egui::Response {
        let (rect, response) = ui.allocate_at_least(desired_size, egui::Sense::click());

        let visuals = ui.style().interact_selectable(&response, is_open);
        let rect = rect.expand(visuals.expansion);
        ui.painter()
            .rect(rect, visuals.rounding, visuals.bg_fill, visuals.bg_stroke);

        if let Some(this) = this {
            let viewport_size = rect.size();
            let translation = (viewport_size - this.sprite_size) / 2.;
            this.viewport.set(
                &update_state.graphics.render_state,
                glam::vec2(viewport_size.x, viewport_size.y),
                glam::vec2(translation.x, translation.y),
                glam::Vec2::ONE,
            );
            let callback = luminol_egui_wgpu::Callback::new_paint_callback(
                rect,
                Painter::new(this.sprite.prepare(&update_state.graphics)),
            );
            ui.painter().add(callback);
        }

        response
    }
}

impl Entry {
    fn load(
        // FIXME error handling
        update_state: &UpdateState<'_>,
        directory: &camino::Utf8Path,
    ) -> Vec<Self> {
        let mut entries: Vec<_> = update_state
            .filesystem
            .read_dir(directory)
            .unwrap()
            .into_iter()
            .map(|m| Entry {
                path: m.path.file_name().unwrap_or_default().into(),
                invalid: false,
            })
            .collect();
        entries.sort_unstable_by(|a, b| {
            lexical_sort::natural_lexical_cmp(a.path.as_str(), b.path.as_str())
        });
        entries
    }

    fn filter(entries: &[Self], filter: &str) -> Vec<Entry> {
        let matcher = fuzzy_matcher::skim::SkimMatcherV2::default();
        entries
            .iter()
            .filter(|entry| matcher.fuzzy(entry.path.as_str(), filter, false).is_some())
            .cloned()
            .collect()
    }

    fn ui(
        entries: &mut [Self],
        directory: &camino::Utf8Path,
        update_state: &UpdateState<'_>,
        ui: &mut egui::Ui,
        rows: std::ops::Range<usize>,
        selected: &mut Selected,
        load_preview_sprite: impl Fn(&camino::Utf8Path) -> PreviewSprite,
    ) {
        let selected_name = match &selected {
            Selected::Entry { path, .. } => update_state
                .filesystem
                .desensitize(directory.join(path))
                .ok()
                .map(|path| camino::Utf8PathBuf::from(path.file_name().unwrap_or_default())),
            Selected::None => None,
        };
        for i in entries[rows.clone()].iter_mut().enumerate() {
            let (i, Self { path, invalid }) = i;
            let checked = selected_name.as_deref() == Some(path);
            let mut text = egui::RichText::new(path.file_name().unwrap_or_default());
            if *invalid {
                text = text.color(egui::Color32::LIGHT_RED);
            }
            let faint = (i + rows.start) % 2 == 0;
            ui.with_stripe(faint, |ui| {
                let res = ui.add_enabled(!*invalid, egui::SelectableLabel::new(checked, text));

                if res.clicked() {
                    *selected = Selected::Entry {
                        sprite: load_preview_sprite(path.file_name().unwrap_or_default().into()),
                        path: path.file_stem().unwrap_or_default().into(),
                    };
                }
            });
        }
    }

    fn find_matching_entry(
        entries: &[Self],
        directory: &camino::Utf8Path,
        update_state: &UpdateState<'_>,
        selected: &Selected,
    ) -> Option<usize> {
        let selected_name = match &selected {
            Selected::Entry { path, .. } => update_state
                .filesystem
                .desensitize(directory.join(path))
                .ok()
                .map(|path| camino::Utf8PathBuf::from(path.file_name().unwrap_or_default())),
            Selected::None => None,
        };
        for i in entries.iter().enumerate() {
            let (i, Self { path, .. }) = i;
            let checked = selected_name.as_deref() == Some(path);
            if checked {
                return Some(i);
            }
        }
        None
    }
}

impl PreviewSprite {
    fn ui(
        &mut self,
        ui: &mut egui::Ui,
        viewport: egui::Rect,
        update_state: &UpdateState<'_>,
    ) -> egui::Response {
        let (canvas_rect, response) =
            ui.allocate_exact_size(self.sprite_size, egui::Sense::click());

        let absolute_scroll_rect = ui
            .ctx()
            .screen_rect()
            .intersect(viewport.translate(canvas_rect.min.to_vec2()));
        let scroll_rect = absolute_scroll_rect.translate(-canvas_rect.min.to_vec2());
        self.sprite.transform.set_position(
            &update_state.graphics.render_state,
            glam::vec2(-scroll_rect.left(), -scroll_rect.top()),
        );

        self.viewport.set(
            &update_state.graphics.render_state,
            glam::vec2(absolute_scroll_rect.width(), absolute_scroll_rect.height()),
            glam::Vec2::ZERO,
            glam::Vec2::ONE,
        );

        let painter = Painter::new(self.sprite.prepare(&update_state.graphics));
        ui.painter()
            .add(luminol_egui_wgpu::Callback::new_paint_callback(
                absolute_scroll_rect,
                painter,
            ));

        response
    }
}
