var Buffer = require("buffer").Buffer;

function JSInflater(/*Buffer*/input) {

    var WSIZE = 0x8000,
        slide = new Buffer(0x10000),
        windowPos = 0,
        fixedTableList = null,
        fixedTableDist,
        fixedLookup,
        bitBuf = 0,
        bitLen = 0,
        method = -1,
        eof = false,
        copyLen = 0,
        copyDist = 0,
        tblList, tblDist, bitList, bitdist,

        inputPosition = 0,

        MASK_BITS = [0x0000, 0x0001, 0x0003, 0x0007, 0x000f, 0x001f, 0x003f, 0x007f, 0x00ff, 0x01ff, 0x03ff, 0x07ff, 0x0fff, 0x1fff, 0x3fff, 0x7fff, 0xffff],
        LENS = [3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258, 0, 0],
        LEXT = [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, 99, 99],
        DISTS = [1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577],
        DEXT = [0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13],
        BITORDER = [16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15];

    function HuffTable(clen, cnum, cval, blist, elist, lookupm) {

        this.status = 0;
        this.root = null;
        this.maxbit = 0;

        var el, f, tail,
            offsets = [],
            countTbl = [],
            sTbl = [],
            values = [],
            tentry = {extra: 0, bitcnt: 0, lbase: 0, next: null};

        tail = this.root = null;
        for(var i = 0; i < 0x11; i++)  { countTbl[i] = 0; sTbl[i] = 0; offsets[i] = 0; }
        for(i = 0; i < 0x120; i++) values[i] = 0;

        el = cnum > 256 ? clen[256] : 16;

        var pidx = -1;
        while (++pidx < cnum) countTbl[clen[pidx]]++;

        if(countTbl[0] == cnum) return;

        for(var j = 1; j <= 16; j++) if(countTbl[j] != 0) break;
        var bitLen = j;
        for(i = 16; i != 0; i--) if(countTbl[i] != 0) break;
        var maxLen = i;

        lookupm < j && (lookupm = j);

        var dCodes = 1 << j;
        for(; j < i; j++, dCodes <<= 1)
            if((dCodes -= countTbl[j]) < 0) {
                this.status = 2;
                this.maxbit = lookupm;
                return;
            }

        if((dCodes -= countTbl[i]) < 0) {
            this.status = 2;
            this.maxbit = lookupm;
            return;
        }

        countTbl[i] += dCodes;
        offsets[1] = j = 0;
        pidx = 1;
        var xp = 2;
        while(--i > 0) offsets[xp++] = (j += countTbl[pidx++]);
        pidx = 0;
        i = 0;
        do {
            (j = clen[pidx++]) && (values[offsets[j]++] = i);
        } while(++i < cnum);
        cnum = offsets[maxLen];
        offsets[0] = i = 0;
        pidx = 0;

        var level = -1,
            w = sTbl[0] = 0,
            cnode = null,
            tblCnt = 0,
            tblStack = [];

        for(; bitLen <= maxLen; bitLen++) {
            var kccnt = countTbl[bitLen];
            while(kccnt-- > 0) {
                while(bitLen > w + sTbl[1 + level]) {
                    w += sTbl[1 + level];
                    level++;
                    tblCnt = (tblCnt = maxLen - w) > lookupm ? lookupm : tblCnt;
                    if((f = 1 << (j = bitLen - w)) > kccnt + 1) {
                        f -= kccnt + 1;
                        xp = bitLen;
                        while(++j < tblCnt) {
                            if((f <<= 1) <= countTbl[++xp]) break;
                            f -= countTbl[xp];
                        }
                    }
                    if(w + j > el && w < el) j = el - w;
                    tblCnt = 1 << j;
                    sTbl[1 + level] = j;
                    cnode = [];
                    while (cnode.length < tblCnt) cnode.push({extra: 0, bitcnt: 0, lbase: 0, next: null});
                    if (tail == null) {
                        tail = this.root = {next:null, list:null};
                    } else {
                        tail = tail.next = {next:null, list:null}
                    }
                    tail.next = null;
                    tail.list = cnode;

                    tblStack[level] = cnode;

                    if(level > 0) {
                        offsets[level] = i;
                        tentry.bitcnt = sTbl[level];
                        tentry.extra = 16 + j;
                        tentry.next = cnode;
                        j = (i & ((1 << w) - 1)) >> (w - sTbl[level]);

                        tblStack[level-1][j].extra = tentry.extra;
                        tblStack[level-1][j].bitcnt = tentry.bitcnt;
                        tblStack[level-1][j].lbase = tentry.lbase;
                        tblStack[level-1][j].next = tentry.next;
                    }
                }
                tentry.bitcnt = bitLen - w;
                if(pidx >= cnum)
                    tentry.extra = 99;
                else if(values[pidx] < cval) {
                    tentry.extra = (values[pidx] < 256 ? 16 : 15);
                    tentry.lbase = values[pidx++];
                } else {
                    tentry.extra = elist[values[pidx] - cval];
                    tentry.lbase = blist[values[pidx++] - cval];
                }

                f = 1 << (bitLen - w);
                for(j = i >> w; j < tblCnt; j += f) {
                    cnode[j].extra = tentry.extra;
                    cnode[j].bitcnt = tentry.bitcnt;
                    cnode[j].lbase = tentry.lbase;
                    cnode[j].next = tentry.next;
                }
                for(j = 1 << (bitLen - 1); (i & j) != 0; j >>= 1)
                    i ^= j;
                i ^= j;
                while((i & ((1 << w) - 1)) != offsets[level]) {
                    w -= sTbl[level];
                    level--;
                }
            }
        }

        this.maxbit = sTbl[1];
        this.status = ((dCodes != 0 && maxLen != 1) ? 1 : 0);
    }

    function addBits(n) {
        while(bitLen < n) {
            bitBuf |= input[inputPosition++] << bitLen;
            bitLen += 8;
        }
        return bitBuf;
    }

    function cutBits(n) {
        bitLen -= n;
        return bitBuf >>= n;
    }

    function maskBits(n) {
        while(bitLen < n) {
            bitBuf |= input[inputPosition++] << bitLen;
            bitLen += 8;
        }
        var res = bitBuf & MASK_BITS[n];
        bitBuf >>= n;
        bitLen -= n;
        return res;
    }

    function codes(buff, off, size) {
        var e, t;
        if(size == 0) return 0;

        var n = 0;
        for(;;) {
            t = tblList.list[addBits(bitList) & MASK_BITS[bitList]];
            e = t.extra;
            while(e > 16) {
                if(e == 99) return -1;
                cutBits(t.bitcnt);
                e -= 16;
                t = t.next[addBits(e) & MASK_BITS[e]];
                e = t.extra;
            }
            cutBits(t.bitcnt);
            if(e == 16) {
                windowPos &= WSIZE - 1;
                buff[off + n++] = slide[windowPos++] = t.lbase;
                if(n == size) return size;
                continue;
            }
            if(e == 15) break;

            copyLen = t.lbase + maskBits(e);
            t = tblDist.list[addBits(bitdist) & MASK_BITS[bitdist]];
            e = t.extra;

            while(e > 16) {
                if(e == 99) return -1;
                cutBits(t.bitcnt);
                e -= 16;
                t = t.next[addBits(e) & MASK_BITS[e]];
                e = t.extra
            }
            cutBits(t.bitcnt);
            copyDist = windowPos - t.lbase - maskBits(e);

            while(copyLen > 0 && n < size) {
                copyLen--;
                copyDist &= WSIZE - 1;
                windowPos &= WSIZE - 1;
                buff[off + n++] = slide[windowPos++] = slide[copyDist++];
            }

            if(n == size) return size;
        }

        method = -1; // done
        return n;
    }

    function stored(buff, off, size) {
        cutBits(bitLen & 7);
        var n = maskBits(0x10);
        if(n != ((~maskBits(0x10)) & 0xffff)) return -1;
        copyLen = n;

        n = 0;
        while(copyLen > 0 && n < size) {
            copyLen--;
            windowPos &= WSIZE - 1;
            buff[off + n++] = slide[windowPos++] = maskBits(8);
        }

        if(copyLen == 0) method = -1;
        return n;
    }

    function fixed(buff, off, size) {
        var fixed_bd = 0;
        if(fixedTableList == null) {
            var lengths = [];

            for(var symbol = 0; symbol < 144; symbol++) lengths[symbol] = 8;
            for(; symbol < 256; symbol++) lengths[symbol] = 9;
            for(; symbol < 280; symbol++) lengths[symbol] = 7;
            for(; symbol < 288; symbol++) lengths[symbol] = 8;

            fixedLookup = 7;

            var htbl = new HuffTable(lengths, 288, 257, LENS, LEXT, fixedLookup);

            if(htbl.status != 0) return -1;

            fixedTableList = htbl.root;
            fixedLookup = htbl.maxbit;

            for(symbol = 0; symbol < 30; symbol++) lengths[symbol] = 5;
            fixed_bd = 5;

            htbl = new HuffTable(lengths, 30, 0, DISTS, DEXT, fixed_bd);
            if(htbl.status > 1) {
                fixedTableList = null;
                return -1;
            }
            fixedTableDist = htbl.root;
            fixed_bd = htbl.maxbit;
        }

        tblList = fixedTableList;
        tblDist = fixedTableDist;
        bitList = fixedLookup;
        bitdist = fixed_bd;
        return codes(buff, off, size);
    }

    function dynamic(buff, off, size) {
        var ll = new Array(0x023C);

        for (var m = 0; m < 0x023C; m++) ll[m] = 0;

        var llencnt = 257 + maskBits(5),
            dcodescnt = 1 + maskBits(5),
            bitlencnt = 4 + maskBits(4);

        if(llencnt > 286 || dcodescnt > 30) return -1;

        for(var j = 0; j < bitlencnt; j++) ll[BITORDER[j]] = maskBits(3);
        for(; j < 19; j++) ll[BITORDER[j]] = 0;

        // build decoding table for trees--single level, 7 bit lookup
        bitList = 7;
        var hufTable = new HuffTable(ll, 19, 19, null, null, bitList);
        if(hufTable.status != 0)
            return -1;	// incomplete code set

        tblList = hufTable.root;
        bitList = hufTable.maxbit;
        var lencnt = llencnt + dcodescnt,
            i = 0,
            lastLen = 0;
        while(i < lencnt) {
            var hufLcode = tblList.list[addBits(bitList) & MASK_BITS[bitList]];
            j = hufLcode.bitcnt;
            cutBits(j);
            j = hufLcode.lbase;
            if(j < 16)
                ll[i++] = lastLen = j;
            else if(j == 16) {
                j = 3 + maskBits(2);
                if(i + j > lencnt) return -1;
                while(j-- > 0) ll[i++] = lastLen;
            } else if(j == 17) {
                j = 3 + maskBits(3);
                if(i + j > lencnt) return -1;
                while(j-- > 0) ll[i++] = 0;
                lastLen = 0;
            } else {
                j = 11 + maskBits(7);
                if(i + j > lencnt) return -1;
                while(j-- > 0) ll[i++] = 0;
                lastLen = 0;
            }
        }
        bitList = 9;
        hufTable = new HuffTable(ll, llencnt, 257, LENS, LEXT, bitList);
        bitList == 0 && (hufTable.status = 1);

        if (hufTable.status != 0) return -1;

        tblList = hufTable.root;
        bitList = hufTable.maxbit;

        for(i = 0; i < dcodescnt; i++) ll[i] = ll[i + llencnt];
        bitdist = 6;
        hufTable = new HuffTable(ll, dcodescnt, 0, DISTS, DEXT, bitdist);
        tblDist = hufTable.root;
        bitdist = hufTable.maxbit;

        if((bitdist == 0 && llencnt > 257) || hufTable.status != 0) return -1;

        return codes(buff, off, size);
    }

    return {
        inflate : function(/*Buffer*/outputBuffer) {
            tblList = null;

            var size = outputBuffer.length,
                offset = 0, i;

            while(offset < size) {
                if(eof && method == -1) return;
                if(copyLen > 0) {
                    if(method != 0) {
                        while(copyLen > 0 && offset < size) {
                            copyLen--;
                            copyDist &= WSIZE - 1;
                            windowPos &= WSIZE - 1;
                            outputBuffer[offset++] = (slide[windowPos++] = slide[copyDist++]);
                        }
                    } else {
                        while(copyLen > 0 && offset < size) {
                            copyLen--;
                            windowPos &= WSIZE - 1;
                            outputBuffer[offset++] = (slide[windowPos++] = maskBits(8));
                        }
                        copyLen == 0 && (method = -1); // done
                    }
                    if (offset == size) return;
                }

                if(method == -1) {
                    if(eof) break;
                    eof = maskBits(1) != 0;
                    method = maskBits(2);
                    tblList = null;
                    copyLen = 0;
                }
                switch(method) {
                    case 0: i = stored(outputBuffer, offset, size - offset); break;
                    case 1: i = tblList != null ? codes(outputBuffer, offset, size - offset) : fixed(outputBuffer, offset, size - offset); break;
                    case 2: i = tblList != null ? codes(outputBuffer, offset, size - offset) : dynamic(outputBuffer, offset, size - offset); break;
                    default: i = -1; break;
                }

                if(i == -1) return;
                offset += i;
            }
        }
    };
}

module.exports = function(/*Buffer*/inbuf) {
    var zlib = require("zlib");
    return {
        inflateAsync : function(/*Function*/callback) {
            var tmp = zlib.createInflateRaw(),
                parts = [], total = 0;
            tmp.on('data', function(data) {
                parts.push(data);
                total += data.length;
            });
            tmp.on('end', function() {
                var buf = new Buffer(total), written = 0;
                buf.fill(0);

                for (var i = 0; i < parts.length; i++) {
                    var part = parts[i];
                    part.copy(buf, written);
                    written += part.length;
                }
                callback && callback(buf);
            });
            tmp.end(inbuf)
        },

        inflate : function(/*Buffer*/outputBuffer) {
            var x = new JSInflater(inbuf);
            x.inflate(outputBuffer);
            delete(x);
        }
    }
};
