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

use llrt_utils::{
    clone::clone_platform_object,
    hash,
    object::ObjectExt,
    primordials::{BasePrimordials, Primordial},
};
use rquickjs::{
    atom::PredefinedAtom,
    function::{Constructor, Opt, This},
    Array, ArrayBuffer, Ctx, Exception, Function, IntoJs, Null, Object, Result, Type, Value,
};

use crate::modules::crypto::CryptoKey;

#[derive(Debug)]
enum StackItem<'js> {
    Value(usize, Value<'js>, Option<String>, Option<usize>),
    ObjectEnd,
}

#[derive(Debug)]
enum ObjectType {
    Set,
    Map,
}

#[derive(Debug)]
enum TapeValue<'js> {
    Array(Array<'js>),
    Object(Object<'js>),
    Value(Value<'js>),
    Collection(Option<Value<'js>>, ObjectType),
}

#[derive(Debug)]
struct TapeItem<'js> {
    parent: usize,
    object_key: Option<String>,
    array_index: Option<usize>,
    value: TapeValue<'js>,
}

pub fn structured_clone<'js>(
    ctx: &Ctx<'js>,
    value: Value<'js>,
    options: Opt<Object<'js>>,
) -> Result<Value<'js>> {
    let primordials = BasePrimordials::get(ctx)?;
    let mut transfer_set = None;

    if let Some(options) = options.0 {
        if let Some(transfer_array) = options.get_optional::<_, Array>("transfer")? {
            let mut set = HashSet::with_capacity(transfer_array.len());

            for item in transfer_array.iter::<Value>() {
                set.insert(item?);
            }
            transfer_set = Some(set);
        }
    }

    let mut tape = Vec::<TapeItem>::with_capacity(10);
    let mut stack = Vec::with_capacity(10);
    let mut visited = Vec::<(usize, usize)>::with_capacity(10);
    let mut index = 0usize;

    stack.push(StackItem::Value(0, value, None, None));

    while let Some(item) = stack.pop() {
        match item {
            StackItem::Value(parent, value, mut object_key, array_index) => {
                if let Some(set) = &transfer_set {
                    if let Some(value) = set.get(&value) {
                        append_transfer_value(&mut tape, value, parent, object_key, array_index)?;
                        index += 1;
                        continue;
                    }
                }
                match value.type_of() {
                    Type::Proxy => {
                        return Err(Exception::throw_type(
                            ctx,
                            "A Proxy value could not be cloned",
                        ));
                    },
                    Type::Object => {
                        if check_circular(
                            &mut tape,
                            &mut visited,
                            &value,
                            parent,
                            &mut object_key,
                            array_index,
                            index,
                        ) {
                            index += 1;
                            continue;
                        }

                        //unsafe OK since we're guaranteed to be object by the match
                        let object = unsafe { value.as_object().unwrap_unchecked() };

                        if let Some(value) = try_clone_platform_object(ctx, object)? {
                            tape.push(TapeItem {
                                parent,
                                object_key,
                                array_index,
                                value: TapeValue::Value(value),
                            });
                            index += 1;
                            continue;
                        }

                        if object.is_instance_of(&primordials.constructor_date) {
                            append_ctor_value(
                                &mut tape,
                                object,
                                &primordials.constructor_date,
                                parent,
                                object_key,
                                array_index,
                            )?;
                            index += 1;
                            continue;
                        }

                        if object.is_instance_of(&primordials.constructor_regexp) {
                            append_ctor_value(
                                &mut tape,
                                object,
                                &primordials.constructor_regexp,
                                parent,
                                object_key,
                                array_index,
                            )?;
                            index += 1;
                            continue;
                        }

                        let is_collection = if object.is_instance_of(&primordials.constructor_set) {
                            Some(ObjectType::Set)
                        } else if object.is_instance_of(&primordials.constructor_map) {
                            Some(ObjectType::Map)
                        } else {
                            None
                        };

                        if let Some(collection_type) = is_collection {
                            append_collection(
                                &mut tape,
                                &primordials.function_array_from,
                                object,
                                parent,
                                object_key,
                                array_index,
                                collection_type,
                                &mut stack,
                                index,
                            )?;

                            index += 1;
                            continue;
                        }

                        if primordials
                            .function_array_buffer_is_view
                            .call::<_, bool>((value.clone(),))?
                        {
                            append_buffer(&mut tape, object, parent, object_key, array_index)?;
                            index += 1;
                            continue;
                        }

                        let new: Object<'_> =
                            if object.is_instance_of(&primordials.constructor_error) {
                                primordials.constructor_error.construct(("",))
                            } else {
                                Object::new(ctx.clone())
                            }?;

                        tape.push(TapeItem {
                            parent,
                            object_key,
                            array_index,
                            value: TapeValue::Object(new),
                        });
                        stack.push(StackItem::ObjectEnd);

                        for key in object.keys::<String>() {
                            let key = key?;
                            let value = object.get(&key)?;
                            stack.push(StackItem::Value(index, value, Some(key), None));
                        }
                    },
                    Type::Array => {
                        if check_circular(
                            &mut tape,
                            &mut visited,
                            &value,
                            parent,
                            &mut object_key,
                            array_index,
                            index,
                        ) {
                            index += 1;
                            continue;
                        }
                        let new = Array::new(ctx.clone())?;
                        tape.push(TapeItem {
                            parent,
                            object_key,
                            array_index,
                            value: TapeValue::Array(new),
                        });
                        stack.push(StackItem::ObjectEnd);
                        //unsafe OK since we're guaranteed to be object by the match
                        let array = unsafe { value.as_array().unwrap_unchecked() };

                        //reverse for loop of items in array
                        for array_index in (0usize..array.len()).rev() {
                            stack.push(StackItem::Value(
                                index,
                                array.get(array_index)?,
                                None,
                                Some(array_index),
                            ));
                        }
                    },
                    _ => {
                        tape.push(TapeItem {
                            parent,
                            object_key,
                            array_index,
                            value: TapeValue::Value(value),
                        });
                    },
                }
                index += 1;
            },
            StackItem::ObjectEnd => {
                visited.pop();
            },
        }
    }

    while let Some(item) = tape.pop() {
        let value = match item.value {
            TapeValue::Array(array) => array.into_value(),
            TapeValue::Object(object) => object.into_value(),
            TapeValue::Value(value) => value,
            TapeValue::Collection(mut value, _) => value.take().unwrap(),
        };
        if tape.is_empty() {
            return Ok(value);
        }
        let parent = &mut tape[item.parent];
        let array_index = item.array_index;
        let object_key = item.object_key;
        match &mut parent.value {
            TapeValue::Array(array) => {
                array.set(array_index.unwrap(), value)?;
            },
            TapeValue::Object(object) => {
                let string = object_key.unwrap();
                object.set(string, value)?;
            },
            TapeValue::Collection(collection_value, collection_type) => {
                match collection_type {
                    ObjectType::Set => {
                        collection_value.replace(primordials.constructor_set.construct((value,))?);
                    },
                    ObjectType::Map => {
                        collection_value.replace(primordials.constructor_map.construct((value,))?);
                    },
                };
            },
            _ => {},
        };
    }

    Null.into_js(ctx)
}

