use std::sync::Arc;

use prost::{bytes::Bytes, Message};

use crate::{
    descriptor::{
        build::{
            join_path, resolve_name,
            visit::{visit, Visitor},
            DescriptorPoolOffsets, ResolveNameFilter,
        },
        error::{DescriptorErrorKind, Label},
        tag,
        types::{
            uninterpreted_option, DescriptorProto, EnumDescriptorProto, EnumValueDescriptorProto,
            FieldDescriptorProto, FileDescriptorProto, MethodDescriptorProto, OneofDescriptorProto,
            Options, ServiceDescriptorProto, UninterpretedOption,
        },
        Definition, DefinitionKind, EnumIndex, EnumValueIndex, ExtensionIndex, FieldIndex,
        FileIndex, MessageIndex, MethodIndex, OneofIndex, ServiceIndex, MAP_ENTRY_KEY_NUMBER,
        MAP_ENTRY_VALUE_NUMBER,
    },
    dynamic::{fmt_string, FieldDescriptorLike},
    Cardinality, DescriptorError, DescriptorPool, DynamicMessage, EnumDescriptor,
    ExtensionDescriptor, MapKey, MessageDescriptor, ReflectMessage, Value,
};

impl DescriptorPool {
    pub(super) fn resolve_options(
        &mut self,
        offsets: DescriptorPoolOffsets,
        files: &[FileDescriptorProto],
    ) -> Result<(), DescriptorError> {
        debug_assert_eq!(Arc::strong_count(&self.inner), 1);
        let mut visitor = OptionsVisitor {
            pool: self,
            errors: Vec::new(),
            options: Vec::new(),
            locations: Vec::new(),
        };
        visit(offsets, files, &mut visitor);

        if !visitor.errors.is_empty() {
            return Err(DescriptorError::new(visitor.errors));
        }

        debug_assert_eq!(Arc::strong_count(&visitor.pool.inner), 1);
        let inner = Arc::get_mut(&mut visitor.pool.inner).unwrap();
        for (file, path, encoded) in visitor.options {
            let file = &mut inner.files[file as usize].raw;
            set_file_option(file, &path, &encoded);
        }

        for (file, from, to) in visitor.locations {
            let file = &mut inner.files[file as usize].raw;
            if let Some(source_code_info) = &mut file.source_code_info {
                for location in &mut source_code_info.location {
                    if location.path.starts_with(&from) {
                        location.path.splice(..from.len(), to.iter().copied());
                    }
                }
            }
        }

        Ok(())
    }
}

struct OptionsVisitor<'a> {
    pool: &'a mut DescriptorPool,
    errors: Vec<DescriptorErrorKind>,
    options: Vec<(FileIndex, Box<[i32]>, Vec<u8>)>,
    #[allow(clippy::type_complexity)]
    locations: Vec<(FileIndex, Box<[i32]>, Box<[i32]>)>,
}

