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

import java.util.EnumSet;

import javax.annotation.Nullable;

/**
 * A full representation of a ZIP file entry.
 *
 * <p>See <a href="http://www.pkware.com/documents/casestudies/APPNOTE.TXT">ZIP Format</a> for
 * a description of the entry fields. (Section 4.3.7 and 4.4)
 */
public final class ZipFileEntry {

  /** Compression method for ZIP entries. */
  public enum Compression {
    STORED((short) 0, Feature.STORED),
    DEFLATED((short) 8, Feature.DEFLATED);

    public static Compression fromValue(int value) {
      for (Compression c : Compression.values()) {
        if (c.getValue() == value) {
          return c;
        }
      }
      return null;
    }

    private short value;
    private Feature feature;

    private Compression(short value, Feature feature) {
      this.value = value;
      this.feature = feature;
    }

    public short getValue() {
      return value;
    }

    public short getMinVersion() {
      return feature.getMinVersion();
    }

    Feature getFeature() {
      return feature;
    }
  }

  /** General purpose bit flag for ZIP entries. */
  public enum Flag {
    DATA_DESCRIPTOR(3);

    private int bit;

    private Flag(int bit) {
      this.bit = bit;
    }

    public int getBit() {
      return bit;
    }
  }

  /** Zip file features that entries may use. */
  enum Feature {
    DEFAULT((short) 0x0a),
    STORED((short) 0x0a),
    DEFLATED((short) 0x14),
    ZIP64_SIZE((short) 0x2d),
    ZIP64_CSIZE((short) 0x2d),
    ZIP64_OFFSET((short) 0x2d);

    private short minVersion;

    private Feature(short minVersion) {
      this.minVersion = minVersion;
    }

    public short getMinVersion() {
      return minVersion;
    }

    static short getMinRequiredVersion(EnumSet<Feature> featureSet) {
      short minVersion = Feature.DEFAULT.getMinVersion();
      for (Feature feature : featureSet) {
        minVersion = (short) Math.max(minVersion, feature.getMinVersion());
      }
      return minVersion;
    }
  }

  private String name;
  private long time = -1;
  private long crc = -1;
  private long size = -1;
  private long csize = -1;
  private Compression method;
  private short version = -1;
  private short versionNeeded = -1;
  private short flags;
  private short internalAttributes;
  private int externalAttributes;
  private long localHeaderOffset = -1;
  private ExtraDataList extra;
  @Nullable private String comment;

  private EnumSet<Feature> featureSet;

  /**
   * Creates a new ZIP entry with the specified name.
   *
   * @throws NullPointerException if the entry name is null
   */
  public ZipFileEntry(String name) {
    this.featureSet = EnumSet.of(Feature.DEFAULT);
    setName(name);
    setMethod(Compression.STORED);
    setExtra(new ExtraDataList());
  }

  /**
   * Creates a new ZIP entry with fields taken from the specified ZIP entry.
   */
  public ZipFileEntry(ZipFileEntry e) {
    this.name = e.getName();
    this.time = e.getTime();
    this.crc = e.getCrc();
    this.size = e.getSize();
    this.csize = e.getCompressedSize();
    this.method = e.getMethod();
    this.version = e.getVersion();
    this.versionNeeded = e.getVersionNeeded();
    this.flags = e.getFlags();
    this.internalAttributes = e.getInternalAttributes();
    this.externalAttributes = e.getExternalAttributes();
    this.localHeaderOffset = e.getLocalHeaderOffset();
    this.extra = new ExtraDataList(e.getExtra());
    this.comment = e.getComment();
    this.featureSet = EnumSet.copyOf(e.getFeatureSet());
  }

  /**
   * Sets the name of the entry.
   */
  public void setName(String name) {
    if (name == null) {
      throw new NullPointerException();
    }
    this.name = name;
  }

  /**
   * Returns the name of the entry.
   */
  public String getName() {
    return name;
  }

  /**
   * Sets the modification time of the entry.
   *
   * @param time the entry modification time in number of milliseconds since the epoch
   */
  public void setTime(long time) {
    this.time = time;
  }

  /**
   * Returns the modification time of the entry, or -1 if not specified.
   */
  public long getTime() {
    return time;
  }

  /**
   * Sets the CRC-32 checksum of the uncompressed entry data.
   *
   * @throws IllegalArgumentException if the specified CRC-32 value is less than 0 or greater than
   *     0xFFFFFFFF
   */
  public void setCrc(long crc) {
    if (crc < 0 || crc > 0xffffffffL) {
      throw new IllegalArgumentException("invalid entry crc-32");
    }
    this.crc = crc;
  }

  /**
   * Returns the CRC-32 checksum of the uncompressed entry data, or -1 if not known.
   */
  public long getCrc() {
    return crc;
  }

  /**
   * Sets the uncompressed size of the entry data in bytes.
   *
   * @throws IllegalArgumentException if the specified size is less than 0
   */
  public void setSize(long size) {
    if (size < 0) {
      throw new IllegalArgumentException("invalid entry size");
    }
    if (size > 0xffffffffL) {
      featureSet.add(Feature.ZIP64_SIZE);
    } else {
      featureSet.remove(Feature.ZIP64_SIZE);
    }
    this.size = size;
  }