#[inline(always)]
#[cold]
fn append_buffer<'js>(
    tape: &mut Vec<TapeItem<'js>>,
    object: &Object<'js>,
    parent: usize,
    object_key: Option<String>,
    array_index: Option<usize>,
) -> Result<()> {
    let ctor: Constructor = object.get(PredefinedAtom::Constructor)?;
    let slice: Function = object.get("slice")?;
    let clone: Value = slice.call((This(object.clone()),))?;
    let new = ctor.construct((clone,))?;
    tape.push(TapeItem {
        parent,
        object_key,
        array_index,
        value: TapeValue::Value(new),
    });
    Ok(())
}

#[inline(always)]
#[cold]
#[allow(clippy::too_many_arguments)]
fn append_collection<'js>(
    tape: &mut Vec<TapeItem<'js>>,
    array_from: &Function<'js>,
    object: &Object<'js>,
    parent: usize,
    object_key: Option<String>,
    array_index: Option<usize>,
    collection_type: ObjectType,
    stack: &mut Vec<StackItem<'js>>,
    index: usize,
) -> Result<()> {
    let array: Array = array_from.call((object.clone(),))?;
    tape.push(TapeItem {
        parent,
        object_key,
        array_index,
        value: TapeValue::Collection(None, collection_type),
    });
    stack.push(StackItem::ObjectEnd);
    stack.push(StackItem::Value(index, array.into(), None, None));
    Ok(())
}

