use std::{cell::RefCell, rc::Rc};

use button::setup_custom_button;
use glam::{vec2, Affine2, Vec2};
use label::setup_custom_label;
use wgui::{
    assets::AssetPath,
    drawing,
    event::{
        Event as WguiEvent, EventAlterables, EventCallback, EventListenerID, EventListenerKind,
        InternalStateChangeEvent, MouseButtonIndex, MouseDownEvent, MouseLeaveEvent,
        MouseMotionEvent, MouseUpEvent, MouseWheelEvent,
    },
    gfx::cmd::WGfxClearMode,
    layout::{Layout, LayoutParams, WidgetID},
    parser::{CustomAttribsInfoOwned, ParserState},
    renderer_vk::context::Context as WguiContext,
    taffy,
    widget::{label::WidgetLabel, rectangle::WidgetRectangle, EventResult},
};

use crate::{
    backend::input::{Haptics, HoverResult, PointerHit, PointerMode},
    state::AppState,
    subsystem::hid::WheelDelta,
    windowing::backend::{
        ui_transform, FrameMeta, OverlayBackend, OverlayEventData, RenderResources, ShouldRender,
    },
};

use super::{timer::GuiTimer, timestep::Timestep};

pub mod button;
mod helper;
mod label;

const DEFAULT_MAX_SIZE: f32 = 2048.0;

const COLOR_ERR: drawing::Color = drawing::Color::new(1., 0., 1., 1.);

pub type OnNotifyFunc<S> =
    Box<dyn Fn(&mut GuiPanel<S>, &mut AppState, OverlayEventData) -> anyhow::Result<()>>;

pub struct GuiPanel<S> {
    pub layout: Layout,
    pub state: S,
    pub timers: Vec<GuiTimer>,
    pub parser_state: ParserState,
    pub max_size: Vec2,
    pub gui_scale: f32,
    pub on_notify: Option<OnNotifyFunc<S>>,
    interaction_transform: Option<Affine2>,
    context: WguiContext,
    timestep: Timestep,
}

pub type OnCustomIdFunc<S> = Box<
    dyn Fn(
        Rc<str>,
        WidgetID,
        &wgui::parser::ParseDocumentParams,
        &mut Layout,
        &mut ParserState,
        &mut S,
    ) -> anyhow::Result<()>,
>;

pub type OnCustomAttribFunc = Box<dyn Fn(&mut Layout, &CustomAttribsInfoOwned, &AppState)>;

pub struct NewGuiPanelParams<S> {
    pub on_custom_id: Option<OnCustomIdFunc<S>>, // used only in `new_from_template`
    pub on_custom_attrib: Option<OnCustomAttribFunc>, // used only in `new_from_template`
    pub resize_to_parent: bool,
    pub gui_scale: f32,
}

impl<S> Default for NewGuiPanelParams<S> {
    fn default() -> Self {
        Self {
            on_custom_id: None,
            on_custom_attrib: None,
            resize_to_parent: false,
            gui_scale: 1.0,
        }
    }
}

