// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0

use crate::{
    ReadOnlyRustModel, RgbaColor, SlintBrush, SlintDataTransfer, SlintImageData, SlintKeys,
    SlintStyledText, js_into_rust_model, rust_into_js_model,
};
use i_slint_compiler::langtype::Type;
use i_slint_core::graphics::{Image, Rgba8Pixel, SharedPixelBuffer};
use i_slint_core::model::{ModelRc, SharedVectorModel};
use i_slint_core::{Brush, Color, SharedVector};
use napi::bindgen_prelude::*;
use napi::{Env, JsValue, Result, ValueType};
use napi_derive::napi;
use slint_interpreter::Value;
use smol_str::SmolStr;

/// A dynamic-length argument list for calling JS functions with a variable
/// number of arguments. Implements `JsValuesTupleIntoVec` so it can be used
/// directly with `Function::call`.
pub struct DynArgs(pub Vec<napi::sys::napi_value>);

impl JsValuesTupleIntoVec for DynArgs {
    fn into_vec(self, _env: napi::sys::napi_env) -> Result<Vec<napi::sys::napi_value>> {
        Ok(self.0)
    }
}

/// Safely extract a f64 from an Unknown, failing if the type is wrong.
fn expect_number(unknown: Unknown<'_>) -> Result<f64> {
    match unknown.get_type()? {
        ValueType::Number => unknown.coerce_to_number()?.get_double(),
        vt => Err(napi::Error::new(
            napi::Status::NumberExpected,
            format!("expect Number, got: {vt:?}"),
        )),
    }
}

/// Safely extract a String from an Unknown, failing if the type is wrong.
fn expect_string(unknown: Unknown<'_>) -> Result<String> {
    match unknown.get_type()? {
        ValueType::String => Ok(unknown.coerce_to_string()?.into_utf8()?.as_str()?.to_owned()),
        vt => Err(napi::Error::new(
            napi::Status::StringExpected,
            format!("expect String, got: {vt:?}"),
        )),
    }
}

/// Safely extract a bool from an Unknown, failing if the type is wrong.
fn expect_bool(unknown: Unknown<'_>) -> Result<bool> {
    match unknown.get_type()? {
        ValueType::Boolean => Ok(unknown.coerce_to_bool()?),
        vt => Err(napi::Error::new(
            napi::Status::BooleanExpected,
            format!("expect Boolean, got: {vt:?}"),
        )),
    }
}

#[napi(js_name = "ValueType")]
pub enum JsValueType {
    Void,
    Number,
    String,
    Bool,
    Model,
    Struct,
    Brush,
    Image,
    Keys,
}

impl From<slint_interpreter::ValueType> for JsValueType {
    fn from(value_type: slint_interpreter::ValueType) -> Self {
        match value_type {
            slint_interpreter::ValueType::Number => JsValueType::Number,
            slint_interpreter::ValueType::String => JsValueType::String,
            slint_interpreter::ValueType::Bool => JsValueType::Bool,
            slint_interpreter::ValueType::Model => JsValueType::Model,
            slint_interpreter::ValueType::Struct => JsValueType::Struct,
            slint_interpreter::ValueType::Brush => JsValueType::Brush,
            slint_interpreter::ValueType::Image => JsValueType::Image,
            _ => JsValueType::Void,
        }
    }
}

#[napi(js_name = "Property")]
pub struct JsProperty {
    pub name: String,
    pub value_type: JsValueType,
}

