/*
 * 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.build.gradle.internal.pipeline;

import static com.android.SdkConstants.DOT_JAR;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.build.api.transform.DirectoryInput;
import com.android.build.api.transform.Format;
import com.android.build.api.transform.JarInput;
import com.android.build.api.transform.QualifiedContent.ContentType;
import com.android.build.api.transform.QualifiedContent.Scope;
import com.android.build.api.transform.Status;
import com.android.build.api.transform.TransformInput;
import com.android.utils.FileUtils;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;

import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.gradle.api.tasks.incremental.InputFileDetails;

import java.io.File;
import java.util.List;
import java.util.Set;

/**
 * Helper to handle the folder structure in the output of transforms.
 */
public class IntermediateFolderUtils {

    public static final String FOLDERS = "folders";
    public static final String JARS = "jars";

    /**
     * Returns the location of content for a given set of Scopes, Content Types, and format.
     *
     * If the format is {@link Format#DIRECTORY} then the result is the file location of the folder.
     * If the format is {@link Format#JAR} then the result is a file representing the jar to create.
     *
     * @param rootLocation the root location from which to create the content.
     * @param name a unique name for the content. For a given set of scopes/types/format it must
     *             be unique.
     * @param types the content types associated with this content.
     * @param scopes the scopes associated with this content.
     * @param format the format of the content.
     * @return the location of the content.
     */
    @NonNull
    public static File getContentLocation(
            @NonNull File rootLocation,
            @NonNull String name,
            @NonNull Set<ContentType> types,
            @NonNull Set<Scope> scopes,
            @NonNull Format format) {
        // runtime check these since it's (indirectly) called by 3rd party transforms.
        checkNotNull(name);
        checkNotNull(types);
        checkNotNull(scopes);
        checkNotNull(format);
        checkState(!name.isEmpty());
        checkState(!types.isEmpty());
        checkState(!scopes.isEmpty());

        switch (format) {
            case DIRECTORY: {
                File location = FileUtils.join(rootLocation,
                        FOLDERS,
                        typesToString(types),
                        scopesToString(scopes),
                        name);
                return location;
            }
            case JAR: {
                File location = FileUtils.join(rootLocation,
                        JARS,
                        typesToString(types),
                        scopesToString(scopes),
                        name + DOT_JAR);
                return location;
            }
            default:
                throw new RuntimeException("Unexpected Format: " + format);
        }
    }

    @NonNull
    public static TransformInput computeNonIncrementalInputFromFolder(
            @NonNull File folder,
            @NonNull Set<ContentType> requiredTypes,
            @NonNull Set<Scope> requiredScopes) {
        final List<JarInput> jarInputs = Lists.newArrayList();
        final List<DirectoryInput> directoryInputs = Lists.newArrayList();

        File jarsFolder = new File(folder, JARS);
        if (jarsFolder.isDirectory()) {
            parseTypeLevelFolders(
                    jarsFolder,
                    requiredTypes,
                    requiredScopes,
                    new InputGenerator() {
                        @Override
                        public boolean accept(@NonNull File file) {
                            return file.isFile() && file.getName().endsWith(DOT_JAR);
                        }

                        @Override
                        public void generate(
                                @NonNull File file,
                                @NonNull Set<ContentType> types,
                                @NonNull Set<Scope> scopes) {
                            jarInputs.add(new ImmutableJarInput(
                                    file.getName().substring(0,
                                            file.getName().length() - DOT_JAR.length()),
                                    file,
                                    Status.NOTCHANGED,
                                    types,
                                    scopes));

                        }
                    });
        }

        File foldersFolder = new File(folder, FOLDERS);
        if (foldersFolder.isDirectory()) {
            parseTypeLevelFolders(
                    foldersFolder,
                    requiredTypes,
                    requiredScopes,
                    new InputGenerator() {
                        @Override
                        public boolean accept(@NonNull File file) {
                            return file.isDirectory();
                        }

                        @Override
                        public void generate(@NonNull File file, @NonNull Set<ContentType> types,
                                @NonNull Set<Scope> scopes) {
                            directoryInputs.add(new ImmutableDirectoryInput(
                                    file.getName(),
                                    file,
                                    types,
                                    scopes));
                        }
                    });
        }

        return new ImmutableTransformInput(jarInputs, directoryInputs, folder);
    }

    static class IntermediateTransformInput extends IncrementalTransformInput {

        @NonNull
        private final File inputRoot;
        private List<String> rootLocationSegments = null;

        IntermediateTransformInput(@NonNull File inputRoot) {
            this.inputRoot = inputRoot;
        }

