// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The SFC licenses this file
// to you 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.

'use strict';

const jszip = require('jszip');
const path = require('path');

const io = require('./index');
const {InvalidArgumentError} = require('../lib/error');

/**
 * Manages a zip archive.
 */
class Zip {
  constructor() {
    /** @private @const */
    this.z_ = new jszip;

    /** @private @const {!Set<!Promise<?>>} */
    this.pendingAdds_ = new Set;
  }

  /**
   * Adds a file to this zip.
   *
   * @param {string} filePath path to the file to add.
   * @param {string=} zipPath path to the file in the zip archive, defaults
   *     to the basename of `filePath`.
   * @return {!Promise<?>} a promise that will resolve when added.
   */
  addFile(filePath, zipPath = path.basename(filePath)) {
    let add = io.read(filePath)
        .then(buffer => this.z_.file(/** @type {string} */(zipPath), buffer));
    this.pendingAdds_.add(add);
    return add.then(
        () => this.pendingAdds_.delete(add),
        (e) => {
          this.pendingAdds_.delete(add);
          throw e;
        });
  }

  /**
   * Recursively adds a directory and all of its contents to this archive.
   *
   * @param {string} dirPath path to the directory to add.
   * @param {string=} zipPath path to the folder in the archive to add the
   *     directory contents to. Defaults to the root folder.
   * @return {!Promise<?>} returns a promise that will resolve when the
   *     the operation is complete.
   */
  addDir(dirPath, zipPath = '') {
    return io.walkDir(dirPath).then(entries => {
      let archive = this.z_;
      if (zipPath) {
        archive = archive.folder(zipPath);
      }

      let files = [];
      entries.forEach(spec => {
        if (spec.dir) {
          archive.folder(spec.path);
        } else {
          files.push(
              this.addFile(
                  path.join(dirPath, spec.path),
                  path.join(zipPath, spec.path)));
        }
      });

      return Promise.all(files);
    });
  }

  /**
   * @param {string} path File path to test for within the archive.
   * @return {boolean} Whether this zip archive contains an entry with the given
   *     path.
   */
  has(path) {
    return this.z_.file(path) !== null;
  }

  /**
   * Returns the contents of the file in this zip archive with the given `path`.
   * The returned promise will be rejected with an {@link InvalidArgumentError}
   * if either `path` does not exist within the archive, or if `path` refers
   * to a directory.
   *
   * @param {string} path the path to the file whose contents to return.
   * @return {!Promise<!Buffer>} a promise that will be resolved with the file's
   *     contents as a buffer.
   */
  getFile(path) {
    let file = this.z_.file(path);
    if (!file) {
      return Promise.reject(
          new InvalidArgumentError(`No such file in zip archive: ${path}`));
    }

    if (file.dir) {
      return Promise.reject(
          new InvalidArgumentError(
              `The requested file is a directory: ${path}`));
    }

    return Promise.resolve(file.async('nodebuffer'));
  }

  /**
   * Returns the compressed data for this archive in a buffer. _This method will
   * not wait for any outstanding {@link #addFile add}
   * {@link #addDir operations} before encoding the archive._
   *
   * @param {string} compression The desired compression.
   *     Must be `STORE` (the default) or `DEFLATE`.
   * @return {!Promise<!Buffer>} a promise that will resolve with this archive
   *     as a buffer.
   */
  toBuffer(compression = 'STORE') {
    if (compression !== 'STORE' && compression !== 'DEFLATE') {
      return Promise.reject(
          new InvalidArgumentError(
              `compression must be one of {STORE, DEFLATE}, got ${compression}`));
    }
    return Promise.resolve(
        this.z_.generateAsync({compression, type: 'nodebuffer'}));
  }
}


/**
 * Asynchronously opens a zip archive.
 *
 * @param {string} path to the zip archive to load.
 * @return {!Promise<!Zip>} a promise that will resolve with the opened
 *     archive.
 */
function load(path) {
  return io.read(path).then(data => {
    let zip = new Zip;
    return zip.z_.loadAsync(data).then(() => zip);
  });
}


/**
 * Asynchronously unzips an archive file.
 *
 * @param {string} src path to the source file to unzip.
 * @param {string} dst path to the destination directory.
 * @return {!Promise<string>} a promise that will resolve with `dst` once the
 *     archive has been unzipped.
 */
function unzip(src, dst) {
  return load(src).then(zip => {
    let promisedDirs = new Map;
    let promises = [];

    zip.z_.forEach((relPath, file) => {
      let p;
      if (file.dir) {
        p = createDir(relPath);
      } else {
        let dirname = path.dirname(relPath);
        if (dirname === '.') {
          p = writeFile(relPath, file);
        } else {
          p = createDir(dirname).then(() => writeFile(relPath, file));
        }
      }
      promises.push(p);
    });

    return Promise.all(promises).then(() => dst);

    function createDir(dir) {
      let p = promisedDirs.get(dir);
      if (!p) {
        p = io.mkdirp(path.join(dst, dir));
        promisedDirs.set(dir, p);
      }
      return p;
    }

    function writeFile(relPath, file) {
      return file.async('nodebuffer')
          .then(buffer => io.write(path.join(dst, relPath), buffer));
    }
  });
}


// PUBLIC API


exports.Zip = Zip;
exports.load = load;
exports.unzip = unzip;