  /**
   * Returns the uncompressed size of the entry data, or -1 if not known.
   */
  public long getSize() {
    return size;
  }

  /**
   * Sets the size of the compressed entry data in bytes.
   *
   * @throws IllegalArgumentException if the specified size is less than 0
   */
  public void setCompressedSize(long csize) {
    if (csize < 0) {
      throw new IllegalArgumentException("invalid entry size");
    }
    if (csize > 0xffffffffL) {
      featureSet.add(Feature.ZIP64_CSIZE);
    } else {
      featureSet.remove(Feature.ZIP64_CSIZE);
    }
    this.csize = csize;
  }

  /**
   * Returns the size of the compressed entry data, or -1 if not known. In the case of a stored
   * entry, the compressed size will be the same as the uncompressed size of the entry.
   */
  public long getCompressedSize() {
    return csize;
  }

  /**
   * Sets the compression method for the entry.
   */
  public void setMethod(Compression method) {
    if (method == null) {
      throw new NullPointerException();
    }
    if (this.method != null) {
      featureSet.remove(this.method.getFeature());
    }
    this.method = method;
    featureSet.add(this.method.getFeature());
  }

  /**
   * Returns the compression method of the entry.
   */
  public Compression getMethod() {
    return method;
  }

  /**
   * Sets the made by version for the entry.
   */
  public void setVersion(short version) {
    this.version = version;
  }

  /**
   * Returns the made by version of the entry, accounting for assigned version and feature set.
   */
  public short getVersion() {
    return (short) Math.max(version, Feature.getMinRequiredVersion(featureSet));
  }

  /**
   * Sets the version needed to extract the entry.
   */
  public void setVersionNeeded(short versionNeeded) {
    this.versionNeeded = versionNeeded;
  }

  /**
   * Returns the version needed to extract the entry, accounting for assigned version and feature
   * set.
   */
  public short getVersionNeeded() {
    return (short) Math.max(versionNeeded, Feature.getMinRequiredVersion(featureSet));
  }

  /**
   * Sets the general purpose bit flags for the entry.
   */
  public void setFlags(short flags) {
    this.flags = flags;
  }

  /**
   * Sets or clears the specified bit of the general purpose bit flags.
   *
   * @param flag the flag to set or clear
   * @param set whether the flag is to be set or cleared
   */
  public void setFlag(Flag flag, boolean set) {
    short mask = 0x0000;
    mask |= 1 << flag.getBit();
    if (set) {
      flags |= mask;
    } else {
      flags &= ~mask;
    }
  }

  /**
   * Returns the general purpose bit flags of the entry.
   *
   * <p>See <a href="http://www.pkware.com/documents/casestudies/APPNOTE.TXT">ZIP Format</a>
   * section 4.4.4.
   */
  public short getFlags() {
    return flags;
  }

  /**
   * Sets the internal file attributes of the entry.
   */
  public void setInternalAttributes(short internalAttributes) {
    this.internalAttributes = internalAttributes;
  }

  /**
   * Returns the internal file attributes of the entry.
   */
  public short getInternalAttributes() {
    return internalAttributes;
  }

  /**
   * Sets the external file attributes of the entry.
   */
  public void setExternalAttributes(int externalAttributes) {
    this.externalAttributes = externalAttributes;
  }

  /**
   * Returns the external file attributes of the entry.
   */
  public int getExternalAttributes() {
    return externalAttributes;
  }

  /**
   * Sets the file offset, in bytes, of the location of the local file header for the entry.
   *
   * <p>See <a href="http://www.pkware.com/documents/casestudies/APPNOTE.TXT">ZIP Format</a>
   * section 4.4.16
   *
   * @throws IllegalArgumentException if the specified local header offset is less than 0
   */
  void setLocalHeaderOffset(long localHeaderOffset) {
    if (localHeaderOffset < 0) {
      throw new IllegalArgumentException("invalid local header offset");
    }
    if (localHeaderOffset > 0xffffffffL) {
      featureSet.add(Feature.ZIP64_OFFSET);
    } else {
      featureSet.remove(Feature.ZIP64_OFFSET);
    }
    this.localHeaderOffset = localHeaderOffset;
  }

  /**
   * Returns the file offset of the local header of the entry.
   */
  public long getLocalHeaderOffset() {
    return localHeaderOffset;
  }

  /**
   * Sets the optional extra field data for the entry.
   *
   * @throws IllegalArgumentException if the length of the specified extra field data is greater
   *    than 0xFFFF bytes
   */
  public void setExtra(ExtraDataList extra) {
    if (extra == null) {
      throw new NullPointerException();
    }
    if (extra.getLength() > 0xffff) {
      throw new IllegalArgumentException("invalid extra field length");
    }
    this.extra = extra;
  }

  /**
   * Returns the extra field data for the entry.
   */
  public ExtraDataList getExtra() {
    return extra;
  }

  /**
   * Sets the optional comment string for the entry.
   */
  public void setComment(@Nullable String comment) {
    this.comment = comment;
  }

  /**
   * Returns the comment string for the entry, or null if none.
   */
  public String getComment() {
    return comment;
  }

  /**
   * Returns the feature set that this entry uses.
   */
  EnumSet<Feature> getFeatureSet() {
    return featureSet;
  }

  @Override
  public String toString() {
    return "ZipFileEntry[" + name + "]";
  }
}