pub fn to_js_unknown<'a>(env: &'a Env, value: &Value) -> Result<Unknown<'a>> {
    match value {
        Value::Void => Null.into_unknown(env),
        Value::Number(number) => (*number).into_unknown(env),
        Value::String(string) => string.as_str().into_unknown(env),
        Value::Bool(value) => (*value).into_unknown(env),
        Value::Image(image) => {
            SlintImageData::from(image.clone()).into_instance(env)?.as_object(env).into_unknown(env)
        }
        Value::Struct(struct_value) => {
            let mut o = Object::new(env)?;
            for (field_name, field_value) in struct_value.iter() {
                let key = env.create_string(field_name.replace('-', "_"))?;
                let val = to_js_unknown(env, field_value)?;
                o.set_property(key, val)?;
            }
            o.into_unknown(env)
        }
        Value::Keys(keys) => {
            SlintKeys::from(keys.clone()).into_instance(env)?.as_object(env).into_unknown(env)
        }
        Value::Brush(brush) => {
            SlintBrush::from(brush.clone()).into_instance(env)?.as_object(env).into_unknown(env)
        }
        Value::DataTransfer(data) => {
            let instance = SlintDataTransfer::from(data.clone()).into_instance(env)?;
            let mut obj = instance.as_object(env);
            instance.anchor_js_user_data(&mut obj)?;
            obj.into_unknown(env)
        }
        Value::Model(model) => {
            if let Some(maybe_js_model) = rust_into_js_model(env, model) {
                maybe_js_model
            } else {
                let model_wrapper: ReadOnlyRustModel = model.clone().into();
                model_wrapper.into_js(env)
            }
        }
        Value::EnumerationValue(_, value) => value.as_str().into_unknown(env),
        Value::StyledText(styled_text) => SlintStyledText::from(styled_text.clone())
            .into_instance(env)?
            .as_object(env)
            .into_unknown(env),
        _ => ().into_unknown(env),
    }
}

/// A capability handle for installing hidden anchor properties on an owning
/// JS object (currently always a [`JsComponentInstance`](crate::JsComponentInstance)).
///
/// Used by both JS-backed models and `DataTransfer` user_data to keep their
/// JS values reachable from V8 (instead of via hidden NAPI strong refs that
/// V8 can't trace), so cycles can be collected.
///
/// The `seq` field doubles as a finalization guard:
/// when the instance is dropped its `Rc` goes away,
/// so `seq.upgrade()` returns `None` and the pinned side's `Drop`
/// implementation knows that NAPI calls are no longer safe.
#[derive(Clone)]
pub struct JsAnchorOwner {
    /// Weak ref to the JS wrapper object — used to get/set properties.
    pub owner_weak: WeakReference<crate::JsComponentInstance>,
    /// Per-instance anchor-ID counter (shared via `Rc`).
    /// Also used as a finalization guard (`Weak::upgrade` → `None`).
    pub seq: std::rc::Weak<std::cell::Cell<u32>>,
}

impl JsAnchorOwner {
    /// Allocate the next anchor ID from the owning instance.
    pub fn next_anchor_id(&self) -> u32 {
        let Some(cell) = self.seq.upgrade() else { return 0 };
        let id = cell.get();
        cell.set(id + 1);
        id
    }
}