        @Override
        protected boolean checkRemovedFolder(
                @NonNull Set<Scope> transformScopes,
                @NonNull Set<ContentType> transformInputTypes,
                @NonNull File file,
                @NonNull List<String> fileSegments) {
            if (!checkRootSegments(fileSegments)) {
                return false;
            }

            // there must be at least 5 additional segments (4 to the root of the folder and 1 for
            // the file inside.
            if (fileSegments.size() <= rootLocationSegments.size() + 4) {
                return false;
            }

            // now check that the segments after the root are what we expect.
            int index = rootLocationSegments.size();
            if (!fileSegments.get(index++).equals(FOLDERS)) {
                return false;
            }

            // get the types.
            Set<ContentType> types = stringToTypes(fileSegments.get(index++));
            if (types == null) {
                return false;
            }

            // if the transform is not interested in this folder type, we don't care about those
            // changes
            if (Sets.intersection(transformInputTypes, types).isEmpty()) {
                return true;
            }

            // get the scopes.
            Set<Scope> scopes = stringToScopes(fileSegments.get(index++));
            if (scopes == null) {
                return false;
            }

            // if the scopes do not match the transform scope, we don't care about those changes.
            if (Sets.intersection(transformScopes, scopes).isEmpty()) {
                return true;
            }

            String name = fileSegments.get(index);

            // create the folder input. a mutable one so that it can be directly used
            // for other removed files from the same folder.
            // The root location of this folder is fileSegments, up to rootLocation + 4 (as the
            // rest is the changed file.
            File root = new File(FileUtils.join(
                    fileSegments.subList(0, rootLocationSegments.size() + 4)));
            MutableDirectoryInput folder = new MutableDirectoryInput(name, root, types, scopes);
            // add this file to it.
            Logging.getLogger(TransformManager.class).info(
                    "Tagged" + file.getAbsolutePath() + " as removed");
            folder.addChangedFile(file, Status.REMOVED);

            // add it to the list.
            addFolderInput(folder);

            return true;
        }

        @Override
        boolean checkRemovedJarFile(
                @NonNull Set<Scope> transformScopes,
                @NonNull Set<ContentType> transformInputTypes,
                @NonNull File file,
                @NonNull List<String> fileSegments) {
            if (!checkRootSegments(fileSegments)) {
                return false;
            }

            // there must be only 4 additional segments.
            if (fileSegments.size() != rootLocationSegments.size() + 4) {
                return false;
            }

            // last segment must end in .jar
            if (!file.getPath().endsWith(DOT_JAR)) {
                return false;
            }

            // now check that the segments after the root are what we expect.
            int index = rootLocationSegments.size();
            if (!fileSegments.get(index++).equals(JARS)) {
                return false;
            }

            // get the types.
            Set<ContentType> types = stringToTypes(fileSegments.get(index++));
            if (types == null) {
                return false;
            }
            // if the transform is not interested in this jar type, we don't care about those
            // changes
            if (Sets.intersection(transformInputTypes, types).isEmpty()) {
                return true;
            }

            // get the scopes.
            Set<Scope> scopes = stringToScopes(fileSegments.get(index++));
            if (scopes == null) {
                return false;
            }

            // if the scopes do not match the transform scope, we don't care about those changes.
            if (Sets.intersection(transformScopes, scopes).isEmpty()) {
                return true;
            }

            String name = fileSegments.get(index);

            // create the jar input
            addImmutableJar(new ImmutableJarInput(name, file, Status.REMOVED, types, scopes));

            return true;
        }

        private boolean checkRootSegments(@NonNull List<String> fileSegments) {
            if (rootLocationSegments == null) {
                rootLocationSegments = Lists.newArrayList(
                        Splitter.on(File.separatorChar).split(inputRoot.getAbsolutePath()));
            }

            if (fileSegments.size() <= rootLocationSegments.size()) {
                return false;
            }

            // compare segments going backward as the leafs are more likely to be different.
            // We can ignore the segments of the file that are beyond the segments for the folder.
            for (int i = rootLocationSegments.size() - 1 ; i >= 0 ; i--) {
                if (!rootLocationSegments.get(i).equals(fileSegments.get(i))) {
                    return false;
                }
            }

            return true;
        }

    }

    @NonNull
    public static IncrementalTransformInput computeIncrementalInputFromFolder(
            @NonNull File rootLocation,
            @NonNull Set<ContentType> requiredTypes,
            @NonNull Set<Scope> requiredScopes) {
        final IncrementalTransformInput input = new IntermediateTransformInput(rootLocation);

        File jarsFolder = new File(rootLocation, JARS);
        if (jarsFolder.isDirectory()) {
            parseTypeLevelFolders(
                    jarsFolder,
                    requiredTypes,
                    requiredScopes,
                    new InputGenerator() {
                        @Override
                        public boolean accept(@NonNull File file) {
                            return file.isFile() && file.getName().endsWith(DOT_JAR);
                        }

                        @Override
                        public void generate(
                                @NonNull File file,
                                @NonNull Set<ContentType> types,
                                @NonNull Set<Scope> scopes) {
                            input.addJarInput(new QualifiedContentImpl(
                                    file.getName().substring(0,
                                            file.getName().length() - DOT_JAR.length()),
                                    file,
                                    types,
                                    scopes) {
                            });

                        }
                    });
        }

        File foldersFolder = new File(rootLocation, FOLDERS);
        if (foldersFolder.isDirectory()) {
            parseTypeLevelFolders(
                    foldersFolder,
                    requiredTypes,
                    requiredScopes,
                    new InputGenerator() {
                        @Override
                        public boolean accept(@NonNull File file) {
                            return file.isDirectory();
                        }

                        @Override
                        public void generate(@NonNull File file, @NonNull Set<ContentType> types,
                                @NonNull Set<Scope> scopes) {
                            input.addFolderInput(new MutableDirectoryInput(
                                    file.getName(),
                                    file,
                                    types,
                                    scopes));
                        }
                    });
        }

        return input;
    }

