// Copyright 2015 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.ziputils;

import static com.google.devtools.build.android.ziputils.DataDescriptor.EXTCRC;
import static com.google.devtools.build.android.ziputils.DataDescriptor.EXTLEN;
import static com.google.devtools.build.android.ziputils.DataDescriptor.EXTSIZ;
import static com.google.devtools.build.android.ziputils.DirectoryEntry.CENCRC;
import static com.google.devtools.build.android.ziputils.DirectoryEntry.CENLEN;
import static com.google.devtools.build.android.ziputils.DirectoryEntry.CENSIZ;
import static com.google.devtools.build.android.ziputils.DirectoryEntry.CENTIM;
import static com.google.devtools.build.android.ziputils.LocalFileHeader.LOCFLG;
import static com.google.devtools.build.android.ziputils.LocalFileHeader.LOCTIM;

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.Options;
import com.google.devtools.common.options.OptionsBase;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;

/**
 * Command-line entry point for the dex reducer utility. This utility extracts .dex files
 * from one or more archives, and packaging them in a single output archive, renaming entries
 * to: classes.dex, classes2.dex, ...
 *
 * <p>This utility is intended used to consolidate the result of compiling the output produced
 * by the dex mapper utility.</p>
 */
public class DexReducer implements EntryHandler {
  private static final String SUFFIX = ".dex";
  private static final String BASENAME = "classes";
  private ZipOut out;
  private int count = 0;
  private String outFile;
  private List<String> paths;

  DexReducer() {
    outFile = null;
    paths = new ArrayList<>();
  }

  /**
   * Command-line entry point.
   * @param args
   */
  public static void main(String[] args) {
    try {
     DexReducer dexDexReducer = new DexReducer();
     dexDexReducer.parseArguments(args);
     dexDexReducer.run();
    } catch (Exception ex) {
      System.err.println("DexReducer threw exception: " + ex.getMessage());
      System.exit(1);
    }
  }

  private void parseArguments(String[] args) {
    DexReducerOptions options =
        Options.parseAndExitUponError(DexReducerOptions.class, /*allowResidue=*/ true, args)
            .getOptions();
    paths = options.inputZips;
    outFile = options.outputZip;
  }

  private void run() throws IOException {
    out = new ZipOut(new FileOutputStream(outFile, false).getChannel(), outFile);
    for (String filename : paths) {
      ZipIn in = new ZipIn(new FileInputStream(filename).getChannel(), filename);
      in.scanEntries(this);
    }
    out.close();
  }

  @Override
  public void handle(ZipIn in, LocalFileHeader header, DirectoryEntry dirEntry, ByteBuffer data)
      throws IOException {
    String path = header.getFilename();
    if (!path.endsWith(".dex")) {
      return;
    }
    count++;
    String filename = BASENAME + (count == 1 ? "" : Integer.toString(count)) + SUFFIX;
    String comment = dirEntry.getComment();
    byte[] extra = dirEntry.getExtraData();
    out.nextEntry(dirEntry.clone(filename, extra, comment).set(CENTIM, DosTime.EPOCHISH.time));
    out.write(header.clone(filename, extra).set(LOCTIM, DosTime.EPOCHISH.time));
    out.write(data);
    if ((header.get(LOCFLG) & LocalFileHeader.SIZE_MASKED_FLAG) != 0) {
      DataDescriptor desc = DataDescriptor.allocate()
          .set(EXTCRC, dirEntry.get(CENCRC))
          .set(EXTSIZ, dirEntry.get(CENSIZ))
          .set(EXTLEN, dirEntry.get(CENLEN));
      out.write(desc);
    }
  }

  /** Commandline options. */
  public static class DexReducerOptions extends OptionsBase {
    @Option(
      name = "input_zip",
      defaultValue = "null",
      category = "input",
      allowMultiple = true,
      abbrev = 'i',
      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
      effectTags = {OptionEffectTag.UNKNOWN},
      help = "Input zip file containing entries to collect and enumerate."
    )
    public List<String> inputZips;

    @Option(
      name = "output_zip",
      defaultValue = "null",
      category = "output",
      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
      effectTags = {OptionEffectTag.UNKNOWN},
      abbrev = 'o',
      help = "Output zip file, containing enumerated entries."
    )
    public String outputZip;
  }
}
