//! Implements just enough of a JSON schema for what `ocpi-tariffs` crates requires.
//!
//! Namely, detecting that a given field is allowed be the given spec.
//! If the field is not known to the spec it will be added to an `unexpected_fields` list in a
//! report associated with the given function.
//!
//! The schema does not enforce cardinality or optional elements.
//! It's up to the processing function to enforce requirements of each `Element`.

#[cfg(test)]
mod test_json_schema_macro;

use std::{collections::BTreeMap, sync::Arc};

pub type Fields = Arc<BTreeMap<&'static str, Option<Arc<Element>>>>;

/// An element generated by the `json_schema` macro.
///
/// This is used to express the static schema that is given to a function that processes JSON.
/// The `json::parse_with_schema` fn converts these nodes into dynamic/stateful `Expect` nodes
/// that are updated as the JSON is parsed.
#[derive(Debug)]
pub enum Element {
    /// Any scalar value is expected.
    ///
    /// A scalar value if defined as any JSON value that is not a compound object such as an
    /// `Array` or an `Object`. This is only used when representing an `Array` of scalars as
    /// opposed to an `Array` of `Object`s.
    Scalar,

    /// An `Array` of a given type (Element) is expected.
    ///
    /// An `Array` of `Array`s is not supported or required.
    Array(Arc<Element>),

    /// An `Object` is expected.
    Object(Fields),
}

impl Element {
    /// Convert the `Element` to a dynamic `Expect` ready for parsing updates.
    pub fn to_expectation(&self) -> Expect {
        match self {
            Element::Scalar => Expect::Scalar,
            Element::Array(element) => Expect::Array(Arc::clone(element)),
            Element::Object(fields) => Expect::Object(Arc::clone(fields)),
        }
    }
}

/// A dynamic schema expectation that's updated as the `json::parse_with_schema` fn parses the given JSON.
#[derive(Debug)]
pub enum Expect {
    /// A scalar value was expected and has been found.
    Scalar,

    /// An `Array` `Element` was expected and has been found.
    Array(Arc<Element>),

    /// An `Object` `Element` was expected and has been found.
    Object(Fields),

    /// A scalar value was expected and has not been found.
    UnmatchedScalar,

    /// An `Array` `Element` was expected and has not been found.
    UnmatchedArray,

    /// An `Object` `Element` was expected and has not been found.
    UnmatchedObject,

    /// The current JSON `Element` is outside of the schema: There is no need to try match the `Element`.
    ///
    /// If the current JSON `Element` is outside the schema and the parser descends into a compound
    /// object, then an `OutOfSchema` node is pushed on to the `expect` stack.
    OutOfSchema,
}

