// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
use std::collections::BTreeMap;

use rquickjs::{
    atom::PredefinedAtom,
    function::{Constructor, IntoJsFunc},
    object::Property,
    prelude::Func,
    Array, Coerced, Ctx, Error, Exception, FromJs, IntoAtom, IntoJs, Object, Result, Undefined,
    Value,
};

use crate::primordials::{BasePrimordials, Primordial};

pub trait ObjectExt<'js> {
    fn get_optional<K: IntoAtom<'js> + Clone, V: FromJs<'js>>(&self, k: K) -> Result<Option<V>>;
    fn get_required<K: AsRef<str>, V: FromJs<'js>>(
        &self,
        k: K,
        object_name: &'static str,
    ) -> Result<V>;
    fn into_object_or_throw(self, ctx: &Ctx<'js>, object_name: &'static str)
        -> Result<Object<'js>>;
}

impl<'js> ObjectExt<'js> for Object<'js> {
    fn get_optional<K: IntoAtom<'js> + Clone, V: FromJs<'js> + Sized>(
        &self,
        k: K,
    ) -> Result<Option<V>> {
        self.get::<K, Option<V>>(k)
    }

    fn get_required<K: AsRef<str>, V: FromJs<'js>>(
        &self,
        k: K,
        object_name: &'static str,
    ) -> Result<V> {
        let k = k.as_ref();
        self.get::<&str, Option<V>>(k)?.ok_or_else(|| {
            Exception::throw_type(
                self.ctx(),
                &[object_name, " '", k, "' property required"].concat(),
            )
        })
    }

    fn into_object_or_throw(self, _: &Ctx<'js>, _: &'static str) -> Result<Object<'js>> {
        Ok(self)
    }
}

impl<'js> ObjectExt<'js> for Value<'js> {
    fn get_optional<K: IntoAtom<'js> + Clone, V: FromJs<'js>>(&self, k: K) -> Result<Option<V>> {
        if let Some(obj) = self.as_object() {
            return obj.get_optional(k);
        }
        Ok(None)
    }

    fn get_required<K: AsRef<str>, V: FromJs<'js>>(
        &self,
        k: K,
        object_name: &'static str,
    ) -> Result<V> {
        self.as_object()
            .ok_or_else(|| not_a_object_error(self.ctx(), object_name))?
            .get_required(k, object_name)
    }

    fn into_object_or_throw(
        self,
        ctx: &Ctx<'js>,
        object_name: &'static str,
    ) -> Result<Object<'js>> {
        self.into_object()
            .ok_or_else(|| not_a_object_error(ctx, object_name))
    }
}

pub fn not_a_object_error(ctx: &Ctx<'_>, object_name: &str) -> Error {
    Exception::throw_type(ctx, &[object_name, " is not an object"].concat())
}

pub struct Proxy<'js> {
    target: Value<'js>,
    options: Object<'js>,
}

impl<'js> IntoJs<'js> for Proxy<'js> {
    fn into_js(self, ctx: &Ctx<'js>) -> Result<Value<'js>> {
        BasePrimordials::get(ctx)?
            .constructor_proxy
            .construct::<_, Value>((self.target, self.options))
    }
}

impl<'js> Proxy<'js> {
    pub fn new(ctx: Ctx<'js>) -> Result<Self> {
        let options = Object::new(ctx.clone())?;
        Ok(Self {
            target: Undefined.into_value(ctx),
            options,
        })
    }

    pub fn with_target(ctx: Ctx<'js>, target: Value<'js>) -> Result<Self> {
        let options = Object::new(ctx)?;
        Ok(Self { target, options })
    }

    pub fn setter<T, P: 'js>(&self, setter: Func<T, P>) -> Result<()>
    where
        T: IntoJsFunc<'js, P> + 'js,
    {
        self.options.set(PredefinedAtom::Setter, setter)?;
        Ok(())
    }

    pub fn getter<T, P: 'js>(&self, getter: Func<T, P>) -> Result<()>
    where
        T: IntoJsFunc<'js, P> + 'js,
    {
        self.options.set(PredefinedAtom::Getter, getter)?;
        Ok(())
    }
}

pub fn array_to_btree_map<'js>(
    ctx: &Ctx<'js>,
    array: Array<'js>,
) -> Result<BTreeMap<String, Coerced<String>>> {
    let value = object_from_entries(ctx, array)?;
    let value = value.into_value();
    BTreeMap::from_js(ctx, value)
}

pub fn object_from_entries<'js>(ctx: &Ctx<'js>, array: Array<'js>) -> Result<Object<'js>> {
    let obj = Object::new(ctx.clone())?;
    for value in array.into_iter().flatten() {
        if let Some(entry) = value.as_array() {
            if let Ok(key) = entry.get::<Value>(0) {
                if let Ok(value) = entry.get::<Value>(1) {
                    let _ = obj.set(key, value); //ignore result of failed
                }
            }
        }
    }
    Ok(obj)
}

/// Build a constructor that behaves like `class Name extends Parent`
pub fn define_subclass<'js, F, P>(
    ctx: &Ctx<'js>,
    name: &str,
    parent: &Constructor<'js>,
    construct: F,
) -> Result<Constructor<'js>>
where
    F: IntoJsFunc<'js, P> + 'js,
{
    let parent_proto: Object = parent.get(PredefinedAtom::Prototype)?;
    let proto = Object::new(ctx.clone())?;
    proto.set_prototype(Some(&parent_proto))?;
    let constructor = Constructor::new_prototype(ctx, proto, construct)?;
    constructor.set_prototype(parent.as_object())?;
    constructor.prop(PredefinedAtom::Name, Property::from(name).configurable())?;
    Ok(constructor)
}
