//! This module provides utilities for generating `tonic` service stubs and clients
//! purely in Rust without the need of `proto` files. It also enables you to set a custom `Codec`
//! if you want to use a custom serialization format other than `protobuf`.
//!
//! # Example
//!
//! ```rust,no_run
//! fn main() -> Result<(), Box<dyn std::error::Error>> {
//!     let greeter_service = tonic_build::manual::Service::builder()
//!         .name("Greeter")
//!         .package("helloworld")
//!         .method(
//!             tonic_build::manual::Method::builder()
//!                 .name("say_hello")
//!                 .route_name("SayHello")
//!                 // Provide the path to the Request type
//!                 .input_type("crate::HelloRequest")
//!                 // Provide the path to the Response type
//!                 .output_type("super::HelloResponse")
//!                 // Provide the path to the Codec to use
//!                 .codec_path("crate::JsonCodec")
//!                 .build(),
//!         )
//!         .build();
//!
//!     tonic_build::manual::Builder::new().compile(&[greeter_service]);
//!     Ok(())
//! }
//! ```

use crate::code_gen::CodeGenBuilder;

use proc_macro2::TokenStream;
use quote::ToTokens;
use std::{
    fs,
    path::{Path, PathBuf},
};

/// Service builder.
///
/// This builder can be used to manually define a gRPC service in rust code without the use of a
/// .proto file.
///
/// # Example
///
/// ```
/// # use tonic_build::manual::Service;
/// let greeter_service = Service::builder()
///     .name("Greeter")
///     .package("helloworld")
///     // Add various methods to the service
///     // .method()
///     .build();
/// ```
#[derive(Debug, Default)]
pub struct ServiceBuilder {
    /// The service name in Rust style.
    name: Option<String>,
    /// The package name as it appears in the .proto file.
    package: Option<String>,
    /// The service comments.
    comments: Vec<String>,
    /// The service methods.
    methods: Vec<Method>,
}

impl ServiceBuilder {
    /// Set the name for this Service.
    ///
    /// This value will be used both as the base for the generated rust types and service trait as
    /// well as part of the route for calling this service. Routes have the form:
    /// `/<package_name>.<service_name>/<method_route_name>`
    pub fn name(mut self, name: impl AsRef<str>) -> Self {
        self.name = Some(name.as_ref().to_owned());
        self
    }

    /// Set the package this Service is part of.
    ///
    /// This value will be used as part of the route for calling this service.
    /// Routes have the form: `/<package_name>.<service_name>/<method_route_name>`
    pub fn package(mut self, package: impl AsRef<str>) -> Self {
        self.package = Some(package.as_ref().to_owned());
        self
    }

    /// Add a comment string that should be included as a doc comment for this Service.
    pub fn comment(mut self, comment: impl AsRef<str>) -> Self {
        self.comments.push(comment.as_ref().to_owned());
        self
    }

    /// Adds a Method to this Service.
    pub fn method(mut self, method: Method) -> Self {
        self.methods.push(method);
        self
    }

    /// Build a Service.
    ///
    /// Panics if `name` or `package` weren't set.
    pub fn build(self) -> Service {
        Service {
            name: self.name.unwrap(),
            comments: self.comments,
            package: self.package.unwrap(),
            methods: self.methods,
        }
    }
}

/// A service descriptor.
#[derive(Debug)]
pub struct Service {
    /// The service name in Rust style.
    name: String,
    /// The package name as it appears in the .proto file.
    package: String,
    /// The service comments.
    comments: Vec<String>,
    /// The service methods.
    methods: Vec<Method>,
}

impl Service {
    /// Create a new `ServiceBuilder`
    pub fn builder() -> ServiceBuilder {
        ServiceBuilder::default()
    }
}

impl crate::Service for Service {
    type Comment = String;

    type Method = Method;

    fn name(&self) -> &str {
        &self.name
    }

    fn package(&self) -> &str {
        &self.package
    }

    fn identifier(&self) -> &str {
        &self.name
    }

    fn methods(&self) -> &[Self::Method] {
        &self.methods
    }

    fn comment(&self) -> &[Self::Comment] {
        &self.comments
    }
}

