package com.fasterxml.jackson.dataformat.protobuf.schema;

import java.util.EnumMap;
import java.util.Map;

import com.squareup.protoparser.DataType;
import com.squareup.protoparser.EnumConstantElement;
import com.squareup.protoparser.EnumElement;
import com.squareup.protoparser.FieldElement;
import com.squareup.protoparser.MessageElement;
import com.squareup.protoparser.OptionElement;
import com.squareup.protoparser.ProtoFile;

/**
 * @since 2.9
 */
public class FileDescriptorSet
{
    protected FileDescriptorProto[] file;

    // for deserializer
    protected FileDescriptorSet() { }

    public FileDescriptorSet(FileDescriptorProto[] f) {
        file = f;
    }

    // needed to "expose" non-public setter without annotations
    public FileDescriptorProto[] getFile() { return file; }

    /**
     * Accessor for finding low-level definition with given name,
     * if one contained.
     *
     * @return Descriptor matching the name, if any; `null` if none
     */
    public FileDescriptorProto findDescriptor(String fileName)
    {
        for (FileDescriptorProto fdp : file) {
            if (fdp.name.equals(fileName)) {
                return fdp;
            }
        }
        return null;
    }

    /**
     * Accessor for getting low-level definition with given name, contained
     * in this descriptor set.
     *
     * @return Descriptor matching the name, if any; `null` if none
     *
     * @throws IllegalArgumentException if no descriptor with given name found
     */
    public FileDescriptorProto getDescriptor(String fileName)
    {
        FileDescriptorProto p = findDescriptor(fileName);
        if (p == null) {
            throw new IllegalArgumentException(fileName + " not found");
        }
        return p;
    }

    public ProtobufSchema schemaForFirstType()
    {
        ProtoFile protoFile = buildProtoFile(file[0].name);
        return NativeProtobufSchema.construct(protoFile).forFirstType();
    }

    public ProtobufSchema schemaFor(String rootTypeName)
    {
        for (FileDescriptorProto fdp : file) {
            for (DescriptorProto dp : fdp.message_type) {
                if (dp.name.equals(rootTypeName)) {
                    ProtoFile protoFile = buildProtoFile(fdp.name);
                    NativeProtobufSchema nps = NativeProtobufSchema.construct(protoFile);
                    return nps.forType(rootTypeName);
                }
            }
        }
        throw new IllegalArgumentException(rootTypeName + " not found");
    }

    private ProtoFile buildProtoFile(String fileName)
    {
        FileDescriptorProto fdp = getDescriptor(fileName);
        ProtoFile.Builder builder = ProtoFile.builder(fdp.name);
        builder.syntax(fdp.getSyntax());
        builder.packageName(fdp.getPackage());

        // dependency file names.
        if (fdp.dependency != null) {
            for (String dependency : fdp.dependency) {
                FileDescriptorProto dep = getDescriptor(dependency);
                for (DescriptorProto dp : dep.message_type) {
                    MessageElement me = dp.buildMessageElement();
                    builder.addType(me);
                }
            }
        }

        // FIXME: public dependency file names.
//        if (fdp.public_dependency) {
//            for (DescriptorProto dp : fdp.public_dependency) {
//                String dep = fdp.getDependency();
//                builder.addPublicDependency(dep);
//            }
//        }

        // types
        for (DescriptorProto dp : fdp.message_type) {
            MessageElement me = dp.buildMessageElement();
            builder.addType(me);
        }

        // FIXME: implement following features
        // services
        // extendDeclarations
        // options

        return builder.build();
    }

    // POJOs for the .desc file Protobuf
    public static class FileDescriptorProto
    {
        public String name;

        // Need to use different name as `package` is reserved name in Java
        protected String _package; // 'packageName'

        public String[] dependency;
        public int[] public_dependency;
        public int[] weak_dependency;
        public DescriptorProto[] message_type;
        public EnumDescriptorProto[] enum_type;
        public ServiceDescriptorProto[] service;
        public FieldDescriptorProto[] extension;
        public FileOptions options;
        public SourceCodeInfo source_code_info;
        public String syntax;

        public ProtoFile.Syntax getSyntax()
        {
            if (syntax == null) {
                return ProtoFile.Syntax.PROTO_2;
            }
            return ProtoFile.Syntax.valueOf(syntax);
        }

        public void setPackage(String p) { _package = p; }
        public String getPackage() { return _package; }
    }

    public static class DescriptorProto
    {
        public String name;
        public FieldDescriptorProto[] field;
        public FieldDescriptorProto[] extension;
        public DescriptorProto[] nested_type;
        public EnumDescriptorProto[] enum_type;

        static class ExtensionRange
        {
            public int start;
            public int end;
        }

        public ExtensionRange[] extension_range;
        public OneofDescriptorProto[] oneof_decl;
        public MessageOptions options;

        static class ReservedRange
        {
            public int start; // Inclusive.
            public int end;   // Exclusive.
        }

        public ReservedRange[] reserved_range;
        public String[] reserved_name;

