/* jshint esversion: 6 */
/* eslint "indent": [ "error", 4, { "SwitchCase": 1 } ] */

var pickle = pickle || {};

pickle.Unpickler = class {

    constructor(buffer) {
        this._reader = new pickle.Reader(buffer, 0);
    }

    load(function_call, persistent_load) {
        let reader = this._reader;
        let marker = [];
        let stack = [];
        let memo = new Map();
        while (reader.position < reader.length) {
            const opcode = reader.byte();
            switch (opcode) {
                case pickle.OpCode.PROTO: {
                    const version = reader.byte();
                    if (version > 4) {
                        throw new pickle.Error("Unsupported protocol version '" + version + "'.");
                    }
                    break;
                }
                case pickle.OpCode.GLOBAL:
                    stack.push([ reader.line(), reader.line() ].join('.'));
                    break;
                case pickle.OpCode.STACK_GLOBAL:
                    stack.push([ stack.pop(), stack.pop() ].reverse().join('.'));
                    break;
                case pickle.OpCode.PUT: {
                    const index = parseInt(reader.line(), 10);
                    memo.set(index, stack[stack.length - 1]);
                    break;
                }
                case pickle.OpCode.OBJ: {
                    const items = stack;
                    stack = marker.pop();
                    stack.push(function_call(items.pop(), items));
                    break;
                }
                case pickle.OpCode.GET: {
                    const index = parseInt(reader.line(), 10);
                    stack.push(memo.get(index));
                    break;
                }
                case pickle.OpCode.POP:
                    stack.pop();
                    break;
                case pickle.OpCode.POP_MARK:
                    stack = marker.pop();
                    break;
                case pickle.OpCode.DUP:
                    stack.push(stack[stack.length-1]);
                    break;
                case pickle.OpCode.PERSID:
                    stack.push(persistent_load(reader.line()));
                    break;
                case pickle.OpCode.BINPERSID:
                    stack.push(persistent_load(stack.pop()));
                    break;
                case pickle.OpCode.REDUCE: {
                    const items = stack.pop();
                    const type = stack.pop();
                    stack.push(function_call(type, items));
                    break;
                }
                case pickle.OpCode.NEWOBJ: {
                    const items = stack.pop();
                    const type = stack.pop();
                    stack.push(function_call(type, items));
                    break;
                }
                case pickle.OpCode.BINGET:
                    stack.push(memo.get(reader.byte()));
                    break;
                case pickle.OpCode.LONG_BINGET:
                    stack.push(memo.get(reader.uint32()));
                    break;
                case pickle.OpCode.BINPUT:
                    memo.set(reader.byte(), stack[stack.length - 1]);
                    break;
                case pickle.OpCode.LONG_BINPUT:
                    memo.set(reader.uint32(), stack[stack.length - 1]);
                    break;
                case pickle.OpCode.BININT:
                    stack.push(reader.int32());
                    break;
                case pickle.OpCode.BININT1:
                    stack.push(reader.byte());
                    break;
                case pickle.OpCode.LONG:
                    stack.push(parseInt(reader.line(), 10));
                    break;
                case pickle.OpCode.BININT2:
                    stack.push(reader.uint16());
                    break;
                case pickle.OpCode.BINBYTES:
                    stack.push(reader.bytes(reader.int32()));
                    break;
                case pickle.OpCode.SHORT_BINBYTES:
                    stack.push(reader.bytes(reader.byte()));
                    break;
                case pickle.OpCode.FLOAT:
                    stack.push(parseFloat(reader.line()));
                    break;
                case pickle.OpCode.BINFLOAT:
                    stack.push(reader.float64());
                    break;
                case pickle.OpCode.INT: {
                    const value = reader.line();
                    if (value == '01') {
                        stack.push(true);
                    }
                    else if (value == '00') {
                        stack.push(false);
                    }
                    else {
                        stack.push(parseInt(value, 10));
                    }
                    break;
                }
                case pickle.OpCode.EMPTY_LIST:
                    stack.push([]);
                    break;
                case pickle.OpCode.EMPTY_TUPLE:
                    stack.push([]);
                    break;
                case pickle.OpCode.EMPTY_SET:
                    stack.push([]);
                    break;
                case pickle.OpCode.ADDITEMS: {
                    const items = stack;
                    stack = marker.pop();
                    let obj = stack[stack.length - 1];
                    for (let i = 0; i < items.length; i++) {
                        obj.push(items[i]);
                    }
                    break;
                }
                case pickle.OpCode.DICT: {
                    const items = stack;
                    stack = marker.pop();
                    let dict = {};
                    for (let i = 0; i < items.length; i += 2) {
                        dict[items[i]] = items[i + 1];
                    }
                    stack.push(dict);
                    break;
                }
                case pickle.OpCode.LIST: {
                    const items = stack;
                    stack = marker.pop();
                    stack.push(items);
                    break;
                }
                case pickle.OpCode.TUPLE: {
                    const items = stack;
                    stack = marker.pop();
                    stack.push(items);
                    break;
                }
                case pickle.OpCode.SETITEM: {
                    const value = stack.pop();
                    const key = stack.pop();
                    let obj = stack[stack.length - 1];
                    if (obj.__setitem__) {
                        obj.__setitem__(key, value);
                    }
                    else {
                        obj[key] = value;
                    }
                    break;
                }
                case pickle.OpCode.SETITEMS: {
                    const items = stack;
                    stack = marker.pop();
                    let obj = stack[stack.length - 1];
                    for (let i = 0; i < items.length; i += 2) {
                        if (obj.__setitem__) {
                            obj.__setitem__(items[i], items[i + 1]);
                        }
                        else {
                            obj[items[i]] = items[i + 1];
                        }
                    }
                    break;
                }
                case pickle.OpCode.EMPTY_DICT:
                    stack.push({});
                    break;
                case pickle.OpCode.APPEND: {
                    const append = stack.pop();
                    stack[stack.length-1].push(append);
                    break;
                }
                case pickle.OpCode.APPENDS: {
                    const appends = stack;
                    stack = marker.pop();
                    let list = stack[stack.length - 1];
                    list.push.apply(list, appends);
                    break;
                }
                case pickle.OpCode.STRING: {
                    const str = reader.line();
                    stack.push(str.substr(1, str.length - 2));
                    break;
                }
                case pickle.OpCode.BINSTRING:
                    stack.push(reader.string(reader.uint32()));
                    break;
                case pickle.OpCode.SHORT_BINSTRING:
                    stack.push(reader.string(reader.byte()));
                    break;
                case pickle.OpCode.UNICODE:
                    stack.push(reader.line());
                    break;
                case pickle.OpCode.BINUNICODE:
                    stack.push(reader.string(reader.uint32(), 'utf-8'));
                    break;
                case pickle.OpCode.SHORT_BINUNICODE:
                    stack.push(reader.string(reader.byte(), 'utf-8'));
                    break;
                case pickle.OpCode.BUILD: {
                    const state = stack.pop();
                    let obj = stack.pop();
                    if (obj.__setstate__) {
                        if (obj.__setstate__.__call__) {
                            obj.__setstate__.__call__([ obj, state ]);
                        }
                        else {
                            obj.__setstate__(state);
                        }
                    }
                    else {
                        for (const p in state) {
                            obj[p] = state[p];
                        }
                    }
                    if (obj.__read__) {
                        obj = obj.__read__(this);
                    }
                    stack.push(obj);
                    break;
                }
                case pickle.OpCode.MARK:
                    marker.push(stack);
                    stack = [];
                    break;
                case pickle.OpCode.NEWTRUE:
                    stack.push(true);
                    break;
                case pickle.OpCode.NEWFALSE:
                    stack.push(false);
                    break;
                case pickle.OpCode.LONG1: {
                    const data = reader.bytes(reader.byte());
                    let number = 0;
                    switch (data.length) {
                        case 0: number = 0; break;
                        case 1: number = data[0]; break;
                        case 2: number = data[1] << 8 | data[0]; break;
                        case 3: number = data[2] << 16 | data[1] << 8 | data[0]; break;
                        case 4: number = data[3] << 24 | data[2] << 16 | data[1] << 8 | data[0]; break;
                        default: number = Array.prototype.slice.call(data, 0); break; 
                    }
                    stack.push(number);
                    break;
                }
                case pickle.OpCode.LONG4:
                    // TODO decode LONG4
                    stack.push(reader.bytes(reader.uint32()));
                    break;
                case pickle.OpCode.TUPLE1:
                    stack.push([ stack.pop() ]);
                    break;
                case pickle.OpCode.TUPLE2: {
                    const b = stack.pop();
                    const a = stack.pop();
                    stack.push([ a, b ]);
                    break;
                }
                case pickle.OpCode.TUPLE3: {
                    const c = stack.pop();
                    const b = stack.pop();
                    const a = stack.pop();
                    stack.push([ a, b, c ]);
                    break;
                }
                case pickle.OpCode.MEMOIZE:
                    memo.set(memo.size, stack[stack.length - 1]);
                    break;
                case pickle.OpCode.FRAME:
                    reader.bytes(8);
                    break;
                case pickle.OpCode.NONE:
                    stack.push(null);
                    break;
                case pickle.OpCode.STOP:
                    return stack.pop();
                default:
                    throw new pickle.Error("Unknown opcode '" + opcode + "'.");
            }
        }
        throw new pickle.Error('Unexpected end of file.');
    }

    read(size) {
        return this._reader.bytes(size);
    }

    unescape(token, size) {
        const length = token.length;
        const a = new Uint8Array(length);
        if (size && size == length) {
            for (let p = 0; p < size; p++) {
                a[p] = token.charCodeAt(p);
            }
            return a;
        }
        let i = 0;
        let o = 0;
        while (i < length) {
            let c = token.charCodeAt(i++);
            if (c !== 0x5C || i >= length) {
                a[o++] = c;
            }
            else {
                c = token.charCodeAt(i++);
                switch (c) {
                    case 0x27: a[o++] = 0x27; break; // '
                    case 0x5C: a[o++] = 0x5C; break; // \\
                    case 0x22: a[o++] = 0x22; break; // "
                    case 0x72: a[o++] = 0x0D; break; // \r
                    case 0x6E: a[o++] = 0x0A; break; // \n
                    case 0x74: a[o++] = 0x09; break; // \t
                    case 0x62: a[o++] = 0x08; break; // \b
                    case 0x58: // x
                    case 0x78: { // X 
                        const xsi = i - 1;
                        const xso = o;
                        for (let xi = 0; xi < 2; xi++) {
                            if (i >= length) {
                                i = xsi;
                                o = xso;
                                a[o] = 0x5c;
                                break;
                            }
                            let xd = token.charCodeAt(i++);
                            xd = xd >= 65 && xd <= 70 ? xd - 55 : xd >= 97 && xd <= 102 ? xd - 87 : xd >= 48 && xd <= 57 ? xd - 48 : -1;
                            if (xd === -1) {
                                i = xsi;
                                o = xso;
                                a[o] = 0x5c;
                                break;
                            }
                            a[o] = a[o] << 4 | xd;
                        }
                        o++;
                        break;
                    }
                    default:
                        if (c < 48 || c > 57) { // 0-9
                            a[o++] = 0x5c;
                            a[o++] = c;
                        }
                        else {
                            i--;
                            let osi = i;
                            let oso = o;
                            for (let oi = 0; oi < 3; oi++) {
                                if (i >= length) {
                                    i = osi;
                                    o = oso;
                                    a[o] = 0x5c;
                                    break;
                                }
                                let od = token.charCodeAt(i++);
                                if (od < 48 || od > 57) {
                                    i = osi;
                                    o = oso;
                                    a[o] = 0x5c;
                                    break;
                                }
                                a[o] = a[o] << 3 | od - 48;
                            }
                            o++;
                        }
                        break;
                }
            }
        }
        return a.slice(0, o);
    }
};

