use crate::descriptor::{
    build::DescriptorPoolOffsets,
    tag, to_index,
    types::{
        DescriptorProto, EnumDescriptorProto, EnumValueDescriptorProto, FieldDescriptorProto,
        FileDescriptorProto, MethodDescriptorProto, OneofDescriptorProto, ServiceDescriptorProto,
    },
    EnumIndex, EnumValueIndex, ExtensionIndex, FieldIndex, FileIndex, MessageIndex, MethodIndex,
    OneofIndex, ServiceIndex,
};

pub(super) trait Visitor {
    fn visit_file(&mut self, _path: &[i32], _index: FileIndex, _file: &FileDescriptorProto) {}

    fn visit_message(
        &mut self,
        _path: &[i32],
        _full_name: &str,
        _file: FileIndex,
        _parent_message: Option<MessageIndex>,
        _index: MessageIndex,
        _message: &DescriptorProto,
    ) {
    }

    fn visit_field(
        &mut self,
        _path: &[i32],
        _full_name: &str,
        _file: FileIndex,
        _message: MessageIndex,
        _index: FieldIndex,
        _field: &FieldDescriptorProto,
    ) {
    }

    fn visit_oneof(
        &mut self,
        _path: &[i32],
        _full_name: &str,
        _file: FileIndex,
        _message: MessageIndex,
        _index: OneofIndex,
        _oneof: &OneofDescriptorProto,
    ) {
    }

    fn visit_service(
        &mut self,
        _path: &[i32],
        _full_name: &str,
        _file: FileIndex,
        _index: ServiceIndex,
        _service: &ServiceDescriptorProto,
    ) {
    }

    fn visit_method(
        &mut self,
        _path: &[i32],
        _full_name: &str,
        _file: FileIndex,
        _service: ServiceIndex,
        _index: MethodIndex,
        _method: &MethodDescriptorProto,
    ) {
    }

    fn visit_enum(
        &mut self,
        _path: &[i32],
        _full_name: &str,
        _file: FileIndex,
        _parent_message: Option<MessageIndex>,
        _index: EnumIndex,
        _enum: &EnumDescriptorProto,
    ) {
    }

    fn visit_enum_value(
        &mut self,
        _path: &[i32],
        _full_name: &str,
        _file: FileIndex,
        _enum_: EnumIndex,
        _index: EnumValueIndex,
        _value: &EnumValueDescriptorProto,
    ) {
    }

    fn visit_extension(
        &mut self,
        _path: &[i32],
        _full_name: &str,
        _file: FileIndex,
        _parent_message: Option<MessageIndex>,
        _index: ExtensionIndex,
        _extension: &FieldDescriptorProto,
    ) {
    }
}

pub(super) fn visit(
    offsets: DescriptorPoolOffsets,
    files: &[FileDescriptorProto],
    visitor: &mut dyn Visitor,
) {
    let mut context = Context {
        path: Vec::new(),
        scope: String::new(),
        offsets,
    };

    for file in files {
        context.visit_file(file, visitor);
    }
}

struct Context {
    path: Vec<i32>,
    scope: String,
    offsets: DescriptorPoolOffsets,
}

impl Context {
    fn visit_file(&mut self, file: &FileDescriptorProto, visitor: &mut dyn Visitor) {
        if !file.package().is_empty() {
            self.push_scope(file.package());
        }

        let index = post_inc(&mut self.offsets.file);
        visitor.visit_file(&self.path, index, file);

        self.push_path(tag::file::MESSAGE_TYPE);
        for (i, message) in file.message_type.iter().enumerate() {
            self.push_path(i as i32);
            self.visit_message(message, visitor, index, None);
            self.pop_path();
        }
        self.pop_path();

        self.push_path(tag::file::ENUM_TYPE);
        for (i, enum_) in file.enum_type.iter().enumerate() {
            self.push_path(i as i32);
            self.visit_enum(enum_, visitor, index, None);
            self.pop_path();
        }
        self.pop_path();

        self.push_path(tag::file::SERVICE);
        for (i, service) in file.service.iter().enumerate() {
            self.push_path(i as i32);
            self.visit_service(service, visitor, index);
            self.pop_path();
        }
        self.pop_path();

        self.push_path(tag::file::EXTENSION);
        for (i, extension) in file.extension.iter().enumerate() {
            self.push_path(i as i32);
            self.visit_extension(extension, visitor, index, None);
            self.pop_path();
        }
        self.pop_path();

        if !file.package().is_empty() {
            self.pop_scope(file.package());
        }
    }

