
var gzip = gzip || {};
var zip = zip || require('./zip');

gzip.Archive = class {

    static open(data) {
        const stream = data instanceof Uint8Array ? new gzip.BinaryReader(data) : data;
        const signature = [ 0x1f, 0x8b ];
        if (stream.length > 18 && stream.peek(2).every((value, index) => value === signature[index])) {
            return new gzip.Archive(stream);
        }
        return null;
    }

    constructor(stream) {
        const position = stream.position;
        const entry = new gzip.Entry(stream);
        this._entries = new Map([ [ entry.name, entry.stream ] ]);
        stream.seek(position);
    }

    get entries() {
        return this._entries;
    }
};

gzip.Entry = class {

    constructor(stream) {
        const signature = [ 0x1f, 0x8b ];
        if (stream.position + 2 > stream.length ||
            !stream.read(2).every((value, index) => value === signature[index])) {
            throw new gzip.Error('Invalid gzip signature.');
        }
        const string = () => {
            let content = '';
            while (stream.position < stream.length) {
                const value = stream.byte();
                if (value === 0x00) {
                    break;
                }
                content += String.fromCharCode(value);
            }
            return content;
        };
        const reader = new gzip.BinaryReader(stream.read(8));
        const compressionMethod = reader.byte();
        if (compressionMethod != 8) {
            throw new gzip.Error("Invalid compression method '" + compressionMethod.toString() + "'.");
        }
        const flags = reader.byte();
        reader.uint32(); // MTIME
        reader.byte(); // XFL
        reader.byte(); // OS
        if ((flags & 4) != 0) { // FEXTRA
            const xlen = stream.byte() | (stream.byte() << 8);
            stream.skip(xlen);
        }
        this._name = (flags & 8) != 0 ? string() : ''; // FNAME
        if ((flags & 16) != 0) { // FCOMMENT
            string();
        }
        if ((flags & 1) != 0) { // FHCRC
            stream.skip(2);
        }
        this._stream = new gzip.InflaterStream(stream);
    }

    get name() {
        return this._name;
    }

    get stream() {
        return this._stream;
    }
};

gzip.InflaterStream = class {

    constructor(stream) {
        this._stream = stream.stream(stream.length - stream.position - 8);
        const reader = new gzip.BinaryReader(stream.read(8));
        reader.uint32(); // CRC32
        this._length = reader.uint32(); // ISIZE
        this._position = 0;
    }

    get position() {
        return this._position;
    }

    get length() {
        return this._length;
    }

    seek(position) {
        if (this._buffer === undefined) {
            this._inflate();
        }
        this._position = position >= 0 ? position : this._length + position;
    }

    skip(offset) {
        if (this._buffer === undefined) {
            this._inflate();
        }
        this._position += offset;
    }

    stream(length) {
        return new gzip.BinaryReader(this.read(length));
    }

    peek(length) {
        const position = this._position;
        length = length !== undefined ? length : this._length - this._position;
        this.skip(length);
        const end = this._position;
        this.seek(position);
        if (position === 0 && length === this._length) {
            return this._buffer;
        }
        return this._buffer.subarray(position, end);
    }

    read(length) {
        const position = this._position;
        length = length !== undefined ? length : this._length - this._position;
        this.skip(length);
        if (position === 0 && length === this._length) {
            return this._buffer;
        }
        return this._buffer.subarray(position, this._position);
    }

    byte() {
        const position = this._position;
        this.skip(1);
        return this._buffer[position];
    }

    _inflate() {
        if (this._buffer === undefined) {
            const buffer = this._stream.peek();
            this._buffer = new zip.Inflater().inflateRaw(buffer, this._length);
            delete this._stream;
        }
    }
};

gzip.BinaryReader = class {

    constructor(buffer) {
        this._buffer = buffer;
        this._length = buffer.length;
        this._position = 0;
        this._view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
    }

    get position() {
        return this._position;
    }

    get length() {
        return this._length;
    }

    create(buffer) {
        return new gzip.BinaryReader(buffer);
    }

    stream(length) {
        return this.create(this.read(length));
    }

    seek(position) {
        this._position = position >= 0 ? position : this._length + position;
    }

    skip(offset) {
        this._position += offset;
    }

    peek(length) {
        if (this._position === 0 && length === undefined) {
            return this._buffer;
        }
        const position = this._position;
        this.skip(length !== undefined ? length : this._length - this._position);
        const end = this._position;
        this.seek(position);
        return this._buffer.subarray(position, end);
    }

    read(length) {
        if (this._position === 0 && length === undefined) {
            this._position = this._length;
            return this._buffer;
        }
        const position = this._position;
        this.skip(length !== undefined ? length : this._length - this._position);
        return this._buffer.subarray(position, this._position);
    }

    byte() {
        const position = this._position;
        this.skip(1);
        return this._buffer[position];
    }

    uint16() {
        const position = this._position;
        this.skip(2);
        return this._view.getUint16(position, true);
    }

    uint32() {
        const position = this._position;
        this.skip(4);
        return this._view.getUint32(position, true);
    }
};

gzip.Error = class extends Error {
    constructor(message) {
        super(message);
        this.name = 'Gzip Error';
    }
};

if (typeof module !== 'undefined' && typeof module.exports === 'object') {
    module.exports.Archive = gzip.Archive;
}