/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF 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 org.apache.dubbo.metadata.rest;

import org.apache.dubbo.common.compact.Dubbo2CompactUtils;
import org.apache.dubbo.common.utils.MethodComparator;
import org.apache.dubbo.common.utils.ServiceAnnotationResolver;
import org.apache.dubbo.config.annotation.DubboService;
import org.apache.dubbo.config.annotation.Service;
import org.apache.dubbo.metadata.definition.MethodDefinitionBuilder;
import org.apache.dubbo.metadata.definition.model.MethodDefinition;
import org.apache.dubbo.rpc.model.ApplicationModel;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;

import static java.util.Collections.emptyList;
import static java.util.Collections.sort;
import static java.util.Collections.unmodifiableMap;
import static org.apache.dubbo.common.function.ThrowableFunction.execute;
import static org.apache.dubbo.common.utils.AnnotationUtils.isAnnotationPresent;
import static org.apache.dubbo.common.utils.AnnotationUtils.isAnyAnnotationPresent;
import static org.apache.dubbo.common.utils.ClassUtils.forName;
import static org.apache.dubbo.common.utils.ClassUtils.getAllInterfaces;
import static org.apache.dubbo.common.utils.MemberUtils.isPrivate;
import static org.apache.dubbo.common.utils.MemberUtils.isStatic;
import static org.apache.dubbo.common.utils.MethodUtils.excludedDeclaredClass;
import static org.apache.dubbo.common.utils.MethodUtils.getAllMethods;
import static org.apache.dubbo.common.utils.MethodUtils.overrides;

/**
 * The abstract {@link ServiceRestMetadataResolver} class to provider some template methods assemble the instance of
 * {@link ServiceRestMetadata} will extended by the sub-classes.
 *
 * @since 2.7.6
 */
public abstract class AbstractServiceRestMetadataResolver implements ServiceRestMetadataResolver {

    private final Map<String, List<AnnotatedMethodParameterProcessor>> parameterProcessorsMap;
    private final Set<NoAnnotatedParameterRequestTagProcessor> noAnnotatedParameterRequestTagProcessors;

    public AbstractServiceRestMetadataResolver(ApplicationModel applicationModel) {
        this.parameterProcessorsMap = loadAnnotatedMethodParameterProcessors(applicationModel);
        this.noAnnotatedParameterRequestTagProcessors = loadNoAnnotatedMethodParameterProcessors(applicationModel);
    }

    @Override
    public final boolean supports(Class<?> serviceType) {
        return supports(serviceType, false);
    }

    @Override
    public final boolean supports(Class<?> serviceType, boolean consumer) {

        if (serviceType == null) {
            return false;
        }

        // for consumer
        //  it is possible serviceType is impl
        // for provider
        // for xml config bean  && isServiceAnnotationPresent(serviceType)
        //  isImplementedInterface(serviceType) SpringController
        return supports0(serviceType);
    }

    protected final boolean isImplementedInterface(Class<?> serviceType) {
        return !getAllInterfaces(serviceType).isEmpty();
    }

    protected final boolean isServiceAnnotationPresent(Class<?> serviceType) {
        if (Dubbo2CompactUtils.isEnabled() && Dubbo2CompactUtils.isServiceClassLoaded()) {
            return isAnyAnnotationPresent(serviceType, DubboService.class, Service.class,
                Dubbo2CompactUtils.getServiceClass());
        } else {
            return isAnyAnnotationPresent(serviceType, DubboService.class, Service.class);
        }
    }

    /**
     * internal support method
     *
     * @param serviceType Dubbo Service interface or type
     * @return If supports, return <code>true</code>, or <code>false</code>
     */
    protected abstract boolean supports0(Class<?> serviceType);

    @Override
    public final ServiceRestMetadata resolve(Class<?> serviceType) {
        ServiceRestMetadata serviceRestMetadata = new ServiceRestMetadata();

        // Process ServiceRestMetadata
        processServiceRestMetadata(serviceRestMetadata, serviceType);

        return resolve(serviceType, serviceRestMetadata);
    }


    @Override
    public final ServiceRestMetadata resolve(Class<?> serviceType, ServiceRestMetadata serviceRestMetadata) {
        serviceRestMetadata.setCodeStyle(this.getClass());
        // Process RestMethodMetadata
        processAllRestMethodMetadata(serviceRestMetadata, serviceType);

        return serviceRestMetadata;
    }

