'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }

var buffer = require('buffer');
require('stream');
var pad = _interopDefault(require('pad'));
var pako = _interopDefault(require('pako'));
var crypto = _interopDefault(require('crypto'));
var models_js = require('./models.js');
var managers_js = require('./managers.js');
var through2 = _interopDefault(require('through2'));
var listpack = _interopDefault(require('git-list-pack'));
var peek = _interopDefault(require('buffer-peek-stream'));
var applyDelta = _interopDefault(require('git-apply-delta'));
var marky = _interopDefault(require('marky'));
require('./utils.js');

/**
 * Read and/or write to the git config file(s)
 * @param {GitRepo} repo - A {@link Git} object matching `{gitdir, fs}`
 * @param {Object} args - Arguments object
 * @param {string} args.path -  The key of the git config entry.
 * @param {string} [args.value] - A value to store at that path.
 * @returns {Promise<any>} - Resolves with the config value
 *
 * If no `value` is provided, it does a read.
 * If a `value` is provided, it does a write.
 *
 * @example
 * let repo = new Git({fs, dir: '.'})
 *
 * // Write config value
 * await config(repo, {
 *   path: 'user.name',
 *   value: 'Mr. Test'
 * })
 *
 * // Read config value
 * let value = await config(repo, {
 *   path: 'user.name'
 * })
 */

const types = {
  commit: 0b0010000,
  tree: 0b0100000,
  blob: 0b0110000,
  tag: 0b1000000

  /**
   * Push a branch
   *
   * @param {GitRepo} repo - A {@link Git} object matching `{gitdir, fs}`
   * @param {Object} args - Arguments object
   * @param {integer} [args.depth=0] - Determines how much of the git repository's history to retrieve. If not specified it defaults to 0 which means the entire repo history.
   * @param {string} [args.ref=undefined] - Which branch to push. By default this is the currently checked out branch of the repository.
   * @param {string} [args.authUsername=undefined] - The username to use with Basic Auth
   * @param {string} [args.authPassword=undefined] - The password to use with Basic Auth
   * @param {string} [args.url=undefined] - The URL of the remote git server. The default is the value set in the git config for that remote.
   * @param {string} [args.remote='origin'] - If URL is not specified, determines which remote to use.
   * @returns {Promise<void>} - Resolves successfully when push completes
   *
   * @example
   * let repo = new Git({fs, dir: '.'})
   * await push(repo, {
   *   remote: 'origin',
   *   ref: 'master',
   *   authUsername: process.env.GITHUB_TOKEN,
   *   authPassword: process.env.GITHUB_TOKEN
   * })
   */
};

/**
 * @ignore
 */
async function listCommits({ gitdir, fs: _fs }, { start, finish }) {
  const fs = new models_js.FileSystem(_fs);
  let startingSet = new Set();
  let finishingSet = new Set();
  for (let ref of start) {
    startingSet.add((await managers_js.GitRefManager.resolve({ fs, gitdir, ref })));
  }
  for (let ref of finish) {
    // We may not have these refs locally so we must try/catch
    try {
      let oid = await managers_js.GitRefManager.resolve({ fs, gitdir, ref });
      finishingSet.add(oid);
    } catch (err) {}
  }
  let visited /*: Set<string> */ = new Set();

  // Because git commits are named by their hash, there is no
  // way to construct a cycle. Therefore we won't worry about
  // setting a default recursion limit.
  async function walk(oid) {
    visited.add(oid);
    let { type, object } = await managers_js.GitObjectManager.read({ fs, gitdir, oid });
    if (type !== 'commit') {
      throw new Error(`Expected type commit but type is ${type}`);
    }
    let commit = models_js.GitCommit.from(object);
    let parents = commit.headers().parent;
    for (oid of parents) {
      if (!finishingSet.has(oid) && !visited.has(oid)) {
        await walk(oid);
      }
    }
  }

  // Let's go walking!
  for (let oid of startingSet) {
    await walk(oid);
  }
  return visited;
}

/**
 * @ignore
 */
async function listObjects({ gitdir, fs: _fs }, { oids /*: { oids: Set<string> } */
}) {
  const fs = new models_js.FileSystem(_fs);
  let visited /*: Set<string> */ = new Set();

  // We don't do the purest simplest recursion, because we can
  // avoid reading Blob objects entirely since the Tree objects
  // tell us which oids are Blobs and which are Trees.
  async function walk(oid) {
    visited.add(oid);
    let { type, object } = await managers_js.GitObjectManager.read({ fs, gitdir, oid });
    if (type === 'commit') {
      let commit = models_js.GitCommit.from(object);
      let tree = commit.headers().tree;
      await walk(tree);
    } else if (type === 'tree') {
      let tree = models_js.GitTree.from(object);
      for (let entry /*: TreeEntry */ of tree) {
        visited.add(entry.oid);
        // only recurse for trees
        if (entry.type === 'tree') {
          await walk(entry.oid);
        }
      }
    }
  }

  // Let's go walking!
  for (let oid of oids) {
    await walk(oid);
  }
  return visited;
}

