"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BufferDisk = exports.FileDisk = exports.Disk = exports.withOpenFile = exports.DiskStream = exports.DiskChunk = exports.DiscardDiskChunk = exports.BufferDiskChunk = void 0;
const fs_1 = require("fs");
const stream_1 = require("stream");
const diskchunk_1 = require("./diskchunk");
Object.defineProperty(exports, "BufferDiskChunk", { enumerable: true, get: function () { return diskchunk_1.BufferDiskChunk; } });
Object.defineProperty(exports, "DiscardDiskChunk", { enumerable: true, get: function () { return diskchunk_1.DiscardDiskChunk; } });
Object.defineProperty(exports, "DiskChunk", { enumerable: true, get: function () { return diskchunk_1.DiskChunk; } });
const interval_intersection_1 = require("./interval-intersection");
const mapped_ranges_1 = require("./mapped-ranges");
const MIN_HIGH_WATER_MARK = 16;
const DEFAULT_HIGH_WATER_MARK = 16384;
class DiskStream extends stream_1.Readable {
    constructor(disk, capacity, highWaterMark, position) {
        super({ highWaterMark: Math.max(highWaterMark, MIN_HIGH_WATER_MARK) });
        this.disk = disk;
        this.capacity = capacity;
        this.position = position;
        this.isReading = false;
    }
    async __read(size) {
        if (this.isReading) {
            // We're already reading, return.
            return;
        }
        this.isReading = true;
        while (this.isReading) {
            // Don't read out of bounds
            const length = Math.min(size, this.capacity - this.position);
            if (length <= 0) {
                // Nothing left to read: push null to signal end of stream.
                this.isReading = this.push(null);
                return;
            }
            let bytesRead;
            let buffer;
            try {
                ({ bytesRead, buffer } = await this.disk.read(Buffer.allocUnsafe(length), 0, length, this.position));
            }
            catch (err) {
                this.emit('error', err);
                return;
            }
            this.position += bytesRead;
            // this.push() returns true if we need to continue reading.
            this.isReading = this.push(buffer);
        }
    }
    _read(size) {
        this.__read(size);
    }
}
exports.DiskStream = DiskStream;
class DiskTransformStream extends stream_1.Transform {
    constructor(disk) {
        super();
        this.disk = disk;
        // Adds the recorded writes to the original disk stream.
        this.position = 0;
        this.chunks = this.getKnownChunks();
        this.currentChunk = this.chunks.next().value;
    }
    *getKnownChunks() {
        for (const chunk of this.disk.knownChunks) {
            if (chunk instanceof diskchunk_1.BufferDiskChunk) {
                yield chunk;
            }
        }
    }
    _transform(chunk, _enc, cb) {
        const start = this.position;
        const end = start + chunk.length - 1;
        const interval = [start, end];
        while (this.currentChunk) {
            if (interval_intersection_1.intervalIntersection(this.currentChunk.interval(), interval)) {
                const buf = this.currentChunk.data();
                const startShift = this.currentChunk.start - start;
                const endShift = this.currentChunk.end - end;
                const sourceStart = startShift < 0 ? -startShift : 0;
                const sourceEnd = buf.length - Math.max(endShift, 0);
                const targetStart = Math.max(startShift, 0);
                buf.copy(chunk, targetStart, sourceStart, sourceEnd);
            }
            if (this.currentChunk.end > end) {
                break;
            }
            else {
                this.currentChunk = this.chunks.next().value;
            }
        }
        this.push(chunk);
        this.position = end + 1;
        cb();
    }
}
async function withOpenFile(path, flags, fn) {
    // Opens a file and closes it when you're done using it.
    // Arguments are the same that for `fs.promises.open()`
    // Example:
    // await withOpenFile('/some/path', 'r+', async (handle) => {
    //   doSomething(handle);
    // });
    const handle = await fs_1.promises.open(path, flags);
    try {
        return await fn(handle);
    }
    finally {
        await handle.close();
    }
}
exports.withOpenFile = withOpenFile;
class Disk {
    constructor(readOnly = false, recordWrites = false, recordReads = false, discardIsZero = true) {
        this.readOnly = readOnly;
        this.recordWrites = recordWrites;
        this.recordReads = recordReads;
        this.discardIsZero = discardIsZero;
        // Subclasses need to implement:
        // * _getCapacity(): Promise<Number>
        // * _read(buffer, bufferOffset, length, fileOffset): Promise<{ bytesRead: Number, buffer: Buffer }>
        // * _write(buffer, bufferOffset, length, fileOffset): Promise<{ bytesWritten: Number, buffer: Buffer }> [only for writable disks]
        // * _flush(): Promise<void> [only for writable disks]
        //
        // Users of instances of subclasses can use:
        // * getCapacity(): Promise<Number>
        // * read(buffer, bufferOffset, length, fileOffset): Promise<{ bytesRead: Number, buffer: Buffer }>
        // * write(buffer, bufferOffset, length, fileOffset): Promise<{ bytesWritten: Number, buffer: Buffer }>
        // * flush(): Promise<void>
        // * discard(offset, length): Promise<void>
        // * getStream([position, [length, [highWaterMark]]]): Promise<stream.Readable>
        //   * position: start reading from this offset (defaults to zero)
        //   * length: read that amount of bytes (defaults to (disk capacity - position))
        //   * highWaterMark: size of chunks that will be read (default 16384, minimum 16)
        this.knownChunks = []; // sorted list of non overlapping DiskChunks
        this.capacity = null;
    }
    getTransformStream() {
        // Returns a Transform that adds the recorded writes to the original image stream.
        return new DiskTransformStream(this);
    }
    async read(buffer, _bufferOffset, length, fileOffset) {
        const plan = this.createReadPlan(fileOffset, length);
        return await this.readAccordingToPlan(buffer, plan);
    }
    async write(buffer, bufferOffset, length, fileOffset) {
        if (this.recordWrites) {
            const chunk = new diskchunk_1.BufferDiskChunk(buffer.slice(bufferOffset, bufferOffset + length), fileOffset);
            await this.insertDiskChunk(chunk);
        }
        else {
            // Special case: we do not record writes but we may have recorded
            // some discards. We want to remove any discard overlapping this
            // write.
            // In order to do this we do as if we were inserting a chunk: this
            // will slice existing discards in this area if there are any.
            const chunk = new diskchunk_1.DiscardDiskChunk(fileOffset, length);
            // The `false` below means "don't insert the chunk into knownChunks"
            await this.insertDiskChunk(chunk, false);
        }
        if (this.readOnly) {
            return { bytesWritten: length, buffer };
        }
        else {
            return await this._write(buffer, bufferOffset, length, fileOffset);
        }
    }
    async flush() {
        if (!this.readOnly) {
            return await this._flush();
        }
    }
    async discard(offset, length) {
        await this.insertDiskChunk(new diskchunk_1.DiscardDiskChunk(offset, length));
    }
    async getCapacity() {
        if (this.capacity === null) {
            this.capacity = await this._getCapacity();
        }
        return this.capacity;
    }
    async getStream(position = 0, length = null, highWaterMark = DEFAULT_HIGH_WATER_MARK) {
        let end = await this.getCapacity();
        if (length !== null) {
            end = Math.min(position + length, end);
        }
        return new DiskStream(this, end, highWaterMark, position);
    }
    getDiscardedChunks() {
        return this.knownChunks.filter((chunk) => {
            return chunk instanceof diskchunk_1.DiscardDiskChunk;
        });
    }
    async getRanges(blockSize) {
        return Array.from(await mapped_ranges_1.getRanges(this, blockSize));
    }
    async insertDiskChunk(chunk, insert = true) {
        const capacity = await this.getCapacity();
        if (chunk.start < 0 || chunk.end > capacity) {
            // Invalid chunk
            return;
        }
        let insertAt = 0;
        for (let i = 0; i < this.knownChunks.length; i++) {
            const other = this.knownChunks[i];
            if (other.start > chunk.end) {
                break;
            }
            if (other.start < chunk.start) {
                insertAt = i + 1;
            }
            else {
                insertAt = i;
            }
            if (!chunk.intersects(other)) {
                continue;
            }
            else if (other.includedIn(chunk)) {
                // Delete other
                this.knownChunks.splice(i, 1);
                i--;
            }
            else {
                // Cut other
                const newChunks = other.cut(chunk);
                this.knownChunks.splice(i, 1, ...newChunks);
                i += newChunks.length - 1;
            }
        }
        if (insert) {
            this.knownChunks.splice(insertAt, 0, chunk);
        }
    }
    createReadPlan(offset, length) {
        const end = offset + length - 1;
        const interval = [offset, end];
        let chunks = this.knownChunks;
        if (!this.discardIsZero) {
            chunks = chunks.filter((c) => !(c instanceof diskchunk_1.DiscardDiskChunk));
        }
        const intersections = [];
        chunks.forEach((c) => {
            const inter = interval_intersection_1.intervalIntersection(interval, c.interval());
            if (inter !== null) {
                intersections.push(c.slice(inter[0], inter[1]));
            }
        });
        if (intersections.length === 0) {
            return [interval];
        }
        const readPlan = [];
        let chunk;
        for (chunk of intersections) {
            if (offset < chunk.start) {
                readPlan.push([offset, chunk.start - 1]);
            }
            readPlan.push(chunk);
            offset = chunk.end + 1;
        }
        if (chunk && end > chunk.end) {
            readPlan.push([chunk.end + 1, end]);
        }
        return readPlan;
    }
    async readAccordingToPlan(buffer, plan) {
        let offset = 0;
        for (const entry of plan) {
            if (entry instanceof diskchunk_1.DiskChunk) {
                const data = entry.data();
                const length = Math.min(data.length, buffer.length - offset);
                data.copy(buffer, offset, 0, length);
                offset += length;
            }
            else {
                const length = entry[1] - entry[0] + 1;
                await this._read(buffer, offset, length, entry[0]);
                if (this.recordReads) {
                    const chunk = new diskchunk_1.BufferDiskChunk(Buffer.from(buffer.slice(offset, offset + length)), entry[0]);
                    await this.insertDiskChunk(chunk);
                }
                offset += length;
            }
        }
        return { bytesRead: offset, buffer };
    }
}
exports.Disk = Disk;
class FileDisk extends Disk {
    constructor(handle, readOnly = false, recordWrites = false, recordReads = false, discardIsZero = true) {
        super(readOnly, recordWrites, recordReads, discardIsZero);
        this.handle = handle;
    }
    async _getCapacity() {
        const stats = await this.handle.stat();
        return stats.size;
    }
    async _read(buffer, bufferOffset, length, fileOffset) {
        return await this.handle.read(buffer, bufferOffset, length, fileOffset);
    }
    async _write(buffer, bufferOffset, length, fileOffset) {
        return await this.handle.write(buffer, bufferOffset, length, fileOffset);
    }
    async _flush() {
        await this.handle.datasync();
    }
}
exports.FileDisk = FileDisk;
class BufferDisk extends Disk {
    constructor(buffer, readOnly = false, recordWrites = false, recordReads = false, discardIsZero = true) {
        super(readOnly, recordWrites, recordReads, discardIsZero);
        this.buffer = buffer;
    }
    async _getCapacity() {
        return this.buffer.length;
    }
    async _read(buffer, bufferOffset, length, fileOffset) {
        const bytesRead = this.buffer.copy(buffer, bufferOffset, fileOffset, fileOffset + length);
        return { buffer, bytesRead };
    }
    async _write(buffer, bufferOffset, length, fileOffset) {
        const bytesWritten = buffer.copy(this.buffer, fileOffset, bufferOffset, bufferOffset + length);
        return { buffer, bytesWritten };
    }
    async _flush() {
        // Nothing to do to flush a BufferDisk
    }
}
exports.BufferDisk = BufferDisk;
//# sourceMappingURL=index.js.map