/**************************************************************************
 *
 * Copyright 2012 Jose Fonseca
 * All Rights Reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 **************************************************************************/

/*
 * Python pickle writer
 */

#pragma once

#include <assert.h>
#include <stddef.h>
#include <stdint.h>
#include <wchar.h>

#include <ostream>
#include <string>
#include <limits>


class PickleWriter
{
private:
    std::ostream &os;

    /*
     * Python pickle opcodes.  See pickle.py and pickletools.py from Python
     * standard library for details.
     */
    enum Opcode {
        MARK            = '(',
        STOP            = '.',
        POP             = '0',
        POP_MARK        = '1',
        DUP             = '2',
        BINBYTES        = 'B',
        SHORT_BINBYTES  = 'C',
        FLOAT           = 'F',
        INT             = 'I',
        BININT          = 'J',
        BININT1         = 'K',
        LONG            = 'L',
        BININT2         = 'M',
        NONE            = 'N',
        PERSID          = 'P',
        BINPERSID       = 'Q',
        REDUCE          = 'R',
        STRING          = 'S',
        BINSTRING       = 'T',
        SHORT_BINSTRING = 'U',
        UNICODE         = 'V',
        BINUNICODE      = 'X',
        APPEND          = 'a',
        BUILD           = 'b',
        GLOBAL          = 'c',
        DICT            = 'd',
        EMPTY_DICT      = '}',
        APPENDS         = 'e',
        GET             = 'g',
        BINGET          = 'h',
        INST            = 'i',
        LONG_BINGET     = 'j',
        LIST            = 'l',
        EMPTY_LIST      = ']',
        OBJ             = 'o',
        PUT             = 'p',
        BINPUT          = 'q',
        LONG_BINPUT     = 'r',
        SETITEM         = 's',
        TUPLE           = 't',
        EMPTY_TUPLE     = ')',
        SETITEMS        = 'u',
        BINFLOAT        = 'G',

        PROTO           = '\x80',
        NEWOBJ          = '\x81',
        EXT1            = '\x82',
        EXT2            = '\x83',
        EXT4            = '\x84',
        TUPLE1          = '\x85',
        TUPLE2          = '\x86',
        TUPLE3          = '\x87',
        NEWTRUE         = '\x88',
        NEWFALSE        = '\x89',
        LONG1           = '\x8a',
        LONG4           = '\x8b',
    };

public:
    PickleWriter(std::ostream &_os) :
        os(_os) {
    }

    inline void begin() {
        os.put(PROTO);
        os.put(3);
    }

    inline void end() {
        os.put(STOP);
    }

    inline void beginDict() {
        os.put(EMPTY_DICT);
        os.put(BINPUT);
        os.put(1);
    }

    inline void endDict() {
    }

    inline void beginItem() {
    }

    inline void beginItem(const char * name) {
        writeString(name);
    }

    inline void beginItem(const std::string &name) {
        beginItem(name.c_str());
    }

    inline void endItem(void) {
        os.put(SETITEM);
    }

    inline void beginList() {
        os.put(EMPTY_LIST);
        os.put(BINPUT);
        os.put(1);
        os.put(MARK);
    }

    inline void endList(void) {
        os.put(APPENDS);
    }

    inline void beginTuple() {
        os.put(MARK);
    }

    inline void endTuple(void) {
        os.put(TUPLE);
    }

    inline void beginTuple(unsigned length) {
        if (length >= 4) {
            os.put(MARK);
        }
    }

    inline void endTuple(unsigned length) {
        static const Opcode ops[4] = {
            EMPTY_TUPLE,
            TUPLE1,
            TUPLE2,
            TUPLE3,
        };
        Opcode op = length < 4 ? ops[length] : TUPLE;
        os.put(op);
    }

    inline void writeString(const char *s, size_t length) {
        if (!s) {
            writeNone();
            return;
        }

        if (length < 256) {
            os.put(SHORT_BINSTRING);
            os.put(length);
        } else {
            os.put(BINSTRING);
            putInt32(length);
        }

        /* FIXME: handle non-ASCII better */
        for (size_t i = 0; i < length; ++i) {
            char c = s[i];
            c = c >= 0 && c <= 0x7f ? c : '?';
            os.put(c);
        }

        os.put(BINPUT);
        os.put(1);
    }