/**
 * @ignore
 */
async function pack({ gitdir, fs: _fs }, { oids, outputStream }) {
  const fs = new models_js.FileSystem(_fs);
  let hash = crypto.createHash('sha1');
  function write(chunk, enc) {
    outputStream.write(chunk, enc);
    hash.update(chunk, enc);
  }
  function writeObject({ stype, object }) {
    let lastFour, multibyte, length;
    // Object type is encoded in bits 654
    let type = types[stype];
    if (type === undefined) throw new Error('Unrecognized type: ' + stype);
    // The length encoding get complicated.
    length = object.length;
    // Whether the next byte is part of the variable-length encoded number
    // is encoded in bit 7
    multibyte = length > 0b1111 ? 0b10000000 : 0b0;
    // Last four bits of length is encoded in bits 3210
    lastFour = length & 0b1111;
    // Discard those bits
    length = length >>> 4;
    // The first byte is then (1-bit multibyte?), (3-bit type), (4-bit least sig 4-bits of length)
    let byte = (multibyte | type | lastFour).toString(16);
    write(byte, 'hex');
    // Now we keep chopping away at length 7-bits at a time until its zero,
    // writing out the bytes in what amounts to little-endian order.
    while (multibyte) {
      multibyte = length > 0b01111111 ? 0b10000000 : 0b0;
      byte = multibyte | length & 0b01111111;
      write(pad(2, byte.toString(16), '0'), 'hex');
      length = length >>> 7;
    }
    // Lastly, we can compress and write the object.
    write(buffer.Buffer.from(pako.deflate(object)));
  }

  write('PACK');
  write('00000002', 'hex');
  // Write a 4 byte (32-bit) int
  write(pad(8, oids.length.toString(16), '0'), 'hex');
  for (let oid of oids) {
    let { type, object } = await managers_js.GitObjectManager.read({ fs, gitdir, oid });
    writeObject({ write, object, stype: type });
  }
  // Write SHA1 checksum
  let digest = hash.digest();
  outputStream.end(digest);
  return outputStream;
}

/**
 * Fetch commits
 *
 * @param {GitRepo} repo - A {@link Git} object matching `{workdir, gitdir, fs}`
 * @param {Object} args - Arguments object
 * @param {string} [args.url=undefined] - The URL of the remote git server. The default is the value set in the git config for that remote.
 * @param {string} [args.remote='origin'] - If `url` is not specified, determines which remote to use.
 * @param {string} [args.ref=undefined] - Which branch to fetch from. By default this is the currently checked out branch.
 * @param {string} [args.authUsername=undefined] - The username to use with Basic Auth
 * @param {string} [args.authPassword=undefined] - The password to use with Basic Auth
 * @param {integer} [args.depth=undefined] - Determines how much of the git repository's history to retrieve.
 * @param {Date} [args.since=undefined] - Only fetch commits created after the given date. Mutually exclusive with `depth`.
 * @param {string[]} [args.exclude=[]] - A list of branches or tags. Instructs the remote server not to send us any commits reachable from these refs.
 * @param {boolean} [args.relative=false] - Changes the meaning of `depth` to be measured from the current shallow depth rather than from the branch tip.
 * @param {Function} [args.onprogress=undefined] - Callback to receive [ProgressEvent](https://developer.mozilla.org/en-US/docs/Web/API/ProgressEvent)s for the operation.
 * @returns {Promise<void>} - Resolves successfully when clone completes
 *
 * @example
 * let repo = new Git({fs, dir: '.'})
 * await fetch(repo, {
 *   url: 'https://cors-buster-jfpactjnem.now.sh/github.com/wmhilton/isomorphic-git',
 *   depth: 1
 * })
 */


const types$1 = {
  1: 'commit',
  2: 'tree',
  3: 'blob',
  4: 'tag',
  6: 'ofs-delta',
  7: 'ref-delta'
};

function parseVarInt(buffer$$1 /*: Buffer */) {
  let n = 0;
  for (var i = 0; i < buffer$$1.byteLength; i++) {
    n = (buffer$$1[i] & 0b01111111) + (n << 7);
    if ((buffer$$1[i] & 0b10000000) === 0) {
      if (i !== buffer$$1.byteLength - 1) throw new Error('Invalid varint buffer');
      return n;
    }
  }
  throw new Error('Invalid varint buffer');
}