impl Visitor for OptionsVisitor<'_> {
    fn visit_file(&mut self, path: &[i32], index: FileIndex, file: &FileDescriptorProto) {
        if let Some(options) = &file.options {
            let path = join_path(path, &[tag::file::OPTIONS]);
            let encoded = self.resolve_options(
                "google.protobuf.FileOptions",
                options,
                &options.value.uninterpreted_option,
                file.package(),
                index,
                &path,
            );
            self.options.push((index, path, encoded));
        }
    }

    fn visit_message(
        &mut self,
        path: &[i32],
        full_name: &str,
        file: FileIndex,
        _: Option<MessageIndex>,
        _: MessageIndex,
        message: &DescriptorProto,
    ) {
        if let Some(options) = &message.options {
            let path = join_path(path, &[tag::message::OPTIONS]);
            let encoded = self.resolve_options(
                "google.protobuf.MessageOptions",
                options,
                &options.value.uninterpreted_option,
                full_name,
                file,
                &path,
            );
            self.options.push((file, path, encoded));
        }

        for (i, extension_range) in message.extension_range.iter().enumerate() {
            let path = join_path(
                path,
                &[
                    tag::message::EXTENSION_RANGE,
                    i as i32,
                    tag::message::extension_range::OPTIONS,
                ],
            );
            if let Some(options) = &extension_range.options {
                let encoded = self.resolve_options(
                    "google.protobuf.ExtensionRangeOptions",
                    options,
                    &options.value.uninterpreted_option,
                    full_name,
                    file,
                    &path,
                );
                self.options.push((file, path, encoded));
            }
        }
    }

    fn visit_field(
        &mut self,
        path: &[i32],
        full_name: &str,
        file: FileIndex,
        _: MessageIndex,
        _: FieldIndex,
        field: &FieldDescriptorProto,
    ) {
        if let Some(options) = &field.options {
            let path = join_path(path, &[tag::field::OPTIONS]);
            let encoded = self.resolve_options(
                "google.protobuf.FieldOptions",
                options,
                &options.value.uninterpreted_option,
                full_name,
                file,
                &path,
            );
            self.options.push((file, path, encoded));
        }
    }

    fn visit_oneof(
        &mut self,
        path: &[i32],
        full_name: &str,
        file: FileIndex,
        _: MessageIndex,
        _: OneofIndex,
        oneof: &OneofDescriptorProto,
    ) {
        if let Some(options) = &oneof.options {
            let path = join_path(path, &[tag::oneof::OPTIONS]);
            let encoded = self.resolve_options(
                "google.protobuf.OneofOptions",
                options,
                &options.value.uninterpreted_option,
                full_name,
                file,
                &path,
            );
            self.options.push((file, path, encoded));
        }
    }

    fn visit_service(
        &mut self,
        path: &[i32],
        full_name: &str,
        file: FileIndex,
        _: ServiceIndex,
        service: &ServiceDescriptorProto,
    ) {
        if let Some(options) = &service.options {
            let path = join_path(path, &[tag::service::OPTIONS]);
            let encoded = self.resolve_options(
                "google.protobuf.ServiceOptions",
                options,
                &options.value.uninterpreted_option,
                full_name,
                file,
                &path,
            );
            self.options.push((file, path, encoded));
        }
    }

    fn visit_method(
        &mut self,
        path: &[i32],
        full_name: &str,
        file: FileIndex,
        _: ServiceIndex,
        _: MethodIndex,
        method: &MethodDescriptorProto,
    ) {
        if let Some(options) = &method.options {
            let path = join_path(path, &[tag::method::OPTIONS]);
            let encoded = self.resolve_options(
                "google.protobuf.MethodOptions",
                options,
                &options.value.uninterpreted_option,
                full_name,
                file,
                &path,
            );
            self.options.push((file, path, encoded));
        }
    }

    fn visit_enum(
        &mut self,
        path: &[i32],
        full_name: &str,
        file: FileIndex,
        _: Option<MessageIndex>,
        _: EnumIndex,
        enum_: &EnumDescriptorProto,
    ) {
        if let Some(options) = &enum_.options {
            let path = join_path(path, &[tag::enum_::OPTIONS]);
            let encoded = self.resolve_options(
                "google.protobuf.EnumOptions",
                options,
                &options.value.uninterpreted_option,
                full_name,
                file,
                &path,
            );
            self.options.push((file, path, encoded));
        }
    }

    fn visit_enum_value(
        &mut self,
        path: &[i32],
        full_name: &str,
        file: FileIndex,
        _: EnumIndex,
        _: EnumValueIndex,
        value: &EnumValueDescriptorProto,
    ) {
        if let Some(options) = &value.options {
            let path = join_path(path, &[tag::enum_value::OPTIONS]);
            let encoded = self.resolve_options(
                "google.protobuf.EnumValueOptions",
                options,
                &options.value.uninterpreted_option,
                full_name,
                file,
                &path,
            );
            self.options.push((file, path, encoded));
        }
    }

    fn visit_extension(
        &mut self,
        path: &[i32],
        full_name: &str,
        file: FileIndex,
        _: Option<MessageIndex>,
        _: ExtensionIndex,
        extension: &FieldDescriptorProto,
    ) {
        if let Some(options) = &extension.options {
            let path = join_path(path, &[tag::field::OPTIONS]);
            let encoded = self.resolve_options(
                "google.protobuf.FieldOptions",
                options,
                &options.value.uninterpreted_option,
                full_name,
                file,
                &path,
            );
            self.options.push((file, path, encoded));
        }
    }
}

