/*
 * Copyright 2013-present Facebook, Inc.
 *
 * 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.facebook.buck.dalvik;

import com.facebook.buck.jvm.java.classes.FileLike;
import com.google.common.base.Charsets;
import com.google.common.base.Preconditions;
import com.google.common.collect.Sets;
import com.google.common.io.ByteStreams;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/** Helper to write a Zip file used by {@link DalvikAwareZipSplitter}. */
public class DalvikAwareOutputStreamHelper implements ZipOutputStreamHelper {

  private static final int MAX_METHOD_REFERENCES = 64 * 1024;
  // Making this 60k for now instead of 64 because the analyzer doesn't find all field references.
  // This only comes into play in rare cases, so it's not hi-pri to fix.
  private static final int MAX_FIELD_REFERENCES = 60 * 1024;

  private final ZipOutputStream outStream;
  private final Set<String> entryNames = Sets.newHashSet();
  private final long linearAllocLimit;
  private final Writer reportFileWriter;
  private final DalvikStatsCache dalvikStatsCache;

  private final Set<DalvikMemberReference> currentMethodReferences = Sets.newHashSet();
  private final Set<DalvikMemberReference> currentFieldReferences = Sets.newHashSet();
  private long currentLinearAllocSize;

  DalvikAwareOutputStreamHelper(
      Path outputFile, long linearAllocLimit, Path reportDir, DalvikStatsCache dalvikStatsCache)
      throws IOException {
    this.outStream =
        new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(outputFile)));
    this.linearAllocLimit = linearAllocLimit;
    Path reportFile = reportDir.resolve(outputFile.getFileName().toString() + ".txt");
    this.reportFileWriter = Files.newBufferedWriter(reportFile, Charsets.UTF_8);
    this.dalvikStatsCache = dalvikStatsCache;
  }

  private boolean isEntryTooBig(FileLike entry) {
    DalvikStatsTool.Stats stats = dalvikStatsCache.getStats(entry);
    if (currentLinearAllocSize + stats.estimatedLinearAllocSize > linearAllocLimit) {
      return true;
    }
    int newMethodRefs = Sets.difference(stats.methodReferences, currentMethodReferences).size();
    if (currentMethodReferences.size() + newMethodRefs > MAX_METHOD_REFERENCES) {
      return true;
    }
    int newFieldRefs = Sets.difference(stats.fieldReferences, currentFieldReferences).size();
    if (currentFieldReferences.size() + newFieldRefs > MAX_FIELD_REFERENCES) {
      return true;
    }
    return false;
  }

  @Override
  public boolean canPutEntry(FileLike fileLike) {
    return !isEntryTooBig(fileLike);
  }

  @Override
  public boolean containsEntry(FileLike fileLike) {
    return entryNames.contains(fileLike.getRelativePath());
  }

  @Override
  public void putEntry(FileLike fileLike) throws IOException {
    String name = fileLike.getRelativePath();
    // Tracks unique entry names and avoids duplicates.  This is, believe it or not, how
    // proguard seems to handle merging multiple -injars into a single -outjar.
    if (!containsEntry(fileLike)) {
      entryNames.add(name);
      outStream.putNextEntry(new ZipEntry(name));
      try (InputStream in = fileLike.getInput()) {
        ByteStreams.copy(in, outStream);
      }

      // Make sure FileLike#getSize didn't lie (or we forgot to call canPutEntry).
      DalvikStatsTool.Stats stats = dalvikStatsCache.getStats(fileLike);
      Preconditions.checkState(
          !isEntryTooBig(fileLike),
          "Putting entry %s (%s) exceeded maximum size of %s",
          name,
          stats.estimatedLinearAllocSize,
          linearAllocLimit);
      currentLinearAllocSize += stats.estimatedLinearAllocSize;
      currentMethodReferences.addAll(stats.methodReferences);
      currentFieldReferences.addAll(stats.fieldReferences);
      String report =
          String.format(
              "%d %d %s\n", stats.estimatedLinearAllocSize, stats.methodReferences.size(), name);
      reportFileWriter.append(report);
    }
  }

  @Override
  public void close() throws IOException {
    outStream.close();
    reportFileWriter.close();
  }
}

@@ -4571,14 +4571,31 @@
  %25d 
+%25d 
 %25s%5Cn%22,
+%0A             
  sta
@@ -4622,16 +4622,30 @@
 locSize,
+%0A             
  stats.m
@@ -4667,16 +4667,74 @@
 .size(),
+%0A              stats.fieldReferences.size(),%0A             
  name);%0A

Add field count to ZipSplit report