"use strict";
const zlib = require("zlib");
const constants = require("./constants");
const CrcStream = require("./crc");
const ChunkStream = require("./chunk-stream");
const Filter = require("./filter");
const colorTypeToBppMap = {
    0: 1,
    2: 3,
    3: 1,
    4: 2,
    6: 4
};
class Parser extends ChunkStream {
    constructor(option) {
        super();
        this._option = option;
        option.checkCRC = option.checkCRC !== false;
        this._hasIHDR = false;
        this._hasIEND = false;
        this._inflate = null;
        this._filter = null;
        this._crc = null;
        // input flags/metadata
        this._palette = [];
        this._colorType = 0;
        this._chunks = {};
        this._chunks[constants.TYPE_IHDR] = this._handleIHDR.bind(this);
        this._chunks[constants.TYPE_IEND] = this._handleIEND.bind(this);
        this._chunks[constants.TYPE_IDAT] = this._handleIDAT.bind(this);
        this._chunks[constants.TYPE_PLTE] = this._handlePLTE.bind(this);
        this._chunks[constants.TYPE_tRNS] = this._handleTRNS.bind(this);
        this._chunks[constants.TYPE_gAMA] = this._handleGAMA.bind(this);
        this.writable = true;
        this.on("error", this._handleError.bind(this));
        this._handleSignature();
    }
    _handleError() {
        this.writable = false;
        this.destroy();
    }
    _handleSignature() {
        this.read(constants.PNG_SIGNATURE.length, this._parseSignature.bind(this));
    }
    _parseSignature(data) {
        const signature = constants.PNG_SIGNATURE;
        for (let i = 0; i < signature.length; i++) {
            if (data[i] !== signature[i]) {
                this.emit("error", new Error("Invalid file signature"));
                return;
            }
        }
        this.read(8, this._parseChunkBegin.bind(this));
    }
    _parseChunkBegin(data) {
        // chunk content length
        const length = data.readUInt32BE(0);
        // chunk type
        const type = data.readUInt32BE(4);
        let name = "";
        for (let i = 4; i < 8; i++) {
            name += String.fromCharCode(data[i]);
        }
        // console.log("chunk ", name, length);
        // chunk flags
        const ancillary = !!(data[4] & 0x20); // or critical
        //const priv = !!(data[5] & 0x20);  // or public
        //const safeToCopy = !!(data[7] & 0x20);  // or unsafe
        if (!this._hasIHDR && type !== constants.TYPE_IHDR) {
            this.emit("error", new Error("Expected IHDR on beginning"));
            return;
        }
        this._crc = new CrcStream();
        this._crc.write(Buffer.from(name));
        if (this._chunks[type]) {
            return this._chunks[type](length);
        }
        else if (!ancillary) {
            this.emit("error", new Error("Unsupported critical chunk type " + name));
            return;
        }
        else {
            this.read(length + 4, this._skipChunk.bind(this));
        }
    }
    _skipChunk(data) {
        this.read(8, this._parseChunkBegin.bind(this));
    }
    _handleChunkEnd() {
        this.read(4, this._parseChunkEnd.bind(this));
    }
    _parseChunkEnd(data) {
        const fileCrc = data.readInt32BE(0);
        const calcCrc = this._crc.crc32;
        // check CRC
        if (this._option.checkCRC && calcCrc !== fileCrc) {
            this.emit("error", new Error("Crc error"));
            return;
        }
        if (this._hasIEND) {
            this.destroySoon();
        }
        else {
            this.read(8, this._parseChunkBegin.bind(this));
        }
    }
    _handleIHDR(length) {
        this.read(length, this._parseIHDR.bind(this));
    }
    _parseIHDR(data) {
        this._crc.write(data);
        const width = data.readUInt32BE(0);
        const height = data.readUInt32BE(4);
        const depth = data[8];
        const colorType = data[9]; // bits: 1 palette, 2 color, 4 alpha
        const compr = data[10];
        const filter = data[11];
        const interlace = data[12];
        if (depth !== 8) {
            this.emit("error", new Error("Unsupported bit depth " + depth));
            return;
        }
        if (!(colorType in colorTypeToBppMap)) {
            this.emit("error", new Error("Unsupported color type"));
            return;
        }
        if (compr !== 0) {
            this.emit("error", new Error("Unsupported compression method"));
            return;
        }
        if (filter !== 0) {
            this.emit("error", new Error("Unsupported filter method"));
            return;
        }
        if (interlace !== 0) {
            this.emit("error", new Error("Unsupported interlace method"));
            return;
        }
        this._colorType = colorType;
        this._data = Buffer.alloc(width * height * 4);
        this._filter = new Filter(width, height, colorTypeToBppMap[this._colorType], this._data, this._option);
        this._hasIHDR = true;
        this.emit("metadata", {
            width: width,
            height: height,
            palette: !!(colorType & constants.COLOR_PALETTE),
            color: !!(colorType & constants.COLOR_COLOR),
            alpha: !!(colorType & constants.COLOR_ALPHA),
            data: this._data
        });
        this._handleChunkEnd();
    }
    _handlePLTE(length) {
        this.read(length, this._parsePLTE.bind(this));
    }
    _parsePLTE(data) {
        this._crc.write(data);
        const entries = Math.floor(data.length / 3);
        for (let i = 0; i < entries; i++) {
            this._palette.push([
                data.readUInt8(i * 3),
                data.readUInt8(i * 3 + 1),
                data.readUInt8(i * 3 + 2),
                0xff
            ]);
        }
        this._handleChunkEnd();
    }
    _handleTRNS(length) {
        this.read(length, this._parseTRNS.bind(this));
    }
    _parseTRNS(data) {
        this._crc.write(data);
        // palette
        if (this._colorType === 3) {
            if (this._palette.length === 0) {
                this.emit("error", new Error("Transparency chunk must be after palette"));
                return;
            }
            if (data.length > this._palette.length) {
                this.emit("error", new Error("More transparent colors than palette size"));
                return;
            }
            for (let i = 0; i < this._palette.length; i++) {
                this._palette[i][3] = i < data.length ? data.readUInt8(i) : 0xff;
            }
        }
        // for colorType 0 (grayscale) and 2 (rgb)
        // there might be one gray/color defined as transparent
        this._handleChunkEnd();
    }
    _handleGAMA(length) {
        this.read(length, this._parseGAMA.bind(this));
    }
    _parseGAMA(data) {
        this._crc.write(data);
        this.emit("gamma", data.readUInt32BE(0) / 100000);
        this._handleChunkEnd();
    }
    _handleIDAT(length) {
        this.read(-length, this._parseIDAT.bind(this, length));
    }
    _parseIDAT(length, data) {
        this._crc.write(data);
        if (this._colorType === 3 && this._palette.length === 0) {
            throw new Error("Expected palette not found");
        }
        if (!this._inflate) {
            this._inflate = zlib.createInflate();
            this._inflate.on("error", this.emit.bind(this, "error"));
            this._filter.on("complete", this._reverseFiltered.bind(this));
            this._inflate.pipe(this._filter);
        }
        this._inflate.write(data);
        length -= data.length;
        if (length > 0) {
            this._handleIDAT(length);
        }
        else {
            this._handleChunkEnd();
        }
    }
    _handleIEND(length) {
        this.read(length, this._parseIEND.bind(this));
    }
    _parseIEND(data) {
        this._crc.write(data);
        // no more data to inflate
        this._inflate.end();
        this._hasIEND = true;
        this._handleChunkEnd();
    }
    _reverseFiltered(data, width, height) {
        if (this._colorType === 3) { // paletted
            let i, y, x, pxRowPos, pxPos, color;
            // use values from palette
            const pxLineLength = width << 2;
            for (y = 0; y < height; y++) {
                pxRowPos = y * pxLineLength;
                for (x = 0; x < width; x++) {
                    pxPos = pxRowPos + (x << 2),
                        color = this._palette[data[pxPos]];
                    for (i = 0; i < 4; i++) {
                        data[pxPos + i] = color[i];
                    }
                }
            }
        }
        this.emit("parsed", data);
    }
}
module.exports = Parser;
//# sourceMappingURL=parser.js.map