use std::{borrow::Cow, iter};

use ansi_to_tui::IntoText;
use mlua::{AnyUserData, ExternalError, ExternalResult, IntoLua, Lua, ObjectLike, Table, Value};
use tracing::error;
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
use yazi_binding::{Composer, ComposerGet, ComposerSet, Permit, PermitRef, elements::{Line, Rect, Span, Wrap}, runtime};
use yazi_config::LAYOUT;
use yazi_scheduler::AppProxy;
use yazi_shared::replace_to_printable;
use yazi_shim::ratatui::LineIter;
use yazi_term::YIELD_TO_SUBPROCESS;

pub fn compose() -> Composer<ComposerGet, ComposerSet> {
	fn get(lua: &Lua, key: &[u8]) -> mlua::Result<Value> {
		match key {
			b"area" => area(lua)?,
			b"hide" => hide(lua)?,
			b"lines" => lines(lua)?,
			b"printable" => printable(lua)?,
			b"redraw" => redraw(lua)?,
			b"render" => render(lua)?,
			b"truncate" => truncate(lua)?,
			b"width" => width(lua)?,
			_ => return Ok(Value::Nil),
		}
		.into_lua(lua)
	}

	fn set(_: &Lua, _: &[u8], value: Value) -> mlua::Result<Value> { Ok(value) }

	yazi_binding::elements::compose(get, set)
}

pub(super) fn area(lua: &Lua) -> mlua::Result<Value> {
	let f = lua.create_function(|_, s: mlua::String| {
		let layout = LAYOUT.get();
		Ok(match &*s.as_bytes() {
			b"current" => Rect(layout.current),
			b"preview" => Rect(layout.preview),
			b"progress" => Rect(layout.progress),
			_ => Err(format!("unknown area: {}", s.display()).into_lua_err())?,
		})
	})?;

	f.into_lua(lua)
}

pub(super) fn hide(lua: &Lua) -> mlua::Result<Value> {
	let f = lua.create_async_function(|lua, ()| async move {
		if runtime!(lua)?.blocking {
			return Err("Cannot call `ui.hide()` while main thread is blocked".into_lua_err());
		}

		if lua.named_registry_value::<PermitRef>("HIDE_PERMIT").is_ok_and(|h| h.is_some()) {
			return Err("Cannot hide while already hidden".into_lua_err());
		}

		let permit = YIELD_TO_SUBPROCESS.acquire().await.unwrap();
		AppProxy::stop().await;

		lua.set_named_registry_value("HIDE_PERMIT", Permit::new(permit, AppProxy::resume()))?;
		lua.named_registry_value::<AnyUserData>("HIDE_PERMIT")
	})?;

	f.into_lua(lua)
}

pub(super) fn lines(lua: &Lua) -> mlua::Result<Value> {
	let f = lua.create_function(|lua, (s, opts): (mlua::String, Table)| {
		let b = s.as_bytes();
		let s = &*String::from_utf8_lossy(&b);

		let parsed;
		let tab_size = opts.raw_get("tab_size")?;

		let mut it = if opts.raw_get("ansi")? {
			parsed = s.to_text().into_lua_err()?.lines;
			LineIter::parsed(&parsed, tab_size)
		} else {
			LineIter::source(s, tab_size)
		};

		if let Some(wrap) = *opts.raw_get::<Wrap>("wrap")? {
			it = it.wrapped(wrap, opts.raw_get("width")?);
		}

		lua.create_sequence_from(iter::from_fn(|| {
			let (spans, align) = it.next()?;
			Some(Line::from(spans.into_static_line().alignment(align)))
		}))
	})?;

	f.into_lua(lua)
}

pub(super) fn printable(lua: &Lua) -> mlua::Result<Value> {
	let f = lua.create_function(|lua, s: mlua::String| {
		Ok(match replace_to_printable(&s.as_bytes(), false, 1, true) {
			Cow::Borrowed(_) => s,
			Cow::Owned(new) => lua.create_external_string(new)?,
		})
	})?;

	f.into_lua(lua)
}

pub(super) fn redraw(lua: &Lua) -> mlua::Result<Value> {
	let f = lua.create_function(|lua, c: Table| {
		let id: mlua::String = c.get("_id")?;

		let mut layout = LAYOUT.get();
		match &*id.as_bytes() {
			b"current" => layout.current = *c.raw_get::<Rect>("_area")?,
			b"preview" => layout.preview = *c.raw_get::<Rect>("_area")?,
			b"progress" => layout.progress = *c.raw_get::<Rect>("_area")?,
			_ => {}
		}

		LAYOUT.set(layout);
		match c.call_method::<Value>("redraw", ())? {
			Value::Table(tbl) => Ok(tbl),
			Value::UserData(ud) => lua.create_sequence_from([ud]),
			_ => {
				error!(
					"Failed to `redraw()` the `{}` component: expected a table or UserData",
					id.display(),
				);
				lua.create_table()
			}
		}
	})?;

	f.into_lua(lua)
}

pub(super) fn render(lua: &Lua) -> mlua::Result<Value> {
	let f = lua.create_function(|_, ()| {
		yazi_macro::render!();
		Ok(())
	})?;

	f.into_lua(lua)
}