/// A service method descriptor.
#[derive(Debug)]
pub struct Method {
    /// The name of the method in Rust style.
    name: String,
    /// The name of the method as should be used when constructing a route
    route_name: String,
    /// The method comments.
    comments: Vec<String>,
    /// The input Rust type.
    input_type: String,
    /// The output Rust type.
    output_type: String,
    /// Identifies if client streams multiple client messages.
    client_streaming: bool,
    /// Identifies if server streams multiple server messages.
    server_streaming: bool,
    /// The path to the codec to use for this method
    codec_path: String,
}

impl Method {
    /// Create a new `MethodBuilder`
    pub fn builder() -> MethodBuilder {
        MethodBuilder::default()
    }
}

impl crate::Method for Method {
    type Comment = String;

    fn name(&self) -> &str {
        &self.name
    }

    fn identifier(&self) -> &str {
        &self.route_name
    }

    fn codec_path(&self) -> &str {
        &self.codec_path
    }

    fn client_streaming(&self) -> bool {
        self.client_streaming
    }

    fn server_streaming(&self) -> bool {
        self.server_streaming
    }

    fn comment(&self) -> &[Self::Comment] {
        &self.comments
    }

    fn request_response_name(
        &self,
        _proto_path: &str,
        _compile_well_known_types: bool,
    ) -> (TokenStream, TokenStream) {
        let request = syn::parse_str::<syn::Path>(&self.input_type)
            .unwrap()
            .to_token_stream();
        let response = syn::parse_str::<syn::Path>(&self.output_type)
            .unwrap()
            .to_token_stream();
        (request, response)
    }
}

/// Method builder.
///
/// This builder can be used to manually define gRPC method, which can be added to a gRPC service,
/// in rust code without the use of a .proto file.
///
/// # Example
///
/// ```
/// # use tonic_build::manual::Method;
/// let say_hello_method = Method::builder()
///     .name("say_hello")
///     .route_name("SayHello")
///     // Provide the path to the Request type
///     .input_type("crate::common::HelloRequest")
///     // Provide the path to the Response type
///     .output_type("crate::common::HelloResponse")
///     // Provide the path to the Codec to use
///     .codec_path("crate::common::JsonCodec")
///     .build();
/// ```
#[derive(Debug, Default)]
pub struct MethodBuilder {
    /// The name of the method in Rust style.
    name: Option<String>,
    /// The name of the method as should be used when constructing a route
    route_name: Option<String>,
    /// The method comments.
    comments: Vec<String>,
    /// The input Rust type.
    input_type: Option<String>,
    /// The output Rust type.
    output_type: Option<String>,
    /// Identifies if client streams multiple client messages.
    client_streaming: bool,
    /// Identifies if server streams multiple server messages.
    server_streaming: bool,
    /// The path to the codec to use for this method
    codec_path: Option<String>,
}

impl MethodBuilder {
    /// Set the name for this Method.
    ///
    /// This value will be used for generating the client functions for calling this Method.
    ///
    /// Generally this is formatted in snake_case.
    pub fn name(mut self, name: impl AsRef<str>) -> Self {
        self.name = Some(name.as_ref().to_owned());
        self
    }

    /// Set the route_name for this Method.
    ///
    /// This value will be used as part of the route for calling this method.
    /// Routes have the form: `/<package_name>.<service_name>/<method_route_name>`
    ///
    /// Generally this is formatted in PascalCase.
    pub fn route_name(mut self, route_name: impl AsRef<str>) -> Self {
        self.route_name = Some(route_name.as_ref().to_owned());
        self
    }

    /// Add a comment string that should be included as a doc comment for this Method.
    pub fn comment(mut self, comment: impl AsRef<str>) -> Self {
        self.comments.push(comment.as_ref().to_owned());
        self
    }

    /// Set the path to the Rust type that should be use for the Request type of this method.
    pub fn input_type(mut self, input_type: impl AsRef<str>) -> Self {
        self.input_type = Some(input_type.as_ref().to_owned());
        self
    }

    /// Set the path to the Rust type that should be use for the Response type of this method.
    pub fn output_type(mut self, output_type: impl AsRef<str>) -> Self {
        self.output_type = Some(output_type.as_ref().to_owned());
        self
    }

    /// Set the path to the Rust type that should be used as the `Codec` for this method.
    ///
    /// Currently the codegen assumes that this type implements `Default`.
    pub fn codec_path(mut self, codec_path: impl AsRef<str>) -> Self {
        self.codec_path = Some(codec_path.as_ref().to_owned());
        self
    }

    /// Sets if the Method request from the client is streamed.
    pub fn client_streaming(mut self) -> Self {
        self.client_streaming = true;
        self
    }