    /**
     * Process the service type including the sub-routines:
     * <ul>
     *     <li>{@link ServiceRestMetadata#setServiceInterface(String)}</li>
     *     <li>{@link ServiceRestMetadata#setVersion(String)}</li>
     *     <li>{@link ServiceRestMetadata#setGroup(String)}</li>
     * </ul>
     *
     * @param serviceRestMetadata {@link ServiceRestMetadata}
     * @param serviceType         Dubbo Service interface or type
     */
    protected void processServiceRestMetadata(ServiceRestMetadata serviceRestMetadata, Class<?> serviceType) {
        ServiceAnnotationResolver resolver = new ServiceAnnotationResolver(serviceType);
        serviceRestMetadata.setServiceInterface(resolver.resolveInterfaceClassName());
        serviceRestMetadata.setVersion(resolver.resolveVersion());
        serviceRestMetadata.setGroup(resolver.resolveGroup());
    }

    /**
     * Process all {@link RestMethodMetadata}
     *
     * @param serviceRestMetadata {@link ServiceRestMetadata}
     * @param serviceType         Dubbo Service interface or type
     */
    protected void processAllRestMethodMetadata(ServiceRestMetadata serviceRestMetadata, Class<?> serviceType) {
        Class<?> serviceInterfaceClass = resolveServiceInterfaceClass(serviceRestMetadata, serviceType);
        Map<Method, Method> serviceMethodsMap = resolveServiceMethodsMap(serviceType, serviceInterfaceClass);
        for (Map.Entry<Method, Method> entry : serviceMethodsMap.entrySet()) {
            // try the overrider method first
            Method serviceMethod = entry.getKey();
            // If failed, it indicates the overrider method does not contain metadata , then try the declared method
            if (!processRestMethodMetadata(serviceMethod, serviceType, serviceInterfaceClass, serviceRestMetadata::addRestMethodMetadata, serviceRestMetadata)) {
                Method declaredServiceMethod = entry.getValue();
                processRestMethodMetadata(declaredServiceMethod, serviceType, serviceInterfaceClass,
                    serviceRestMetadata::addRestMethodMetadata, serviceRestMetadata);
            }
        }
    }

    /**
     * Resolve a map of all public services methods from the specified service type and its interface class, whose key is the
     * declared method, and the value is the overrider method
     *
     * @param serviceType           the service interface implementation class
     * @param serviceInterfaceClass the service interface class
     * @return non-null read-only {@link Map}
     */
    protected Map<Method, Method> resolveServiceMethodsMap(Class<?> serviceType, Class<?> serviceInterfaceClass) {
        Map<Method, Method> serviceMethodsMap = new LinkedHashMap<>();
        // exclude the public methods declared in java.lang.Object.class
        List<Method> declaredServiceMethods = new ArrayList<>(getAllMethods(serviceInterfaceClass, excludedDeclaredClass(Object.class)));

        // controller class
        if (serviceType.equals(serviceInterfaceClass)) {
            putServiceMethodToMap(serviceMethodsMap, declaredServiceMethods);
            return unmodifiableMap(serviceMethodsMap);
        }

        // for interface , such as consumer interface
        if (serviceType.isInterface()) {
            putServiceMethodToMap(serviceMethodsMap, declaredServiceMethods);
            return unmodifiableMap(serviceMethodsMap);
        }

        List<Method> serviceMethods = new ArrayList<>(getAllMethods(serviceType, excludedDeclaredClass(Object.class)));


        // sort methods
        sort(declaredServiceMethods, MethodComparator.INSTANCE);
        sort(serviceMethods, MethodComparator.INSTANCE);

        for (Method declaredServiceMethod : declaredServiceMethods) {
            for (Method serviceMethod : serviceMethods) {
                if (overrides(serviceMethod, declaredServiceMethod)) {
                    serviceMethodsMap.put(serviceMethod, declaredServiceMethod);
                    // override method count > 1
//                    // once method match ,break for decrease loop  times
//                    break;
                }
            }
        }
        // make them to be read-only
        return unmodifiableMap(serviceMethodsMap);
    }

    private void putServiceMethodToMap(Map<Method, Method> serviceMethodsMap, List<Method> declaredServiceMethods) {
        declaredServiceMethods.stream().forEach(method -> {

            // filter static private default
            if (isStatic(method) || isPrivate(method) || method.isDefault()) {
                return;
            }
            serviceMethodsMap.put(method, method);
        });
    }

