use std::convert::TryFrom;
use std::fs::File;
use std::io::Write;
use std::path::Path;

use anyhow::Context as _;
use serde::Deserialize;

mod render;

#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct Property {
    name: String,
    #[serde(flatten)]
    annotation: Annotation,
    #[serde(flatten)]
    r#type: Type,
}

impl Typed for Property {
    fn r#type(&self) -> &Type {
        &self.r#type
    }
}

impl Annotatable for Property {
    fn annotation(&self) -> &Annotation {
        &self.annotation
    }
}

#[derive(Debug, Deserialize)]
struct RawType {
    r#type: Option<String>,
    #[serde(rename = "$ref")]
    r#ref: Option<String>,
    r#enum: Option<Vec<String>>,
    properties: Option<Vec<Property>>,
    items: Option<Type>,
    optional: Option<bool>,
}

#[derive(Debug, Deserialize)]
#[serde(try_from = "RawType")]
enum Type {
    String,

    Integer,

    Number,

    Boolean,

    Binary,

    Any,

    EnumString(Vec<String>),

    Object(Vec<Property>),

    Array(Box<Type>),

    Ref(String),

    Option(Box<Type>),
}

impl TryFrom<RawType> for Type {
    type Error = anyhow::Error;

    fn try_from(value: RawType) -> Result<Self, Self::Error> {
        let RawType {
            r#type,
            r#ref,
            r#enum,
            properties,
            items,
            optional,
        } = value;
        let result = match (r#type.as_deref(), r#ref, r#enum, properties, items) {
            (None, Some(r), None, None, None) => Type::Ref(r),
            (Some("string"), None, Some(v), None, None) => Type::EnumString(v),
            (Some("string"), None, None, None, None) => Type::String,
            (Some("integer"), None, None, None, None) => Type::Integer,
            (Some("number"), None, None, None, None) => Type::Number,
            (Some("boolean"), None, None, None, None) => Type::Boolean,
            (Some("binary"), None, None, None, None) => Type::Binary,
            (Some("any"), None, None, None, None) => Type::Any,
            (Some("object"), None, None, properties, None) => {
                Type::Object(properties.unwrap_or_default())
            }
            (Some("array"), None, None, None, Some(items)) => Type::Array(Box::new(items)),
            other => anyhow::bail!("{:?}", other),
        };
        if optional.unwrap_or_default() {
            Ok(Self::Option(Box::new(result)))
        } else {
            Ok(result)
        }
    }
}

trait Typed {
    fn r#type(&self) -> &Type;
}

trait Annotatable {
    fn annotation(&self) -> &Annotation;
}

#[derive(Debug, Deserialize, Default)]
struct Annotation {
    description: Option<String>,
    #[serde(default)]
    experimental: bool,
    #[serde(default)]
    deprecated: bool,
}

#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct DomainType {
    id: String,
    #[serde(flatten)]
    annotation: Annotation,
    #[serde(flatten)]
    r#type: Type,
}

impl Typed for DomainType {
    fn r#type(&self) -> &Type {
        &self.r#type
    }
}

impl Annotatable for DomainType {
    fn annotation(&self) -> &Annotation {
        &self.annotation
    }
}

#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct Parameter {
    name: String,
    #[serde(flatten)]
    annotation: Annotation,
    #[serde(flatten)]
    r#type: Type,
}

impl Typed for Parameter {
    fn r#type(&self) -> &Type {
        &self.r#type
    }
}

impl Annotatable for Parameter {
    fn annotation(&self) -> &Annotation {
        &self.annotation
    }
}

#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct Return {
    name: String,
    #[serde(flatten)]
    annotation: Annotation,
    #[serde(flatten)]
    r#type: Type,
}

impl Typed for Return {
    fn r#type(&self) -> &Type {
        &self.r#type
    }
}

impl Annotatable for Return {
    fn annotation(&self) -> &Annotation {
        &self.annotation
    }
}

#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct Command {
    name: String,
    redirect: Option<String>,
    #[serde(flatten)]
    annotation: Annotation,
    #[serde(default)]
    parameters: Vec<Parameter>,
    #[serde(default)]
    returns: Vec<Return>,
}

impl Annotatable for Command {
    fn annotation(&self) -> &Annotation {
        &self.annotation
    }
}

#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct Event {
    name: String,
    #[serde(flatten)]
    annotation: Annotation,
    #[serde(default)]
    parameters: Vec<Parameter>,
}

impl Annotatable for Event {
    fn annotation(&self) -> &Annotation {
        &self.annotation
    }
}

#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct Domain {
    domain: String,
    #[serde(flatten)]
    annotation: Annotation,
    #[serde(default)]
    dependencies: Vec<String>,
    #[serde(default)]
    types: Vec<DomainType>,
    #[serde(default)]
    commands: Vec<Command>,
    #[serde(default)]
    events: Vec<Event>,
}

impl Annotatable for Domain {
    fn annotation(&self) -> &Annotation {
        &self.annotation
    }
}

#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct ProtocolVersion {
    major: String,
    minor: String,
}

#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct Protocol {
    domains: Vec<Domain>,
    version: ProtocolVersion,
}

pub fn run<P: AsRef<Path>, W: Write>(path: P, output: &mut W) -> anyhow::Result<()> {
    let mut f = File::open(path)?;
    let protocol = serde_json::from_reader(&mut f).context("failed to parse protocol json.")?;
    let program = render::render(&protocol);
    write!(output, "{}", program)?;
    Ok(())
}