/// Convert a JS value to a Slint [`Value`].
///
/// Model values and `DataTransfer` user_data encountered during conversion
/// are anchored as hidden properties on the `anchor_owner` so V8 keeps
/// them alive.
pub fn to_value(
    env: &Env,
    unknown: Unknown<'_>,
    typ: &Type,
    anchor_owner: &JsAnchorOwner,
) -> Result<Value> {
    match typ {
        Type::Float32
        | Type::Int32
        | Type::Duration
        | Type::Angle
        | Type::PhysicalLength
        | Type::LogicalLength
        | Type::Rem
        | Type::Percent
        | Type::UnitProduct(_) => Ok(Value::Number(expect_number(unknown)?)),
        Type::String => Ok(Value::String(expect_string(unknown)?.into())),
        Type::Bool => Ok(Value::Bool(expect_bool(unknown)?)),
        Type::Color => {
            match unknown.get_type() {
                Ok(ValueType::String) => {
                    let js_string = unknown.coerce_to_string()?;
                    return string_to_brush(js_string);
                }
                Ok(ValueType::Object) => {
                    let obj = unknown.coerce_to_object()?;
                    if let Some(direct_brush) =
                        obj.get::<ExternalRef<Brush>>("brush").ok().flatten()
                    {
                        return Ok(Value::Brush(direct_brush.color().into()));
                    }
                    return brush_from_color(obj);
                }
                _ => {}
            }
            Err(napi::Error::from_reason(
                            "Cannot convert object to brush, because the given object is neither a brush, color, nor a string".to_string()
                    ))
        }
        Type::Brush => {
            match unknown.get_type() {
                Ok(ValueType::String) => {
                    let js_string = unknown.coerce_to_string()?;
                    return string_to_brush(js_string);
                }
                Ok(ValueType::Object) => {
                    let obj = unknown.coerce_to_object()?;
                    // this is used to make the color property of the `Brush` interface optional.
                    let properties = obj.get_property_names()?;
                    if properties.get_array_length()? == 0 {
                        return Ok(Value::Brush(Brush::default()));
                    }
                    if let Some(color) = obj.get::<RgbaColor>("color").ok().flatten() {
                        if color.red() < 0.
                            || color.green() < 0.
                            || color.blue() < 0.
                            || color.alpha() < 0.
                        {
                            return Err(napi::Error::from_reason(
                                "A channel of Color cannot be negative",
                            ));
                        }

                        return Ok(Value::Brush(Brush::SolidColor(Color::from_argb_u8(
                            color.alpha() as u8,
                            color.red() as u8,
                            color.green() as u8,
                            color.blue() as u8,
                        ))));
                    } else {
                        return brush_from_color(obj);
                    }
                }
                _ => {}
            }
            Err(napi::Error::from_reason(
                            "Cannot convert object to brush, because the given object is neither a brush, color, nor a string".to_string()
                    ))
        }
        Type::Image => {
            let object = unknown.coerce_to_object()?;
            if let Some(direct_image) = object.get::<ExternalRef<Image>>("image").ok().flatten() {
                Ok(Value::Image((*direct_image).clone()))
            } else {
                let get_size_prop = |name: &str| {
                    object
                    .get::<Unknown>(name)
                    .ok()
                    .flatten()
                    .and_then(|p| {
                        p.coerce_to_number().ok()
                            .and_then(|number| number.get_int64().ok())
                            .and_then(|i64_num| i64_num.try_into().ok())
                    })
                    .ok_or_else(
                        || napi::Error::from_reason(
                            format!("Cannot convert object to image, because the provided object does not have an u32 `{name}` property")
                    ))
                };

                fn try_convert_image<BufferType: AsRef<[u8]> + FromNapiValue>(
                    object: &Object,
                    width: u32,
                    height: u32,
                ) -> Result<SharedPixelBuffer<Rgba8Pixel>> {
                    let buffer =
                        object.get::<BufferType>("data").ok().flatten().ok_or_else(|| {
                            napi::Error::from_reason(
                                "data property does not have suitable array buffer type"
                                    .to_string(),
                            )
                        })?;
                    const BPP: usize = core::mem::size_of::<Rgba8Pixel>();
                    let actual_size = buffer.as_ref().len();
                    let expected_size: usize = (width as usize) * (height as usize) * BPP;
                    if actual_size != expected_size {
                        return Err(napi::Error::from_reason(format!(
                            "data property does not have the correct size; expected {width} (width) * {height} (height) * {BPP} = {actual_size}; got {expected_size}"
                        )));
                    }

                    Ok(SharedPixelBuffer::clone_from_slice(buffer.as_ref(), width, height))
                }

                let width: u32 = get_size_prop("width")?;
                let height: u32 = get_size_prop("height")?;

                let pixel_buffer =
                    try_convert_image::<Uint8ClampedArray>(&object, width, height)
                        .or_else(|_| try_convert_image::<Buffer>(&object, width, height))?;

                Ok(Value::Image(Image::from_rgba8(pixel_buffer)))
            }
        }
        Type::Struct(s) => {
            let js_object = unknown.coerce_to_object()?;

            Ok(Value::Struct(
                s.fields
                    .iter()
                    .map(|(pro_name, pro_ty)| {
                        let prop: Unknown =
                            js_object.get_named_property(&pro_name.replace('-', "_"))?;
                        let prop_value = if prop.get_type()? == napi::ValueType::Undefined {
                            slint_interpreter::default_value_for_struct_field(s, pro_name)
                        } else {
                            to_value(env, prop, pro_ty, anchor_owner)?
                        };
                        Ok((pro_name.to_string(), prop_value))
                    })
                    .collect::<Result<_, _>>()?,
            ))
        }
        Type::Array(a) => {
            if unknown.is_array()? {
                let array = Array::from_unknown(unknown)?;
                let mut vec = Vec::new();

                for i in 0..array.len() {
                    vec.push(to_value(
                        env,
                        array.get(i)?.ok_or(napi::Error::from_reason(format!(
                            "Cannot access array element at index {i}"
                        )))?,
                        a,
                        anchor_owner,
                    )?);
                }
                Ok(Value::Model(ModelRc::new(SharedVectorModel::from(SharedVector::from_slice(
                    &vec,
                )))))
            } else {
                let obj = unknown.coerce_to_object()?;
                let rust_model = js_into_rust_model(env, &obj, a, anchor_owner)?;
                Ok(Value::Model(rust_model))
            }
        }
        Type::Enumeration(e) => {
            let js_string = unknown.coerce_to_string()?;
            let value: SmolStr = js_string.into_utf8()?.as_str()?.into();

            if !e.values.contains(&value) {
                return Err(napi::Error::from_reason(format!(
                    "{value} is not a value of enum {}",
                    e.name
                )));
            }

            Ok(Value::EnumerationValue(e.name.to_string(), value.to_string()))
        }
        Type::Keys => {
            let keys_instance: ClassInstance<SlintKeys> = ClassInstance::from_unknown(unknown)?;
            Ok(Value::Keys(keys_instance.inner.clone()))
        }
        Type::DataTransfer => {
            let instance: ClassInstance<SlintDataTransfer> = ClassInstance::from_unknown(unknown)?;
            instance.pin_user_data_on(env, anchor_owner)?;
            Ok(Value::DataTransfer(instance.inner.clone()))
        }
        Type::Invalid
        | Type::Model
        | Type::Void
        | Type::InferredProperty
        | Type::InferredCallback
        | Type::Function { .. }
        | Type::Callback { .. }
        | Type::ComponentFactory
        | Type::Easing
        | Type::PathData
        | Type::LayoutCache
        | Type::ArrayOfU16
        | Type::ElementReference
        | Type::MouseCursor
        | Type::Closure => Err(napi::Error::from_reason("reason")),
        Type::StyledText => {
            let obj = unknown.coerce_to_object()?;
            let styled_instance: ClassInstance<SlintStyledText> =
                ClassInstance::from_unknown(obj.into_unknown(env)?)?;
            Ok(Value::StyledText(styled_instance.inner.clone()))
        }
    }
}

