/*!
# FFI-functions for generating serde_json objects

This module defines functions for generating opaque serde_json::Value objects in Ocaml
which can then be deserialized with Serde on the Rust side. Signatures of the provided functions:
```Ocaml
type serde_json = nativeint (* This stores pointers, so treat this as an opaque type! *)

external build_null: unit -> serde_json = "rs_build_serde_null"
external build_bool: bool -> serde_json = "rs_build_serde_bool"
external build_number: int -> serde_json = "rs_build_serde_number"
external build_string: string -> serde_json = "rs_build_serde_string"
external build_array: serde_json list -> serde_json = "rs_build_serde_array_from_list"
external build_object: (string * serde_json) list -> serde_json = "rs_build_serde_object"
external build_bitvector: string -> serde_json = "rs_build_serde_bitvector"

(* Convert a serde_json object to a json string (used for unit tests). *)
external to_string: serde_json -> string = "rs_convert_json_to_string"
```
*/

use super::OcamlSendable;
use ocaml::{FromValue, ToValue};
use std::rc::Rc;
use std::str::FromStr;

use super::failwith_on_panic;

/// A builder type for serde_json::Value objects.
///
/// Hiding the recursive nature of the data type behind reference counts prevents unneccessary
/// deep copies when creating json objects from Ocaml, which would lead to a runtime quadratic in the size of the json object.
/// However, when converting to serde_json::Value, one deep copy is still necessary.
#[derive(Clone, Debug)]
pub enum JsonBuilder {
    Null,
    Bool(bool),
    Number(isize),
    PositiveNumber(u64), // currently used only for deserialization of bitvector
    String(String),
    Array(Vec<Rc<JsonBuilder>>),
    Object(Vec<(String, Rc<JsonBuilder>)>),
}

impl OcamlSendable for JsonBuilder {}

/// Creating a serde_json::Value performing deep copy.
impl From<&JsonBuilder> for serde_json::Value {
    fn from(builder: &JsonBuilder) -> serde_json::Value {
        match builder {
            JsonBuilder::Null => serde_json::Value::Null,
            JsonBuilder::Bool(val) => serde_json::Value::Bool(*val),
            JsonBuilder::Number(val) => serde_json::Value::Number(serde_json::Number::from(*val)),
            JsonBuilder::PositiveNumber(val) => {
                serde_json::Value::Number(serde_json::Number::from(*val))
            }
            JsonBuilder::String(val) => serde_json::Value::String(val.to_string()),
            JsonBuilder::Array(elem_vec) => elem_vec
                .iter()
                .map(|rc_elem| serde_json::Value::from(&**rc_elem))
                .collect(),
            JsonBuilder::Object(tuple_vec) => serde_json::Value::Object(
                tuple_vec
                    .iter()
                    .map(|(string_ref, json_builder)| {
                        (
                            string_ref.to_string(),
                            serde_json::Value::from(&**json_builder),
                        )
                    })
                    .collect(),
            ),
        }
    }
}

caml!(rs_finalize_json_builder(builder_val) {
    failwith_on_panic( || {
        JsonBuilder::ocaml_finalize(builder_val);
        ocaml::Value::unit()
    })
});

/// Build JsonBuilder::Null as Ocaml value
fn build_serde_null() -> ocaml::Value {
    JsonBuilder::Null.to_ocaml()
}

caml!(rs_build_serde_null(_unit) {
    failwith_on_panic( || {
        build_serde_null()
    })
});

/// Build JsonBuilder::Bool as Ocaml value
fn build_serde_bool(bool_val: ocaml::Value) -> ocaml::Value {
    let boolean: bool = bool::from_value(bool_val);
    JsonBuilder::Bool(boolean).to_ocaml()
}

caml!(rs_build_serde_bool(bool_val) {
    failwith_on_panic( || {
        build_serde_bool(bool_val)
    })
});

/// Build JsonBuilder::Number as Ocaml value
fn build_serde_number(num: ocaml::Value) -> ocaml::Value {
    let num: isize = ocaml::Value::isize_val(&num);
    JsonBuilder::Number(num).to_ocaml()
}

caml!(rs_build_serde_number(number) {
    failwith_on_panic( || {
        build_serde_number(number)
    })
});

