/*
 * Copyright 2017 LINE Corporation
 *
 * LINE Corporation licenses this file to you under the Apache License,
 * version 2.0 (the "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at:
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */

package com.linecorp.armeria.server.grpc;

import static com.google.common.collect.ImmutableMap.toImmutableMap;

import java.io.IOException;
import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.stream.Stream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.protobuf.DescriptorProtos.DescriptorProto;
import com.google.protobuf.DescriptorProtos.EnumDescriptorProto;
import com.google.protobuf.DescriptorProtos.FieldDescriptorProto;
import com.google.protobuf.DescriptorProtos.FileDescriptorProto;
import com.google.protobuf.DescriptorProtos.FileDescriptorSet;
import com.google.protobuf.DescriptorProtos.ServiceDescriptorProto;

import com.linecorp.armeria.server.docs.DocStringExtractor;

/**
 * A DocString extractor for GRPC proto compiled descriptors.
 *
 * <p>To include docstrings in {@link com.linecorp.armeria.server.docs.DocService} pages,
 * configure the protobuf compiler to generate a descriptor set with source info and all imports
 * included. Place the descriptor set in the classpath location {@code META-INF/armeria/grpc} and ensure
 * the file extension is '.dsc'. The classpath location can be changed by setting the
 * {@code com.linecorp.armeria.grpc.descriptorDir} system property.
 *
 * <p>For example, to generate a descriptor set in gradle:
 * <pre>{@code
 *
 * protobuf {
 *     generateProtoTasks {
 *         all().each { task ->
 *             task.generateDescriptorSet = true
 *             task.descriptorSetOptions.includeSourceInfo = true
 *             task.descriptorSetOptions.includeImports = true
 *             task.descriptorSetOptions.path =
 *                     "${buildDir}/resources/main/META-INF/armeria/grpc/service-name.dsc"
 *         }
 *     }
 * }
 *
 * }</pre>
 */
final class GrpcDocStringExtractor extends DocStringExtractor {

    private static final Logger logger = LoggerFactory.getLogger(GrpcDocStringExtractor.class);

    GrpcDocStringExtractor() {
        super("META-INF/armeria/grpc", "com.linecorp.armeria.grpc.descriptorDir");
    }

    @Override
    protected boolean acceptFile(String filename) {
        return filename.endsWith(".dsc");
    }

    @Override
    protected Map<String, String> getDocStringsFromFiles(Map<String, byte[]> files) {
        return files.entrySet().stream()
                    .flatMap(entry -> {
                        try {
                            FileDescriptorSet descriptors = FileDescriptorSet.parseFrom(entry.getValue());
                            return descriptors.getFileList().stream();
                        } catch (IOException e) {
                            logger.info("Could not parse file at '{}', skipping. " +
                                        "Is the file a protobuf descriptor file?",
                                        entry.getKey());
                            return Stream.empty();
                        }
                    })
                    .flatMap(f -> parseFile(f).entrySet().stream())
                    .collect(toImmutableMap(Entry::getKey, Entry::getValue));
    }

    private static Map<String, String> parseFile(FileDescriptorProto descriptor) {
        return descriptor.getSourceCodeInfo().getLocationList().stream()
                         .filter(l -> !l.getLeadingComments().isEmpty())
                         .map(l -> getFullName(descriptor, l.getPathList())
                                 .map(fullName -> new SimpleImmutableEntry<>(fullName, l.getLeadingComments())))
                         .filter(Optional::isPresent)
                         .map(Optional::get)
                         .collect(toImmutableMap(Entry::getKey, Entry::getValue));
    }

    // A path is field number and indices within a list of types, going through a tree of protobuf
    // descriptors. For example, the 2nd field of the 3rd nested message in the 1st message in a file
    // would have path [MESSAGE_TYPE_FIELD_NUMBER, 0, NESTED_TYPE_FIELD_NUMBER, 2, FIELD_FIELD_NUMBER, 1]
    private static Optional<String> getFullName(FileDescriptorProto descriptor, List<Integer> path) {
        String fullNameSoFar = descriptor.getPackage();
        switch (path.get(0)) {
            case FileDescriptorProto.MESSAGE_TYPE_FIELD_NUMBER:
                DescriptorProto message = descriptor.getMessageType(path.get(1));
                return appendMessageToFullName(message, path, fullNameSoFar);
            case FileDescriptorProto.ENUM_TYPE_FIELD_NUMBER:
                EnumDescriptorProto enumDescriptor = descriptor.getEnumType(path.get(1));
                return Optional.of(appendEnumToFullName(enumDescriptor, path, fullNameSoFar));
            case FileDescriptorProto.SERVICE_FIELD_NUMBER:
                ServiceDescriptorProto serviceDescriptor = descriptor.getService(path.get(1));
                fullNameSoFar = appendNameComponent(fullNameSoFar, serviceDescriptor.getName());
                if (path.size() > 2) {
                    fullNameSoFar = appendFieldComponent(
                            fullNameSoFar, serviceDescriptor.getMethod(path.get(3)).getName());
                }
                return Optional.of(fullNameSoFar);
            default:
                return Optional.empty();
        }
    }

    private static Optional<String> appendToFullName(
            DescriptorProto messageDescriptor, List<Integer> path, String fullNameSoFar) {
        switch (path.get(0)) {
            case DescriptorProto.NESTED_TYPE_FIELD_NUMBER:
                DescriptorProto nestedMessage = messageDescriptor.getNestedType(path.get(1));
                return appendMessageToFullName(nestedMessage, path, fullNameSoFar);
            case DescriptorProto.ENUM_TYPE_FIELD_NUMBER:
                EnumDescriptorProto enumDescriptor = messageDescriptor.getEnumType(path.get(1));
                return Optional.of(appendEnumToFullName(enumDescriptor, path, fullNameSoFar));
            case DescriptorProto.FIELD_FIELD_NUMBER:
                FieldDescriptorProto fieldDescriptor = messageDescriptor.getField(path.get(1));
                return Optional.of(appendFieldComponent(fullNameSoFar, fieldDescriptor.getName()));
            default:
                return Optional.empty();
        }
    }

    private static Optional<String> appendMessageToFullName(
            DescriptorProto message, List<Integer> path, String fullNameSoFar) {
        fullNameSoFar = appendNameComponent(fullNameSoFar, message.getName());
        return path.size() > 2 ?
               appendToFullName(message, path.subList(2, path.size()), fullNameSoFar)
                               : Optional.of(fullNameSoFar);
    }

    private static String appendEnumToFullName(
            EnumDescriptorProto enumDescriptor, List<Integer> path, String fullNameSoFar) {
        fullNameSoFar = appendNameComponent(fullNameSoFar, enumDescriptor.getName());
        if (path.size() > 2) {
            fullNameSoFar = appendFieldComponent(fullNameSoFar, enumDescriptor.getValue(path.get(3)).getName());
        }
        return fullNameSoFar;
    }

    private static String appendNameComponent(String nameSoFar, String component) {
        return nameSoFar + "." + component;
    }

    private static String appendFieldComponent(String nameSoFar, String component) {
        return nameSoFar + "/" + component;
    }
}

@@ -3787,32 +3787,58 @@
  Entry::getValue
+, (entry, unused) -%3E entry
 ));%0A    %7D%0A%0A    p

Allow duplicate docstring keys in grpc docstring extractor. (#592)