//! Mapping for the Elasticsearch `keyword` type.

use std::collections::BTreeMap;
use serde::{Serialize, Serializer};
use serde::ser::SerializeStruct;
use ::field::{FieldMapping, SerializeField, Field};
use ::string::mapping::{StringField, IndexOptions};

/// Elasticsearch datatype name.
pub const KEYWORD_DATATYPE: &'static str = "keyword";

#[doc(hidden)]
#[derive(Default)]
pub struct KeywordFormat;

/// The base requirements for mapping a `string` type.
///
/// Custom mappings can be defined by implementing `KeywordMapping`.
///
/// # Examples
///
/// Define a custom `KeywordMapping`:
///
/// ## Derive Mapping
///
/// ```
/// # #[macro_use]
/// # extern crate elastic_types;
/// # extern crate serde;
/// # use elastic_types::prelude::*;
/// #[derive(Default)]
/// struct MyStringMapping;
/// impl KeywordMapping for MyStringMapping {
///     //Overload the mapping functions here
///     fn boost() -> Option<f32> {
///         Some(1.5)
///     }
/// }
/// # fn main() {}
/// ```
///
/// This will produce the following mapping:
///
/// ```
/// # #[macro_use]
/// # extern crate json_str;
/// # #[macro_use]
/// # extern crate elastic_types;
/// # extern crate serde;
/// # #[cfg(feature = "nightly")]
/// # extern crate serde_json;
/// # use elastic_types::prelude::*;
/// # #[derive(Default)]
/// # struct MyStringMapping;
/// # impl KeywordMapping for MyStringMapping {
/// #     //Overload the mapping functions here
/// #     fn boost() -> Option<f32> {
/// #         Some(1.5)
/// #     }
/// # }
/// # fn main() {
/// # let json = json_str!(
/// {
///     "type": "keyword",
///     "boost": 1.5
/// }
/// # );
/// # #[cfg(feature = "nightly")]
/// # let mapping = serde_json::to_string(&Field::from(MyStringMapping)).unwrap();
/// # #[cfg(not(feature = "nightly"))]
/// # let mapping = json.clone();
/// # assert_eq!(json, mapping);
/// # }
/// ```
pub trait KeywordMapping
    where Self: Default
{
    /// The analyzer which should be used for analyzed string fields,
    /// both at index-time and at search-time (unless overridden by the `search_analyzer`).
    /// Defaults to the default index analyzer, or the `standard` analyzer.
    fn analyzer() -> Option<&'static str> {
        None
    }

    /// Field-level index time boosting. Accepts a floating point number, defaults to `1.0`.
    fn boost() -> Option<f32> {
        None
    }

    /// Should the field be stored on disk in a column-stride fashion,
    /// so that it can later be used for sorting, aggregations, or scripting?
    /// Accepts `true` (default) or `false`.
    fn doc_values() -> Option<bool> {
        None
    }

    /// Should global ordinals be loaded eagerly on refresh?
    /// Accepts `true` or `false` (default).
    /// Enabling this is a good idea on fields that are frequently used for (significant) terms aggregations.
    fn eager_global_ordinals() -> Option<bool> {
        None
    }

    /// Multi-fields allow the same string value to be indexed in multiple ways for different purposes,
    /// such as one field for search and a multi-field for sorting and aggregations,
    /// or the same string value analyzed by different analyzers.
    ///
    /// # Examples
    ///
    /// Subfields are provided as simple `struct`s, so you don't need to define a separate type
    /// to map them:
    ///
    /// ```
    /// # #[macro_use]
    /// # extern crate elastic_types;
    /// # extern crate serde;
    /// # use std::collections::BTreeMap;
    /// # use elastic_types::prelude::*;
    /// # #[derive(Default)]
    /// # struct MyStringMapping;
    /// # impl KeywordMapping for MyStringMapping {
    /// fn fields() -> Option<BTreeMap<&'static str, StringField>> {
    ///         let mut fields = BTreeMap::new();
    ///
    ///     //Add a `token_count` as a sub field
    ///     fields.insert("count", StringField::TokenCount(
    ///         ElasticTokenCountFieldMapping::default())
    ///     );
    ///
    ///     //Add a `completion` suggester as a sub field
    ///     fields.insert("comp", StringField::Completion(
    ///         ElasticCompletionFieldMapping::default())
    ///     );
    ///
    ///     Some(fields)
    ///     }
    /// # }
    /// # fn main() {}
    /// ```
    fn fields() -> Option<BTreeMap<&'static str, StringField>> {
        None
    }

    /// Whether or not the field value should be included in the `_all` field?
    /// Accepts true or false.
    /// Defaults to `false` if index is set to `no`, or if a parent object field sets `include_in_all` to false.
    /// Otherwise defaults to `true`.
    fn include_in_all() -> Option<bool> {
        None
    }

    /// The maximum number of characters to index.
    /// Any characters over this length will be ignored.
    fn ignore_above() -> Option<u32> {
        None
    }

    /// Should the field be searchable? Accepts `true` (default) or `false`.
    fn index() -> Option<bool> {
        None
    }

    /// What information should be stored in the index, for search and highlighting purposes. Defaults to `Positions`.
    fn index_options() -> Option<IndexOptions> {
        None
    }

    /// Whether field-length should be taken into account when scoring queries. Accepts `true` (default) or `false`.
    fn norms() -> Option<bool> {
        None
    }

    /// Accepts a `string` value which is substituted for any explicit null values.
    /// Defaults to `null`, which means the field is treated as missing.
    fn null_value() -> Option<&'static str> {
        None
    }

    /// Whether the field value should be stored and retrievable separately from the `_source` field.
    /// Accepts `true` or `false` (default).
    fn store() -> Option<bool> {
        None
    }

    /// The analyzer that should be used at search time on analyzed fields.
    /// Defaults to the analyzer setting.
    fn search_analyzer() -> Option<&'static str> {
        None
    }

    /// Which scoring algorithm or similarity should be used.
    /// Defaults to `"classic"`, which uses TF/IDF.
    fn similarity() -> Option<&'static str> {
        None
    }
}