fn string_to_brush(js_string: napi::JsString<'_>) -> Result<Value> {
    let string = js_string.into_utf8()?.as_str()?.to_string();

    let c = string
        .parse::<css_color_parser2::Color>()
        .map_err(|_| napi::Error::from_reason(format!("Could not convert {string} to Brush.")))?;

    Ok(Value::Brush(Brush::from(Color::from_argb_u8((c.a * 255.) as u8, c.r, c.g, c.b))))
}

fn brush_from_color(rgb_color: Object) -> Result<Value> {
    let red: f64 =
        rgb_color.get("red")?.ok_or(napi::Error::from_reason("Property red is missing"))?;
    let green: f64 =
        rgb_color.get("green")?.ok_or(napi::Error::from_reason("Property green is missing"))?;
    let blue: f64 =
        rgb_color.get("blue")?.ok_or(napi::Error::from_reason("Property blue is missing"))?;
    let alpha: f64 = rgb_color.get("alpha")?.unwrap_or(255.);

    if red < 0. || green < 0. || blue < 0. || alpha < 0. {
        return Err(napi::Error::from_reason("A channel of Color cannot be negative"));
    }

    Ok(Value::Brush(Brush::SolidColor(Color::from_argb_u8(
        alpha as u8,
        red as u8,
        green as u8,
        blue as u8,
    ))))
}