    /**
     * Resolve the class of Dubbo Service interface
     *
     * @param serviceRestMetadata {@link ServiceRestMetadata}
     * @param serviceType         Dubbo Service interface or type
     * @return non-null
     * @throws RuntimeException If the class is not found, the {@link RuntimeException} wraps the cause will be thrown
     */
    protected Class<?> resolveServiceInterfaceClass(ServiceRestMetadata serviceRestMetadata, Class<?> serviceType) {
        return execute(serviceType.getClassLoader(), classLoader -> {
            String serviceInterface = serviceRestMetadata.getServiceInterface();
            return forName(serviceInterface, classLoader);
        });
    }

    /**
     * Process the single {@link RestMethodMetadata} by the specified {@link Consumer} if present
     *
     * @param serviceMethod         Dubbo Service method
     * @param serviceType           Dubbo Service interface or type
     * @param serviceInterfaceClass The type of Dubbo Service interface
     * @param metadataToProcess     {@link RestMethodMetadata} to process if present
     * @return if processed successfully, return <code>true</code>, or <code>false</code>
     */
    protected boolean processRestMethodMetadata(Method serviceMethod, Class<?> serviceType,
                                                Class<?> serviceInterfaceClass,
                                                Consumer<RestMethodMetadata> metadataToProcess,
                                                ServiceRestMetadata serviceRestMetadata) {

        if (!isRestCapableMethod(serviceMethod, serviceType, serviceInterfaceClass)) {
            return false;
        }

        String requestPath = resolveRequestPath(serviceMethod, serviceType, serviceInterfaceClass); // requestPath is required

        if (requestPath == null) {
            return false;
        }

        String requestMethod = resolveRequestMethod(serviceMethod, serviceType, serviceInterfaceClass); // requestMethod is required

        if (requestMethod == null) {
            return false;
        }

        RestMethodMetadata metadata = new RestMethodMetadata();

        metadata.setCodeStyle(this.getClass());

        // to consumer service map
        metadata.setReflectMethod(serviceMethod);

        MethodDefinition methodDefinition = resolveMethodDefinition(serviceMethod, serviceType, serviceInterfaceClass);
        // Set MethodDefinition
        metadata.setMethod(methodDefinition);

        // process produces
        Set<String> produces = new LinkedHashSet<>();
        processProduces(serviceMethod, serviceType, serviceInterfaceClass, produces);

        // process consumes
        Set<String> consumes = new LinkedHashSet<>();
        processConsumes(serviceMethod, serviceType, serviceInterfaceClass, consumes);

        // Initialize RequestMetadata
        RequestMetadata request = metadata.getRequest();
        request.setPath(requestPath);
        request.appendContextPathFromUrl(serviceRestMetadata.getContextPathFromUrl());
        request.setMethod(requestMethod);
        request.setProduces(produces);
        request.setConsumes(consumes);

        // process the annotated method parameters
        processAnnotatedMethodParameters(serviceMethod, serviceType, serviceInterfaceClass, metadata);


        // Post-Process
        postResolveRestMethodMetadata(serviceMethod, serviceType, serviceInterfaceClass, metadata);

        // Accept RestMethodMetadata
        metadataToProcess.accept(metadata);

        return true;
    }

    /**
     * Test the service method is capable of REST or not?
     *
     * @param serviceMethod         Dubbo Service method
     * @param serviceType           Dubbo Service interface or type
     * @param serviceInterfaceClass The type of Dubbo Service interface
     * @return If capable, return <code>true</code>
     */
    protected abstract boolean isRestCapableMethod(Method serviceMethod, Class<?> serviceType, Class<?>
        serviceInterfaceClass);

    /**
     * Resolve the request method
     *
     * @param serviceMethod         Dubbo Service method
     * @param serviceType           Dubbo Service interface or type
     * @param serviceInterfaceClass The type of Dubbo Service interface
     * @return if can't be resolve, return <code>null</code>
     */
    protected abstract String resolveRequestMethod(Method serviceMethod, Class<?> serviceType, Class<?>
        serviceInterfaceClass);

    /**
     * Resolve the request path
     *
     * @param serviceMethod         Dubbo Service method
     * @param serviceType           Dubbo Service interface or type
     * @param serviceInterfaceClass The type of Dubbo Service interface
     * @return if can't be resolve, return <code>null</code>
     */
    protected abstract String resolveRequestPath(Method serviceMethod, Class<?> serviceType, Class<?>
        serviceInterfaceClass);