    private interface InputGenerator {
        boolean accept(@NonNull File file);
        void generate(
                @NonNull File file,
                @NonNull Set<ContentType> types,
                @NonNull Set<Scope> scopes);
    }

    private static void parseTypeLevelFolders(
            @NonNull File rootFolder,
            @NonNull Set<ContentType> requiredTypes,
            @NonNull Set<Scope> requiredScopes,
            @NonNull InputGenerator generator) {
        File[] files = rootFolder.listFiles(File::isDirectory);

        if (files != null && files.length > 0) {
            for (File file : files) {
                Set<ContentType> types = stringToTypes(file.getName());
                if (types != null) {
                    // check these are types we care about and only pass down types we care about.
                    // In this case we can safely return the content with a limited type,
                    // as file extension allows for differentiation.
                    Set<ContentType> limitedTypes = Sets.intersection(requiredTypes, types);
                    if (!limitedTypes.isEmpty()) {
                        parseScopeLevelFolders(file, limitedTypes, requiredScopes, generator);
                    }
                }
            }
        }
    }

    private static void parseScopeLevelFolders(
            @NonNull File rootFolder,
            @NonNull Set<ContentType> types,
            @NonNull Set<Scope> requiredScopes,
            @NonNull InputGenerator generator) {
        File[] files = rootFolder.listFiles(File::isDirectory);

        if (files != null && files.length > 0) {
            for (File file : files) {
                Set<Scope> scopes = stringToScopes(file.getName());
                if (scopes != null) {
                    // we need up to the requiredScopes, but no more.
                    // content that only contains unwanted Scope can be safely dropped, however
                    // content that is both in and out of Scope will trigger a runtime error.
                    // check these are the scope we want, and only pass down scopes we care about.
                    Set<Scope> limitedScopes = Sets.newHashSetWithExpectedSize(requiredScopes.size());
                    boolean foundUnwanted = false;
                    for (Scope scope : scopes) {
                        if (requiredScopes.contains(scope)) {
                            limitedScopes.add(scope);
                        } else {
                            foundUnwanted = true;
                        }
                    }
                    if (!limitedScopes.isEmpty()) {
                        if (foundUnwanted) {
                            throw new RuntimeException("error");
                        }
                        parseContentLevelFolders(file, types, Sets.immutableEnumSet(limitedScopes),
                                generator);
                    }
                }
            }
        }
    }

    private static void parseContentLevelFolders(
            @NonNull File rootFolder,
            @NonNull Set<ContentType> types,
            @NonNull Set<Scope> scopes,
            @NonNull final InputGenerator generator) {

        File[] files = rootFolder.listFiles(generator::accept);

        if (files != null && files.length > 0) {
            for (File file : files) {
                generator.generate(file, types, scopes);
            }
        }
    }

    @Nullable
    private static Set<ContentType> stringToTypes(String folderName) {
        int value;
        try {
            value = Integer.parseInt(folderName, 16);
        } catch (NumberFormatException e) {
            return null;
        }

        ImmutableSet.Builder<ContentType> typesBuilder = ImmutableSet.builder();

        for (ContentType type : ExtendedContentType.getAllContentTypes()) {
            if ((type.getValue() & value) != 0) {
                typesBuilder.add(type);
            }
        }

        Set<ContentType> types = typesBuilder.build();
        if (types.isEmpty()) {
            return null;
        }

        return types;
    }

    private static String typesToString(@NonNull Set<ContentType> types) {
        int value = 0;
        for (ContentType type : types) {
            value += type.getValue();
        }

        return String.format("%x", value);
    }

    @Nullable
    private static Set<Scope> stringToScopes(String folderName) {
        int value;
        try {
            value = Integer.parseInt(folderName, 16);
        } catch (NumberFormatException e) {
            return null;
        }

        Set<Scope> scopes = Sets.newHashSet();

        for (Scope scope : Scope.values()) {
            if ((scope.getValue() & value) != 0) {
                scopes.add(scope);
            }
        }

        if (scopes.isEmpty()) {
            return null;
        }

        return Sets.immutableEnumSet(scopes);
    }

    private static String scopesToString(@NonNull Set<Scope> scopes) {
        int value = 0;
        for (Scope scope : scopes) {
            value += scope.getValue();
        }

        return String.format("%x", value);
    }

    @NonNull
    static Status inputFileDetailsToStatus(@NonNull InputFileDetails inputFileDetails) {
        if (inputFileDetails.isAdded()) return Status.ADDED;
        if (inputFileDetails.isModified()) return Status.CHANGED;
        if (inputFileDetails.isRemoved()) return Status.REMOVED;
        return Status.NOTCHANGED;
    }
}
