use std::{borrow::Cow, ops::{Deref, DerefMut}};

use mlua::{AnyUserData, ExternalError, FromLua, IntoLua, Lua, MetaMethod, Table, UserData, UserDataMethods, Value};
use unicode_width::UnicodeWidthChar;

const EXPECTED: &str = "expected a string or Span";

pub struct Span(pub(super) ratatui::text::Span<'static>);

impl Deref for Span {
	type Target = ratatui::text::Span<'static>;

	fn deref(&self) -> &Self::Target { &self.0 }
}

impl DerefMut for Span {
	fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 }
}

impl Span {
	pub fn compose(lua: &Lua) -> mlua::Result<Value> {
		let new = lua.create_function(|_, (_, span): (Table, Self)| Ok(span))?;

		let span = lua.create_table()?;
		span.set_metatable(Some(lua.create_table_from([(MetaMethod::Call.name(), new)])?))?;

		span.into_lua(lua)
	}

	pub(super) fn truncate(&mut self, max: usize) -> usize {
		if max < 1 {
			match &mut self.0.content {
				Cow::Borrowed(_) => self.0.content = Cow::Borrowed(""),
				Cow::Owned(s) => s.clear(),
			}
			return 0;
		}

		let mut adv = 0;
		let mut last;
		for (i, c) in self.0.content.char_indices() {
			(last, adv) = (adv, adv + c.width().unwrap_or(0));
			if adv < max {
				continue;
			} else if adv == max && self.0.content[i..].chars().nth(1).is_none() {
				return max;
			}
			match &mut self.0.content {
				Cow::Borrowed(s) => self.0.content = format!("{}…", &s[..i]).into(),
				Cow::Owned(s) => {
					s.truncate(i);
					s.push('…');
				}
			}
			return last + 1;
		}
		adv
	}
}

impl FromLua for Span {
	fn from_lua(value: Value, _: &Lua) -> mlua::Result<Self> {
		Ok(Self(match value {
			Value::String(s) => s.to_string_lossy().into(),
			Value::UserData(ud) => {
				if let Ok(Self(span)) = ud.take() {
					span
				} else {
					Err(EXPECTED.into_lua_err())?
				}
			}
			_ => Err(EXPECTED.into_lua_err())?,
		}))
	}
}

impl UserData for Span {
	fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
		crate::impl_style_method!(methods, 0.style);
		crate::impl_style_shorthands!(methods, 0.style);

		methods.add_method("visible", |_, Self(me), ()| {
			Ok(me.content.chars().any(|c| c.width().unwrap_or(0) > 0))
		});
		methods.add_function("truncate", |_, (ud, t): (AnyUserData, Table)| {
			ud.borrow_mut::<Self>()?.truncate(t.raw_get("max")?);
			Ok(ud)
		});
	}
}
