pub(crate) mod ctx;
pub(crate) mod rustproto_proto;

use std::fmt;
use std::ops::Deref;
use std::rc::Rc;

use protobuf::reflect::EnumDescriptor;
use protobuf::reflect::FieldDescriptor;
use protobuf::reflect::FileDescriptor;
use protobuf::reflect::MessageDescriptor;
use protobuf::reflect::OneofDescriptor;

/// Dynamic callback to customize code generation.
pub trait CustomizeCallback: 'static {
    fn file(&self, file: &FileDescriptor) -> Customize {
        let _ = file;
        Customize::default()
    }

    fn message(&self, message: &MessageDescriptor) -> Customize {
        let _ = message;
        Customize::default()
    }

    fn field(&self, field: &FieldDescriptor) -> Customize {
        let _ = field;
        Customize::default()
    }

    fn special_field(&self, message: &MessageDescriptor, field: &str) -> Customize {
        let _ = (message, field);
        Customize::default()
    }

    fn enumeration(&self, enum_type: &EnumDescriptor) -> Customize {
        let _ = enum_type;
        Customize::default()
    }

    fn oneof(&self, oneof: &OneofDescriptor) -> Customize {
        let _ = oneof;
        Customize::default()
    }
}

pub(crate) struct CustomizeCallbackDefault;
impl CustomizeCallback for CustomizeCallbackDefault {}

#[derive(Clone)]
pub(crate) struct CustomizeCallbackHolder(pub(crate) Rc<dyn CustomizeCallback>);

impl CustomizeCallbackHolder {
    pub(crate) fn new(callback: impl CustomizeCallback) -> CustomizeCallbackHolder {
        CustomizeCallbackHolder(Rc::new(callback))
    }
}

impl Deref for CustomizeCallbackHolder {
    type Target = dyn CustomizeCallback;

    fn deref(&self) -> &Self::Target {
        &*self.0
    }
}

impl Default for CustomizeCallbackHolder {
    fn default() -> Self {
        CustomizeCallbackHolder(Rc::new(CustomizeCallbackDefault))
    }
}

impl PartialEq for CustomizeCallbackHolder {
    fn eq(&self, other: &Self) -> bool {
        Rc::ptr_eq(&self.0, &other.0)
    }
}

impl fmt::Debug for CustomizeCallbackHolder {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.debug_struct("CustomizeCallbackWrapper")
            .finish_non_exhaustive()
    }
}

/// Specifies style of generated code.
/// Generated files can be customized using this proto
/// or using `rustproto.proto` options.
#[derive(Default, Debug, Clone, PartialEq)]
pub struct Customize {
    /// Code to insert before the element in the generated file.
    pub(crate) before: Option<String>,
    /// When false, `get_`, `set_`, `mut_` etc. accessors are not generated
    pub(crate) generate_accessors: Option<bool>,
    /// When false, `get_` is not generated even if `syntax = "proto2"`
    pub(crate) generate_getter: Option<bool>,
    /// Use `bytes::Bytes` for `bytes` fields
    pub(crate) tokio_bytes: Option<bool>,
    /// Use `bytes::Bytes` for `string` fields
    pub(crate) tokio_bytes_for_string: Option<bool>,
    /// Enable lite runtime.
    pub(crate) lite_runtime: Option<bool>,
    /// Generate `mod.rs` in the output directory.
    ///
    /// This option allows inclusion of generated files from cargo output directory.
    ///
    /// This option will likely be on by default in rust-protobuf version 3.
    pub(crate) gen_mod_rs: Option<bool>,
    /// Used internally to generate protos bundled in protobuf crate
    /// like `descriptor.proto`
    pub(crate) inside_protobuf: Option<bool>,
}

#[derive(Debug, thiserror::Error)]
pub(crate) enum CustomizeParseParameterError {
    #[error("Cannot parse bool option value: {:?}", .0)]
    CannotParseBool(String),
    #[error("Unknown option name: {:?}", .0)]
    UnknownOptionName(String),
}

impl Customize {
    /// Insert code before the element in the generated file
    /// (e. g. serde annotations, see
    /// [example here](https://github.com/stepancheg/rust-protobuf/tree/master/protobuf-examples/customize-serde)).
    pub fn before(mut self, before: &str) -> Self {
        self.before = Some(before.to_owned());
        self
    }

    pub fn generate_accessors(mut self, generate_accessors: bool) -> Self {
        self.generate_accessors = Some(generate_accessors);
        self
    }

