// 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 java.nio.ByteOrder.LITTLE_ENDIAN;
import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.nio.ByteBuffer;

/**
 * Provides a view of a local file header for a zip file entry.
 */
public class LocalFileHeader extends View<LocalFileHeader> {

  /**
   * Returns a {@code LocalFileHeader} view of the given buffer. The buffer is assumed to contain a
   * valid "central directory entry" record beginning at the buffers current position.
   *
   * @param buffer containing the data of a "central directory entry" record.
   * @return a {@code LocalFileHeader} of the data at the current position of the given byte
   * buffer.
   */
  public static LocalFileHeader viewOf(ByteBuffer buffer) {
    LocalFileHeader view = new LocalFileHeader(buffer.slice());
    view.buffer.limit(view.getSize());
    buffer.position(buffer.position() + view.buffer.remaining());
    return view;
  }

  private LocalFileHeader(ByteBuffer buffer) {
    super(buffer);
  }

  /**
   * Creates a {@code LocalFileHeader} with a heap allocated buffer. Apart from the signature
   * and extra data data (if any), the returned object is uninitialized.
   *
   * @param name entry file name. Cannot be {@code null}.
   * @param extraData extra data, or {@code null}
   * @return a {@code LocalFileHeader} with a heap allocated buffer.
   */
  public static LocalFileHeader allocate(String name, byte[] extraData) {
    byte[] nameData = name.getBytes(UTF_8);
    if (extraData == null) {
      extraData = EMPTY;
    }
    int size = SIZE + nameData.length + extraData.length;
    ByteBuffer buffer = ByteBuffer.allocate(size).order(LITTLE_ENDIAN);
    return new LocalFileHeader(buffer).init(nameData, extraData, size);
  }

  /**
   * Creates a {@code LocalFileHeader} over a writable buffer. The given buffer's position is
   * advanced by the number of bytes consumed by the view. Apart from the signature and extra data
   * (if any), the returned view is uninitialized.
   *
   * @param buffer buffer to hold data for the "central directory entry" record.
   * @param name entry file name. Cannot be {@code null}.
   * @param extraData extra data, or {@code null}
   * @return a {@code DirectoryEntry} with a heap allocated buffer.
   */
  public static LocalFileHeader view(ByteBuffer buffer, String name, byte[] extraData) {
    byte[] nameData = name.getBytes(UTF_8);
    if (extraData == null) {
      extraData = EMPTY;
    }
    int size = SIZE + nameData.length + extraData.length;
    LocalFileHeader view = new LocalFileHeader(buffer.slice()).init(nameData, extraData, size);
    buffer.position(buffer.position() + size);
    return view;
  }

  /**
   * Copies this {@code LocalFileHeader} into a heap allocated buffer, overwriting the current
   * path name and extra data with the given values.
   *
   * @param name entry file name. Cannot be {@code null}.
   * @param extraData extra data, or {@code null}
   * @return a {@code LocalFileHeader} with a heap allocated buffer, initialized with the given
   * name and extra data, and otherwise a copy of this object.
   */
  public LocalFileHeader clone(String name, byte[] extraData) {
    return LocalFileHeader.allocate(name, extraData).copy(this, LOCCRC, LOCSIZ, LOCLEN, LOCFLG,
        LOCHOW, LOCTIM, LOCVER);
  }
  
  /**
   * Copies this {@code LocalFileHeader} into a writable buffer. The copy is made at the
   * buffer's current position, and the position is advanced, by the size of the copy.
   *
   * @param buffer buffer to hold data for the copy.
   * @return a {@code LocalFileHeader} backed by the given buffer.
   */
  public LocalFileHeader copy(ByteBuffer buffer) {
    int size = getSize();
    LocalFileHeader view = new LocalFileHeader(buffer.slice());
    this.buffer.rewind();
    view.buffer.put(this.buffer).flip();
    buffer.position(buffer.position() + size);
    this.buffer.rewind();
    return view;
  }

  @CanIgnoreReturnValue
  private LocalFileHeader init(byte[] name, byte[] extra, int size) {
    buffer.putInt(0, SIGNATURE);
    set(LOCNAM, name.length);
    set(LOCEXT, extra.length);
    buffer.position(SIZE);
    buffer.put(name);
    if (extra.length > 0) {
      buffer.put(extra);
    }
    buffer.position(0).limit(size);
    return this;
  }

  /**
   * Flag used to mark a compressed entry, for which the size is unknown at the time of writing the
   * header.
   */
  public static final int SIZE_MASKED_FLAG = 0x8;

  /**
   * Signature of local file header.
   */
  public static final int SIGNATURE = 0x04034b50; // 67324752L

  /**
   * Size of local file header, not including variable data.
   * Also the offset of the filename.
   */
  public static final int SIZE = 30;

  /**
   * For accessing the local header signature, with the {@link
   * View#get(com.google.devtools.build.android.ziputils.View.IntFieldId)} and {@link
   * View#set(com.google.devtools.build.android.ziputils.View.IntFieldId, long)} methods.
   */
  public static final IntFieldId<LocalFileHeader> LOCSIG = new IntFieldId<>(0);