impl OptionsVisitor<'_> {
    fn resolve_options<T>(
        &mut self,
        desc_name: &str,
        options: &Options<T>,
        uninterpreted: &[UninterpretedOption],
        scope: &str,
        file: FileIndex,
        path: &[i32],
    ) -> Vec<u8> {
        let desc = self.pool.get_message_by_name(desc_name).unwrap_or_else(|| {
            DescriptorPool::global()
                .get_message_by_name(desc_name)
                .unwrap()
        });

        let mut message = match DynamicMessage::decode(desc, options.encoded.as_slice()) {
            Ok(message) => message,
            Err(err) => {
                self.errors
                    .push(DescriptorErrorKind::DecodeFileDescriptorSet { err });
                return Vec::new();
            }
        };

        for (i, option) in uninterpreted.iter().enumerate() {
            if let Err(err) = self.set_option(
                &mut message,
                option,
                scope,
                file,
                join_path(path, &[tag::UNINTERPRETED_OPTION, i as i32]),
            ) {
                self.errors.push(err);
            }
        }

        message.clear_field_by_number(tag::UNINTERPRETED_OPTION as u32);

        message.encode_to_vec()
    }

    #[allow(clippy::result_large_err)]
    fn set_option(
        &mut self,
        mut message: &mut DynamicMessage,
        option: &UninterpretedOption,
        scope: &str,
        file: FileIndex,
        path: Box<[i32]>,
    ) -> Result<(), DescriptorErrorKind> {
        let mut resolved_path = Vec::with_capacity(path.len() - 2 + option.name.len());
        resolved_path.extend_from_slice(&path[..path.len() - 2]);

        for (i, part) in option.name.iter().enumerate() {
            let is_last = i == option.name.len() - 1;

            let desc = message.descriptor();
            if part.is_extension {
                let extension_desc =
                    self.find_extension(scope, &part.name_part, file, &path, &desc)?;
                resolved_path.push(extension_desc.number() as i32);

                if is_last {
                    if extension_desc.cardinality() != Cardinality::Repeated
                        && message.has_extension(&extension_desc)
                    {
                        return Err(DescriptorErrorKind::DuplicateOption {
                            name: fmt_option_name(&option.name),
                            found: Label::new(&self.pool.inner.files, "found here", file, path),
                        });
                    } else {
                        self.set_field_value(
                            message.get_extension_mut(&extension_desc),
                            &mut resolved_path,
                            &extension_desc,
                            option,
                            file,
                            &path,
                        )?;
                    }
                } else if let Value::Message(submessage) =
                    message.get_extension_mut(&extension_desc)
                {
                    message = submessage;
                } else {
                    return Err(DescriptorErrorKind::InvalidOptionType {
                        name: fmt_option_name(&option.name[..i + 1]),
                        ty: fmt_field_ty(&extension_desc),
                        value: fmt_value(option),
                        is_last,
                        found: Label::new(&self.pool.inner.files, "found here", file, path),
                    });
                }
            } else {
                match desc.get_field_by_name(&part.name_part) {
                    Some(field_desc) => {
                        resolved_path.push(field_desc.number() as i32);

                        if is_last {
                            if field_desc.cardinality() != Cardinality::Repeated
                                && message.has_field(&field_desc)
                            {
                                return Err(DescriptorErrorKind::DuplicateOption {
                                    name: fmt_option_name(&option.name),
                                    found: Label::new(
                                        &self.pool.inner.files,
                                        "found here",
                                        file,
                                        path,
                                    ),
                                });
                            } else {
                                self.set_field_value(
                                    message.get_field_mut(&field_desc),
                                    &mut resolved_path,
                                    &field_desc,
                                    option,
                                    file,
                                    &path,
                                )?;
                            }
                        } else if let Value::Message(submessage) =
                            message.get_field_mut(&field_desc)
                        {
                            message = submessage;
                        } else {
                            return Err(DescriptorErrorKind::InvalidOptionType {
                                name: fmt_option_name(&option.name[..i + 1]),
                                ty: fmt_field_ty(&field_desc),
                                value: fmt_value(option),
                                is_last,
                                found: Label::new(&self.pool.inner.files, "found here", file, path),
                            });
                        }
                    }
                    None => {
                        return Err(DescriptorErrorKind::OptionNotFound {
                            name: fmt_option_name(&option.name[..i + 1]),
                            found: Label::new(&self.pool.inner.files, "found here", file, path),
                        })
                    }
                }
            }
        }

        self.locations.push((file, path, resolved_path.into()));

        Ok(())
    }

    #[allow(clippy::result_large_err)]
    fn set_field_value(
        &self,
        value: &mut Value,
        resolved_path: &mut Vec<i32>,
        desc: &impl FieldDescriptorLike,
        option: &UninterpretedOption,
        file: FileIndex,
        path: &[i32],
    ) -> Result<(), DescriptorErrorKind> {
        let err = |()| DescriptorErrorKind::InvalidOptionType {
            name: fmt_option_name(&option.name),
            ty: fmt_field_ty(desc),
            value: fmt_value(option),
            is_last: true,
            found: Label::new(&self.pool.inner.files, "found here", file, path.into()),
        };

        let parse_err = |parse_err| match parse_err {
            #[cfg(feature = "text-format")]
            Some(parse_err) => DescriptorErrorKind::InvalidMessageOption {
                name: fmt_option_name(&option.name),
                ty: fmt_field_ty(desc),
                found: Label::new(&self.pool.inner.files, "found here", file, path.into()),
                err: parse_err,
            },
            _ => err(()),
        };

        match value {
            Value::Bool(value) => *value = option_to_bool(option).map_err(err)?,
            Value::I32(value) => *value = option_to_int(option).map_err(err)?,
            Value::I64(value) => *value = option_to_int(option).map_err(err)?,
            Value::U32(value) => *value = option_to_int(option).map_err(err)?,
            Value::U64(value) => *value = option_to_int(option).map_err(err)?,
            Value::F32(value) => *value = option_to_float(option).map_err(err)? as f32,
            Value::F64(value) => *value = option_to_float(option).map_err(err)?,
            Value::String(value) => *value = option_to_string(option).map_err(err)?,
            Value::Bytes(value) => *value = option_to_bytes(option).map_err(err)?,
            Value::EnumNumber(value) => {
                *value = option_to_enum(option, desc.kind().as_enum().unwrap()).map_err(err)?
            }
            Value::Message(value) => {
                *value = option_to_message(option, desc.kind().as_message().unwrap())
                    .map_err(parse_err)?
            }
            Value::List(value) => {
                resolved_path.push(value.len() as i32);

                let mut entry = Value::default_value(&desc.kind());
                self.set_field_value(&mut entry, resolved_path, desc, option, file, path)?;
                value.push(entry);
            }
            Value::Map(value) => {
                let (entry_key, entry_value) =
                    option_to_map_entry(option, desc.kind().as_message().unwrap())
                        .map_err(parse_err)?;
                value.insert(entry_key, entry_value);
            }
        }

        Ok(())
    }

    #[allow(clippy::result_large_err)]
    fn find_extension(
        &self,
        scope: &str,
        name: &str,
        file: FileIndex,
        path: &[i32],
        extendee: &MessageDescriptor,
    ) -> Result<ExtensionDescriptor, DescriptorErrorKind> {
        let (_, def) = resolve_name(
            &self.pool.inner.files[file as usize].transitive_dependencies,
            &self.pool.inner.names,
            scope,
            name,
            ResolveNameFilter::Extension,
        )
        .into_result(name, &self.pool.inner.files, file, path, &[])?;

        let &Definition {
            kind: DefinitionKind::Extension(index),
            ..
        } = def
        else {
            unreachable!()
        };

        let desc = ExtensionDescriptor {
            pool: self.pool.clone(),
            index,
        };

        if desc.containing_message() == *extendee {
            Ok(desc)
        } else {
            Err(DescriptorErrorKind::InvalidOptionExtendee {
                name: desc.full_name().to_owned(),
                expected_extendee: extendee.full_name().to_owned(),
                actual_extendee: desc.containing_message().full_name().to_owned(),
                found: Label::new(&self.pool.inner.files, "found here", file, path.into()),
            })
        }
    }
}