    /**
     * Resolve the {@link MethodDefinition}
     *
     * @param serviceMethod         Dubbo Service method
     * @param serviceType           Dubbo Service interface or type
     * @param serviceInterfaceClass The type of Dubbo Service interface
     * @return if can't be resolve, return <code>null</code>
     * @see MethodDefinitionBuilder
     */
    protected MethodDefinition resolveMethodDefinition(Method serviceMethod, Class<?> serviceType,
                                                       Class<?> serviceInterfaceClass) {
        MethodDefinitionBuilder builder = new MethodDefinitionBuilder();
        return builder.build(serviceMethod);
    }

    private void processAnnotatedMethodParameters(Method serviceMethod, Class<?> serviceType,
                                                  Class<?> serviceInterfaceClass, RestMethodMetadata metadata) {
        int paramCount = serviceMethod.getParameterCount();
        Parameter[] parameters = serviceMethod.getParameters();
        for (int i = 0; i < paramCount; i++) {
            Parameter parameter = parameters[i];
            // Add indexed parameter name
            metadata.addIndexToName(i, parameter.getName());
            processAnnotatedMethodParameter(parameter, i, serviceMethod, serviceType, serviceInterfaceClass, metadata);
        }
    }

    private void processAnnotatedMethodParameter(Parameter parameter, int parameterIndex, Method serviceMethod,
                                                 Class<?> serviceType, Class<?> serviceInterfaceClass,
                                                 RestMethodMetadata metadata) {
        Annotation[] annotations = parameter.getAnnotations();

        if (annotations == null || annotations.length == 0) {

            for (NoAnnotatedParameterRequestTagProcessor processor : noAnnotatedParameterRequestTagProcessors) {
                // no annotation only one default annotationType
                if (processor.process(parameter, parameterIndex, metadata)) {
                    return;
                }
            }

        }

        for (Annotation annotation : annotations) {
            String annotationType = annotation.annotationType().getName();
            parameterProcessorsMap.getOrDefault(annotationType, emptyList())
                .forEach(processor -> {
                    processor.process(annotation, parameter, parameterIndex, serviceMethod, serviceType,
                        serviceInterfaceClass, metadata);
                });
        }
    }

    protected abstract void processProduces(Method serviceMethod, Class<?> serviceType, Class<?>
        serviceInterfaceClass,
                                            Set<String> produces);

    protected abstract void processConsumes(Method serviceMethod, Class<?> serviceType, Class<?>
        serviceInterfaceClass,
                                            Set<String> consumes);

    protected void postResolveRestMethodMetadata(Method serviceMethod, Class<?> serviceType,
                                                 Class<?> serviceInterfaceClass, RestMethodMetadata metadata) {

        // parse pathVariable index from url by annotation info
        PathUtil.setArgInfoSplitIndex(metadata.getRequest().getPath(), metadata.getArgInfos());
    }

    private static Map<String, List<AnnotatedMethodParameterProcessor>> loadAnnotatedMethodParameterProcessors(ApplicationModel applicationModel) {
        Map<String, List<AnnotatedMethodParameterProcessor>> parameterProcessorsMap = new LinkedHashMap<>();
        applicationModel.getExtensionLoader(AnnotatedMethodParameterProcessor.class)
            .getSupportedExtensionInstances()
            .forEach(processor -> {
                List<AnnotatedMethodParameterProcessor> processors =
                    parameterProcessorsMap.computeIfAbsent(processor.getAnnotationName(), k -> new LinkedList<>());
                processors.add(processor);
            });
        return parameterProcessorsMap;
    }

    private static Set<NoAnnotatedParameterRequestTagProcessor> loadNoAnnotatedMethodParameterProcessors(ApplicationModel applicationModel) {
        Set<NoAnnotatedParameterRequestTagProcessor> supportedExtensionInstances = applicationModel.getExtensionLoader(NoAnnotatedParameterRequestTagProcessor.class)
            .getSupportedExtensionInstances();

        return supportedExtensionInstances;
    }

    public static boolean isServiceMethodAnnotationPresent(Class<?> serviceImpl, String annotationClass) {
        List<Method> allMethods = getAllMethods(serviceImpl, excludedDeclaredClass(Object.class));

        for (Method method : allMethods) {
            if (isAnnotationPresent(method, annotationClass)) {
                return true;
            }
        }

        return false;
    }
}
