use crate::gen::SchemaGenerator;
use crate::schema::{InstanceType, ObjectValidation, Schema, SchemaObject};
use crate::{JsonSchema, Map, Set};
use serde::Serialize;
use serde_json::Value;

// Helper for generating schemas for flattened `Option` fields.
pub fn json_schema_for_flatten<T: ?Sized + JsonSchema>(
    gen: &mut SchemaGenerator,
    required: bool,
) -> Schema {
    let mut schema = T::_schemars_private_non_optional_json_schema(gen);

    if T::_schemars_private_is_option() && !required {
        if let Schema::Object(SchemaObject {
            object: Some(ref mut object_validation),
            ..
        }) = schema
        {
            object_validation.required.clear();
        }
    }

    schema
}

/// Hack to simulate specialization:
/// `MaybeSerializeWrapper(x).maybe_to_value()` will resolve to either
/// - The inherent method `MaybeSerializeWrapper::maybe_to_value(...)` if x is `Serialize`
/// - The trait method `NoSerialize::maybe_to_value(...)` from the blanket impl otherwise
#[doc(hidden)]
#[macro_export]
macro_rules! _schemars_maybe_to_value {
    ($expression:expr) => {{
        #[allow(unused_imports)]
        use $crate::_private::{MaybeSerializeWrapper, NoSerialize as _};

        MaybeSerializeWrapper($expression).maybe_to_value()
    }};
}

pub struct MaybeSerializeWrapper<T>(pub T);

pub trait NoSerialize: Sized {
    fn maybe_to_value(self) -> Option<Value> {
        None
    }
}

impl<T> NoSerialize for T {}

impl<T: Serialize> MaybeSerializeWrapper<T> {
    pub fn maybe_to_value(self) -> Option<Value> {
        serde_json::value::to_value(self.0).ok()
    }
}

/// Create a schema for a unit enum
pub fn new_unit_enum(variant: &str) -> Schema {
    Schema::Object(SchemaObject {
        instance_type: Some(InstanceType::String.into()),
        enum_values: Some(vec![variant.into()]),
        ..SchemaObject::default()
    })
}

/// Create a schema for an externally tagged enum
pub fn new_externally_tagged_enum(variant: &str, sub_schema: Schema) -> Schema {
    Schema::Object(SchemaObject {
        instance_type: Some(InstanceType::Object.into()),
        object: Some(Box::new(ObjectValidation {
            properties: {
                let mut props = Map::new();
                props.insert(variant.to_owned(), sub_schema);
                props
            },
            required: {
                let mut required = Set::new();
                required.insert(variant.to_owned());
                required
            },
            // Externally tagged variants must prohibit additional
            // properties irrespective of the disposition of
            // `deny_unknown_fields`. If additional properties were allowed
            // one could easily construct an object that validated against
            // multiple variants since here it's the properties rather than
            // the values of a property that distingish between variants.
            additional_properties: Some(Box::new(false.into())),
            ..Default::default()
        })),
        ..SchemaObject::default()
    })
}

/// Create a schema for an internally tagged enum
pub fn new_internally_tagged_enum(
    tag_name: &str,
    variant: &str,
    deny_unknown_fields: bool,
) -> Schema {
    let tag_schema = Schema::Object(SchemaObject {
        instance_type: Some(InstanceType::String.into()),
        enum_values: Some(vec![variant.into()]),
        ..Default::default()
    });
    Schema::Object(SchemaObject {
        instance_type: Some(InstanceType::Object.into()),
        object: Some(Box::new(ObjectValidation {
            properties: {
                let mut props = Map::new();
                props.insert(tag_name.to_owned(), tag_schema);
                props
            },
            required: {
                let mut required = Set::new();
                required.insert(tag_name.to_owned());
                required
            },
            additional_properties: deny_unknown_fields.then(|| Box::new(false.into())),
            ..Default::default()
        })),
        ..SchemaObject::default()
    })
}

pub fn insert_object_property<T: ?Sized + JsonSchema>(
    obj: &mut ObjectValidation,
    key: &str,
    has_default: bool,
    required: bool,
    schema: Schema,
) {
    obj.properties.insert(key.to_owned(), schema);
    if !has_default && (required || !T::_schemars_private_is_option()) {
        obj.required.insert(key.to_owned());
    }
}

pub mod metadata {
    use crate::Schema;
    use serde_json::Value;

    macro_rules! add_metadata_fn {
        ($method:ident, $name:ident, $ty:ty) => {
            pub fn $method(schema: Schema, $name: impl Into<$ty>) -> Schema {
                let value = $name.into();
                if value == <$ty>::default() {
                    schema
                } else {
                    let mut schema_obj = schema.into_object();
                    schema_obj.metadata().$name = value.into();
                    Schema::Object(schema_obj)
                }
            }
        };
    }

    add_metadata_fn!(add_description, description, String);
    add_metadata_fn!(add_id, id, String);
    add_metadata_fn!(add_title, title, String);
    add_metadata_fn!(add_deprecated, deprecated, bool);
    add_metadata_fn!(add_read_only, read_only, bool);
    add_metadata_fn!(add_write_only, write_only, bool);
    add_metadata_fn!(add_default, default, Option<Value>);

    pub fn add_examples<I: IntoIterator<Item = Value>>(schema: Schema, examples: I) -> Schema {
        let mut schema_obj = schema.into_object();
        schema_obj.metadata().examples.extend(examples);
        Schema::Object(schema_obj)
    }
}