/**
 * @ignore
 * @param {GitRepo} repo - A {@link Git} object matching `{gitdir, fs}`
 * @param {Object} args - Arguments object
 * @param {ReadableStream} args.inputStream
 * @param {Function} args.onprogress
 */
async function unpack({ gitdir, fs: _fs }, { inputStream, onprogress }) {
  const fs = new models_js.FileSystem(_fs);
  return new Promise(function (resolve, reject) {
    // Read header
    peek(inputStream, 12, (err, data, inputStream) => {
      if (err) return reject(err);
      let iden = data.slice(0, 4).toString('utf8');
      if (iden !== 'PACK') {
        throw new Error(`Packfile started with '${iden}'. Expected 'PACK'`);
      }
      let ver = data.slice(4, 8).toString('hex');
      if (ver !== '00000002') {
        throw new Error(`Unknown packfile version '${ver}'. Expected 00000002.`);
      }
      // Read a 4 byte (32-bit) int
      let numObjects = data.readInt32BE(8);
      if (onprogress !== undefined) {
        onprogress({ loaded: 0, total: numObjects, lengthComputable: true });
      }
      if (numObjects === 0) return resolve();
      // And on our merry way
      let totalTime = 0;
      let totalApplyDeltaTime = 0;
      let totalWriteFileTime = 0;
      let totalReadFileTime = 0;
      let offsetMap = new Map();
      inputStream.pipe(listpack()).pipe(through2.obj(async ({ data, type, reference, offset, num }, enc, next) => {
        type = types$1[type];
        marky.mark(`${type} #${num} ${data.length}B`);
        if (type === 'ref-delta') {
          let oid = buffer.Buffer.from(reference).toString('hex');
          try {
            marky.mark(`readFile`);
            let { object, type } = await managers_js.GitObjectManager.read({
              fs,
              gitdir,
              oid
            });
            totalReadFileTime += marky.stop(`readFile`).duration;
            marky.mark(`applyDelta`);
            let result = applyDelta(data, object);
            totalApplyDeltaTime += marky.stop(`applyDelta`).duration;
            marky.mark(`writeFile`);
            let newoid = await managers_js.GitObjectManager.write({
              fs,
              gitdir,
              type,
              object: result
            });
            totalWriteFileTime += marky.stop(`writeFile`).duration;
            // console.log(`${type} ${newoid} ref-delta ${oid}`)
            offsetMap.set(offset, newoid);
          } catch (err) {
            throw new Error(`Could not find object ${reference} ${oid} that is referenced by a ref-delta object in packfile at byte offset ${offset}.`);
          }
        } else if (type === 'ofs-delta') {
          // Note: this might be not working because offsets might not be
          // guaranteed to be on object boundaries? In which case we'd need
          // to write the packfile to disk first, I think.
          // For now I've "solved" it by simply not advertising ofs-delta as a capability
          // during the HTTP request, so Github will only send ref-deltas not ofs-deltas.
          let absoluteOffset = offset - parseVarInt(reference);
          let referenceOid = offsetMap.get(absoluteOffset);
          // console.log(`${offset} ofs-delta ${absoluteOffset} ${referenceOid}`)
          let { type, object } = await managers_js.GitObjectManager.read({
            fs,
            gitdir,
            oid: referenceOid
          });
          let result = applyDelta(data, object);
          let oid = await managers_js.GitObjectManager.write({
            fs,
            gitdir,
            type,
            object: result
          });
          // console.log(`${offset} ${type} ${oid} ofs-delta ${referenceOid}`)
          offsetMap.set(offset, oid);
        } else {
          marky.mark(`writeFile`);
          let oid = await managers_js.GitObjectManager.write({
            fs,
            gitdir,
            type,
            object: data
          });
          totalWriteFileTime += marky.stop(`writeFile`).duration;
          // console.log(`${offset} ${type} ${oid}`)
          offsetMap.set(offset, oid);
        }
        if (onprogress !== undefined) {
          onprogress({
            loaded: numObjects - num,
            total: numObjects,
            lengthComputable: true
          });
        }
        let perfentry = marky.stop(`${type} #${num} ${data.length}B`);
        totalTime += perfentry.duration;
        if (num === 0) {
          console.log(`Total time unpacking objects: ${totalTime}`);
          console.log(`Total time applying deltas: ${totalApplyDeltaTime}`);
          console.log(`Total time reading files: ${totalReadFileTime}`);
          console.log(`Total time writing files: ${totalWriteFileTime}`);
          return resolve();
        }
        next(null);
      })).on('error', reject).on('finish', resolve);
    });
  });
}

exports.listCommits = listCommits;
exports.listObjects = listObjects;
exports.pack = pack;
exports.unpack = unpack;