    fn visit_message(
        &mut self,
        message: &DescriptorProto,
        visitor: &mut dyn Visitor,
        file: FileIndex,
        parent_message: Option<MessageIndex>,
    ) {
        self.push_scope(message.name());

        let index = post_inc(&mut self.offsets.message);
        visitor.visit_message(
            &self.path,
            &self.scope,
            file,
            parent_message,
            index,
            message,
        );

        self.push_path(tag::message::ONEOF_DECL);
        for (i, oneof) in message.oneof_decl.iter().enumerate() {
            self.push_path(i as i32);
            self.visit_oneof(oneof, visitor, file, index, to_index(i));
            self.pop_path();
        }
        self.pop_path();

        self.push_path(tag::message::FIELD);
        for (i, field) in message.field.iter().enumerate() {
            self.push_path(i as i32);
            self.visit_field(field, visitor, file, index, to_index(i));
            self.pop_path();
        }
        self.pop_path();

        self.push_path(tag::message::NESTED_TYPE);
        for (i, nested) in message.nested_type.iter().enumerate() {
            self.push_path(i as i32);
            self.visit_message(nested, visitor, file, Some(index));
            self.pop_path();
        }
        self.pop_path();

        self.push_path(tag::message::ENUM_TYPE);
        for (i, enum_) in message.enum_type.iter().enumerate() {
            self.push_path(i as i32);
            self.visit_enum(enum_, visitor, file, Some(index));
            self.pop_path();
        }
        self.pop_path();

        self.push_path(tag::message::EXTENSION);
        for (i, extension) in message.extension.iter().enumerate() {
            self.push_path(i as i32);
            self.visit_extension(extension, visitor, file, Some(index));
            self.pop_path();
        }
        self.pop_path();

        self.pop_scope(message.name());
    }

    fn visit_field(
        &mut self,
        field: &FieldDescriptorProto,
        visitor: &mut dyn Visitor,
        file: FileIndex,
        message: MessageIndex,
        index: FieldIndex,
    ) {
        self.push_scope(field.name());
        visitor.visit_field(&self.path, &self.scope, file, message, index, field);
        self.pop_scope(field.name());
    }

    fn visit_oneof(
        &mut self,
        oneof: &OneofDescriptorProto,
        visitor: &mut dyn Visitor,
        file: FileIndex,
        message: MessageIndex,
        index: OneofIndex,
    ) {
        self.push_scope(oneof.name());
        visitor.visit_oneof(&self.path, &self.scope, file, message, index, oneof);
        self.pop_scope(oneof.name());
    }

    fn visit_service(
        &mut self,
        service: &ServiceDescriptorProto,
        visitor: &mut dyn Visitor,
        file: FileIndex,
    ) {
        self.push_scope(service.name());

        let index = post_inc(&mut self.offsets.service);
        visitor.visit_service(&self.path, &self.scope, file, index, service);

        self.push_path(tag::service::METHOD);
        for (i, method) in service.method.iter().enumerate() {
            self.push_path(i as i32);
            self.visit_method(method, visitor, file, index, to_index(i));
            self.pop_path();
        }
        self.pop_path();

        self.pop_scope(service.name());
    }

    fn visit_method(
        &mut self,
        method: &MethodDescriptorProto,
        visitor: &mut dyn Visitor,
        file: FileIndex,
        service: ServiceIndex,
        index: MethodIndex,
    ) {
        self.push_scope(method.name());
        visitor.visit_method(&self.path, &self.scope, file, service, index, method);
        self.pop_scope(method.name());
    }

    fn visit_enum(
        &mut self,
        enum_: &EnumDescriptorProto,
        visitor: &mut dyn Visitor,
        file: FileIndex,
        parent_message: Option<MessageIndex>,
    ) {
        self.push_scope(enum_.name());

        let index = post_inc(&mut self.offsets.enum_);
        visitor.visit_enum(&self.path, &self.scope, file, parent_message, index, enum_);

        self.pop_scope(enum_.name());

        self.push_path(tag::enum_::VALUE);
        for (i, method) in enum_.value.iter().enumerate() {
            self.push_path(i as i32);
            self.visit_enum_value(method, visitor, file, index, to_index(i));
            self.pop_path();
        }
        self.pop_path();
    }

    fn visit_enum_value(
        &mut self,
        value: &EnumValueDescriptorProto,
        visitor: &mut dyn Visitor,
        file: FileIndex,
        enum_: EnumIndex,
        index: EnumValueIndex,
    ) {
        self.push_scope(value.name());
        visitor.visit_enum_value(&self.path, &self.scope, file, enum_, index, value);
        self.pop_scope(value.name());
    }

    fn visit_extension(
        &mut self,
        extension: &FieldDescriptorProto,
        visitor: &mut dyn Visitor,
        file: FileIndex,
        parent_message: Option<MessageIndex>,
    ) {
        self.push_scope(extension.name());
        let index = post_inc(&mut self.offsets.extension);
        visitor.visit_extension(
            &self.path,
            &self.scope,
            file,
            parent_message,
            index,
            extension,
        );
        self.pop_scope(extension.name());
    }

    fn push_path(&mut self, path: i32) {
        self.path.push(path);
    }

    fn pop_path(&mut self) {
        self.path.pop().unwrap();
    }

    fn push_scope(&mut self, scope: &str) {
        if !self.scope.is_empty() {
            self.scope.push('.');
        }
        self.scope.push_str(scope);
    }

    fn pop_scope(&mut self, scope: &str) {
        debug_assert!(self.scope.ends_with(scope));
        self.scope
            .truncate((self.scope.len() - scope.len()).saturating_sub(1));
    }
}

fn post_inc(index: &mut u32) -> u32 {
    let value = *index;
    *index = value + 1;
    value
}