pub(super) fn truncate(lua: &Lua) -> mlua::Result<Value> {
	fn traverse(it: impl Iterator<Item = (usize, char)>, max: usize) -> (Option<usize>, usize, bool) {
		let (mut adv, mut last) = (0, 0);
		let idx = it
			.take_while(|&(_, c)| {
				(last, adv) = (adv, adv + c.width().unwrap_or(0));
				adv <= max
			})
			.map(|(i, _)| i)
			.last();
		(idx, last, adv > max)
	}

	let f = lua.create_function(|lua, (s, t): (mlua::String, Table)| {
		let b = s.as_bytes();
		if b.is_empty() {
			return Ok(s);
		}

		let max = t.raw_get("max")?;
		if b.len() <= max {
			return Ok(s);
		} else if max < 1 {
			return lua.create_string("");
		}

		let lossy = String::from_utf8_lossy(&b);
		let rtl = t.raw_get("rtl")?;
		let (idx, width, remain) = if rtl {
			traverse(lossy.char_indices().rev(), max)
		} else {
			traverse(lossy.char_indices(), max)
		};

		let Some(idx) = idx else { return lua.create_string("…") };
		if !remain {
			return Ok(s);
		}

		let result: Vec<_> = match (rtl, width == max) {
			(false, false) => {
				let len = lossy[idx..].chars().next().map_or(0, |c| c.len_utf8());
				lossy[..idx + len].bytes().chain("…".bytes()).collect()
			}
			(false, true) => lossy[..idx].bytes().chain("…".bytes()).collect(),
			(true, false) => "…".bytes().chain(lossy[idx..].bytes()).collect(),
			(true, true) => {
				let len = lossy[idx..].chars().next().map_or(0, |c| c.len_utf8());
				"…".bytes().chain(lossy[idx + len..].bytes()).collect()
			}
		};
		lua.create_external_string(result)
	})?;

	f.into_lua(lua)
}

pub(super) fn width(lua: &Lua) -> mlua::Result<Value> {
	let f = lua.create_function(|_, v: Value| match v {
		Value::String(s) => {
			let (mut acc, b) = (0, s.as_bytes());
			for c in b.utf8_chunks() {
				acc += c.valid().width();
				if !c.invalid().is_empty() {
					acc += 1;
				}
			}
			Ok(acc)
		}
		Value::UserData(ud) => {
			if let Ok(line) = ud.borrow::<Line>() {
				Ok(line.width())
			} else if let Ok(span) = ud.borrow::<Span>() {
				Ok(span.width())
			} else {
				Err("expected a string, Line, or Span".into_lua_err())?
			}
		}
		_ => Err("expected a string, Line, or Span".into_lua_err())?,
	})?;

	f.into_lua(lua)
}

#[cfg(test)]
mod tests {
	use mlua::{Lua, chunk};

	fn truncate(s: &str, max: usize, rtl: bool) -> String {
		let lua = Lua::new();
		let f = super::truncate(&lua).unwrap();

		lua
			.load(chunk! {
				return $f($s, { max = $max, rtl = $rtl })
			})
			.call(())
			.unwrap()
	}

	#[test]
	fn test_truncate() {
		assert_eq!(truncate("你好，world", 0, false), "");
		assert_eq!(truncate("你好，world", 1, false), "…");
		assert_eq!(truncate("你好，world", 2, false), "…");

		assert_eq!(truncate("你好，世界", 3, false), "你…");
		assert_eq!(truncate("你好，世界", 4, false), "你…");
		assert_eq!(truncate("你好，世界", 5, false), "你好…");

		assert_eq!(truncate("Hello, world", 5, false), "Hell…");
		assert_eq!(truncate("Ni好，世界", 3, false), "Ni…");
	}

	#[test]
	fn test_truncate_rtl() {
		assert_eq!(truncate("world，你好", 0, true), "");
		assert_eq!(truncate("world，你好", 1, true), "…");
		assert_eq!(truncate("world，你好", 2, true), "…");

		assert_eq!(truncate("你好，世界", 3, true), "…界");
		assert_eq!(truncate("你好，世界", 4, true), "…界");
		assert_eq!(truncate("你好，世界", 5, true), "…世界");

		assert_eq!(truncate("Hello, world", 5, true), "…orld");
		assert_eq!(truncate("你好，Shi界", 3, true), "…界");
	}

	#[test]
	fn test_truncate_oboe() {
		assert_eq!(truncate("Hello, world", 11, false), "Hello, wor…");
		assert_eq!(truncate("你好，世界", 9, false), "你好，世…");
		assert_eq!(truncate("你好，世Jie", 9, false), "你好，世…");

		assert_eq!(truncate("Hello, world", 11, true), "…llo, world");
		assert_eq!(truncate("你好，世界", 9, true), "…好，世界");
		assert_eq!(truncate("Ni好，世界", 9, true), "…好，世界");
	}

	#[test]
	fn test_truncate_exact() {
		assert_eq!(truncate("Hello, world", 12, false), "Hello, world");
		assert_eq!(truncate("你好，世界", 10, false), "你好，世界");

		assert_eq!(truncate("Hello, world", 12, true), "Hello, world");
		assert_eq!(truncate("你好，世界", 10, true), "你好，世界");
	}

	#[test]
	fn test_truncate_overflow() {
		assert_eq!(truncate("Hello, world", 13, false), "Hello, world");
		assert_eq!(truncate("你好，世界", 11, false), "你好，世界");

		assert_eq!(truncate("Hello, world", 13, true), "Hello, world");
		assert_eq!(truncate("你好，世界", 11, true), "你好，世界");
	}
}
