// 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.builder.dependency.SymbolFileProvider;
import com.android.resources.ResourceType;
import com.google.common.base.Optional;
import com.google.common.base.Stopwatch;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.devtools.build.android.AndroidResourceProcessor.AaptConfigOptions;
import com.google.devtools.build.android.Converters.DependencyAndroidDataListConverter;
import com.google.devtools.build.android.Converters.PathConverter;
import com.google.devtools.build.android.resources.RClassGenerator;
import com.google.devtools.build.android.resources.ResourceSymbols;
import com.google.devtools.common.options.Option;
import com.google.devtools.common.options.OptionDocumentationCategory;
import com.google.devtools.common.options.OptionEffectTag;
import com.google.devtools.common.options.OptionsBase;
import com.google.devtools.common.options.OptionsParser;
import com.google.devtools.common.options.ShellQuotedParamsFilePreProcessor;
import java.io.Closeable;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

/** This action generates consistent ids R.class files for use in robolectric tests. */
public class GenerateRobolectricResourceSymbolsAction {

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

  private static final class WriteLibraryRClass implements Callable<Boolean> {
    private final Entry<String, Collection<ListenableFuture<ResourceSymbols>>> librarySymbolEntry;
    private final RClassGenerator generator;

    private WriteLibraryRClass(
        Entry<String, Collection<ListenableFuture<ResourceSymbols>>> librarySymbolEntry,
        RClassGenerator generator) {
      this.librarySymbolEntry = librarySymbolEntry;
      this.generator = generator;
    }

    @Override
    public Boolean call() throws Exception {
      List<ResourceSymbols> resourceSymbolsList = new ArrayList<>();
      for (final ListenableFuture<ResourceSymbols> resourceSymbolsReader :
          librarySymbolEntry.getValue()) {
        resourceSymbolsList.add(resourceSymbolsReader.get());
      }

      generator.write(
          librarySymbolEntry.getKey(), ResourceSymbols.merge(resourceSymbolsList).asInitializers());
      return true;
    }
  }

  /** Flag specifications for this action. */
  public static final class Options extends OptionsBase {

    @Option(
      name = "data",
      defaultValue = "",
      converter = DependencyAndroidDataListConverter.class,
      category = "input",
      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
      effectTags = {OptionEffectTag.UNKNOWN},
      help =
          "Data dependencies. The expected format is "
              + DependencyAndroidData.EXPECTED_FORMAT
              + "[&...]"
    )
    public List<DependencyAndroidData> data;

    @Option(
      name = "classJarOutput",
      defaultValue = "null",
      converter = PathConverter.class,
      category = "output",
      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
      effectTags = {OptionEffectTag.UNKNOWN},
      help = "Path for the generated java class jar."
    )
    public Path classJarOutput;
  }

  public static void main(String[] args) throws Exception {

    final Stopwatch timer = Stopwatch.createStarted();
    OptionsParser optionsParser =
        OptionsParser.newOptionsParser(Options.class, AaptConfigOptions.class);
    optionsParser.enableParamsFileSupport(
        new ShellQuotedParamsFilePreProcessor(FileSystems.getDefault()));
    optionsParser.parseAndExitUponError(args);
    AaptConfigOptions aaptConfigOptions = optionsParser.getOptions(AaptConfigOptions.class);
    Options options = optionsParser.getOptions(Options.class);

    try (ScopedTemporaryDirectory scopedTmp =
        new ScopedTemporaryDirectory("robolectric_resources_tmp")) {
      Path tmp = scopedTmp.getPath();
      Path generatedSources = Files.createDirectories(tmp.resolve("generated_resources"));
      // The reported availableProcessors may be higher than the actual resources
      // (on a shared system). On the other hand, a lot of the work is I/O, so it's not completely
      // CPU bound. As a compromise, divide by 2 the reported availableProcessors.
      int numThreads = Math.max(1, Runtime.getRuntime().availableProcessors() / 2);
      ListeningExecutorService executorService =
          MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(numThreads));
      try (Closeable closeable = ExecutorServiceCloser.createWith(executorService)) {

        logger.fine(String.format("Setup finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS)));

        final PlaceholderIdFieldInitializerBuilder robolectricIds =
            PlaceholderIdFieldInitializerBuilder.from(aaptConfigOptions.androidJar);
        ParsedAndroidData.loadedFrom(
                options.data, executorService, AndroidParsedDataDeserializer.create())
            .writeResourcesTo(
                new AndroidResourceSymbolSink() {

                  @Override
                  public void acceptSimpleResource(ResourceType type, String name) {
                    robolectricIds.addSimpleResource(type, name);
                  }

                  @Override
                  public void acceptPublicResource(
                      ResourceType type, String name, Optional<Integer> value) {
                    robolectricIds.addPublicResource(type, name, value);
                  }

                  @Override
                  public void acceptStyleableResource(
                      FullyQualifiedName key, Map<FullyQualifiedName, Boolean> attrs) {
                    robolectricIds.addStyleableResource(key, attrs);
                  }
                });

        final RClassGenerator generator =
            RClassGenerator.with(generatedSources, robolectricIds.build(), false);

        List<SymbolFileProvider> libraries = new ArrayList<>();
        for (DependencyAndroidData dataDep : options.data) {
          SymbolFileProvider library = dataDep.asSymbolFileProvider();
          libraries.add(library);
        }
        List<ListenableFuture<Boolean>> writeSymbolsTask = new ArrayList<>();
        for (final Entry<String, Collection<ListenableFuture<ResourceSymbols>>> librarySymbolEntry :
            ResourceSymbols.loadFrom(libraries, executorService, null).asMap().entrySet()) {
          writeSymbolsTask.add(
              executorService.submit(new WriteLibraryRClass(librarySymbolEntry, generator)));
        }
        FailedFutureAggregator.forIOExceptionsWithMessage("Errors writing symbols.")
            .aggregateAndMaybeThrow(writeSymbolsTask);
      }

      logger.fine(String.format("Merging finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS)));

      AndroidResourceOutputs.createClassJar(generatedSources, options.classJarOutput);
      logger.fine(
          String.format("Create classJar finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS)));

    } catch (Exception e) {
      logger.log(Level.SEVERE, "Unexpected", e);
      throw e;
    }
    logger.fine(String.format("Resources merged in %sms", timer.elapsed(TimeUnit.MILLISECONDS)));
  }
}
