/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed 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.android.builder.core;

import com.android.annotations.NonNull;
import com.android.jack.api.JackProvider;
import com.android.jill.api.JillProvider;
import com.android.sdklib.BuildToolInfo;
import com.android.repository.Revision;
import com.android.utils.ILogger;
import com.google.common.base.Joiner;
import com.google.common.base.Objects;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.ServiceLoader;

/**
 * {@link ServiceLoader} helpers for tools located in the SDK's build-tools folders.
 *
 * This utility will cache {@link ServiceLoader} instances per build-tools version and per target
 * service type.
 */
public enum BuildToolsServiceLoader {

    /**
     * Singleton instance to request {@link ServiceLoader} instances from.
     */
    INSTANCE;

    /**
     * private cache data for a particular build-tool version.
     */
    private static final class LoadedBuildTool {
        private final Revision version;
        private final BuildToolServiceLoader serviceLoader;

        private LoadedBuildTool(Revision version,
                BuildToolServiceLoader serviceLoader) {
            this.version = version;
            this.serviceLoader = serviceLoader;
        }
    }

    private final List<LoadedBuildTool> loadedBuildTools = new ArrayList<LoadedBuildTool>();

    /**
     * Load a built-tools version specific {@link ServiceLoader} helper.
     * @param buildToolInfo the requested build-tools information
     * @return an initialized {@link BuildToolsServiceLoader.BuildToolServiceLoader} to get
     * instances of {@link ServiceLoader} from.
     */
    @NonNull
    public synchronized BuildToolServiceLoader forVersion(BuildToolInfo buildToolInfo) {

        Optional<LoadedBuildTool> loadedBuildToolOptional =
                findVersion(buildToolInfo.getRevision());

        if (loadedBuildToolOptional.isPresent()) {
            return loadedBuildToolOptional.get().serviceLoader;
        }

        LoadedBuildTool loadedBuildTool = new LoadedBuildTool(buildToolInfo.getRevision(),
                    new BuildToolServiceLoader(buildToolInfo));
        loadedBuildTools.add(loadedBuildTool);
        return loadedBuildTool.serviceLoader;
    }

    @NonNull
    private Optional<LoadedBuildTool> findVersion(Revision version) {
        for (LoadedBuildTool loadedBuildTool : loadedBuildTools) {
            if (loadedBuildTool.version.equals(version)) {
                return Optional.of(loadedBuildTool);
            }
        }
        return Optional.absent();
    }

    /**
     * Abstract notion of what a service is. A service must be declared in one of the classpath
     * provided jar files. The service declaration must conforms to {@link ServiceLoader} contract.
     *
     * @param <T> the type of service.
     */
    public static class Service<T> {

        private final Collection<String> classpath;
        private final Class<T> serviceClass;

        protected Service(Collection<String> classpath, Class<T> serviceClass) {
            this.classpath = classpath;
            this.serviceClass = serviceClass;
        }

        public Collection<String> getClasspath() {
            return classpath;
        }
        public Class<T> getServiceClass() {
            return serviceClass;
        }

        @Override
        public String toString() {
            return Objects.toStringHelper(this)
                    .add("serviceClass", serviceClass)
                    .add("classpath", Joiner.on(",").join(classpath))
                    .toString();
        }
    }

    /**
     * Jack service description.
     */
    public static final Service<JackProvider> JACK =
            new Service<JackProvider>(ImmutableList.of("jack.jar"), JackProvider.class);

    /**
     * Jill service description.
     */
    public static final Service<JillProvider> JILL =
            new Service<JillProvider>(ImmutableList.of("jill.jar"), JillProvider.class);

    /**
     * build-tools version specific {@link ServiceLoader} helper.
     */
    public static final class BuildToolServiceLoader {

        /**
         * private cache data for a single {@link ServiceLoader} instance.
         * @param <T> the service loader type.
         */
        private static final class LoadedServiceLoader<T> {
            private final Class<T> serviceType;
            private final ServiceLoader<T> serviceLoader;