fn fmt_option_name(parts: &[uninterpreted_option::NamePart]) -> String {
    let mut result = String::new();
    for part in parts {
        if !result.is_empty() {
            result.push('.');
        }
        if part.is_extension {
            result.push('(');
            result.push_str(&part.name_part);
            result.push(')');
        } else {
            result.push_str(&part.name_part);
        }
    }
    result
}

pub(super) fn option_to_bool(option: &UninterpretedOption) -> Result<bool, ()> {
    match option.identifier_value.as_deref() {
        Some("true") => Ok(true),
        Some("false") => Ok(false),
        _ => Err(()),
    }
}

pub(super) fn option_to_int<T>(option: &UninterpretedOption) -> Result<T, ()>
where
    T: TryFrom<u64> + TryFrom<i64>,
{
    if let Some(int) = option.positive_int_value {
        int.try_into().map_err(drop)
    } else if let Some(int) = option.negative_int_value {
        int.try_into().map_err(drop)
    } else {
        Err(())
    }
}

pub(super) fn option_to_float(option: &UninterpretedOption) -> Result<f64, ()> {
    if let Some(float) = option.double_value {
        Ok(float)
    } else if let Some(int) = option.positive_int_value {
        Ok(int as f64)
    } else if let Some(int) = option.negative_int_value {
        Ok(int as f64)
    } else {
        Err(())
    }
}

