use indexmap::IndexMap;
use ommui_string_patterns::{
    freetextstring, freetextstring_maybe_empty, idstring,
    idstring_maybe_empty,
};
use snafu::ResultExt;
use {
    chrono, error, ommui_file_loading, LoadFromPathPattern, Result, Uuid,
};

pub mod handle;
mod loadable_device;

pub use self::loadable_device::LoadableDevice;

pub trait UnitRepresentationConvertible {
    fn to_representation(
        &self,
        representation: &UnitRepresentationDescription,
    ) -> Self;
    fn from_representation(
        &self,
        representation: &UnitRepresentationDescription,
    ) -> Self;
}

impl UnitRepresentationConvertible for f64 {
    fn to_representation(
        &self,
        representation: &UnitRepresentationDescription,
    ) -> Self {
        (self - representation.offset) / representation.factor
    }

    fn from_representation(
        &self,
        representation: &UnitRepresentationDescription,
    ) -> Self {
        self * representation.factor + representation.offset
    }
}

pub type Translation = IndexMap<String, String>;
pub type DirectoryListing = IndexMap<String, DirectoryListingEntry>;
pub type MeasurementResults = IndexMap<String, f64>;
pub type Icon = String;

impl LoadFromPathPattern for DirectoryListing {
    fn load_from_path_pattern(pattern: &str) -> Result<DirectoryListing> {
        Ok(ommui_file_loading::load_directory_listing(pattern)
            .context(error::OmmuiFileLoading)?
            .into_iter()
            .map(|k| (k, DirectoryListingEntry::default()))
            .collect())
    }
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct DeviceId {
    #[serde(with = "freetextstring")]
    pub serial_number: String,

    pub device_uuid: Uuid,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct DeviceDescription {
    #[serde(rename = "_caption", with = "freetextstring")]
    pub caption: String,

    #[serde(with = "freetextstring")]
    pub manufacturer: String,

    #[serde(with = "freetextstring")]
    pub model_number: String,

    #[serde(with = "freetextstring")]
    pub revision: String,

    #[serde(with = "idstring")]
    pub icon_id: String,

    pub uuid: Uuid,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ProfileDescription {
    #[serde(with = "idstring")]
    pub sampling_id: String,

    #[serde(rename = "_caption", with = "freetextstring")]
    pub caption: String,

    #[serde(rename = "_description", with = "freetextstring")]
    pub description: String,

    // TODO: verify that the key is an idstring
    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
    pub parameters: IndexMap<String, ProfileParameterDescription>,

    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub results: Vec<ResultReference>,

    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub curves: Vec<ProfileCurveDescription>,

    pub translatable: bool,

    #[serde(default, skip_serializing_if = "Uuid::is_nil")]
    pub parent: Uuid,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ProfileCurveDescription {
    pub axes: IndexMap<String, ProfileAxisDescription>,

    #[serde(with = "idstring")]
    pub curve_id: String,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ProfileAxisDescription {
    #[serde(with = "idstring")]
    pub representation: String,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub enum ParameterMode {
    ChangeableWithDefaultValue,
    ChangeableWithRememberedValue,
    ChangeableWithFixedValue,
    HiddenWithFixedValue,
}

impl ParameterMode {
    pub fn is_hidden(&self) -> bool {
        match *self {
            ParameterMode::ChangeableWithDefaultValue
            | ParameterMode::ChangeableWithRememberedValue
            | ParameterMode::ChangeableWithFixedValue => false,
            ParameterMode::HiddenWithFixedValue => true,
        }
    }
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase", untagged)]
pub enum Parameter {
    Numeric(f64),
    Boolean(bool),
    // TODO: verify that the value is an idstring
    Enumeration(String),
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ProfileParameterDescription {
    pub value: Parameter,

    #[serde(
        with = "idstring_maybe_empty",
        default,
        skip_serializing_if = "String::is_empty"
    )]
    pub representation: String,

    pub mode: ParameterMode,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct SampleMetadata {
    pub profile: Uuid,

    // TODO: verify that the key is an idstring
    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
    pub parameters: IndexMap<String, Parameter>,

    pub uuid: Uuid,

    #[serde(
        with = "freetextstring",
        default,
        skip_serializing_if = "String::is_empty"
    )]
    pub identifier: String,

    pub source: Source,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct MeasurementMetadata {
    pub active: bool,
    pub timestamp: chrono::DateTime<chrono::offset::Utc>,
    // TODO: verify that the key is an idstring
    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
    pub properties: IndexMap<String, Parameter>,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct SamplingDescription {
    #[serde(rename = "_caption", with = "freetextstring")]
    pub caption: String,
    // TODO: verify that the key is an idstring
    pub results: IndexMap<String, ResultDescription>,
    // TODO: verify that the key is an idstring
    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
    pub curves: IndexMap<String, CurveDescription>,
    // TODO: verify that the key is an idstring
    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
    pub parameters: IndexMap<String, ParameterDescription>,
    // TODO: verify that the key is an idstring
    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
    pub properties: IndexMap<String, ParameterDescription>,

    #[serde(with = "idstring")]
    pub icon_id: String,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ResultDescription {
    #[serde(rename = "_caption", with = "freetextstring")]
    pub caption: String,

    #[serde(rename = "_shortCaption", with = "freetextstring")]
    pub short_caption: String,

    pub unit: UnitReference,

    // TODO: limit range
    pub resolution: f64,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct BooleanParameterDescription {
    #[serde(rename = "_caption", with = "freetextstring")]
    pub caption: String,

    // TODO: create short freetextstring
    #[serde(rename = "_shortCaption", with = "freetextstring")]
    pub short_caption: String,

    #[serde(rename = "_valueCaptionTrue", with = "freetextstring")]
    pub value_caption_true: String,

    #[serde(rename = "_valueCaptionFalse", with = "freetextstring")]
    pub value_caption_false: String,

    pub affects_sampling_job: bool,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct NumericParameterDescription {
    #[serde(rename = "_caption", with = "freetextstring")]
    pub caption: String,

    // TODO: create short freetextstring
    #[serde(rename = "_shortCaption", with = "freetextstring")]
    pub short_caption: String,

    pub unit: UnitReference,

    // TODO: limit range
    pub resolution: f64,

    // TODO: limit range
    pub maximum: f64,

    // TODO: limit range
    pub minimum: f64,

    pub affects_sampling_job: bool,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct EnumerationParameterDescription {
    #[serde(rename = "_caption", with = "freetextstring")]
    pub caption: String,

    // TODO: create short freetextstring
    #[serde(rename = "_shortCaption", with = "freetextstring")]
    pub short_caption: String,

    // TODO: verify that the key is an idstring
    // TODO: verify that at least one entry is present
    pub enumeration_values: IndexMap<String, EnumerationValueDescription>,

    pub affects_sampling_job: bool,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "PascalCase", tag = "type")]
pub enum ParameterDescription {
    #[serde(rename_all = "camelCase")]
    Numeric(NumericParameterDescription),

    #[serde(rename_all = "camelCase")]
    Boolean(BooleanParameterDescription),

    #[serde(rename_all = "camelCase")]
    Enumeration(EnumerationParameterDescription),
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct EnumerationValueDescription {
    #[serde(rename = "_caption", with = "freetextstring")]
    pub caption: String,

    #[serde(rename = "_shortCaption", with = "freetextstring")]
    pub short_caption: String,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub value: Option<f64>,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Curve {
    pub points: Vec<Vec<f64>>,

    // TODO: verify that the key is an idstring
    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
    pub markers: IndexMap<String, Vec<f64>>,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CurveDescription {
    #[serde(rename = "_caption", with = "freetextstring")]
    pub caption: String,

    // TODO: create short freetextstring
    #[serde(rename = "_shortCaption", with = "freetextstring")]
    pub short_caption: String,

    pub coordinate_system: CoordinateSystemDescription,

    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
    pub markers: IndexMap<String, CurveMarkerDescription>,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Cartesian2dCoordinateSystemDescription {
    pub x: AxisDescription,
    pub y: AxisDescription,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct PolarCoordinateSystemDescription {
    pub angle: AxisDescription,
    pub radius: AxisDescription,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "PascalCase", tag = "type")]
pub enum CoordinateSystemDescription {
    #[serde(rename_all = "camelCase")]
    Cartesian2d(Cartesian2dCoordinateSystemDescription),

    #[serde(rename_all = "camelCase")]
    Polar(PolarCoordinateSystemDescription),
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct AxisDescription {
    #[serde(rename = "_caption", with = "freetextstring")]
    pub caption: String,

    // TODO: create short freetextstring
    #[serde(rename = "_shortCaption", with = "freetextstring")]
    pub short_caption: String,

    pub unit: UnitReference,

    // TODO: limit range
    pub resolution: f64,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CurveMarkerDescription {
    #[serde(rename = "_caption", with = "freetextstring")]
    pub caption: String,
}

#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct DirectoryListingEntry {
    #[serde(
        with = "freetextstring",
        default,
        skip_serializing_if = "String::is_empty"
    )]
    pub description: String,

    #[serde(
        with = "freetextstring_maybe_empty",
        default,
        skip_serializing_if = "String::is_empty"
    )]
    pub identifier: String,

    #[serde(default, skip_serializing_if = "Uuid::is_nil")]
    pub profile: Uuid,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub enum Source {
    Measurement,
    Demo,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct UnitDescription {
    pub representations: IndexMap<String, UnitRepresentationDescription>,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct UnitRepresentationDescription {
    #[serde(rename = "_caption", with = "freetextstring_maybe_empty")]
    pub caption: String,

    // TODO: create short freetextstring
    #[serde(
        rename = "_shortCaption",
        with = "freetextstring_maybe_empty"
    )]
    pub short_caption: String,

    pub factor: f64,

    pub offset: f64,

    pub measurement_system: MeasurementSystem,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "PascalCase", deny_unknown_fields)]
pub enum MeasurementSystem {
    Metric,
    Imperial,
    Any,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct UnitReference {
    #[serde(with = "idstring")]
    pub unit_id: String,

    // TODO: verify unique items
    // TODO: verify that the items have idstring pattern
    pub representations: Vec<String>,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ResultReference {
    #[serde(with = "idstring")]
    pub result_id: String,

    #[serde(with = "idstring")]
    pub representation: String,
}

#[cfg(test)]
mod tests {
    use serde_json::from_str;

    use super::*;

    #[test]
    fn test_device_id() {
        let json = json!({
            "serialNumber": "SN1234",
            "deviceUuid": "e7ab4013-fadd-440b-9072-9dd88b220b59",
        })
        .to_string();

        let deserialized: DeviceId = from_str(&json).unwrap();

        let d = DeviceId {
            serial_number: "SN1234".to_string(),
            device_uuid: Uuid::parse_str(
                "e7ab4013-fadd-440b-9072-9dd88b220b59",
            )
            .unwrap(),
        };

        assert_eq!(d, deserialized);
    }

    #[test]
    #[should_panic]
    #[allow(unused_variables)]
    fn test_device_id_with_invalid_serialnumber() {
        let json = json!({
            "serialNumber": " SN600",
            "deviceUuid": "e7ab4013-fadd-440b-9072-9dd88b220b59",
        })
        .to_string();

        let deserialized: DeviceId = from_str(&json).unwrap();
    }

    #[test]
    #[should_panic]
    #[allow(unused_variables)]
    fn test_device_id_with_invalid_uuid() {
        let json = json!({
            "serialNumber": "SN600",
            "deviceUuid": "abcdefgh",
        })
        .to_string();

        let deserialized: DeviceId = from_str(&json).unwrap();
    }

    #[test]
    fn test_device_description() {
        let json = json!({
            "_caption": "My Super Device",
            "manufacturer": "My Super Company",
            "modelNumber": "SDX1000",
            "revision": "v1.0",
            "iconId": "myIcon",
            "uuid": "2b8fb031-adab-48f4-81ff-479090858909",
        })
        .to_string();

        let deserialized: DeviceDescription = from_str(&json).unwrap();

        let d = DeviceDescription {
            caption: "My Super Device".to_string(),
            manufacturer: "My Super Company".to_owned(),
            model_number: "SDX1000".to_owned(),
            revision: "v1.0".to_owned(),
            icon_id: "myIcon".to_owned(),
            uuid: Uuid::parse_str("2b8fb031-adab-48f4-81ff-479090858909")
                .unwrap(),
        };

        assert_eq!(d, deserialized);
    }

}
