//! `pbjson-build` consumes the descriptor output of [`prost-build`][1] and generates
//! [`serde::Serialize`][2] and [`serde::Deserialize`][3] implementations
//! that are compliant with the [protobuf JSON mapping][4]
//!
//! # Usage
//!
//! _It is recommended you first follow the example in [prost-build][1] to familiarise
//! yourself with `prost`_
//!
//! Add `prost-build`, `prost`, `pbjson`, `pbjson-build` and `pbjson-types` to
//! your `Cargo.toml`
//!
//! ```toml
//! [dependencies]
//! prost = <prost-version>
//! pbjson = <pbjson-version>
//! pbjson-types = <pbjson-version>
//!
//! [build-dependencies]
//! prost-build = <prost-version>
//! pbjson-build = <pbjson-version>
//! ```
//!
//! Next create a `build.rs` containing the following
//!
//! ```ignore
//! // This assumes protobuf files are under a directory called `protos`
//! // and in a protobuf package `mypackage`
//!
//! let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("protos");
//! let proto_files = vec![root.join("myproto.proto")];
//!
//! // Tell cargo to recompile if any of these proto files are changed
//! for proto_file in &proto_files {
//!     println!("cargo:rerun-if-changed={}", proto_file.display());
//! }
//!
//! let descriptor_path = PathBuf::from(env::var("OUT_DIR").unwrap())
//!     .join("proto_descriptor.bin");
//!
//! prost_build::Config::new()
//!     // Save descriptors to file
//!     .file_descriptor_set_path(&descriptor_path)
//!     // Override prost-types with pbjson-types
//!     .compile_well_known_types()
//!     .extern_path(".google.protobuf", "::pbjson_types")
//!     // Generate prost structs
//!     .compile_protos(&proto_files, &[root])?;
//!
//! let descriptor_set = std::fs::read(descriptor_path)?;
//! pbjson_build::Builder::new()
//!     .register_descriptors(&descriptor_set)?
//!     .build(&[".mypackage"])?;
//! ```
//!
//! Finally within `lib.rs`
//!
//! ```ignore
//! /// Generated by [`prost-build`]
//! include!(concat!(env!("OUT_DIR"), "/mypackage.rs"));
//! /// Generated by [`pbjson-build`]
//! include!(concat!(env!("OUT_DIR"), "/mypackage.serde.rs"));
//! ```
//!
//! The module will now contain the generated prost structs for your protobuf definition
//! along with compliant implementations of [serde::Serialize][2] and [serde::Deserialize][3]
//!
//! [1]: https://docs.rs/prost-build
//! [2]: https://docs.rs/serde/1.0.130/serde/trait.Serialize.html
//! [3]: https://docs.rs/serde/1.0.130/serde/trait.Deserialize.html
//! [4]: https://developers.google.com/protocol-buffers/docs/proto3#json

#![deny(rustdoc::broken_intra_doc_links, rustdoc::bare_urls, rust_2018_idioms)]
#![warn(
    missing_debug_implementations,
    clippy::explicit_iter_loop,
    clippy::use_self,
    clippy::clone_on_ref_ptr,
    clippy::future_not_send
)]

use crate::descriptor::{Descriptor, DescriptorSet, Package};
use crate::generator::{generate_enum, generate_message, Config};
use crate::message::resolve_message;
use std::io::{BufWriter, Error, ErrorKind, Result, Write};
use std::path::PathBuf;

mod descriptor;
mod escape;
mod generator;
mod message;

#[derive(Debug, Default)]
pub struct Builder {
    descriptors: descriptor::DescriptorSet,
    exclude: Vec<String>,
    out_dir: Option<PathBuf>,
}

impl Builder {
    /// Create a new `Builder`
    pub fn new() -> Self {
        Self {
            descriptors: DescriptorSet::new(),
            exclude: Default::default(),
            out_dir: None,
        }
    }

    /// Register an encoded `FileDescriptorSet` with this `Builder`
    pub fn register_descriptors(&mut self, descriptors: &[u8]) -> Result<&mut Self> {
        self.descriptors.register_encoded(descriptors)?;
        Ok(self)
    }

    /// Don't generate code for the following type prefixes
    pub fn exclude<S: Into<String>, I: IntoIterator<Item = S>>(
        &mut self,
        prefixes: I,
    ) -> &mut Self {
        self.exclude.extend(prefixes.into_iter().map(Into::into));
        self
    }

    /// Generates code for all registered types where `prefixes` contains a prefix of
    /// the fully-qualified path of the type
    pub fn build<S: AsRef<str>>(&mut self, prefixes: &[S]) -> Result<()> {
        let mut output: PathBuf = self.out_dir.clone().map(Ok).unwrap_or_else(|| {
            std::env::var_os("OUT_DIR")
                .ok_or_else(|| {
                    Error::new(ErrorKind::Other, "OUT_DIR environment variable is not set")
                })
                .map(Into::into)
        })?;
        output.push("FILENAME");

        let write_factory = move |package: &Package| {
            output.set_file_name(format!("{}.serde.rs", package));

            let file = std::fs::OpenOptions::new()
                .write(true)
                .truncate(true)
                .create(true)
                .open(&output)?;

            Ok(BufWriter::new(file))
        };

        let writers = self.generate(prefixes, write_factory)?;
        for (_, mut writer) in writers {
            writer.flush()?;
        }

        Ok(())
    }

    fn generate<S: AsRef<str>, W: Write, F: FnMut(&Package) -> Result<W>>(
        &self,
        prefixes: &[S],
        mut write_factory: F,
    ) -> Result<Vec<(Package, W)>> {
        let config = Config {
            extern_types: Default::default(),
        };

        let iter = self.descriptors.iter().filter(move |(t, _)| {
            let exclude = self
                .exclude
                .iter()
                .any(|prefix| t.matches_prefix(prefix.as_ref()));
            let include = prefixes
                .iter()
                .any(|prefix| t.matches_prefix(prefix.as_ref()));
            include && !exclude
        });

        // Exploit the fact descriptors is ordered to group together types from the same package
        let mut ret: Vec<(Package, W)> = Vec::new();
        for (type_path, descriptor) in iter {
            let writer = match ret.last_mut() {
                Some((package, writer)) if package == type_path.package() => writer,
                _ => {
                    let package = type_path.package();
                    ret.push((package.clone(), write_factory(package)?));
                    &mut ret.last_mut().unwrap().1
                }
            };

            match descriptor {
                Descriptor::Enum(descriptor) => {
                    generate_enum(&config, type_path, descriptor, writer)?
                }
                Descriptor::Message(descriptor) => {
                    if let Some(message) = resolve_message(&self.descriptors, descriptor) {
                        generate_message(&config, &message, writer)?
                    }
                }
            }
        }

        Ok(ret)
    }
}
