pub mod convert;
pub mod errors;
pub mod model;

use prost::Message;
use prost_reflect::{
    Cardinality, DescriptorPool, EnumDescriptor, FieldDescriptor, Kind, MessageDescriptor,
    MethodDescriptor, ServiceDescriptor,
};
use prost_types::FileDescriptorSet;

pub use convert::to_schema::message_to_schema;
pub use errors::ProtoError;
pub use model::{
    ProtoEnum, ProtoField, ProtoFieldCardinality, ProtoFieldType, ProtoMapField, ProtoMessage,
    ProtoMethod, ProtoOneof, ProtoScalarKind, ProtoService,
};

pub fn services_from_descriptor_set_bytes(bytes: &[u8]) -> Result<Vec<ProtoService>, ProtoError> {
    let descriptor_set =
        FileDescriptorSet::decode(bytes).map_err(|_| ProtoError::InvalidDescriptorSet)?;
    let pool = DescriptorPool::from_file_descriptor_set(descriptor_set)
        .map_err(|_| ProtoError::InvalidDescriptorPool)?;

    Ok(pool.services().map(service_from_descriptor).collect())
}

fn service_from_descriptor(service: ServiceDescriptor) -> ProtoService {
    ProtoService {
        package: service.parent_file().package_name().to_owned(),
        name: service.name().to_owned(),
        methods: service.methods().map(method_from_descriptor).collect(),
    }
}

fn method_from_descriptor(method: MethodDescriptor) -> ProtoMethod {
    ProtoMethod {
        name: method.name().to_owned(),
        input: message_from_descriptor(method.input()),
        output: message_from_descriptor(method.output()),
        client_streaming: method.is_client_streaming(),
        server_streaming: method.is_server_streaming(),
    }
}

fn message_from_descriptor(message: MessageDescriptor) -> ProtoMessage {
    let fields = message
        .fields()
        .filter(|field| field.containing_oneof().is_none())
        .map(field_from_descriptor)
        .collect();
    let oneofs = message
        .oneofs()
        .map(|oneof| ProtoOneof {
            name: oneof.name().to_owned(),
            variants: oneof.fields().map(field_from_descriptor).collect(),
        })
        .collect();

    ProtoMessage {
        name: message.name().to_owned(),
        fields,
        oneofs,
    }
}

fn field_from_descriptor(field: FieldDescriptor) -> ProtoField {
    let cardinality = if field.is_list() && !field.is_map() {
        ProtoFieldCardinality::Repeated
    } else {
        match field.cardinality() {
            Cardinality::Optional => ProtoFieldCardinality::Optional,
            Cardinality::Required => ProtoFieldCardinality::Required,
            Cardinality::Repeated => ProtoFieldCardinality::Repeated,
        }
    };

    ProtoField {
        name: field.name().to_owned(),
        json_name: field.json_name().to_owned(),
        cardinality,
        field_type: field_type_from_descriptor(field),
    }
}

fn field_type_from_descriptor(field: FieldDescriptor) -> ProtoFieldType {
    if field.is_map() {
        return map_field_from_descriptor(field);
    }

    match field.kind() {
        Kind::Bool => scalar_field(ProtoScalarKind::Bool),
        Kind::String => scalar_field(ProtoScalarKind::String),
        Kind::Bytes => scalar_field(ProtoScalarKind::Bytes),
        Kind::Int32 | Kind::Sint32 | Kind::Sfixed32 => scalar_field(ProtoScalarKind::Int32),
        Kind::Int64 | Kind::Sint64 | Kind::Sfixed64 => scalar_field(ProtoScalarKind::Int64),
        Kind::Uint32 | Kind::Fixed32 => scalar_field(ProtoScalarKind::Uint32),
        Kind::Uint64 | Kind::Fixed64 => scalar_field(ProtoScalarKind::Uint64),
        Kind::Float => scalar_field(ProtoScalarKind::Float),
        Kind::Double => scalar_field(ProtoScalarKind::Double),
        Kind::Message(message) => ProtoFieldType::Message {
            message: Box::new(message_from_descriptor(message)),
        },
        Kind::Enum(enumeration) => ProtoFieldType::Enum {
            enumeration: enum_from_descriptor(enumeration),
        },
    }
}

fn map_field_from_descriptor(field: FieldDescriptor) -> ProtoFieldType {
    let message = match field.kind() {
        Kind::Message(message) => message,
        _ => unreachable!(),
    };
    let key_field = message.map_entry_key_field();
    let value_field = message.map_entry_value_field();
    let key = match field_type_from_descriptor(key_field) {
        ProtoFieldType::Scalar { scalar } => scalar,
        _ => ProtoScalarKind::String,
    };

    ProtoFieldType::Map {
        map: ProtoMapField {
            key,
            value: Box::new(field_type_from_descriptor(value_field)),
        },
    }
}

fn scalar_field(scalar: ProtoScalarKind) -> ProtoFieldType {
    ProtoFieldType::Scalar { scalar }
}

fn enum_from_descriptor(enumeration: EnumDescriptor) -> ProtoEnum {
    ProtoEnum {
        name: enumeration.name().to_owned(),
        values: enumeration
            .values()
            .map(|value| value.name().to_owned())
            .collect(),
    }
}