/// Build JsonBuilder::Object representing a bitvector from a string generated by `Bitvector.to_string` in Ocaml
fn build_serde_bitvector(bitvector_string_val: ocaml::Value) -> ocaml::Value {
    let string = <&str>::from_value(bitvector_string_val);
    let elements: Vec<&str> = string.split(':').collect();
    let width = usize::from_str(&elements[1][0..(elements[1].len() - 1)])
        .expect("Bitvector width parsing failed");
    assert!(width > 0);

    let mut num_list = Vec::new();
    let mut number_slice: &str = elements[0];
    if number_slice.starts_with("0x") {
        number_slice = &number_slice[2..];
    }
    while !number_slice.is_empty() {
        if number_slice.len() > 16 {
            let digit = u64::from_str_radix(&number_slice[(number_slice.len() - 16)..], 16)
                .expect("Bitvector value parsing failed");
            num_list.push(Rc::new(JsonBuilder::PositiveNumber(digit)));
            number_slice = &number_slice[..(number_slice.len() - 16)];
        } else {
            let digit =
                u64::from_str_radix(&number_slice, 16).expect("Bitvector value parsing failed");
            num_list.push(Rc::new(JsonBuilder::PositiveNumber(digit)));
            number_slice = "";
        };
    }
    while num_list.len() <= (width - 1) / 64 {
        num_list.push(Rc::new(JsonBuilder::PositiveNumber(0)));
    }
    num_list.reverse(); // since the digits were parsed in reverse order

    let mut width_list = Vec::new();
    width_list.push(Rc::new(JsonBuilder::Number(width as isize)));
    let result = JsonBuilder::Object(vec![
        ("digits".to_string(), Rc::new(JsonBuilder::Array(num_list))),
        ("width".to_string(), Rc::new(JsonBuilder::Array(width_list))),
    ]);

    result.to_ocaml()
}

caml!(rs_build_serde_bitvector(bitvector_string) {
    failwith_on_panic( || {
        build_serde_bitvector(bitvector_string)
    })
});

/// Build JsonBuilder::String as Ocaml value
fn build_serde_string(string_val: ocaml::Value) -> ocaml::Value {
    let string = String::from_value(string_val);
    JsonBuilder::String(string).to_ocaml()
}

caml!(rs_build_serde_string(string_val) {
    failwith_on_panic( || {
        build_serde_string(string_val)
    })
});

/// Build JsonBuilder::Array as Ocaml value from an Ocaml list
fn build_serde_array_from_list(list_val: ocaml::Value) -> ocaml::Value {
    let ocaml_list = ocaml::List::from(list_val);
    let value_vec = ocaml_list.to_vec();
    let vec = value_vec
        .into_iter()
        .map(|ocaml_val| unsafe { JsonBuilder::from_ocaml_rc(&ocaml_val) })
        .collect();
    JsonBuilder::Array(vec).to_ocaml()
}

caml!(rs_build_serde_array_from_list(list_val) {
    failwith_on_panic( || {
        build_serde_array_from_list(list_val)
    })
});

/// Build JsonBuilder::Object as Ocaml value from an Ocaml list of tuples
fn build_serde_object(tuple_list_val: ocaml::Value) -> ocaml::Value {
    let ocaml_list = ocaml::List::from(tuple_list_val);
    let pairs_vec = ocaml_list.to_vec();
    let pairs = pairs_vec
        .into_iter()
        .map(|ocaml_tuple| {
            let tuple = ocaml::Tuple::from(ocaml_tuple);
            let key_ocaml = tuple
                .get(0)
                .expect("Error: Ocaml tuple contains no element");
            let key = String::from_value(key_ocaml);
            let value_ocaml: ocaml::Value = tuple
                .get(1)
                .expect("Error: Ocaml tuple contains not enough elements");
            let data = unsafe { JsonBuilder::from_ocaml_rc(&value_ocaml) };
            (key, data)
        })
        .collect();
    JsonBuilder::Object(pairs).to_ocaml()
}

caml!(rs_build_serde_object(tuple_list_val) {
    failwith_on_panic( || {
        build_serde_object(tuple_list_val)
    })
});

/// Get the Json string corresponding to a JsonBuilder object and return it as an Ocaml value.
fn get_json_string(builder_val: ocaml::Value) -> ocaml::Value {
    let builder = unsafe { JsonBuilder::from_ocaml(&builder_val) };
    let json_string = serde_json::Value::from(builder).to_string();
    ocaml::Str::from(&json_string as &str).to_value()
}

caml!(rs_convert_json_to_string(builder_val) {
    failwith_on_panic( || {
        get_json_string(builder_val)
    })
});