            private LoadedServiceLoader(Class<T> serviceType, ServiceLoader<T> serviceLoader) {
                this.serviceType = serviceType;
                this.serviceLoader = serviceLoader;
            }
        }

        private final BuildToolInfo buildToolInfo;
        private final List<LoadedServiceLoader> loadedServicesLoaders =
                new ArrayList<LoadedServiceLoader>();

        private BuildToolServiceLoader(BuildToolInfo buildToolInfo) {
            this.buildToolInfo = buildToolInfo;
        }

        /**
         * Returns a newly allocated or existing {@link ServiceLoader} instance for the passed
         * {@link com.android.builder.core.BuildToolsServiceLoader.Service} type in the context
         * of the build-tools version this instance was created for.
         *
         * @param serviceType the requested service type encapsulation.
         * @param <T> the type of service
         * @return a {@link ServiceLoader} instance for the T service type.
         * @throws ClassNotFoundException
         */
        @NonNull
        public synchronized  <T> ServiceLoader<T> getServiceLoader(Service<T> serviceType)
                throws ClassNotFoundException {

            Optional<ServiceLoader<T>> serviceLoaderOptional =
                    getLoadedServiceLoader(serviceType.getServiceClass());
            if (serviceLoaderOptional.isPresent()) {
                return serviceLoaderOptional.get();
            }

            File buildToolLocation = buildToolInfo.getLocation();
            if (System.getenv("USE_JACK_LOCATION") != null) {
                buildToolLocation = new File(System.getenv("USE_JACK_LOCATION"));
            }
            URL[] urls = new URL[serviceType.classpath.size()];
            int i = 0;
            for (String classpathItem : serviceType.getClasspath()) {
                File jarFile = new File(buildToolLocation, classpathItem);
                try {
                    urls[i++] = jarFile.toURI().toURL();
                } catch (MalformedURLException e) {
                    throw new RuntimeException(e);
                }
            }
            ClassLoader cl = new URLClassLoader(urls, serviceType.getServiceClass().getClassLoader());
            ServiceLoader<T> serviceLoader = ServiceLoader.load(serviceType.getServiceClass(), cl);
            loadedServicesLoaders.add(new LoadedServiceLoader<T>(
                    serviceType.getServiceClass(), serviceLoader));
            return serviceLoader;
        }

        /**
         * Return the first service instance for the requested service type or
         * {@link Optional#absent()} if none exist.
         * @param logger to log resolution.
         * @param serviceType the requested service type encapsulation.
         * @param <T> the requested service class type.
         * @return the instance of T or null of none exist in this context.
         * @throws ClassNotFoundException
         */
        @NonNull
        public synchronized <T> Optional<T> getSingleService(
                ILogger logger,
                Service<T> serviceType) throws ClassNotFoundException {
            logger.verbose("Looking for %1$s", serviceType);
            ServiceLoader<T> serviceLoader = getServiceLoader(serviceType);
            logger.verbose("Got a serviceLoader %1$s",
                    Integer.toHexString(System.identityHashCode(serviceLoader)));
            Iterator<T> serviceIterator = serviceLoader.iterator();
            logger.verbose("Service Iterator =  %1$s ", serviceIterator);
            if (serviceIterator.hasNext()) {
                T service = serviceIterator.next();
                logger.verbose("Got it from %1$s, loaded service = %2$s, type = %3$s",
                        serviceIterator, service, service.getClass());
                return Optional.of(service);
            } else {
                logger.info("Cannot find service implementation %1$s" + serviceType);
                return Optional.absent();
            }
        }

        @NonNull
        @SuppressWarnings("unchecked")
        private <T> Optional<ServiceLoader<T>> getLoadedServiceLoader(Class<T> serviceType) {
            for (LoadedServiceLoader<?> loadedServiceLoader : loadedServicesLoaders) {
                if (loadedServiceLoader.serviceType.equals(serviceType)) {
                    return Optional.of((ServiceLoader<T>) loadedServiceLoader.serviceLoader);
                }
            }
            return Optional.absent();
        }
    }
}
