// Copyright 2017 The Bazel Authors. All rights reserved.
//
// 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.google.devtools.build.android;

import com.android.annotations.Nullable;
import com.android.builder.core.VariantType;
import com.android.ide.common.internal.PngCruncher;
import com.google.common.base.Stopwatch;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.devtools.build.android.AndroidDataMerger.ContentComparingChecker;
import com.google.devtools.build.android.AndroidDataMerger.PathComparingChecker;
import com.google.devtools.build.android.AndroidDataMerger.SourceChecker;
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;

/** Collects all the functionality for an action to merge resources. */
// TODO(bazel-team): Turn into an instance object, in order to use an external ExecutorService.
public class AndroidResourceMerger {
  /** Thrown when there is a unexpected condition during merging. */
  public static class MergingException extends UserException {

    private MergingException(Throwable e) {
      super("Error during merging", e);
    }

    private MergingException(String message) {
      super("Merging Error: \n" + message);
    }

    static MergingException wrapException(Throwable e) {
      return new MergingException(e);
    }

    static MergingException withMessage(String message) {
      return new MergingException(message);
    }
  }

  static final Logger logger = Logger.getLogger(AndroidResourceProcessor.class.getName());

  /** Merges all secondary resources with the primary resources. */
  public static MergedAndroidData mergeData(
      final ParsedAndroidData primary,
      final Path primaryManifest,
      final List<? extends SerializedAndroidData> direct,
      final List<? extends SerializedAndroidData> transitive,
      final Path resourcesOut,
      final Path assetsOut,
      @Nullable final PngCruncher cruncher,
      final VariantType type,
      @Nullable final Path symbolsOut,
      @Nullable AndroidResourceClassWriter rclassWriter,
      AndroidDataDeserializer deserializer,
      boolean throwOnResourceConflict,
      ListeningExecutorService executorService) {
    Stopwatch timer = Stopwatch.createStarted();
    try {
      UnwrittenMergedAndroidData merged =
          mergeData(
              executorService,
              transitive,
              direct,
              primary,
              primaryManifest,
              type != VariantType.LIBRARY,
              deserializer,
              throwOnResourceConflict,
              ContentComparingChecker.create());
      timer.reset().start();
      if (symbolsOut != null) {
        AndroidDataSerializer serializer = AndroidDataSerializer.create();
        merged.serializeTo(serializer);
        serializer.flushTo(symbolsOut);
        logger.fine(
            String.format(
                "serialize merge finished in %sms", timer.elapsed(TimeUnit.MILLISECONDS)));
        timer.reset().start();
      }
      if (rclassWriter != null) {
        merged.writeResourceClass(rclassWriter);
        logger.fine(
            String.format("write classes finished in %sms", timer.elapsed(TimeUnit.MILLISECONDS)));
        timer.reset().start();
      }
      AndroidDataWriter writer =
          AndroidDataWriter.createWith(
              resourcesOut.getParent(), resourcesOut, assetsOut, cruncher, executorService);
      return merged.write(writer);
    } catch (IOException e) {
      throw MergingException.wrapException(e);
    } finally {
      logger.fine(
          String.format("write merge finished in %sms", timer.elapsed(TimeUnit.MILLISECONDS)));
    }
  }

  public static UnwrittenMergedAndroidData mergeData(
      ListeningExecutorService executorService,
      List<? extends SerializedAndroidData> transitive,
      List<? extends SerializedAndroidData> direct,
      ParsedAndroidData primary,
      Path primaryManifest,
      boolean allowPrimaryOverrideAll,
      AndroidDataDeserializer deserializer,
      boolean throwOnResourceConflict,
      SourceChecker checker) {
    Stopwatch timer = Stopwatch.createStarted();
    // TODO(b/74333698): Always check the contents of conflicting resources
    try {
      AndroidDataMerger merger =
          AndroidDataMerger.createWithPathDeduplictor(executorService, deserializer, checker);
      return merger.loadAndMerge(
          transitive,
          direct,
          primary,
          primaryManifest,
          allowPrimaryOverrideAll,
          throwOnResourceConflict);
    } finally {
      logger.fine(String.format("merge finished in %sms", timer.elapsed(TimeUnit.MILLISECONDS)));
    }
  }