        public MessageElement buildMessageElement()
        {
            MessageElement.Builder messageElementBuilder = MessageElement.builder();
            messageElementBuilder.name(name);
            // fields
            if (field != null) {
                for (FieldDescriptorProto f : field) {
                    DataType dataType;
                    String fieldName = f.name;
                    FieldDescriptorProto.Type type = f.type;
                    FieldElement.Label label = f.getLabel();
                    // message and enum fields are named fields
                    if (type.equals(FieldDescriptorProto.Type.TYPE_MESSAGE)
                        || type.equals(FieldDescriptorProto.Type.TYPE_ENUM)) {
                        String fullyQualifiedtypeName = f.type_name; // fully qualified name including package name.
                        String typeName = fullyQualifiedtypeName.substring(fullyQualifiedtypeName.indexOf(".", 2) + 1);
                        dataType = DataType.NamedType.create(typeName);
                    } else {
                        dataType = f.getDataType();
                    }

                    // build field
                    FieldElement.Builder fieldBuilder = FieldElement
                        .builder()
                        .name(fieldName)
                        .type(dataType)
                        .label(label)
                        .tag(f.number);

                    // add field options to the field
                    if (f.json_name != null) {
                        OptionElement.Kind kind = OptionElement.Kind.STRING;
                        OptionElement option = OptionElement.create("json_name", kind, f.json_name);
                        fieldBuilder.addOption(option);
                    }

                    if (f.options != null) {
                        if (f.options.packed) {
                            OptionElement.Kind kind = OptionElement.Kind.STRING;
                            OptionElement option = OptionElement.create("packed", kind, "true");
                            fieldBuilder.addOption(option);
                        }
                    }
                    // add the field to the message
                    messageElementBuilder.addField(fieldBuilder.build());
                }
            }

            // message type declarations
            if (nested_type != null) {
                for (DescriptorProto n : nested_type) {
                    messageElementBuilder.addType(n.buildMessageElement());
                }
            }

            // enum declarations
            if (enum_type != null) {
                for (EnumDescriptorProto e : enum_type) {
                    EnumElement.Builder nestedEnumElement = EnumElement
                        .builder()
                        .name(e.name);
                    for (EnumValueDescriptorProto v : e.value) {
                        EnumConstantElement.Builder c = EnumConstantElement.builder()
                                                                           .name(v.name)
                                                                           .tag(v.number);
                        nestedEnumElement.addConstant(c.build());
                    }
                    messageElementBuilder.addType(nestedEnumElement.build());
                }
            }
            return messageElementBuilder.build();
        }

    }

    public static class FieldDescriptorProto
    {
        public enum Type
        {
            TYPE_DOUBLE,
            TYPE_FLOAT,
            TYPE_INT64,
            TYPE_UINT64,
            TYPE_INT32,
            TYPE_FIXED64,
            TYPE_FIXED32,
            TYPE_BOOL,
            TYPE_STRING,
            TYPE_GROUP,
            TYPE_MESSAGE,
            TYPE_BYTES,
            TYPE_UINT32,
            TYPE_ENUM,
            TYPE_SFIXED32,
            TYPE_SFIXED64,
            TYPE_SINT32,
            TYPE_SINT64
        }

        public enum Label
        {
            LABEL_OPTIONAL,
            LABEL_REQUIRED,
            LABEL_REPEATED
        }

        public String name;
        public int number;
        public Label label;
        public Type type;
        public String type_name;
        public String extendee;
        public String default_value;
        public int oneof_index;
        public String json_name;
        public FieldOptions options;

        static private Map<Type, DataType> scalarTypeMap = new EnumMap<>(Type.class);
        static private Map<Label, FieldElement.Label> labelMap = new EnumMap<>(Label.class);

        static {
            scalarTypeMap.put(Type.TYPE_DOUBLE, DataType.ScalarType.DOUBLE);
            scalarTypeMap.put(Type.TYPE_FLOAT, DataType.ScalarType.FLOAT);
            scalarTypeMap.put(Type.TYPE_INT64, DataType.ScalarType.INT64);
            scalarTypeMap.put(Type.TYPE_UINT64, DataType.ScalarType.UINT64);
            scalarTypeMap.put(Type.TYPE_INT32, DataType.ScalarType.INT32);
            scalarTypeMap.put(Type.TYPE_FIXED64, DataType.ScalarType.FIXED64);
            scalarTypeMap.put(Type.TYPE_FIXED32, DataType.ScalarType.FIXED32);
            scalarTypeMap.put(Type.TYPE_BOOL, DataType.ScalarType.BOOL);
            scalarTypeMap.put(Type.TYPE_STRING, DataType.ScalarType.STRING);
            scalarTypeMap.put(Type.TYPE_BYTES, DataType.ScalarType.BYTES);
            scalarTypeMap.put(Type.TYPE_UINT32, DataType.ScalarType.UINT32);
            scalarTypeMap.put(Type.TYPE_SFIXED32, DataType.ScalarType.SFIXED32);
            scalarTypeMap.put(Type.TYPE_SFIXED64, DataType.ScalarType.SFIXED64);
            scalarTypeMap.put(Type.TYPE_SINT32, DataType.ScalarType.SINT32);
            scalarTypeMap.put(Type.TYPE_SINT64, DataType.ScalarType.SINT64);

            labelMap.put(Label.LABEL_OPTIONAL, FieldElement.Label.OPTIONAL);
            labelMap.put(Label.LABEL_REQUIRED, FieldElement.Label.REQUIRED);
            labelMap.put(Label.LABEL_REPEATED, FieldElement.Label.REPEATED);
        }

