// Copyright 2014 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.xcode.zip;

import static com.google.devtools.build.singlejar.ZipCombiner.DOS_EPOCH;

import com.google.common.collect.ImmutableList;
import com.google.devtools.build.singlejar.ZipCombiner;
import com.google.devtools.build.xcode.util.Value;
import com.google.devtools.build.zip.ZipFileEntry;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;

/**
 * Describes an entry in a zip file when the zip file is being created.
 */
public class ZipInputEntry extends Value<ZipInputEntry> {
  /**
   * The external file attribute used for files by default. This indicates a non-executable regular
   * file that is readable by group and world.
   */
  public static final int DEFAULT_EXTERNAL_FILE_ATTRIBUTE = (0100644 << 16);

  /**
   * An external file attribute that indicates an executable regular file that is readable and
   * executable by group and world.
   */
  public static final int EXECUTABLE_EXTERNAL_FILE_ATTRIBUTE = (0100755 << 16);

  /**
   * Made by version of .ipa files built by Xcode. Upper byte indicates Unix host. Lower byte
   * indicates version of encoding software (note that 0x1e = 30 = (3.0 * 10), so 0x1e translates
   * to 3.0). The Unix host value in the upper byte is what causes the external file attribute to
   * be interpreted as POSIX permission and file type bits.
   */
  public static final short MADE_BY_VERSION = (short) 0x031e;

  private final Path source;
  private final String zipPath;
  private final int externalFileAttribute;

  public ZipInputEntry(Path source, String zipPath) {
    this(source, zipPath, DEFAULT_EXTERNAL_FILE_ATTRIBUTE);
  }

  public ZipInputEntry(Path source, String zipPath, int externalFileAttribute) {
    super(source, zipPath, externalFileAttribute);
    this.source = source;
    this.zipPath = zipPath;
    this.externalFileAttribute = externalFileAttribute;
  }

  /**
   * The location of the source file to place in the zip.
   */
  public Path getSource() {
    return source;
  }

  /**
   * The full path of the item in the zip.
   */
  public String getZipPath() {
    return zipPath;
  }

  /**
   * The external file attribute field of the zip entry in the central directory record. On
   * Unix-originated .zips, this corresponds to the permission bits (e.g. 0755 for an excutable
   * file).
   */
  public int getExternalFileAttribute() {
    return externalFileAttribute;
  }

  /**
   * Adds this entry to a zip using the given {@code ZipCombiner}. Entry can be either a directory
   * or a file.
   */
  public void add(ZipCombiner combiner) throws IOException {
    ZipFileEntry entry = new ZipFileEntry(zipPath);
    if (Files.isDirectory(source)) {
      String name = entry.getName();
      if (!name.endsWith("/")) {
        name = name + "/";
      }
      combiner.addDirectory(name, DOS_EPOCH);
      return;
    }
    try (InputStream inputStream = Files.newInputStream(source)) {
      entry.setTime(DOS_EPOCH.getTime());
      entry.setVersion(MADE_BY_VERSION);
      entry.setExternalAttributes(externalFileAttribute);
      combiner.addFile(entry, inputStream);
    }
  }

  public static void addAll(ZipCombiner combiner, Iterable<ZipInputEntry> inputs)
      throws IOException {
    for (ZipInputEntry input : inputs) {
      input.add(combiner);
    }
  }

  /**
   * Returns the entries to place in a zip file as if the zip file mirrors some directory structure.
   * For instance, if {@code rootDirectory} is /tmp/foo, and the following files exist:
   * <ul>
   *   <li>/tmp/foo/a
   *   <li>/tmp/foo/bar/c
   *   <li>/tmp/foo/baz/d
   * </ul>
   * This function will return entries which point to these files and have in-zip paths of:
   * <ul>
   *   <li>a
   *   <li>bar/c
   *   <li>baz/d
   * </ul>
   * Note that currently this doesn't add directory entries.
   */
  public static Iterable<ZipInputEntry> fromDirectory(final Path rootDirectory) throws IOException {
    final ImmutableList.Builder<ZipInputEntry> zipInputs = new ImmutableList.Builder<>();
    Files.walkFileTree(rootDirectory, new SimpleFileVisitor<Path>() {
      @Override
      public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
        // TODO(bazel-team): Set the external file attribute based on the attribute of the
        // permissions of the file on-disk.
        zipInputs.add(new ZipInputEntry(file, rootDirectory.relativize(file).toString()));
        return FileVisitResult.CONTINUE;
      }
    });
    return zipInputs.build();
  }
}