    pub fn generate_getter(mut self, generate_getter: bool) -> Self {
        self.generate_getter = Some(generate_getter);
        self
    }

    pub fn tokio_bytes(mut self, tokio_bytes: bool) -> Self {
        self.tokio_bytes = Some(tokio_bytes);
        self
    }

    pub fn tokio_bytes_for_string(mut self, tokio_bytes_for_string: bool) -> Self {
        self.tokio_bytes_for_string = Some(tokio_bytes_for_string);
        self
    }

    /// Generate code for "lite runtime". Generated code contains no code for reflection.
    /// So the generated code (and more importantly, generated binary size) is smaller,
    /// but reflection, text format, JSON serialization won't work.
    ///
    /// Note when using `protoc` plugin `protoc-gen-rust`, the option name is just `lite`.
    pub fn lite_runtime(mut self, lite_runtime: bool) -> Self {
        self.lite_runtime = Some(lite_runtime);
        self
    }

    /// Generate `mod.rs` with all the generated modules.
    /// This option is on by default in rust-protobuf version 3.
    pub fn gen_mod_rs(mut self, gen_mod_rs: bool) -> Self {
        self.gen_mod_rs = Some(gen_mod_rs);
        self
    }

    /// Generate code bundled in protobuf crate. Regular users don't need this option.
    pub fn inside_protobuf(mut self, inside_protobuf: bool) -> Self {
        self.inside_protobuf = Some(inside_protobuf);
        self
    }

    /// Update fields of self with fields defined in other customize
    pub fn update_with(&mut self, that: &Customize) {
        if let Some(v) = &that.before {
            self.before = Some(v.clone());
        }
        if let Some(v) = that.generate_accessors {
            self.generate_accessors = Some(v);
        }
        if let Some(v) = that.generate_getter {
            self.generate_getter = Some(v);
        }
        if let Some(v) = that.tokio_bytes {
            self.tokio_bytes = Some(v);
        }
        if let Some(v) = that.tokio_bytes_for_string {
            self.tokio_bytes_for_string = Some(v);
        }
        if let Some(v) = that.lite_runtime {
            self.lite_runtime = Some(v);
        }
        if let Some(v) = that.gen_mod_rs {
            self.gen_mod_rs = Some(v);
        }
        if let Some(v) = that.inside_protobuf {
            self.inside_protobuf = Some(v);
        }
    }

    /// Update unset fields of self with fields from other customize
    pub fn set_defaults_from(&mut self, other: &Customize) {
        let mut tmp = other.clone();
        tmp.update_with(self);
        *self = tmp;
    }

    /// Parse customize options from a string passed via protoc flag.
    pub fn parse_from_parameter(parameter: &str) -> anyhow::Result<Customize> {
        fn parse_bool(v: &str) -> anyhow::Result<bool> {
            v.parse()
                .map_err(|_| CustomizeParseParameterError::CannotParseBool(v.to_owned()).into())
        }

        let mut r = Customize::default();
        for nv in parameter.split_whitespace() {
            let (n, v) = match nv.find('=') {
                Some(eq) => {
                    let n = &nv[..eq];
                    let v = &nv[eq + 1..];
                    (n, v)
                }
                None => (nv, "true"),
            };

            if n == "generate_accessors" {
                r.generate_accessors = Some(parse_bool(v)?);
            } else if n == "generate_getter" {
                r.generate_getter = Some(parse_bool(v)?);
            } else if n == "tokio_bytes" {
                r.tokio_bytes = Some(parse_bool(v)?);
            } else if n == "tokio_bytes_for_string" {
                r.tokio_bytes_for_string = Some(parse_bool(v)?);
            } else if n == "lite_runtime" {
                r.lite_runtime = Some(parse_bool(v)?);
            } else if n == "gen_mod_rs" {
                r.gen_mod_rs = Some(parse_bool(v)?);
            } else if n == "inside_protobuf" {
                r.inside_protobuf = Some(parse_bool(v)?);
            } else if n == "lite" {
                // Support Java and C++ protoc plugin syntax:
                // https://github.com/protocolbuffers/protobuf/issues/6489
                r.lite_runtime = Some(parse_bool(v)?);
            } else {
                return Err(CustomizeParseParameterError::UnknownOptionName(n.to_owned()).into());
            }
        }
        Ok(r)
    }
}
