use std::any::{Any, TypeId};
use std::cell::{Cell as StdCell, RefCell};
use std::rc::Rc;

use ruin_ui::ElementId;

use crate::ContextKey;
use crate::view::View;

#[derive(Clone)]
pub(crate) struct ContextEntry {
    pub(crate) key: TypeId,
    pub(crate) value: Rc<dyn Any>,
}

impl ContextEntry {
    pub(crate) fn new<C: ContextKey>(value: C::Value) -> Self {
        Self {
            key: TypeId::of::<C>(),
            value: Rc::new(value),
        }
    }
}

#[derive(Default)]
pub(crate) struct RenderState {
    pub(crate) hooks: RefCell<Vec<Box<dyn Any>>>,
    pub(crate) element_ids: RefCell<Vec<ElementId>>,
    pub(crate) next_element_id: StdCell<u64>,
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) struct InteractionSnapshot {
    pub(crate) hovered: Vec<ElementId>,
    pub(crate) pressed: Vec<ElementId>,
}

#[derive(Clone, Default)]
pub(crate) struct RenderSideEffects {
    pub(crate) window_title: Option<String>,
    pub(crate) shortcuts: Vec<crate::input::ShortcutBinding>,
}

#[derive(Clone)]
pub(crate) struct RenderContext {
    pub(crate) state: Rc<RenderState>,
    pub(crate) hook_index: Rc<StdCell<usize>>,
    pub(crate) element_index: Rc<StdCell<usize>>,
    pub(crate) side_effects: Rc<RefCell<RenderSideEffects>>,
    pub(crate) context_entries: Rc<Vec<ContextEntry>>,
    pub(crate) interaction: Rc<InteractionSnapshot>,
}

impl RenderContext {
    pub(crate) fn with_context_entries(&self, context_entries: Rc<Vec<ContextEntry>>) -> Self {
        Self {
            state: Rc::clone(&self.state),
            hook_index: Rc::clone(&self.hook_index),
            element_index: Rc::clone(&self.element_index),
            side_effects: Rc::clone(&self.side_effects),
            context_entries,
            interaction: Rc::clone(&self.interaction),
        }
    }
}

pub(crate) struct RenderOutput {
    pub(crate) view: View,
    pub(crate) side_effects: RenderSideEffects,
}

thread_local! {
    pub(crate) static CURRENT_RENDER_CONTEXT: RefCell<Option<RenderContext>> = const { RefCell::new(None) };
}

pub(crate) fn render_with_context(
    state: Rc<RenderState>,
    render: impl FnOnce() -> View,
) -> RenderOutput {
    render_with_context_and_interaction(state, Rc::new(InteractionSnapshot::default()), render)
}

pub(crate) fn render_with_context_and_interaction(
    state: Rc<RenderState>,
    interaction: Rc<InteractionSnapshot>,
    render: impl FnOnce() -> View,
) -> RenderOutput {
    let context = RenderContext {
        state,
        hook_index: Rc::new(StdCell::new(0)),
        element_index: Rc::new(StdCell::new(0)),
        side_effects: Rc::new(RefCell::new(RenderSideEffects::default())),
        context_entries: Rc::new(Vec::new()),
        interaction,
    };

    let view = with_render_context(context.clone(), render);
    let side_effects = context.side_effects.borrow().clone();
    RenderOutput { view, side_effects }
}

pub(crate) fn with_render_context(context: RenderContext, render: impl FnOnce() -> View) -> View {
    CURRENT_RENDER_CONTEXT.with(|slot| {
        let previous = slot.replace(Some(context.clone()));

        struct Guard<'a> {
            slot: &'a RefCell<Option<RenderContext>>,
            previous: Option<RenderContext>,
        }

        impl Drop for Guard<'_> {
            fn drop(&mut self) {
                let _ = self.slot.replace(self.previous.take());
            }
        }

        let _guard = Guard { slot, previous };
        render()
    })
}

pub(crate) fn with_render_context_state<R>(f: impl FnOnce(&RenderContext) -> R) -> R {
    CURRENT_RENDER_CONTEXT.with(|slot| {
        let context = slot
            .borrow()
            .clone()
            .expect("ruin_app hooks can only run while rendering a mounted component");
        f(&context)
    })
}

pub(crate) fn with_hook_slot<T: 'static, R>(
    init: impl FnOnce() -> T,
    f: impl FnOnce(&mut T) -> R,
) -> R {
    with_render_context_state(|context| {
        let index = context.hook_index.get();
        context.hook_index.set(index + 1);

        let mut hooks = context.state.hooks.borrow_mut();
        if hooks.len() == index {
            hooks.push(Box::new(init()));
        }

        let slot = hooks[index]
            .downcast_mut::<T>()
            .expect("ruin_app hook call order changed between renders");
        f(slot)
    })
}

pub(crate) fn allocate_element_id() -> ElementId {
    with_render_context_state(|context| {
        let index = context.element_index.get();
        context.element_index.set(index + 1);

        let mut element_ids = context.state.element_ids.borrow_mut();
        if element_ids.len() == index {
            let next = context.state.next_element_id.get().wrapping_add(1);
            context.state.next_element_id.set(next);
            element_ids.push(ElementId::new(next));
        }
        element_ids[index]
    })
}

pub(crate) struct MemoSlot<T> {
    pub(crate) compute: Rc<RefCell<Box<dyn Fn() -> T>>>,
    pub(crate) handle: crate::hooks::Memo<T>,
}

pub(crate) struct ResourceSlot<T, E> {
    pub(crate) resource: crate::hooks::Resource<T, E>,
    pub(crate) _effect: ruin_reactivity::EffectHandle,
}

impl<T: 'static> MemoSlot<T> {
    pub(crate) fn new(compute: Rc<RefCell<Box<dyn Fn() -> T>>>) -> Self {
        let handle = crate::hooks::Memo {
            compute: Rc::new({
                let compute = Rc::clone(&compute);
                move || {
                    let compute = compute.borrow();
                    (compute.as_ref())()
                }
            }),
        };
        Self { compute, handle }
    }

    pub(crate) fn replace_compute(&mut self, compute: Rc<RefCell<Box<dyn Fn() -> T>>>) {
        self.compute = Rc::clone(&compute);
        self.handle.compute = Rc::new({
            let compute = Rc::clone(&compute);
            move || {
                let compute = compute.borrow();
                (compute.as_ref())()
            }
        });
    }
}