// https://svn.python.org/projects/python/trunk/Lib/pickletools.py
// https://github.com/python/cpython/blob/master/Lib/pickle.py
pickle.OpCode = {
    MARK: 40,              // '('
    EMPTY_TUPLE: 41,       // ')'
    STOP: 46,              // '.'
    POP: 48,               // '0'
    POP_MARK: 49,          // '1'
    DUP: 50,               // '2'
    BINBYTES: 66,          // 'B' (Protocol 3)
    SHORT_BINBYTES: 67,    // 'C' (Protocol 3)
    FLOAT: 70,             // 'F'
    BINFLOAT: 71,          // 'G'
    INT: 73,               // 'I'
    BININT: 74,            // 'J'
    BININT1: 75,           // 'K'
    LONG: 76,              // 'L'
    BININT2: 77,           // 'M'
    NONE: 78,              // 'N'
    PERSID: 80,            // 'P'
    BINPERSID: 81,         // 'Q'
    REDUCE: 82,            // 'R'
    STRING: 83,             // 'S'
    BINSTRING: 84,         // 'T'
    SHORT_BINSTRING: 85,   // 'U'
    UNICODE: 86,           // 'V'
    BINUNICODE: 88,        // 'X'
    EMPTY_LIST: 93,        // ']'
    APPEND: 97,            // 'a'
    BUILD: 98,             // 'b'
    GLOBAL: 99,            // 'c'
    DICT: 100,             // 'd'
    APPENDS: 101,          // 'e'
    GET: 103,              // 'g'
    BINGET: 104,           // 'h'
    LONG_BINGET: 106,      // 'j'
    LIST: 108,             // 'l'
    OBJ: 111,              // 'o'
    PUT: 112,              // 'p'
    BINPUT: 113,           // 'q'
    LONG_BINPUT: 114,      // 'r'
    SETITEM: 115,          // 's'
    TUPLE: 116,            // 't'
    SETITEMS: 117,         // 'u'
    EMPTY_DICT: 125,       // '}'
    PROTO: 128,
    NEWOBJ: 129,
    TUPLE1: 133,           // '\x85'
    TUPLE2: 134,           // '\x86'
    TUPLE3: 135,           // '\x87'
    NEWTRUE: 136,          // '\x88'
    NEWFALSE: 137,         // '\x89'
    LONG1: 138,            // '\x8a'
    LONG4: 139,            // '\x8b'
    SHORT_BINUNICODE: 140, // '\x8c' (Protocol 4)
    BINUNICODE8: 141,      // '\x8d' (Protocol 4)
    BINBYTES8: 142,        // '\x8e' (Protocol 4)
    EMPTY_SET: 143,        // '\x8f' (Protocol 4)
    ADDITEMS: 144,         // '\x90' (Protocol 4)
    FROZENSET: 145,        // '\x91' (Protocol 4)
    NEWOBJ_EX: 146,        // '\x92' (Protocol 4)
    STACK_GLOBAL: 147,     // '\x93' (Protocol 4)
    MEMOIZE: 148,          // '\x94' (Protocol 4)
    FRAME: 149             // '\x95' (Protocol 4)
};

