use std::any::TypeId;

use mlua::{AnyUserData, ExternalError, FromLua, Lua, Value};
use ratatui_core::widgets::Widget;
use yazi_binding::{elements::{Area, Bar, Border, Fill, Gauge, Line, List, Spatial, Table, Text}, position::Position};

use crate::{clear::Clear, input::InputArc};

#[derive(Clone, Debug)]
pub enum Renderable {
	Line(Line),
	Text(Text),
	List(Box<List>),
	Bar(Bar),
	Clear(Clear),
	Fill(Fill),
	Border(Border),
	Input(InputArc),
	Gauge(Box<Gauge>),
	Table(Box<Table>),
}

impl Renderable {
	pub fn with_area(mut self, area: impl Into<Area>) -> Self {
		self.set_area(area.into());
		self
	}

	pub fn render_with<T>(self, buf: &mut ratatui_core::buffer::Buffer, trans: T)
	where
		T: FnOnce(Position) -> ratatui_core::layout::Rect,
	{
		let rect = self.area().transform(trans);
		self.render(rect, buf);
	}
}

impl TryFrom<&AnyUserData> for Renderable {
	type Error = mlua::Error;

	fn try_from(ud: &AnyUserData) -> Result<Self, Self::Error> {
		Ok(match ud.type_id() {
			Some(t) if t == TypeId::of::<Line>() => Self::Line(ud.try_into()?),
			Some(t) if t == TypeId::of::<Text>() => Self::Text(ud.try_into()?),
			Some(t) if t == TypeId::of::<List>() => Self::List(Box::new(ud.try_into()?)),
			Some(t) if t == TypeId::of::<Bar>() => Self::Bar(ud.try_into()?),
			Some(t) if t == TypeId::of::<Clear>() => Self::Clear(ud.try_into()?),
			Some(t) if t == TypeId::of::<Fill>() => Self::Fill(ud.try_into()?),
			Some(t) if t == TypeId::of::<Border>() => Self::Border(ud.try_into()?),
			Some(t) if t == TypeId::of::<InputArc>() => Self::Input(ud.try_into()?),
			Some(t) if t == TypeId::of::<Gauge>() => Self::Gauge(Box::new(ud.try_into()?)),
			Some(t) if t == TypeId::of::<Table>() => Self::Table(Box::new(ud.try_into()?)),
			_ => Err(format!("expected a renderable userdata, not: {ud:#?}").into_lua_err())?,
		})
	}
}

impl From<yazi_shim::fs::Error> for Renderable {
	fn from(error: yazi_shim::fs::Error) -> Self {
		Self::Text(Text::from(error.to_string()).wrap(ratatui_widgets::paragraph::Wrap { trim: false }))
	}
}

impl Spatial for Renderable {
	fn area(&self) -> Area {
		match self {
			Self::Line(line) => line.area(),
			Self::Text(text) => text.area(),
			Self::List(list) => list.area(),
			Self::Bar(bar) => bar.area(),
			Self::Clear(clear) => clear.area(),
			Self::Fill(fill) => fill.area(),
			Self::Border(border) => border.area(),
			Self::Input(input) => input.area(),
			Self::Gauge(gauge) => gauge.area(),
			Self::Table(table) => table.area(),
		}
	}

	fn set_area(&mut self, area: Area) {
		match self {
			Self::Line(line) => line.set_area(area),
			Self::Text(text) => text.set_area(area),
			Self::List(list) => list.set_area(area),
			Self::Bar(bar) => bar.set_area(area),
			Self::Clear(clear) => clear.set_area(area),
			Self::Fill(fill) => fill.set_area(area),
			Self::Border(border) => border.set_area(area),
			Self::Input(input) => input.set_area(area),
			Self::Gauge(gauge) => gauge.set_area(area),
			Self::Table(table) => table.set_area(area),
		}
	}
}

impl Widget for Renderable {
	fn render(self, rect: ratatui_core::layout::Rect, buf: &mut ratatui_core::buffer::Buffer)
	where
		Self: Sized,
	{
		match self {
			Self::Line(line) => line.render(rect, buf),
			Self::Text(text) => text.render(rect, buf),
			Self::List(list) => list.render(rect, buf),
			Self::Bar(bar) => bar.render(rect, buf),
			Self::Clear(clear) => clear.render(rect, buf),
			Self::Fill(fill) => fill.render(rect, buf),
			Self::Border(border) => border.render(rect, buf),
			Self::Input(input) => input.render(rect, buf),
			Self::Gauge(gauge) => gauge.render(rect, buf),
			Self::Table(table) => table.render(rect, buf),
		}
	}
}

impl Widget for &Renderable {
	fn render(self, rect: ratatui_core::layout::Rect, buf: &mut ratatui_core::buffer::Buffer)
	where
		Self: Sized,
	{
		match self {
			Renderable::Line(line) => line.render(rect, buf),
			Renderable::Text(text) => text.render(rect, buf),
			Renderable::List(list) => (&**list).render(rect, buf),
			Renderable::Bar(bar) => bar.render(rect, buf),
			Renderable::Clear(clear) => clear.render(rect, buf),
			Renderable::Fill(fill) => fill.render(rect, buf),
			Renderable::Border(border) => border.render(rect, buf),
			Renderable::Input(input) => input.render(rect, buf),
			Renderable::Gauge(gauge) => (&**gauge).render(rect, buf),
			Renderable::Table(table) => (&**table).render(rect, buf),
		}
	}
}

impl FromLua for Renderable {
	fn from_lua(value: Value, _: &Lua) -> mlua::Result<Self> {
		match value {
			Value::UserData(ud) => Self::try_from(&ud),
			_ => Err(format!("expected a renderable userdata, not: {value:#?}").into_lua_err()),
		}
	}
}