impl<S: 'static> GuiPanel<S> {
    pub fn new_from_template(
        app: &mut AppState,
        path: &str,
        mut state: S,
        params: NewGuiPanelParams<S>,
    ) -> anyhow::Result<Self> {
        let custom_elems = Rc::new(RefCell::new(vec![]));

        let doc_params = wgui::parser::ParseDocumentParams {
            globals: app.wgui_globals.clone(),
            path: AssetPath::BuiltIn(path),
            extra: wgui::parser::ParseDocumentExtra {
                on_custom_attribs: Some(Box::new({
                    let custom_elems = custom_elems.clone();
                    move |attribs| {
                        custom_elems.borrow_mut().push(attribs.to_owned());
                    }
                })),
                ..Default::default()
            },
        };

        let (mut layout, mut parser_state) = wgui::parser::new_layout_from_assets(
            &doc_params,
            &LayoutParams {
                resize_to_parent: params.resize_to_parent,
            },
        )?;

        if let Some(on_element_id) = params.on_custom_id {
            let ids = parser_state.data.ids.clone(); // FIXME: copying all ids?

            for (id, widget) in ids {
                on_element_id(
                    id.clone(),
                    widget,
                    &doc_params,
                    &mut layout,
                    &mut parser_state,
                    &mut state,
                )?;
            }
        }

        for elem in custom_elems.borrow().iter() {
            if layout
                .state
                .widgets
                .get_as::<WidgetLabel>(elem.widget_id)
                .is_some()
            {
                setup_custom_label::<S>(&mut layout, elem, app);
            } else if layout
                .state
                .widgets
                .get_as::<WidgetRectangle>(elem.widget_id)
                .is_some()
            {
                setup_custom_button::<S>(&mut layout, elem, app);
            }

            if let Some(on_custom_attrib) = &params.on_custom_attrib {
                on_custom_attrib(&mut layout, elem, app);
            }
        }

        let context = WguiContext::new(&mut app.wgui_shared, 1.0)?;
        let mut timestep = Timestep::new();
        timestep.set_tps(60.0);

        Ok(Self {
            layout,
            context,
            timestep,
            state,
            parser_state,
            max_size: vec2(DEFAULT_MAX_SIZE as _, DEFAULT_MAX_SIZE as _),
            timers: vec![],
            interaction_transform: None,
            on_notify: None,
            gui_scale: params.gui_scale,
        })
    }

    pub fn new_blank(
        app: &mut AppState,
        state: S,
        params: NewGuiPanelParams<S>,
    ) -> anyhow::Result<Self> {
        let layout = Layout::new(
            app.wgui_globals.clone(),
            &LayoutParams {
                resize_to_parent: params.resize_to_parent,
            },
        )?;
        let context = WguiContext::new(&mut app.wgui_shared, 1.0)?;
        let mut timestep = Timestep::new();
        timestep.set_tps(60.0);

        Ok(Self {
            layout,
            context,
            timestep,
            state,
            parser_state: ParserState::default(),
            max_size: vec2(DEFAULT_MAX_SIZE as _, DEFAULT_MAX_SIZE as _),
            timers: vec![],
            on_notify: None,
            interaction_transform: None,
            gui_scale: params.gui_scale,
        })
    }

    pub fn update_layout(&mut self) -> anyhow::Result<()> {
        self.layout.update(self.max_size, 0.0)
    }

    pub fn push_event(&mut self, app: &mut AppState, event: &WguiEvent) -> EventResult {
        match self.layout.push_event(event, app, &mut self.state) {
            Ok(r) => r,
            Err(e) => {
                log::error!("Failed to push event: {e:?}");
                EventResult::NoHit
            }
        }
    }

    pub fn add_event_listener(
        &mut self,
        widget_id: WidgetID,
        kind: EventListenerKind,
        callback: EventCallback<AppState, S>,
    ) -> Option<EventListenerID> {
        self.layout.add_event_listener(widget_id, kind, callback)
    }

    pub fn widget_set_display(
        &self,
        widget_id: WidgetID,
        display: taffy::Display,
        alterables: &mut EventAlterables,
    ) {
        let mut style = self
            .layout
            .state
            .get_widget_style(widget_id)
            .unwrap_or(&taffy::Style::DEFAULT)
            .clone();
        style.display = display;
        alterables.set_style(widget_id, style);
    }
}