  /**
   * For accessing the "needed version" local header field, with the {@link
   * View#get(com.google.devtools.build.android.ziputils.View.ShortFieldId)} and {@link
   * View#set(com.google.devtools.build.android.ziputils.View.ShortFieldId, int)} methods.
   */
  public static final ShortFieldId<LocalFileHeader> LOCVER = new ShortFieldId<>(4);

  /**
   * For accessing the "flags" local header field, with the {@link
   * View#get(com.google.devtools.build.android.ziputils.View.ShortFieldId)} and {@link
   * View#set(com.google.devtools.build.android.ziputils.View.ShortFieldId, int)} methods.
   */
  public static final ShortFieldId<LocalFileHeader> LOCFLG = new ShortFieldId<>(6);

  /**
   * For accessing the "method" local header field, with the {@link
   * View#get(com.google.devtools.build.android.ziputils.View.ShortFieldId)} and {@link
   * View#set(com.google.devtools.build.android.ziputils.View.ShortFieldId, int)} methods.
   */
  public static final ShortFieldId<LocalFileHeader> LOCHOW = new ShortFieldId<>(8);

  /**
   * For accessing the "modified time" local header field, with the {@link
   * View#get(com.google.devtools.build.android.ziputils.View.IntFieldId)} and {@link
   * View#set(com.google.devtools.build.android.ziputils.View.IntFieldId, long)} methods.
   */
  public static final IntFieldId<LocalFileHeader> LOCTIM = new IntFieldId<>(10);

  /**
   * For accessing the "crc" local header field, with the {@link
   * View#get(com.google.devtools.build.android.ziputils.View.IntFieldId)} and {@link
   * View#set(com.google.devtools.build.android.ziputils.View.IntFieldId, long)} methods.
   */
  public static final IntFieldId<LocalFileHeader> LOCCRC = new IntFieldId<>(14);

  /**
   * For accessing the "compressed size" local header field, with the {@link
   * View#get(com.google.devtools.build.android.ziputils.View.IntFieldId)} and {@link
   * View#set(com.google.devtools.build.android.ziputils.View.IntFieldId, long)} methods.
   */
  public static final IntFieldId<LocalFileHeader> LOCSIZ = new IntFieldId<>(18);

  /**
   * For accessing the "uncompressed size" local header field, with the {@link
   * View#get(com.google.devtools.build.android.ziputils.View.IntFieldId)} and {@link
   * View#set(com.google.devtools.build.android.ziputils.View.IntFieldId, long)} methods.
   */
  public static final IntFieldId<LocalFileHeader> LOCLEN = new IntFieldId<>(22);

  /**
   * For accessing the "filename length" local header field, with the {@link
   * View#get(com.google.devtools.build.android.ziputils.View.ShortFieldId)} and {@link
   * View#set(com.google.devtools.build.android.ziputils.View.ShortFieldId, int)} methods.
   */
  public static final ShortFieldId<LocalFileHeader> LOCNAM = new ShortFieldId<>(26);

  /**
   * For accessing the "extra data length" local header field, with the {@link
   * View#get(com.google.devtools.build.android.ziputils.View.ShortFieldId)} and {@link
   * View#set(com.google.devtools.build.android.ziputils.View.ShortFieldId, int)} methods.
   */
  public static final ShortFieldId<LocalFileHeader> LOCEXT = new ShortFieldId<>(28);

  /**
   * Returns the filename for this entry.
   */
  public final String getFilename() {
    return getString(SIZE, get(LOCNAM));
  }

  /**
   * Returns the extra data for this entry.
   * @return an array of 0 or more bytes.
   */
  public final byte[] getExtraData() {
    return getBytes(SIZE + get(LOCNAM), get(LOCEXT));
  }

  /**
   * Returns the size of this header, including filename, and extra data (if any).
   */
  public final int getSize() {
    return SIZE + get(LOCNAM) + get(LOCEXT);
  }

  /**
   * Returns entry data size, based on directory entry information. For a valid zip file, this will
   * be the correct size of the entry data, or -1, if the size cannot be determined from the header.
   * Notice, if ths method returns 0, it may be because the writer of the zip file forgot to set the
   * {@link #SIZE_MASKED_FLAG}.
   *
   * @return if the {@link #LOCHOW} field is 0, returns the value of the {@link #LOCLEN} field
   *     (uncompressed size). If {@link #LOCHOW} is not 0, and the {@link #SIZE_MASKED_FLAG} is not
   *     set, returns the value of the {@link #LOCSIZ} field (compressed size). Otherwise return -1
   *     (size unknown).
   */
  public long dataSize() {
    return get(LOCHOW) == 0 ? get(LOCLEN)
        : (get(LOCFLG) & SIZE_MASKED_FLAG) == 0 ? get(LOCSIZ) : -1;
  }
}
