/*
 * Copyright 2016 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.docs;

import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.reflections.Reflections;
import org.reflections.scanners.ResourcesScanner;
import org.reflections.util.ClasspathHelper;
import org.reflections.util.ConfigurationBuilder;
import org.reflections.util.FilterBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;

/**
 * {@link ThriftDocString} is a DocString extractor for Thrift IDL JSON.
 *
 * To include docstrings in {@link DocService} pages, use a recent development version of the Thrift compiler
 * (0.9.3 will not work) and compile your thrift files with 'json' code generation and
 * include the resulting json files in the classpath location META-INF/armeria/thrift.
 * The classpath location can be changed by setting the armeria.thrift.json.dir system property.
 */
final class ThriftDocString {

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

    private static final String THRIFT_JSON_PATH;

    private static final TypeReference JSON_VALUE_TYPE = new TypeReference<HashMap<String, Object>>() {};

    private static final String FQCN_DELIM = ".";

    private static final String DELIM = "#";

    private static Map<ClassLoader, Map<String, String>> cached = new ConcurrentHashMap<>();

    static {
        final String propertyName = "com.linecorp.armeria.thrift.jsonDir";
        String dir = System.getProperty(propertyName, "META-INF/armeria/thrift");
        if (dir.startsWith("/") || dir.startsWith("\\")) {
            dir = dir.substring(1);
        }
        if (dir.endsWith("/") || dir.endsWith("\\")) {
            dir = dir.substring(0, dir.length() - 1);
        }
        logger.info("{}: {}", propertyName, dir);
        THRIFT_JSON_PATH = dir;
    }

    private ThriftDocString() {
    }

    /**
     * Extracts all DocStrings from all Thrift IDL JSON Resources.
     * @return a map with key is FQCN and value is document string.
     */
    static Map<String, String> getAllDocStrings(ClassLoader classLoader) {
        return cached.computeIfAbsent(classLoader, loader -> parseDocStrings(loader, getAllThriftJsons(loader)));
    }

    /**
     * Parses DocStrings from input Thrift IDL JSON Resources.
     * @return a map with key is FQCN and value is document string.
     */
    static Map<String, String> parseDocStrings(ClassLoader classLoader, Iterable<String> jsonPaths) {
        final ImmutableMap.Builder<String, String> docStrings = ImmutableMap.builder();
        for (String jsonPath : jsonPaths) {
            docStrings.putAll(getDocStringsFromJsonResource(classLoader, jsonPath));
        }
        return docStrings.build();
    }

    static Iterable<String> getAllThriftJsons(ClassLoader classLoader) {
        final Reflections reflections = new Reflections(
                new ConfigurationBuilder()
                        .filterInputsBy(new FilterBuilder().includePackage(THRIFT_JSON_PATH))
                        .setUrls(ClasspathHelper.forPackage(THRIFT_JSON_PATH))
                        .addClassLoader(classLoader)
                        .addScanners(new ResourcesScanner()));
        return reflections.getResources(filename -> filename.endsWith(".json"));
    }

    /**
     * Gets the namespace key of names.
     * @param names name list.
     * @return merged key.
     */
    static String key(String... names) {
        return Stream.of(names).filter(s -> !Strings.isNullOrEmpty(s)).collect(Collectors.joining(DELIM));
    }

    @VisibleForTesting
    static Map<String, String> getDocStringsFromJsonResource(ClassLoader classLoader, String jsonResourcePath) {
        ImmutableMap.Builder<String, String> docStrings = ImmutableMap.builder();
        try (InputStream in = classLoader.getResourceAsStream(jsonResourcePath)) {
            if (in == null) {
                throw new IllegalStateException("not found: " + jsonResourcePath);
            }

            final Map<String, Object> json = new ObjectMapper().readValue(in, JSON_VALUE_TYPE);
            @SuppressWarnings("unchecked")
            final Map<String, Object> namespaces = (Map<String, Object>) json.getOrDefault("namespaces",
                                                                                           ImmutableMap.of());
            final String packageName = (String) namespaces.get("java");
            json.forEach((key, children) -> {
                if (children instanceof Collection) {
                    ((Collection) children).forEach(grandChild -> {
                        traverseChildren(docStrings, packageName, FQCN_DELIM, grandChild);
                    });
                }
            });

            return docStrings.build();
        } catch (IOException e) {
            throw new IllegalStateException(e);
        }
    }

    private static void traverseChildren(ImmutableMap.Builder<String, String> docStrings, String prefix,
                                         String delimiter, Object node) {
        if (node instanceof Map) {
            @SuppressWarnings("unchecked")
            final Map<String, Object> map = (Map<String, Object>) node;
            final String name = (String) map.get("name");
            final String doc = (String) map.get("doc");
            String childPrefix;
            if (name != null) {
                childPrefix = (prefix != null ? prefix : "") + delimiter + name;
                if (doc != null) {
                    docStrings.put(childPrefix, doc);
                }
            } else {
                childPrefix = prefix;
            }
            map.forEach((key, value) -> traverseChildren(docStrings, childPrefix, DELIM, value));
        } else if (node instanceof Iterable) {
            @SuppressWarnings("unchecked")
            final Iterable<Object> children = (Iterable<Object>) node;
            children.forEach(child -> traverseChildren(docStrings, prefix, DELIM, child));
        }
    }
}

@@ -751,24 +751,54 @@
 Collection;%0A
+import java.util.Collections;%0A
 import java.
@@ -949,16 +949,54 @@
 tream;%0A%0A
+import org.reflections.Configuration;%0A
 import o
@@ -3931,66 +3931,37 @@
 nal 
-Reflections reflections = new Reflections(%0A               
+Configuration configuration =
  new
@@ -4000,24 +4000,16 @@
         
-     
-   
 .filterI
@@ -4086,24 +4086,16 @@
         
-        
 .setUrls
@@ -4153,32 +4153,24 @@
             
-     
-   
 .addClassLoa
@@ -4186,24 +4186,16 @@
 Loader)%0A
-        
         
@@ -4238,19 +4238,156 @@
 anner())
+;%0A        if (configuration.getUrls() == null %7C%7C configuration.getUrls().isEmpty()) %7B%0A            return Collections.emptyList(
 );%0A
+        %7D%0A
         
@@ -4393,17 +4393,21 @@
  return 
-r
+new R
 eflectio
@@ -4408,16 +4408,31 @@
 lections
+(configuration)
 .getReso

Fix Reflections' WARN log when jsonDir doesn't exist.