    inline void writeString(const char *s) {
        if (!s) {
            writeNone();
            return;
        }

        writeString(s, strlen(s));
    }

    inline void writeString(const std::string &s) {
        writeString(s.c_str(), s.size());
    }

    inline void writeWString(const wchar_t *s, size_t length) {
        if (!s) {
            writeNone();
            return;
        }

        /* FIXME: emit UTF-8 */
        os.put(BINUNICODE);
        putInt32(length);
        for (size_t i = 0; i < length; ++i) {
            wchar_t wc = s[i];
            char c = wc >= 0 && wc < 0x80 ? (char)wc : '?';
            os.put(c);
        }

        os.put(BINPUT);
        os.put(1);
    }

    inline void writeWString(const wchar_t *s) {
        if (!s) {
            writeNone();
            return;
        }

        writeWString(s, wcslen(s));
    }

    inline void writeNone(void) {
        os.put(NONE);
    }

    inline void writeBool(bool b) {
        os.put(b ? NEWTRUE : NEWFALSE);
    }

    inline void writeInt(uint8_t i) {
        os.put(BININT1);
        os.put(i);
    }

    inline void writeInt(uint16_t i) {
        if (i < 0x100) {
            writeInt((uint8_t)i);
        } else {
            os.put(BININT2);
            putInt16(i);
        }
    }

    inline void writeInt(int32_t i) {
        if (0 <= i && i < 0x10000) {
            writeInt((uint16_t)i);
        } else {
            os.put(BININT);
            putInt32(i);
        }
    }

    inline void writeInt(uint32_t i) {
        if (i < 0x8000000) {
            writeInt((int32_t)i);
        } else {
            writeLong(i);
        }
    }

    inline void writeInt(long long i) {
        if (-0x8000000 <= i && i < 0x8000000) {
            writeInt((int32_t)i);
        } else {
            writeLong(i);
        }
    }

    inline void writeInt(unsigned long long i) {
        if (i < 0x8000000) {
            writeInt((int32_t)i);
        } else {
            writeLong(i);
        }
    }

    inline void writeFloat(double f) {
        union {
            double f;
            char c[8];
        } u;

        static_assert(sizeof u.f == sizeof u.c, "double is not 8 bytes");
        u.f = f;

        os.put(BINFLOAT);
        os.put(u.c[7]);
        os.put(u.c[6]);
        os.put(u.c[5]);
        os.put(u.c[4]);
        os.put(u.c[3]);
        os.put(u.c[2]);
        os.put(u.c[1]);
        os.put(u.c[0]);
    }

    inline void writeBytes(const char *s, size_t length) {
        if (!s) {
            writeNone();
            return;
        }

        if (length < 256) {
            os.put(SHORT_BINBYTES);
            os.put(length);
        } else {
            os.put(BINBYTES);
            putInt32(length);
        }
        os.write(s, length);

        os.put(BINPUT);
        os.put(1);
    }

    inline void writePointer(unsigned long long addr) {
        os.put(GLOBAL);
        os << "unpickle\nPointer\n";
        os.put(BINPUT);
        os.put(1);
        writeInt(addr);
        os.put(TUPLE1);
        os.put(REDUCE);
    }

protected:
    inline void putInt16(uint16_t i) {
        os.put( i        & 0xff);
        os.put( i >>  8        );
    }

    inline void putInt32(uint32_t i) {
        os.put( i        & 0xff);
        os.put((i >>  8) & 0xff);
        os.put((i >> 16) & 0xff);
        os.put( i >> 24        );
    }

    template< class T >
    inline void writeLong(T l) {
        os.put(LONG1);

        if (l == 0) {
            os.put(0);
            return;
        }

        // Same as l >> (8 * sizeof l), but without the warnings
        T sign;
        if (std::numeric_limits<T>::is_signed) {
            sign = l < 0 ? ~0 : 0;
        } else {
            sign = 0;
        }

        // Count how many bytes we need to represent the long integer.
        T sl = l;
        unsigned c = 0;
        do {
            ++c;
            sl >>= 8;
        } while (sl != sign);

        // Add an extra byte if sign bit doesn't match
        if (((l >> (8 * c - 1)) & 1) != ((l >> (8 * sizeof l - 1)) & 1)) {
            ++c;
        }
        os.put(c);

        for (unsigned i = 0; i < c; ++ i) {
            os.put(l & 0xff);
            l >>= 8;
        }
    }
};