pickle.Reader = class { 

    constructor(buffer) {
        if (buffer) {
            this._buffer = buffer;
            this._dataView = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
            this._position = 0;
        }
        pickle.Reader._utf8Decoder = pickle.Reader._utf8Decoder || new TextDecoder('utf-8');
        pickle.Reader._asciiDecoder = pickle.Reader._asciiDecoder || new TextDecoder('ascii');
    }

    get length() {
        return this._buffer.byteLength;
    }

    get position() {
        return this._position;
    }

    byte() {
        const position = this._position;
        this.skip(1);
        return this._dataView.getUint8(position);
    }

    bytes(length) {
        const position = this._position;
        this.skip(length);
        return this._buffer.subarray(position, this._position);
    }

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

    int32() {
        const position = this.position;
        this.skip(4);
        return this._dataView.getInt32(position, true);
    }

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

    float32() {
        const position = this.position;
        this.skip(4);
        return this._dataView.getFloat32(position, true);
    }

    float64() {
        const position = this.position;
        this.skip(8);
        return this._dataView.getFloat64(position, true);
    }

    skip(offset) {
        this._position += offset;
        if (this._position > this._buffer.length) {
            throw new pickle.Error('Expected ' + (this._position - this._buffer.length) + ' more bytes. The file might be corrupted. Unexpected end of file.');
        }
    }

    string(size, encoding) {
        const data = this.bytes(size);
        return (encoding == 'utf-8') ?
            pickle.Reader._utf8Decoder.decode(data) :
            pickle.Reader._asciiDecoder.decode(data);
    }

    line() {
        const index = this._buffer.indexOf(0x0A, this._position);
        if (index == -1) {
            throw new pickle.Error("Could not find end of line.");
        }
        const size = index - this._position;
        const text = this.string(size, 'ascii');
        this.skip(1);
        return text;
    }
};


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

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