"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
const fs = require("fs");
const util_1 = require("util");
const path_1 = require("path");
const stream_1 = require("stream");
const MultiStream = require("multistream");
const lstat = util_1.promisify(fs.lstat), realpath = util_1.promisify(fs.realpath), createReadStream = fs.createReadStream, stat = util_1.promisify(fs.stat);
function makeRelative(cwd, path) {
    return './' + path_1.relative(cwd, path);
}
function sortEntry(a, b) {
    return a[0] > b[0] ? 1 : -1;
}
function toStream(content) {
    const readable = new stream_1.Readable({
        read() {
            this.push(content);
            this.push(null);
        },
    });
    return readable;
}
exports.toStream = toStream;
function createFile(absoluteFileName) {
    return __awaiter(this, void 0, void 0, function* () {
        const stats = yield lstat(absoluteFileName), file = {
            size: stats.size,
            contents: '',
            absPath: absoluteFileName,
            deps: {},
        };
        if (stats.isSymbolicLink()) {
            file.size = stats.size;
            const [realPath, realStat] = yield Promise.all([
                realpath(absoluteFileName),
                stat(absoluteFileName),
            ]);
            file.realPath = realPath;
            file.realSize = realStat.size;
        }
        return file;
    });
}
class Bundle {
    constructor({ cwd } = { cwd: process.cwd() }) {
        this.size = 0;
        this.rendered = false;
        this.offset = 0;
        this.index = {};
        this.files = {};
        this.streams = [];
        this.cwd = cwd;
    }
    get list() {
        return Object.keys(this.files);
    }
    addResource(absoluteFileName, content) {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.files[absoluteFileName]) {
                return this.size;
            }
            if (typeof content === 'string' || Buffer.isBuffer(content)) {
                this.files[absoluteFileName] = {
                    size: Buffer.byteLength(content),
                    contents: content,
                    deps: {},
                    absPath: absoluteFileName,
                };
            }
            else if (content) {
                this.files[content.absPath] = content;
            }
            else {
                this.files[absoluteFileName] = {
                    absPath: absoluteFileName,
                    contents: '',
                    deps: {},
                    size: 0,
                };
                this.files[absoluteFileName] = yield createFile(absoluteFileName);
            }
            return (this.size += this.files[absoluteFileName].size);
        });
    }
    /**
     * De-dupe files by absolute path, partition by symlink/real
     * Iterate over real, add entries
     * Iterate over symlinks, add symlinks
     */
    renderIndex() {
        if (this.rendered) {
            throw new Error('Bundle index already rendered');
        }
        const files = Object.entries(this.files), realFiles = [], symLinks = [];
        for (const entry of files) {
            if (entry[1].realPath) {
                symLinks.push(entry);
            }
            else {
                realFiles.push(entry);
            }
        }
        realFiles.sort(sortEntry);
        symLinks.sort(sortEntry);
        for (const [absPath, file] of realFiles) {
            this.addEntry(absPath, file);
        }
        for (const [absPath, file] of symLinks) {
            this.addEntry(file.realPath, file);
            this.addEntry(absPath, file, file.realPath);
        }
        this.rendered = true;
        return this.index;
    }
    /**
     * Add a stream if needed and an entry with the required offset and size
     * Ensure the calling order of this method is idempotent (eg, while iterating a sorted set)
     * @param entryPath
     * @param file
     * @param useEntry
     */
    addEntry(entryPath, file, useEntry) {
        var _a;
        const existingName = useEntry && makeRelative(this.cwd, useEntry), name = makeRelative(this.cwd, entryPath), size = (_a = file.realSize) !== null && _a !== void 0 ? _a : file.size, existingEntry = this.index[existingName !== null && existingName !== void 0 ? existingName : name];
        this.index[name] = existingEntry || [this.offset, size];
        if (!existingEntry) {
            this.streams.push(() => file.contents ? toStream(file.contents) : createReadStream(file.absPath));
            this.offset += size;
        }
    }
    toStream() {
        return new MultiStream(this.streams);
    }
}
exports.Bundle = Bundle;