impl<S: 'static> OverlayBackend for GuiPanel<S> {
    fn init(&mut self, _app: &mut AppState) -> anyhow::Result<()> {
        if self.layout.content_size.x * self.layout.content_size.y != 0.0 {
            self.update_layout()?;
            self.interaction_transform = Some(ui_transform([
                //TODO: dynamic
                self.layout.content_size.x as _,
                self.layout.content_size.y as _,
            ]));
        }
        Ok(())
    }

    fn pause(&mut self, _app: &mut AppState) -> anyhow::Result<()> {
        Ok(())
    }

    fn resume(&mut self, _app: &mut AppState) -> anyhow::Result<()> {
        self.timestep.reset();
        Ok(())
    }

    fn should_render(&mut self, app: &mut AppState) -> anyhow::Result<ShouldRender> {
        //TODO: this only executes one timer event per frame
        if let Some(signal) = self.timers.iter_mut().find_map(GuiTimer::check_tick) {
            self.push_event(
                app,
                &WguiEvent::InternalStateChange(InternalStateChangeEvent { metadata: signal }),
            );
        }

        while self.timestep.on_tick() {
            self.layout.tick()?;
        }

        if self.layout.content_size.x * self.layout.content_size.y == 0.0 {
            log::trace!("Unable to render: content size 0");
            return Ok(ShouldRender::Unable);
        }

        Ok(if self.layout.check_toggle_needs_redraw() {
            ShouldRender::Should
        } else {
            ShouldRender::Can
        })
    }

    fn render(&mut self, app: &mut AppState, rdr: &mut RenderResources) -> anyhow::Result<()> {
        self.context
            .update_viewport(&mut app.wgui_shared, rdr.extent, self.gui_scale)?;
        self.layout
            .update(self.max_size / self.gui_scale, self.timestep.alpha)?;

        let globals = self.layout.state.globals.clone(); // sorry
        let mut globals = globals.get();

        let primitives = wgui::drawing::draw(&mut wgui::drawing::DrawParams {
            globals: &mut globals,
            layout: &mut self.layout,
            debug_draw: false,
            timestep_alpha: self.timestep.alpha,
        })?;
        self.context.draw(
            &globals.font_system,
            &mut app.wgui_shared,
            &mut rdr.cmd_buf,
            &primitives,
        )?;
        Ok(())
    }

    fn frame_meta(&mut self) -> Option<FrameMeta> {
        Some(FrameMeta {
            clear: WGfxClearMode::Clear([0., 0., 0., 0.]),
            extent: [
                self.max_size.x.min(self.layout.content_size.x) as _,
                self.max_size.y.min(self.layout.content_size.y) as _,
                1,
            ],
            ..Default::default()
        })
    }

    fn notify(&mut self, app: &mut AppState, data: OverlayEventData) -> anyhow::Result<()> {
        let Some(on_notify) = self.on_notify.take() else {
            return Ok(());
        };
        on_notify(self, app, data)?;
        self.on_notify = Some(on_notify);
        Ok(())
    }

    fn on_scroll(&mut self, app: &mut AppState, hit: &PointerHit, delta: WheelDelta) {
        let e = WguiEvent::MouseWheel(MouseWheelEvent {
            delta: vec2(delta.x, delta.y),
            pos: hit.uv * self.layout.content_size,
            device: hit.pointer,
        });
        self.push_event(app, &e);
    }

    fn on_hover(&mut self, app: &mut AppState, hit: &PointerHit) -> HoverResult {
        let e = &WguiEvent::MouseMotion(MouseMotionEvent {
            pos: hit.uv * self.layout.content_size,
            device: hit.pointer,
        });
        let result = self.push_event(app, e);

        HoverResult {
            consume: result != EventResult::NoHit,
            haptics: self
                .layout
                .check_toggle_haptics_triggered()
                .then_some(Haptics {
                    intensity: 0.1,
                    duration: 0.01,
                    frequency: 5.0,
                }),
        }
    }

    fn on_left(&mut self, app: &mut AppState, pointer: usize) {
        log::debug!("panel: on left");
        let e = WguiEvent::MouseLeave(MouseLeaveEvent { device: pointer });
        self.push_event(app, &e);
    }

    fn on_pointer(&mut self, app: &mut AppState, hit: &PointerHit, pressed: bool) {
        let index = match hit.mode {
            PointerMode::Left => MouseButtonIndex::Left,
            PointerMode::Right => MouseButtonIndex::Right,
            PointerMode::Middle => MouseButtonIndex::Middle,
            _ => return,
        };

        let e = if pressed {
            WguiEvent::MouseDown(MouseDownEvent {
                pos: hit.uv * self.layout.content_size,
                index,
                device: hit.pointer,
            })
        } else {
            WguiEvent::MouseUp(MouseUpEvent {
                pos: hit.uv * self.layout.content_size,
                index,
                device: hit.pointer,
            })
        };

        self.push_event(app, &e);
    }

    fn get_interaction_transform(&mut self) -> Option<Affine2> {
        self.interaction_transform
    }
}
