//! UI runtime: `h`, `Fragment`, vnode shapes compatible with Lattish, minimal hooks, and [`Host`].

mod hooks;

use std::sync::Arc;

pub use hooks::{
    alloc_root_id, current_root_id, drop_host_for_root, install_host_for_root,
    install_thread_local_host, native_create_root,
    native_use_effect, native_use_layout_effect, native_use_memo, native_use_ref,
    native_use_state, run_with_current_root,
    schedule_flush,
    unregister_root, unregister_root_hooks_and_effects, with_host_for_root,
    with_thread_local_host, HookState, RootId, LEGACY_ROOT_ID,
};

use tishlang_core::{ObjectMap, Value, VmRef};

/// Sentinel string for `Fragment` (native). Full runtimes may also use `Symbol.for("tish.fragment")`
/// when the global `Symbol` is present; [`is_fragment_tag`] accepts both that registry symbol and
/// this string for compatibility.
pub const FRAGMENT_SENTINEL: &str = "__tish_ui_Fragment__";

/// `Fragment` marker value for `h(Fragment, null, children)`.
pub fn fragment_value() -> Value {
    Value::String(FRAGMENT_SENTINEL.into())
}

/// Returns true if `tag` refers to [`fragment_value`].
pub fn is_fragment_tag(tag: &Value) -> bool {
    match tag {
        Value::String(s) => s.as_str() == FRAGMENT_SENTINEL,
        Value::Symbol(s) => s.registry_key.as_deref() == Some("tish.fragment"),
        _ => false,
    }
}

/// `text(s)` helper — returns string as `Value::String` for JSX text nodes.
pub fn ui_text(args: &[Value]) -> Value {
    let s = args
        .first()
        .map(|v| v.to_display_string())
        .unwrap_or_default();
    Value::String(s.into())
}

/// Vnode factory: `h(tag, props, children)` (Lattish-compatible shape).
pub fn ui_h(args: &[Value]) -> Value {
    let tag = args.first().cloned().unwrap_or(Value::Null);
    let props = args.get(1).cloned().unwrap_or(Value::Null);
    let children_arg = args.get(2).cloned().unwrap_or(Value::Null);

    let children_vec = normalize_children_list(children_arg);

    if let Value::Function(f) = &tag {
        let mut merged: ObjectMap = if matches!(props, Value::Null) {
            ObjectMap::default()
        } else if let Value::Object(obj) = props {
            // `ObjectData.strings` is an insertion-ordered `PropMap`; `Value::object` takes an
            // `ObjectMap` (`AHashMap`). Copy the entries across (object property order is not
            // observable for a merged props map here).
            obj.borrow()
                .strings
                .iter()
                .map(|(k, v)| (Arc::clone(k), v.clone()))
                .collect()
        } else {
            ObjectMap::default()
        };
        if !children_vec.is_empty() {
            merged.insert(
                Arc::from("children"),
                Value::Array(VmRef::new(children_vec.clone())),
            );
        }
        return f.call(&[Value::object(merged)]);
    }

    if is_fragment_tag(&tag) {
        return vnode_fragment(children_vec);
    }

    let tag_str: Arc<str> = match tag {
        Value::String(s) => Arc::from(s.as_str()),
        _ => return Value::Null,
    };

    vnode_element(tag_str, props, children_vec)
}

fn normalize_children_list(children_arg: Value) -> Vec<Value> {
    match children_arg {
        Value::Null => vec![],
        Value::Array(a) => flatten_vnode_children(&a.borrow()),
        other => vec![other],
    }
}

/// JSX often passes `{items.map(...)}` as one slot in the children array. Treat nested `Value::Array`
/// like React: splice them into the parent list so hosts never see a raw array vnode.
fn flatten_vnode_children(items: &[Value]) -> Vec<Value> {
    let mut out = Vec::new();
    for c in items {
        match c {
            Value::Array(inner) => out.extend(flatten_vnode_children(&inner.borrow())),
            Value::Null => {}
            _ => out.push(c.clone()),
        }
    }
    out
}

fn vnode_element(tag: Arc<str>, props: Value, children: Vec<Value>) -> Value {
    let mut m = ObjectMap::default();
    m.insert(Arc::from("tag"), Value::String(tag.as_ref().into()));
    m.insert(
        Arc::from("props"),
        if matches!(props, Value::Null) {
            Value::Null
        } else {
            props
        },
    );
    m.insert(Arc::from("children"), Value::Array(VmRef::new(children)));
    m.insert(Arc::from("_el"), Value::Null);
    Value::object(m)
}

fn vnode_fragment(children: Vec<Value>) -> Value {
    let mut m = ObjectMap::default();
    m.insert(Arc::from("tag"), fragment_value());
    m.insert(Arc::from("props"), Value::Null);
    m.insert(Arc::from("children"), Value::Array(VmRef::new(children)));
    m.insert(Arc::from("_el"), Value::Null);
    Value::object(m)
}

/// Pluggable UI backend (Floem, DOM, SwiftUI, …). Main-thread / single-threaded by default.
pub trait Host {
    /// Apply a new root vnode (after each render flush).
    fn commit_root(&mut self, vnode: &Value);
    /// Content area width changed (e.g. window resize); default no-op.
    fn content_width_changed(&mut self, _width: f64) {}
    /// Called once from the main queue shortly after the window is ordered on-screen. Split /
    /// sidebar hosts can use this to re-layout when pane bounds were still provisional during the
    /// first commit.
    fn after_window_shown(&mut self) {}
    /// Clear native control target/action (and similar) before the host is dropped — e.g. window close.
    fn detach_native_actions(&mut self) {}
}

/// No-op / test host that only stores the last committed tree.
#[derive(Default)]
pub struct HeadlessHost {
    pub last: Option<Value>,
}


impl Host for HeadlessHost {
    fn commit_root(&mut self, vnode: &Value) {
        self.last = Some(vnode.clone());
    }
}

/// Tag registry hook for future host-specific intrinsic mapping (HTML tag → component kind).
#[derive(Default)]
pub struct TagRegistry;

impl TagRegistry {
    pub fn new() -> Self {
        Self
    }
}

/// Placeholder for subset CSS / style object interpretation.
#[derive(Default)]
pub struct StyleInterpreter;

impl StyleInterpreter {
    pub fn new() -> Self {
        Self
    }
}