    /// Sets if the Method response from the server is streamed.
    pub fn server_streaming(mut self) -> Self {
        self.server_streaming = true;
        self
    }

    /// Build a Method
    ///
    /// Panics if `name`, `route_name`, `input_type`, `output_type`, or `codec_path` weren't set.
    pub fn build(self) -> Method {
        Method {
            name: self.name.unwrap(),
            route_name: self.route_name.unwrap(),
            comments: self.comments,
            input_type: self.input_type.unwrap(),
            output_type: self.output_type.unwrap(),
            client_streaming: self.client_streaming,
            server_streaming: self.server_streaming,
            codec_path: self.codec_path.unwrap(),
        }
    }
}

struct ServiceGenerator {
    builder: Builder,
    clients: TokenStream,
    servers: TokenStream,
}

impl ServiceGenerator {
    fn generate(&mut self, service: &Service) {
        if self.builder.build_server {
            let server = CodeGenBuilder::new()
                .emit_package(true)
                .compile_well_known_types(false)
                .generate_server(service, "");

            self.servers.extend(server);
        }

        if self.builder.build_client {
            let client = CodeGenBuilder::new()
                .emit_package(true)
                .compile_well_known_types(false)
                .build_transport(self.builder.build_transport)
                .generate_client(service, "");

            self.clients.extend(client);
        }
    }

    fn finalize(&mut self, buf: &mut String) {
        if self.builder.build_client && !self.clients.is_empty() {
            let clients = &self.clients;

            let client_service = quote::quote! {
                #clients
            };

            let ast: syn::File = syn::parse2(client_service).expect("not a valid tokenstream");
            let code = prettyplease::unparse(&ast);
            buf.push_str(&code);

            self.clients = TokenStream::default();
        }

        if self.builder.build_server && !self.servers.is_empty() {
            let servers = &self.servers;

            let server_service = quote::quote! {
                #servers
            };

            let ast: syn::File = syn::parse2(server_service).expect("not a valid tokenstream");
            let code = prettyplease::unparse(&ast);
            buf.push_str(&code);

            self.servers = TokenStream::default();
        }
    }
}

/// Service generator builder.
#[derive(Debug)]
pub struct Builder {
    build_server: bool,
    build_client: bool,
    build_transport: bool,

    out_dir: Option<PathBuf>,
}

impl Default for Builder {
    fn default() -> Self {
        Self {
            build_server: true,
            build_client: true,
            build_transport: true,
            out_dir: None,
        }
    }
}

impl Builder {
    /// Create a new Builder
    pub fn new() -> Self {
        Self::default()
    }

    /// Enable or disable gRPC client code generation.
    ///
    /// Defaults to enabling client code generation.
    pub fn build_client(mut self, enable: bool) -> Self {
        self.build_client = enable;
        self
    }

    /// Enable or disable gRPC server code generation.
    ///
    /// Defaults to enabling server code generation.
    pub fn build_server(mut self, enable: bool) -> Self {
        self.build_server = enable;
        self
    }

    /// Enable or disable generated clients and servers to have built-in tonic
    /// transport features.
    ///
    /// When the `transport` feature is disabled this does nothing.
    pub fn build_transport(mut self, enable: bool) -> Self {
        self.build_transport = enable;
        self
    }

    /// Set the output directory to generate code to.
    ///
    /// Defaults to the `OUT_DIR` environment variable.
    pub fn out_dir(mut self, out_dir: impl AsRef<Path>) -> Self {
        self.out_dir = Some(out_dir.as_ref().to_path_buf());
        self
    }

    /// Performs code generation for the provided services.
    ///
    /// Generated services will be output into the directory specified by `out_dir`
    /// with files named `<package_name>.<service_name>.rs`.
    pub fn compile(self, services: &[Service]) {
        let out_dir = if let Some(out_dir) = self.out_dir.as_ref() {
            out_dir.clone()
        } else {
            PathBuf::from(std::env::var("OUT_DIR").unwrap())
        };

        let mut generator = ServiceGenerator {
            builder: self,
            clients: TokenStream::default(),
            servers: TokenStream::default(),
        };

        for service in services {
            generator.generate(service);
            let mut output = String::new();
            generator.finalize(&mut output);

            let out_file = out_dir.join(format!("{}.{}.rs", service.package, service.name));
            fs::write(out_file, output).unwrap();
        }
    }
}
