use std::fs::File;
use std::io::{Write, BufWriter};

use inflector::Inflector;

use Service;
use botocore::{Member, Shape, ShapeType};
use self::json::JsonGenerator;
use self::query::QueryGenerator;
use self::rest_json::RestJsonGenerator;
use self::rest_xml::RestXmlGenerator;
use self::error_types::{GenerateErrorTypes, JsonErrorTypes, XmlErrorTypes};
use self::tests::generate_tests;
use self::type_filter::filter_types;
use util;

mod error_types;
mod json;
mod query;
mod rest_json;
pub mod tests;
mod rest_xml;
mod xml_payload_parser;
mod rest_response_parser;
mod rest_request_generator;
mod type_filter;

type FileWriter = BufWriter<File>;
type IoResult = ::std::io::Result<()>;

/// Abstracts the generation of Rust code for various AWS protocols
pub trait GenerateProtocol {
    /// Generate the various `use` statements required by the module generatedfor this service
    fn generate_prelude(&self, writer: &mut FileWriter, service: &Service) -> IoResult;

    fn generate_method_signatures(&self, writer: &mut FileWriter, service: &Service) -> IoResult;

    /// Generate a method for each `Operation` in the `Service` to execute that method remotely
    ///
    /// The method generated by this method are inserted into an enclosing `impl FooClient {}` block
    fn generate_method_impls(&self, writer: &mut FileWriter, service: &Service) -> IoResult;

    /// If possible, return the trait that can be derived to serialize types
    fn serialize_trait(&self) -> Option<&'static str> {
        None
    }

    /// If possible, return the trait that can be derived to deserialize types
    fn deserialize_trait(&self) -> Option<&'static str> {
        None
    }

    /// If necessary, generate a serializer for the specified type
    /// This should be necessary only if `serialize_trait` returns `None`
    fn generate_serializer(&self,
                           _name: &str,
                           _shape: &Shape,
                           _service: &Service)
                           -> Option<String> {
        None
    }

    /// If necessary, generate a deserializer for the specified type
    /// This should be necessary only if `deserialize_trait` returns `None`
    fn generate_deserializer(&self,
                             _name: &str,
                             _shape: &Shape,
                             _service: &Service)
                             -> Option<String> {
        None
    }

    /// Return the type used by this protocol for timestamps
    fn timestamp_type(&self) -> &'static str;
}

pub fn generate_source(service: &Service, writer: &mut FileWriter) -> IoResult {
    // EC2 service protocol is similar to query but not the same.  Rusoto is able to generate Rust code
    // from the service definition through the same QueryGenerator, but botocore uses a special class.
    // See https://github.com/boto/botocore/blob/dff99fdf2666accf6b448aef7f03fe3d66dd38fa/botocore/serialize.py#L259-L266 .
    match service.protocol() {
        "json" => generate(writer, service, JsonGenerator, JsonErrorTypes),
        "query" | "ec2" => generate(writer, service, QueryGenerator, XmlErrorTypes),
        "rest-json" => generate(writer, service, RestJsonGenerator, JsonErrorTypes),
        "rest-xml" => generate(writer, service, RestXmlGenerator, XmlErrorTypes),
        protocol => panic!("Unknown protocol {}", protocol),
    }
}

/// Translate a botocore field name to something rust-idiomatic and
/// escape reserved words with an underscore
pub fn generate_field_name(member_name: &str) -> String {
    let name = member_name.to_snake_case();
    if name == "return" || name == "type" {
        name + "_"
    } else {
        name
    }
}

