// 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.DirectoryEntry.CENOFF;

import com.google.common.base.Preconditions;

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.NavigableMap;
import java.util.TreeMap;

/**
 * Provides a view of a zip file's central directory. For reading, a single memory mapped view is
 * used. For writing, the central directory is stored as one or more views, each backed by a direct
 * byte buffer.
 */
public class CentralDirectory extends View<CentralDirectory> {

  // Cached map from entry name to directory entry.
  private NavigableMap<String, DirectoryEntry> mapByNameSorted;
  // Cached map from entry file offset to directory entry.
  private NavigableMap<Integer, DirectoryEntry> mapByOffsetSorted;
  // Number of directory entries in this view.
  private int count;
  // Parsed or added entries
  private List<DirectoryEntry> entries;

  /**
   * Gets the number of directory entries in this view.
   */
  public int getCount() {
    return count;
  }

  /**
   * Returns a list of directory entries, in the order they occur in the central directory.
   * This will typically also be the order of entries in the zip file, although that's not
   * guaranteed.
   */
  public List<DirectoryEntry> list() {
    return entries;
  }

  /**
   * Returns a navigable map of directory entries, by zip entry file offset.
   */
  public NavigableMap<Integer, DirectoryEntry> mapByOffset() {
    if (entries == null) {
      return null;
    }
    return mapEntriesByOffset();
  }

  /**
   * Returns a navigable map of directory entries, by entry filename.
   */
  public NavigableMap<String, DirectoryEntry> mapByFilename() {
    if (entries == null) {
      return null;
    }
    return mapEntriesByName();
  }

  /**
   * Returns a {@code CentralDirectory} of the given buffer. This may be a full or a partial
   * central directory. This method assumes ownership of the underlying buffer. Unlike most
   * "view-of" methods, this method doesn't slice the argument buffer, and rather than advancing
   * the buffer position, it sets it to 0.
   *
   * @param buffer containing data of a central directory.
   * @return a {@code CentralDirectory} of the data at the current position of the given byte
   * buffer.
   */
  public static CentralDirectory viewOf(ByteBuffer buffer) {
    buffer.position(0);
    return new CentralDirectory(buffer);
  }

  private CentralDirectory(ByteBuffer buffer) {
    super(buffer);
    count = -1;
  }

  /**
   * Parses this central directory, and maps the contained entries with {@link DirectoryEntry}s.
   *
   * @return this central directory view
   * @throws IllegalStateException if the file offset is not set prior to parsing
   */
  public CentralDirectory parse() throws IllegalStateException {
    Preconditions.checkState(fileOffset != -1, "File offset not set prior to parsing");
    count = 0;
    clearMaps();
    int relPos = 0;
    buffer.position(0);
    while (buffer.hasRemaining() && buffer.getInt(buffer.position()) == DirectoryEntry.SIGNATURE) {
      count++;
      DirectoryEntry entry = DirectoryEntry.viewOf(buffer).at(fileOffset + relPos);
      entries.add(entry);
      relPos += entry.getSize();
      buffer.position(relPos);
    }
    return this;
  }

  /**
   * Creates a new directory entry for output. The given entry is copied into the buffer of this
   * central directory, and a view of the copied data is returned.
   *
   * @param entry prototype directory entry, typically an entry read from another zip file, for
   * an entry being copied.
   * @return a directory entry view of the copied entry.
   */
  public DirectoryEntry nextEntry(DirectoryEntry entry) {
    DirectoryEntry clone = entry.copy(buffer);
    if (count == -1) {
      clearMaps();
      count = 1;
    } else {
      count++;
    }
    entries.add(clone);
    return clone;
  }

  private NavigableMap<String, DirectoryEntry> mapEntriesByName() {
    if (mapByNameSorted == null) {
      mapByNameSorted = new TreeMap<>();
      for (DirectoryEntry entry : entries) {
        mapByNameSorted.put(entry.getFilename(), entry);
      }
    }
    return mapByNameSorted;
  }

  private NavigableMap<Integer, DirectoryEntry> mapEntriesByOffset() {
    if (mapByOffsetSorted == null) {
      mapByOffsetSorted = new TreeMap<>();
      for (DirectoryEntry entry : entries) {
        mapByOffsetSorted.put(entry.get(CENOFF), entry);
      }
    }
    return mapByOffsetSorted;
  }

  private void clearMaps() {
    entries = new ArrayList<>();
    mapByOffsetSorted = null;
    mapByNameSorted = null;
  }
}