pub(super) fn option_to_string(option: &UninterpretedOption) -> Result<String, ()> {
    if let Some(bytes) = &option.string_value {
        String::from_utf8(bytes.clone()).map_err(drop)
    } else {
        Err(())
    }
}

pub(super) fn option_to_bytes(option: &UninterpretedOption) -> Result<Bytes, ()> {
    if let Some(bytes) = &option.string_value {
        Ok(Bytes::copy_from_slice(bytes))
    } else {
        Err(())
    }
}

pub(super) fn option_to_enum(
    option: &UninterpretedOption,
    desc: &EnumDescriptor,
) -> Result<i32, ()> {
    if let Some(ident) = &option.identifier_value {
        if let Some(value) = desc.get_value_by_name(ident) {
            Ok(value.number())
        } else {
            Err(())
        }
    } else {
        Err(())
    }
}

#[cfg(feature = "text-format")]
type ParseError = crate::text_format::ParseError;
#[cfg(not(feature = "text-format"))]
type ParseError = ();

#[cfg(feature = "text-format")]
pub(super) fn option_to_message(
    option: &UninterpretedOption,
    desc: &MessageDescriptor,
) -> Result<DynamicMessage, Option<ParseError>> {
    if let Some(text_format) = &option.aggregate_value {
        DynamicMessage::parse_text_format(desc.clone(), text_format).map_err(Some)
    } else {
        Err(None)
    }
}

#[cfg(not(feature = "text-format"))]
pub(super) fn option_to_message(
    option: &UninterpretedOption,
    desc: &MessageDescriptor,
) -> Result<DynamicMessage, Option<ParseError>> {
    if option.aggregate_value.is_some() {
        Ok(DynamicMessage::new(desc.clone()))
    } else {
        Err(None)
    }
}

pub(super) fn option_to_map_entry(
    option: &UninterpretedOption,
    desc: &MessageDescriptor,
) -> Result<(MapKey, Value), Option<ParseError>> {
    debug_assert!(desc.is_map_entry());
    let entry = option_to_message(option, desc)?;
    let key = entry
        .get_field_by_number(MAP_ENTRY_KEY_NUMBER)
        .ok_or(None)?
        .into_owned()
        .into_map_key()
        .ok_or(None)?;
    let value = entry
        .get_field_by_number(MAP_ENTRY_VALUE_NUMBER)
        .ok_or(None)?
        .into_owned();
    Ok((key, value))
}

fn fmt_field_ty(field: &impl FieldDescriptorLike) -> String {
    if field.is_map() {
        let entry = field.kind();
        let entry = entry.as_message().unwrap();
        format!(
            "map<{:?}, {:?}>",
            entry.map_entry_key_field().kind(),
            entry.map_entry_value_field().kind()
        )
    } else if field.is_list() {
        format!("repeated {:?}", field.kind())
    } else {
        format!("{:?}", field.kind())
    }
}