  /**
   * Merges all secondary resources with the primary resources, given that the primary resources
   * have not yet been parsed and serialized.
   */
  public static MergedAndroidData mergeData(
      final UnvalidatedAndroidData primary,
      final List<? extends SerializedAndroidData> direct,
      final List<? extends SerializedAndroidData> transitive,
      final Path resourcesOut,
      final Path assetsOut,
      @Nullable final PngCruncher cruncher,
      final VariantType type,
      @Nullable final Path symbolsOut,
      final List<String> filteredResources,
      boolean throwOnResourceConflict) {
    try (ExecutorServiceCloser executorService = ExecutorServiceCloser.createWithFixedPoolOf(15)) {
      final ParsedAndroidData parsedPrimary = ParsedAndroidData.from(primary);
      return mergeData(
          parsedPrimary,
          primary.getManifest(),
          direct,
          transitive,
          resourcesOut,
          assetsOut,
          cruncher,
          type,
          symbolsOut,
          /* rclassWriter= */ null,
          AndroidParsedDataDeserializer.withFilteredResources(filteredResources),
          throwOnResourceConflict,
          executorService);
    } catch (IOException e) {
      throw MergingException.wrapException(e);
    }
  }

  /**
   * Merges all secondary resources with the primary resources, given that the primary resources
   * have been separately parsed and serialized.
   */
  public static MergedAndroidData mergeData(
      final SerializedAndroidData primary,
      final Path primaryManifest,
      final List<? extends SerializedAndroidData> direct,
      final List<? extends SerializedAndroidData> transitive,
      final Path resourcesOut,
      final Path assetsOut,
      @Nullable final PngCruncher cruncher,
      final VariantType type,
      @Nullable final Path symbolsOut,
      @Nullable final AndroidResourceClassWriter rclassWriter,
      boolean throwOnResourceConflict,
      ListeningExecutorService executorService) {
    final ParsedAndroidData.Builder primaryBuilder = ParsedAndroidData.Builder.newBuilder();
    final AndroidDataDeserializer deserializer = AndroidParsedDataDeserializer.create();
    primary.deserialize(deserializer, primaryBuilder.consumers());
    ParsedAndroidData primaryData = primaryBuilder.build();
    return mergeData(
        primaryData,
        primaryManifest,
        direct,
        transitive,
        resourcesOut,
        assetsOut,
        cruncher,
        type,
        symbolsOut,
        rclassWriter,
        deserializer,
        throwOnResourceConflict,
        executorService);
  }

  /**
   * Merges all secondary compiled resources with the primary compiled resources, given that the
   * primary resources have been separately compiled
   */
  public static void mergeCompiledData(
      final SerializedAndroidData primary,
      final Path primaryManifest,
      final List<? extends SerializedAndroidData> direct,
      final List<? extends SerializedAndroidData> transitive,
      @Nullable final AndroidResourceClassWriter rclassWriter,
      boolean throwOnResourceConflict,
      ListeningExecutorService executorService) {
    final ParsedAndroidData.Builder primaryBuilder = ParsedAndroidData.Builder.newBuilder();
    final AndroidDataDeserializer deserializer = AndroidCompiledDataDeserializer.create();
    primary.deserialize(deserializer, primaryBuilder.consumers());
    ParsedAndroidData primaryData = primaryBuilder.build();
    Stopwatch timer = Stopwatch.createStarted();
    try {
      UnwrittenMergedAndroidData merged =
          mergeData(
              executorService,
              transitive,
              direct,
              primaryData,
              primaryManifest,
              false,
              deserializer,
              throwOnResourceConflict,
              PathComparingChecker.create());
      timer.reset().start();
      merged.writeResourceClass(rclassWriter);
      logger.fine(
          String.format("write classes finished in %sms", timer.elapsed(TimeUnit.MILLISECONDS)));
      timer.reset().start();
    } catch (IOException e) {
      throw MergingException.wrapException(e);
    } finally {
      logger.fine(
          String.format("write merge finished in %sms", timer.elapsed(TimeUnit.MILLISECONDS)));
    }
  }
}