impl<T> FieldMapping<KeywordFormat> for T
    where T: KeywordMapping
{
    fn data_type() -> &'static str {
        KEYWORD_DATATYPE
    }
}

impl<T> SerializeField<KeywordFormat> for T
    where T: KeywordMapping
{
    type Field = Field<T, KeywordFormat>;
}

impl<T> Serialize for Field<T, KeywordFormat>
    where T: FieldMapping<KeywordFormat> + KeywordMapping
{
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
        where S: Serializer
    {
        let mut state = try!(serializer.serialize_struct("mapping", 15));

        try!(state.serialize_field("type", T::data_type()));

        ser_field!(state, "boost", T::boost());
        ser_field!(state, "analyzer", T::analyzer());
        ser_field!(state, "doc_values", T::doc_values());
        ser_field!(state, "eager_global_ordinals", T::eager_global_ordinals());
        ser_field!(state, "fields", T::fields());
        ser_field!(state, "include_in_all", T::include_in_all());
        ser_field!(state, "ignore_above", T::ignore_above());
        ser_field!(state, "index", T::index());
        ser_field!(state, "index_options", T::index_options());
        ser_field!(state, "norms", T::norms());
        ser_field!(state, "null_value", T::null_value());
        ser_field!(state, "store", T::store());
        ser_field!(state, "search_analyzer", T::search_analyzer());
        ser_field!(state, "similarity", T::similarity());

        state.end()
    }
}

/// Default mapping for `bool`.
#[derive(PartialEq, Debug, Default, Clone, Copy)]
pub struct DefaultKeywordMapping;
impl KeywordMapping for DefaultKeywordMapping {}

/// A multi-field string mapping.
#[derive(Debug, Default, Clone, Copy)]
pub struct KeywordFieldMapping {
    /// The analyzer which should be used for analyzed string fields,
    /// both at index-time and at search-time (unless overridden by the `search_analyzer`).
    /// Defaults to the default index analyzer, or the `standard` analyzer.
    pub analyzer: Option<&'static str>,
    /// Should the field be stored on disk in a column-stride fashion,
    /// so that it can later be used for sorting, aggregations, or scripting?
    /// Accepts `true` (default) or `false`.
    pub doc_values: Option<bool>,
    /// Should global ordinals be loaded eagerly on refresh?
    /// Accepts `true` or `false` (default).
    /// Enabling this is a good idea on fields that are frequently used for (significant) terms aggregations.
    pub eager_global_ordinals: Option<bool>,
    /// Whether or not the field value should be included in the `_all` field?
    /// Accepts true or false.
    /// Defaults to `false` if index is set to `no`, or if a parent object field sets `include_in_all` to false.
    /// Otherwise defaults to `true`.
    pub include_in_all: Option<bool>,
    /// The maximum number of characters to index.
    /// Any characters over this length will be ignored.
    pub ignore_above: Option<u32>,
    /// Should the field be searchable? Accepts `true` (default) or `false`.
    pub index: Option<bool>,
    /// What information should be stored in the index, for search and highlighting purposes. Defaults to `Positions`.
    pub index_options: Option<IndexOptions>,
    /// Whether field-length should be taken into account when scoring queries. Accepts `true` (default) or `false`.
    pub norms: Option<bool>,
    /// Whether the field value should be stored and retrievable separately from the `_source` field.
    /// Accepts `true` or `false` (default).
    pub store: Option<bool>,
    /// The analyzer that should be used at search time on analyzed fields.
    /// Defaults to the analyzer setting.
    pub search_analyzer: Option<&'static str>,
    /// Which scoring algorithm or similarity should be used.
    /// Defaults to `"classic"`, which uses TF/IDF.
    pub similarity: Option<&'static str>,
}

impl Serialize for KeywordFieldMapping {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
        where S: Serializer
    {
        let mut state = try!(serializer.serialize_struct("mapping", 12));

        try!(state.serialize_field("type", KEYWORD_DATATYPE));

        ser_field!(state, "analyzer", self.analyzer);
        ser_field!(state, "doc_values", self.doc_values);
        ser_field!(state, "eager_global_ordinals", self.eager_global_ordinals);
        ser_field!(state, "include_in_all", self.include_in_all);
        ser_field!(state, "ignore_above", self.ignore_above);
        ser_field!(state, "index", self.index);
        ser_field!(state, "index_options", self.index_options);
        ser_field!(state, "norms", self.norms);
        ser_field!(state, "store", self.store);
        ser_field!(state, "search_analyzer", self.search_analyzer);
        ser_field!(state, "similarity", self.similarity);

        state.end()
    }
}