fn fmt_value(option: &UninterpretedOption) -> String {
    if let Some(value) = &option.identifier_value {
        value.clone()
    } else if let Some(value) = &option.positive_int_value {
        value.to_string()
    } else if let Some(value) = &option.negative_int_value {
        value.to_string()
    } else if let Some(value) = &option.double_value {
        value.to_string()
    } else if let Some(value) = &option.string_value {
        let mut string = String::new();
        fmt_string(&mut string, value).unwrap();
        string
    } else if let Some(value) = &option.aggregate_value {
        value.clone()
    } else {
        String::new()
    }
}

fn set_file_option(file: &mut FileDescriptorProto, path: &[i32], encoded: &[u8]) {
    match path[0] {
        tag::file::OPTIONS => {
            debug_assert_eq!(path.len(), 1);
            file.options = Some(Options::decode(encoded).unwrap());
        }
        tag::file::MESSAGE_TYPE => {
            let message = &mut file.message_type[path[1] as usize];
            set_message_option(message, &path[2..], encoded);
        }
        tag::file::ENUM_TYPE => {
            let enum_ = &mut file.enum_type[path[1] as usize];
            set_enum_option(enum_, &path[2..], encoded);
        }
        tag::file::SERVICE => {
            let service = &mut file.service[path[1] as usize];
            match path[2] {
                tag::service::OPTIONS => service.options = Some(Options::decode(encoded).unwrap()),
                tag::service::METHOD => {
                    debug_assert_eq!(path.len(), 5);
                    debug_assert_eq!(path[4], tag::method::OPTIONS);
                    let value = &mut service.method[path[3] as usize];
                    value.options = Some(Options::decode(encoded).unwrap());
                }
                p => panic!("unknown path element {p}"),
            }
        }
        tag::file::EXTENSION => {
            debug_assert_eq!(path.len(), 3);
            debug_assert_eq!(path[2], tag::field::OPTIONS);
            let field = &mut file.extension[path[1] as usize];
            field.options = Some(Options::decode(encoded).unwrap());
        }
        p => panic!("unknown path element {p}"),
    }
}

fn set_message_option(message: &mut DescriptorProto, path: &[i32], encoded: &[u8]) {
    match path[0] {
        tag::message::OPTIONS => {
            debug_assert_eq!(path.len(), 1);
            message.options = Some(Options::decode(encoded).unwrap());
        }
        tag::message::EXTENSION_RANGE => {
            debug_assert_eq!(path.len(), 3);
            debug_assert_eq!(path[2], tag::message::extension_range::OPTIONS);
            let extension_range = &mut message.extension_range[path[1] as usize];
            extension_range.options = Some(Options::decode(encoded).unwrap());
        }
        tag::message::FIELD => {
            debug_assert_eq!(path.len(), 3);
            debug_assert_eq!(path[2], tag::field::OPTIONS);
            let field = &mut message.field[path[1] as usize];
            field.options = Some(Options::decode(encoded).unwrap());
        }
        tag::message::ONEOF_DECL => {
            debug_assert_eq!(path.len(), 3);
            debug_assert_eq!(path[2], tag::oneof::OPTIONS);
            let field = &mut message.oneof_decl[path[1] as usize];
            field.options = Some(Options::decode(encoded).unwrap());
        }
        tag::message::NESTED_TYPE => {
            let nested_message = &mut message.nested_type[path[1] as usize];
            set_message_option(nested_message, &path[2..], encoded);
        }
        tag::message::ENUM_TYPE => {
            let enum_ = &mut message.enum_type[path[1] as usize];
            set_enum_option(enum_, &path[2..], encoded);
        }
        tag::message::EXTENSION => {
            debug_assert_eq!(path.len(), 3);
            debug_assert_eq!(path[2], tag::field::OPTIONS);
            let field = &mut message.extension[path[1] as usize];
            field.options = Some(Options::decode(encoded).unwrap());
        }
        p => panic!("unknown path element {p}"),
    }
}

fn set_enum_option(enum_: &mut EnumDescriptorProto, path: &[i32], encoded: &[u8]) {
    match path[0] {
        tag::enum_::OPTIONS => enum_.options = Some(Options::decode(encoded).unwrap()),
        tag::enum_::VALUE => {
            debug_assert_eq!(path.len(), 3);
            debug_assert_eq!(path[2], tag::enum_value::OPTIONS);
            let value = &mut enum_.value[path[1] as usize];
            value.options = Some(Options::decode(encoded).unwrap());
        }
        p => panic!("unknown path element {p}"),
    }
}