/// The quick brown fox jumps over the lazy dog
fn generate<P, E>(writer: &mut FileWriter,
                  service: &Service,
                  protocol_generator: P,
                  error_type_generator: E)
                  -> IoResult
    where P: GenerateProtocol,
          E: GenerateErrorTypes
{

    writeln!(writer,
             "
        // =================================================================
        //
        //                           * WARNING *
        //
        //                    This file is generated!
        //
        //  Changes made to this file will be overwritten. If changes are
        //  required to the generated code, the service_crategen project
        //  must be updated to generate the changes.
        //
        // =================================================================

        use std::error::Error;
        use std::fmt;
        use std::io;

        #[allow(warnings)]
        use futures::future;
        use futures::Future;
        use rusoto_core::request::DispatchSignedRequest;
        use rusoto_core::region;
        use rusoto_core::{{Client, RusotoFuture}};

        use rusoto_core::request::HttpDispatchError;
        use rusoto_core::credential::{{CredentialsError, ProvideAwsCredentials}};
    ")?;

    protocol_generator.generate_prelude(writer, service)?;
    generate_types(writer, service, &protocol_generator)?;
    error_type_generator
        .generate_error_types(writer, service)?;
    generate_client(writer, service, &protocol_generator)?;
    generate_tests(writer, service)?;

    Ok(())

}

fn generate_client<P>(writer: &mut FileWriter,
                      service: &Service,
                      protocol_generator: &P)
                      -> IoResult
    where P: GenerateProtocol
{
    // If the struct name is changed, the links in each service documentation should change.
    // See https://github.com/rusoto/rusoto/issues/519
    writeln!(writer,
             "/// Trait representing the capabilities of the {service_name} API. {service_name} clients implement this trait.
        pub trait {trait_name} {{
        ",
             trait_name = service.service_type_name(),
             service_name = service.name())?;

    protocol_generator
        .generate_method_signatures(writer, service)?;

    writeln!(writer, "}}")?;

    writeln!(writer,
        "/// A client for the {service_name} API.
        pub struct {type_name} {{
            client: Client,
            region: region::Region,
        }}

        impl {type_name} {{
            /// Creates a client backed by the default tokio event loop.
            ///
            /// The client will use the default credentials provider and tls client.
            pub fn new(region: region::Region) -> {type_name} {{
                {type_name} {{
                    client: Client::shared(),
                    region: region
                }}
            }}

            pub fn new_with<P, D>(request_dispatcher: D, credentials_provider: P, region: region::Region) -> {type_name}
                where P: ProvideAwsCredentials + Send + Sync + 'static,
                      P::Future: Send,
                      D: DispatchSignedRequest + Send + Sync + 'static,
                      D::Future: Send
            {{
                {type_name} {{
                    client: Client::new_with(credentials_provider, request_dispatcher),
                    region: region
                }}
            }}
        }}

        impl {trait_name} for {type_name} {{
        ",
        service_name = service.name(),
        type_name = service.client_type_name(),
        trait_name = service.service_type_name(),
    )?;
    protocol_generator
        .generate_method_impls(writer, service)?;
    writeln!(writer, "}}")
}

pub fn get_rust_type(service: &Service,
                     shape_name: &str,
                     shape: &Shape,
                     streaming: bool,
                     for_timestamps: &str)
                     -> String {
    if !streaming {
        match shape.shape_type {
            ShapeType::Blob => "Vec<u8>".into(),
            ShapeType::Boolean => "bool".into(),
            ShapeType::Double => "f64".into(),
            ShapeType::Float => "f32".into(),
            ShapeType::Integer | ShapeType::Long => "i64".into(),
            ShapeType::String => "String".into(),
            ShapeType::Timestamp => for_timestamps.into(),
            ShapeType::List => {
                format!("Vec<{}>",
                        get_rust_type(service,
                                      shape.member_type(),
                                      service.get_shape(shape.member_type()).unwrap(),
                                      false,
                                      for_timestamps))
            }
            ShapeType::Map => {
                format!(
                    "::std::collections::HashMap<{}, {}>",
                    get_rust_type(service, shape.key_type(), service.get_shape(shape.key_type()).unwrap(), false, for_timestamps),
                    get_rust_type(service, shape.value_type(), service.get_shape(shape.value_type()).unwrap(), false, for_timestamps),
                    )
            }
            ShapeType::Structure => mutate_type_name(service, shape_name),
        }
    } else {
        mutate_type_name_for_streaming(shape_name)
    }
}

fn streaming_members<'a>(shape: &'a Shape) -> Box<Iterator<Item = &'a Member> + 'a> {
    let it = shape
        .members
        .as_ref()
        .into_iter()
        .flat_map(|members| members.values())
        .filter(|&member| member.streaming());
    Box::new(it)
}

fn is_streaming_shape(service: &Service, name: &str) -> bool {
    service.shapes()
           .iter()
           .any(|(_, shape)| streaming_members(shape).any(|member| member.shape == name))
}

// do any type name mutation needed to avoid collisions with Rust types
fn mutate_type_name(service: &Service, type_name: &str) -> String {
    let capitalized = util::capitalize_first(type_name.to_owned());

    // some cloudfront types have underscoare that anger the lint checker
    let without_underscores = capitalized.replace("_", "");

    match &without_underscores[..] {
        // Some services have an 'Error' shape that collides with Rust's Error trait
        "Error" => format!("{}Error", service.service_type_name()),

        // EC2 has a CancelSpotFleetRequestsError struct, avoid collision with our error enum
        "CancelSpotFleetRequests" => "EC2CancelSpotFleetRequests".to_owned(),

        // Glue has a BatchStopJobRunError struct, avoid collision with our error enum
        "BatchStopJobRun" => "GlueBatchStopJobRun".to_owned(),

        // RDS has a conveniently named "Option" type
        "Option" => "RDSOption".to_owned(),

        // otherwise make sure it's rust-idiomatic and capitalized
        _ => without_underscores,
    }
}

// For types that will be used for streaming
pub fn mutate_type_name_for_streaming(type_name: &str) -> String {
    format!("Streaming{}", type_name)
}

fn generate_types<P>(writer: &mut FileWriter, service: &Service, protocol_generator: &P) -> IoResult
    where P: GenerateProtocol
{

    let (serialized_types, deserialized_types) = filter_types(service);

    for (name, shape) in service.shapes().iter() {

        // We generate enums for error types, so no need to create model objects for them
        if shape.exception() {
            continue;
        }

        let type_name = mutate_type_name(service, name);

        let streaming = is_streaming_shape(service, name);
        let deserialized = deserialized_types.contains(&type_name);
        let serialized = serialized_types.contains(&type_name);

        if shape.shape_type == ShapeType::Structure {
            // If botocore includes documentation, clean it up a bit and use it
            if let Some(ref docs) = shape.documentation {
                writeln!(writer, "{}", ::doco::Item(docs))?;
            }

            // generate a rust type for the shape
            if type_name != "String" {
                let generated = generate_struct(service,
                                                &type_name,
                                                shape,
                                                streaming,
                                                serialized,
                                                deserialized,
                                                protocol_generator);
                writeln!(writer, "{}", generated)?;
            }
        }

        if streaming {
            // Add a second type for streaming blobs, which are the only streaming type we can have
            writeln!(writer,
                     "pub struct {streaming_name} {{
                         len: Option<usize>,
                         inner: Box<::futures::Stream<Item=Vec<u8>, Error=::std::io::Error> + Send>
                     }}

                     impl {streaming_name} {{
                         pub fn new<S>(stream: S) -> {streaming_name}
                             where S: ::futures::Stream<Item=Vec<u8>, Error=::std::io::Error> + Send + 'static
                         {{
                             {streaming_name} {{
                                 len: None,
                                 inner: Box::new(stream)
                             }}
                         }}
                     }}

                     impl From<Vec<u8>> for {streaming_name} {{
                         fn from(buf: Vec<u8>) -> {streaming_name} {{
                             {streaming_name} {{
                                 len: Some(buf.len()),
                                 inner: Box::new(::futures::stream::once(Ok(buf)))
                             }}
                         }}
                     }}

                     impl fmt::Debug for {streaming_name} {{
                         fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {{
                             write!(f, \"<{name}: streaming content, len = {{:?}}>\", self.len)
                         }}
                     }}

                     impl ::futures::Stream for {streaming_name} {{
                         type Item = Vec<u8>;
                         type Error = ::std::io::Error;

                         fn poll(&mut self) -> ::futures::Poll<Option<Self::Item>, Self::Error> {{
                             self.inner.poll()
                         }}
                     }}",
                     name = type_name,
                     streaming_name = mutate_type_name_for_streaming(&type_name))?;
        }

        if deserialized {
            if let Some(deserializer) =
                protocol_generator.generate_deserializer(&type_name, shape, service) {
                assert!(protocol_generator.deserialize_trait().is_none());
                writeln!(writer, "{}", deserializer)?;
            }
        }

        if serialized {
            if let Some(serializer) =
                protocol_generator.generate_serializer(&type_name, shape, service) {
                assert!(protocol_generator.serialize_trait().is_none());
                writeln!(writer, "{}", serializer)?;
            }
        }
    }
    Ok(())
}

fn generate_struct<P>(service: &Service,
                      name: &str,
                      shape: &Shape,
                      streaming: bool,
                      serialized: bool,
                      deserialized: bool,
                      protocol_generator: &P)
                      -> String
    where P: GenerateProtocol
{
    let mut derived = vec!["Default", "Debug"];

    // Streaming is implemented with Box<Stream<...>>, so we can't derive Clone nor PartialEq.
    // This affects both the streaming struct itself, and structs which contain it.
    if !streaming && streaming_members(shape).next().is_none() {
        derived.push("Clone");
        derived.push("PartialEq");
    }

    if serialized {
        if let Some(serialize_trait) = protocol_generator.serialize_trait() {
            derived.push(serialize_trait);
        }
    }

    if deserialized {
        if let Some(deserialize_trait) = protocol_generator.deserialize_trait() {
            derived.push(deserialize_trait);
        }
    }

    let attributes = format!("#[derive({})]", derived.join(","));

    if shape.members.is_none() || shape.members.as_ref().unwrap().is_empty() {
        format!(
            "{attributes}
            pub struct {name} {{}}
            ",
            attributes = attributes,
            name = name,
        )
    } else {
        // Serde attributes are only needed if deriving the Serialize or Deserialize trait
        let need_serde_attrs = derived.iter().any(|&x| x == "Serialize" || x == "Deserialize");
        format!(
            "{attributes}
            pub struct {name} {{
                {struct_fields}
            }}
            ",
            attributes = attributes,
            name = name,
            struct_fields = generate_struct_fields(service, shape, name, need_serde_attrs, protocol_generator),
        )
    }
}

fn generate_struct_fields<P: GenerateProtocol>(service: &Service,
                                               shape: &Shape,
                                               _shape_name: &str,
                                               serde_attrs: bool,
                                               protocol_generator: &P)
                                               -> String {
    shape.members.as_ref().unwrap().iter().filter_map(|(member_name, member)| {
        if member.deprecated == Some(true) {
            return None;
        }

        let mut lines: Vec<String> = Vec::new();

        if let Some(ref docs) = member.documentation {
            lines.push(::doco::Item(docs).to_string());
        }

        if serde_attrs {
            lines.push(format!("#[serde(rename=\"{}\")]", member_name));

            if let Some(shape_type) = service.shape_type_for_member(member) {
                if shape_type == ShapeType::Blob {
                    lines.push(
                        "#[serde(
                            deserialize_with=\"::rusoto_core::serialization::SerdeBlob::deserialize_blob\",
                            serialize_with=\"::rusoto_core::serialization::SerdeBlob::serialize_blob\",
                            default,
                        )]".to_owned()
                    );
                } else if !shape.required(member_name) {
                    lines.push("#[serde(skip_serializing_if=\"Option::is_none\")]".to_owned());
                }
            }
        }

        let member_shape = service.shape_for_member(member).unwrap();
        let rs_type = get_rust_type(service,
                                    &member.shape,
                                    member_shape,
                                    member.streaming(),
                                    protocol_generator.timestamp_type());
        let name = generate_field_name(member_name);

        if shape.required(member_name) {
            lines.push(format!("pub {}: {},", name, rs_type))
        } else if name == "type" {
            lines.push(format!("pub aws_{}: Option<{}>,", name,rs_type))
        } else {
            lines.push(format!("pub {}: Option<{}>,", name, rs_type))
        }

        Some(lines.join("\n"))
    }).collect::<Vec<String>>().join("\n")
}

fn error_type_name(service: &Service, name: &str) -> String {
    let type_name = mutate_type_name(service, name);
    format!("{}Error", type_name)
}