#[inline(always)]
fn check_circular(
    tape: &mut Vec<TapeItem>,
    visited: &mut Vec<(usize, usize)>,
    value: &Value<'_>,
    parent: usize,
    object_key: &mut Option<String>,
    array_index: Option<usize>,
    index: usize,
) -> bool {
    let hash = hash::default_hash(value);
    if let Some(visited) = visited.iter().find(|v| v.0 == hash) {
        append_circular(tape, visited, object_key, parent, array_index);
        return true;
    }
    visited.push((hash, index));
    false
}

#[inline(always)]
#[cold]
fn append_transfer_value<'js>(
    tape: &mut Vec<TapeItem<'js>>,
    value: &Value<'js>,
    parent: usize,
    object_key: Option<String>,
    array_index: Option<usize>,
) -> Result<()> {
    let value = if let Some(ab) = ArrayBuffer::from_value(value.clone()) {
        ab.get::<_, Function>("transfer")?.call((This(ab),))?
    } else {
        value.clone()
    };

    tape.push(TapeItem {
        parent,
        object_key,
        array_index,
        value: TapeValue::Value(value),
    });
    Ok(())
}

#[inline(always)]
#[cold]
fn append_circular(
    tape: &mut Vec<TapeItem<'_>>,
    visited: &(usize, usize),
    object_key: &mut Option<String>,
    parent: usize,
    array_index: Option<usize>,
) {
    let value = match &tape[visited.1].value {
        TapeValue::Array(array) => array.clone().into_value(),
        TapeValue::Object(object) => object.clone().into_value(),
        TapeValue::Value(value) => value.clone(),
        TapeValue::Collection(value, _) => value.clone().unwrap(),
    };

    let object_key = object_key.take();

    tape.push(TapeItem {
        parent,
        object_key,
        array_index,
        value: TapeValue::Value(value),
    });
}

#[inline(always)]
#[cold]
fn append_ctor_value<'js>(
    tape: &mut Vec<TapeItem<'js>>,
    object: &Object<'js>,
    ctor: &Constructor<'js>,
    parent: usize,
    object_key: Option<String>,
    array_index: Option<usize>,
) -> Result<()> {
    let clone: Value = ctor.construct((object.clone(),))?;
    tape.push(TapeItem {
        parent,
        object_key,
        array_index,
        value: TapeValue::Value(clone),
    });
    Ok(())
}

fn try_clone_platform_object<'js>(
    ctx: &Ctx<'js>,
    object: &Object<'js>,
) -> Result<Option<Value<'js>>> {
    if let Some(value) = clone_platform_object::<CryptoKey>(ctx, object)? {
        return Ok(Some(value));
    }
    Ok(None)
}

#[cfg(test)]
mod tests {

    use llrt_test::test_sync_with;
    use rquickjs::{function::Opt, Object, Value};

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

    use super::structured_clone;

    #[tokio::test]
    async fn clone() {
        test_sync_with(|ctx| {
            BasePrimordials::init(&ctx)?;
            let value: Object = ctx.eval(
                r#"
const a = {
   "foo":{
      "bar":"baz"
   },
   "foo1":{
      "bar1":"baz1",
      "bar11":"baz11"
   }
};
a
"#,
            )?;

            let cloned = structured_clone(&ctx, value.clone().into_value(), Opt(None))?
                .into_object()
                .unwrap();

            let json = ctx
                .json_stringify(value.clone())?
                .unwrap()
                .to_string()?
                .to_string();

            let clone_json = ctx
                .json_stringify(cloned.clone())?
                .unwrap()
                .to_string()?
                .to_string();

            assert_eq!(json, clone_json);

            assert_ne!(
                value.get::<_, Value>("foo")?,
                cloned.get::<_, Value>("foo")?
            );

            Ok(())
        })
        .await
    }

    #[tokio::test]
    async fn clone_circular() {
        test_sync_with(|ctx| {
            BasePrimordials::init(&ctx)?;
            let _value: Object = ctx.eval(
                r#"
const originalObject = { foo: { bar: "baz",arr: [1,2,3] }  };
originalObject.foo.circularRef = originalObject;
originalObject.foo.circularRef2 = originalObject;
originalObject.foo.circularRef3 = originalObject.foo;
originalObject.ref2 = originalObject;
"#,
            )?;

            Ok(())
        })
        .await
    }
}