#[macro_export]
#[doc(hidden)]
macro_rules! json_schema {
    //////////////////////////////////////////////////////////////////////////
    // TT muncher for parsing the inside of an array [...].
    // A schema array is allowed to contain scalar values `[]` or a single type of `Object` `[{}]`.
    // Array's of arrays are not supported and not required.
    //
    // Must be invoked as: json_schema!(@array [] $($tt)*)
    //////////////////////////////////////////////////////////////////////////

    // Next element is a map.
    (@array [$($elems:expr)*] {$($map:tt)*} $($rest:tt)*) => {
        std::sync::Arc::new({$crate::json::schema::Element::Object({
            let mut object = std::collections::BTreeMap::new();
            $crate::json_schema!(@object object () ($($map)+) ($($map)+));
            std::sync::Arc::new(object)
        })})
    };

    //////////////////////////////////////////////////////////////////////////
    // TT muncher for parsing the inside of an object {...}. Each entry is
    // inserted into the given map variable.
    //
    // Must be invoked as: json_schema!(@object $map () ($($tt)*) ($($tt)*))
    //
    // We require two copies of the input tokens so that we can match on one
    // copy and trigger errors on the other copy.
    //////////////////////////////////////////////////////////////////////////

    // Done.
    (@object $object:ident () () ()) => {};

    // Insert the current entry followed by trailing comma.
    (@object $object:ident [$($key:tt)+] , $($rest:tt)*) => {
        let _unused = $object.insert(($($key:tt)+).into(), None);
        $crate::json_schema!(@object $object () ($($rest)*) ($($rest)*));
    };

    // Insert the current entry followed by trailing comma.
    (@object $object:ident [$($key:tt)+] ($value:expr) , $($rest:tt)*) => {
        let _unused = $object.insert(($($key)+).into(), Some(std::sync::Arc::new($value)));
        $crate::json_schema!(@object $object () ($($rest)*) ($($rest)*));
    };

    // Insert the current entry.
    (@object $object:ident [$($key:tt)+] ($value:expr)) => {
        let _unused = $object.insert(($($key)+).into(), Some(std::sync::Arc::new($value)));
        $crate::json_schema!(@object $object () () ());
    };

    (@object $object:ident [$($key:tt)+] (, $($rest:tt)*)) => {
        let _unused = $object.insert(($($key)+).into(), None);
        $crate::json_schema!(@object $object () ($($rest)*) ($($rest)*));
    };

    // Insert the last entry without trailing comma.
    (@object $object:ident [$($key:tt)+] ()) => {
        let _unused = $object.insert(($($key)+).into(), None);
    };

    // Next value is an array.
    (@object $object:ident ($($key:tt)+) (: [$($array:tt)*] $($rest:tt)*) $copy:tt) => {
        $crate::json_schema!(@object $object [$($key)+] ($crate::json_schema!([$($array)*])) $($rest)*);
    };

    // Next value is a map.
    (@object $object:ident ($($key:tt)+) (: {$($map:tt)*} $($rest:tt)*) $copy:tt) => {
        $crate::json_schema!(@object $object [$($key)+] ($crate::json_schema!({$($map)*})) $($rest)*);
    };

    // Next value is an expression followed by comma.
    (@object $object:ident ($($key:tt)+) (: $value:expr , $($rest:tt)*) $copy:tt) => {
        $crate::json_schema!(@object $object [$($key)+] ($crate::json_schema!($value)) , $($rest)*);
    };

    // Next value is key followed by comma.
    (@object $object:ident ($($key:tt)+) (, $($rest:tt)*) $copy:tt) => {
        let _unused = $object.insert(($($key)+).into(), None);
        $crate::json_schema!(@object $object [$($key)+] (, $($rest)*));
    };

    // Next value is key followed by comma.
    (@object $object:ident ($($key:tt)+) ($($rest:tt)*) $copy:tt) => {
        let __unused = $object.insert(($($key)+).into(), None);
        $crate::json_schema!(@object $object [$($key)+] ($($rest)*));
    };

    // Munch a token into the current key.
    (@object $object:ident ($($key:tt)*) ($json:tt $($rest:tt)*) $copy:tt) => {
        $crate::json_schema!(@object $object ($($key)* $json) ($($rest)*) ($($rest)*));
    };

    //////////////////////////////////////////////////////////////////////////
    // The entry points.
    //////////////////////////////////////////////////////////////////////////

    ([]) => {
        $crate::json::schema::Element::Array(std::sync::Arc::new($crate::json::schema::Element::Scalar))
    };

    ([ $($json:tt)+ ]) => {
        $crate::json::schema::Element::Array($crate::json_schema!(@array [] $($json)+))
    };

    ({}) => {
        $crate::json::schema::Element::Object(std::sync::Arc::new(std::collections::BTreeMap::new()))
    };

    ({ $($json:tt)+ }) => {
        $crate::json::schema::Element::Object({
            let mut object = std::collections::BTreeMap::new();
            $crate::json_schema!(@object object () ($($json)+) ($($json)+));
            std::sync::Arc::new(object)
        })
    };

    // Any Serialize type: numbers, strings, struct literals, variables etc.
    // Must be below every other rule.
    ($other:expr) => {
        $other
    };

}
#[macro_export]
#[doc(hidden)]
macro_rules! json_unexpected {
    () => {};
}

#[macro_export]
#[doc(hidden)]
macro_rules! json_expect_expr_comma {
    ($e:expr , $($tt:tt)*) => {};
}