        public DataType getDataType()
        {
            return scalarTypeMap.get(type);
        }

        public FieldElement.Label getLabel()
        {
            return labelMap.get(label);
        }
    }

    public static class OneofDescriptorProto
    {
        public String name;
        public OneofOptions options;
    }

    public static class EnumDescriptorProto
    {
        public String name;
        public EnumValueDescriptorProto[] value;
        public EnumOptions options;
    }

    public static class EnumValueDescriptorProto
    {
        public String name;
        public int number;
        public EnumValueOptions options;
    }

    public static class ServiceDescriptorProto
    {
        public String name;
        public MethodDescriptorProto[] method;
        public ServiceOptions options;
    }

    public static class MethodDescriptorProto
    {
        public String name;
        public String input_type;
        public String output_type;
        public MethodOptions options;
        public boolean client_streaming; // [default=false];
        public boolean server_streaming; // [default=false];
    }

    public static class FileOptions
    {
        public String java_package;
        public String java_outer_classname;
        public boolean java_multiple_files; // [default=false];
        public boolean java_generate_equals_and_hash; // [deprecated=true];
        public boolean java_String_check_utf8; // [default=false];

        enum OptimizeMode
        {
            SPEED,
            CODE_SIZE,
            LITE_RUNTIME
        }

        public OptimizeMode optimize_for; // [default=SPEED];
        public String go_package;
        public boolean cc_generic_services; //  [default=false];
        public boolean java_generic_services; // [default=false];
        public boolean py_generic_services; // [default=false];
        public boolean deprecated; //  [default=false];
        public boolean cc_enable_arenas; // [default=false];
        public String objc_class_prefix;
        public String csharp_namespace;
        public String swift_prefix;
        public String php_class_prefix;
        public UninterpretedOption[] uninterpreted_option;
//        extensions 1000 to max;
    }

    public static class MessageOptions
    {
        public boolean message_set_wire_format; // [default=false];
        public boolean no_standard_descriptor_accessor; // [default=false];
        public boolean deprecated; // [default=false];
        public boolean map_entry;
        public UninterpretedOption[] uninterpreted_option;
//        extensions 1000 to max;
    }

    public static class FieldOptions
    {
        public CType ctype; // [default = STRING];

        enum CType
        {
            STRING,
            CORD,
            STRING_PIECE
        }

        public boolean packed;
        public JSType jstype; // [default = JS_NORMAL];

        enum JSType
        {
            JS_NORMAL,
            JS_STRING,
            JS_NUMBER
        }

        public boolean lazy; // [default=false];
        public boolean deprecated; // [default=false];
        public boolean weak; //  [default=false];
        public UninterpretedOption[] uninterpreted_option;
//        extensions 1000 to max;
    }

    public static class OneofOptions
    {
        public UninterpretedOption[] uninterpreted_option;
//        extensions 1000 to max;
    }

    public static class EnumOptions
    {
        public boolean allow_alias;
        public boolean deprecated; // [default=false];
        public UninterpretedOption[] uninterpreted_option;
//        extensions 1000 to max;
    }

    public static class EnumValueOptions
    {
        public boolean deprecated; //  [default=false];
        public UninterpretedOption[] uninterpreted_option;
//        extensions 1000 to max;
    }

    public static class ServiceOptions
    {
        public boolean deprecated; // [default=false];
        public UninterpretedOption[] uninterpreted_option;
//        extensions 1000 to max;
    }

    public static class MethodOptions
    {
        public boolean deprecated; // [default=false];

        enum IdempotencyLevel
        {
            IDEMPOTENCY_UNKNOWN,
            NO_SIDE_EFFECTS,
            IDEMPOTENT
        }

        public IdempotencyLevel idempotency_level; // [default=IDEMPOTENCY_UNKNOWN];
        public UninterpretedOption[] uninterpreted_option;
//        extensions 1000 to max;
    }

    public static class UninterpretedOption
    {
        static class NamePart
        {
            public String name_part;
            public boolean is_extension;
        }

        public NamePart[] name;
        public String identifier_value;
        public long positive_int_value;
        public long negative_int_value;
        public double double_value;
        public byte[] string_value;
        public String aggregate_value;
    }

    public static class SourceCodeInfo
    {
        public Location[] location;

        public static class Location
        {
            public int[] path; //  [packed=true];
            public int[] span; //  [packed=true];
            public String leading_comments;
            public String trailing_comments;
            public String[] leading_detached_comments;
        }
    }

    /*
    private static class GeneratedCodeInfo
    {
        public CodeAnnotation[] annotation;

        public static class CodeAnnotation
        {
            public long[] path; // [packed=true];
            public String source_file;
            public int begin;
            public int end;
        }
    }
    */
}
