#!/usr/bin/env python2
#
#  Python utilities shared by the build scripts.
#

import datetime
import json

class BitEncoder:
    "Bitstream encoder."

    _bits = None
    _varuint_dist = None
    _varuint_cats = None
    _varuint_count = None
    _varuint_bits = None

    def __init__(self):
        self._bits = []
        self._varuint_dist = [ 0 ] * 65536
        self._varuint_cats = [0] * 5
        self._varuint_count = 0
        self._varuint_bits = 0

    def bits(self, x, nbits):
        if (x >> nbits) != 0:
            raise Exception('input value has too many bits (value: %d, bits: %d)' % (x, nbits))
        for shift in xrange(nbits - 1, -1, -1):  # nbits - 1, nbits - 2, ..., 0
            self._bits.append((x >> shift) & 0x01)

    def string(self, x):
        for i in xrange(len(x)):
            ch = ord(x[i])
            for shift in xrange(7, -1, -1):  # 7, 6, ..., 0
                self._bits.append((ch >> shift) & 0x01)

    # Shared varint encoding.
    def varuint(self, x):
        assert(x >= 0)
        if x <= 0xffff:
            self._varuint_dist[x] += 1
        self._varuint_count += 1

        if x == 0:
            self.bits(0, 2)
            self._varuint_bits += 2
            self._varuint_cats[0] += 1
        elif x <= 4:
            self.bits(1, 2)
            self.bits(x - 1, 2)
            self._varuint_bits += 2 + 2
            self._varuint_cats[1] += 1
        elif x <= 36:
            self.bits(2, 2)
            self.bits(x - 5, 5)
            self._varuint_bits += 2 + 5
            self._varuint_cats[2] += 1
        elif x <= 163:
            self.bits(3, 2)
            self.bits(x - 37 + 1, 7)
            self._varuint_bits += 2 + 7
            self._varuint_cats[3] += 1
        else:
            self.bits(3, 2)
            self.bits(0, 7)
            self.bits(x, 20)
            self._varuint_bits += 2 + 7 + 20
            self._varuint_cats[4] += 1

    def getNumBits(self):
        "Get current number of encoded bits."
        return len(self._bits)

    def getNumBytes(self):
        "Get current number of encoded bytes, rounded up."
        nbits = len(self._bits)
        while (nbits % 8) != 0:
            nbits += 1
        return nbits / 8

    def getBytes(self):
        "Get current bitstream as a byte sequence, padded with zero bits."
        bytes = []

        for i in xrange(self.getNumBytes()):
            t = 0
            for j in xrange(8):
                off = i*8 + j
                if off >= len(self._bits):
                    t = (t << 1)
                else:
                    t = (t << 1) + self._bits[off]
            bytes.append(t)

        return bytes

    def getByteString(self):
        "Get current bitstream as a string."
        return ''.join([chr(i) for i in self.getBytes()])

class GenerateC:
    "Helper for generating C source and header files."

    _data = None
    wrap_col = 76

    def __init__(self):
        self._data = []

    def emitRaw(self, text):
        "Emit raw text (without automatic newline)."
        self._data.append(text)

    def emitLine(self, text):
        "Emit a raw line (with automatic newline)."
        self._data.append(text + '\n')

    def emitHeader(self, autogen_by):
        "Emit file header comments."

        # Note: a timestamp would be nice but it breaks incremental building
        self.emitLine('/*')
        self.emitLine(' *  Automatically generated by %s, do not edit!' % autogen_by)
        self.emitLine(' */')
        self.emitLine('')

    def emitArray(self, data, tablename, visibility=None, typename='char', size=None, intvalues=False, const=True):
        "Emit an array as a C array."

        # lenient input
        if isinstance(data, unicode):
            data = data.encode('utf-8')
        if isinstance(data, str):
            tmp = []
            for i in xrange(len(data)):
                tmp.append(ord(data[i]))
            data = tmp

        size_spec = ''
        if size is not None:
            size_spec = '%d' % size
        visib_qual = ''
        if visibility is not None:
            visib_qual = visibility + ' '
        const_qual = ''
        if const:
            const_qual = 'const '
        self.emitLine('%s%s%s %s[%s] = {' % (visib_qual, const_qual, typename, tablename, size_spec))

        line = ''
        for i in xrange(len(data)):
            if intvalues:
                suffix = ''
                if data[i] < -32768 or data[i] > 32767:
                    suffix = 'L'
                t = "%d%s," % (data[i], suffix)
            else:
                t = "(%s)'\\x%02x', " % (typename, data[i])
            if len(line) + len(t) >= self.wrap_col:
                self.emitLine(line)
                line = t
            else:
                line += t
        if line != '':
            self.emitLine(line)
        self.emitLine('};')

    def emitDefine(self, name, value, comment=None):
        "Emit a C define with an optional comment."

        # XXX: there is no escaping right now (for comment or value)
        if comment is not None:
            self.emitLine('#define %-60s  %-30s /* %s */' % (name, value, comment))
        else:
            self.emitLine('#define %-60s  %s' % (name, value))

    def getString(self):
        "Get the entire file as a string."
        return ''.join(self._data)

def json_encode(x):
    "JSON encode a value."
    try:
        return json.dumps(x)
    except AttributeError:
        pass

    # for older library versions
    return json.write(x)

def json_decode(x):
    "JSON decode a value."
    try:
        return json.loads(x)
    except AttributeError:
        pass

    # for older library versions
    return json.read(x)

# Compute a byte hash identical to duk_util_hashbytes().
DUK__MAGIC_M = 0x5bd1e995
DUK__MAGIC_R = 24
def duk_util_hashbytes(x, off, nbytes, str_seed, big_endian):
    h = (str_seed ^ nbytes) & 0xffffffff

    while nbytes >= 4:
        # 4-byte fetch byte order:
        #  - native (endian dependent) if unaligned accesses allowed
        #  - little endian if unaligned accesses not allowed

        if big_endian:
            k = ord(x[off + 3]) + (ord(x[off + 2]) << 8) + \
                (ord(x[off + 1]) << 16) + (ord(x[off + 0]) << 24)
        else:
            k = ord(x[off]) + (ord(x[off + 1]) << 8) + \
                (ord(x[off + 2]) << 16) + (ord(x[off + 3]) << 24)

        k = (k * DUK__MAGIC_M) & 0xffffffff
        k = (k ^ (k >> DUK__MAGIC_R)) & 0xffffffff
        k = (k * DUK__MAGIC_M) & 0xffffffff
        h = (h * DUK__MAGIC_M) & 0xffffffff
        h = (h ^ k) & 0xffffffff

        off += 4
        nbytes -= 4

    if nbytes >= 3:
        h = (h ^ (ord(x[off + 2]) << 16)) & 0xffffffff
    if nbytes >= 2:
        h = (h ^ (ord(x[off + 1]) << 8)) & 0xffffffff
    if nbytes >= 1:
        h = (h ^ ord(x[off])) & 0xffffffff
        h = (h * DUK__MAGIC_M) & 0xffffffff

    h = (h ^ (h >> 13)) & 0xffffffff
    h = (h * DUK__MAGIC_M) & 0xffffffff
    h = (h ^ (h >> 15)) & 0xffffffff

    return h

# Compute a string hash identical to duk_heap_hashstring() when dense
# hashing is enabled.
DUK__STRHASH_SHORTSTRING = 4096
DUK__STRHASH_MEDIUMSTRING = 256 * 1024
DUK__STRHASH_BLOCKSIZE = 256
def duk_heap_hashstring_dense(x, hash_seed, big_endian=False, strhash16=False):
    str_seed = (hash_seed ^ len(x)) & 0xffffffff

    if len(x) <= DUK__STRHASH_SHORTSTRING:
        res = duk_util_hashbytes(x, 0, len(x), str_seed, big_endian)
    else:
        if len(x) <= DUK__STRHASH_MEDIUMSTRING:
            skip = 16 * DUK__STRHASH_BLOCKSIZE + DUK__STRHASH_BLOCKSIZE
        else:
            skip = 256 * DUK__STRHASH_BLOCKSIZE + DUK__STRHASH_BLOCKSIZE

        res = duk_util_hashbytes(x, 0, DUK__STRHASH_SHORTSTRING, str_seed, big_endian)
        off = DUK__STRHASH_SHORTSTRING + (skip * (res % 256)) / 256

        while off < len(x):
            left = len(x) - off
            now = left
            if now > DUK__STRHASH_BLOCKSIZE:
                now = DUK__STRHASH_BLOCKSIZE
            res = (res ^ duk_util_hashbytes(str, off, now, str_seed, big_endian)) & 0xffffffff
            off += skip

    if strhash16:
        res &= 0xffff

    return res

# Compute a string hash identical to duk_heap_hashstring() when sparse
# hashing is enabled.
DUK__STRHASH_SKIP_SHIFT = 5   # XXX: assumes default value
def duk_heap_hashstring_sparse(x, hash_seed, strhash16=False):
    res = (hash_seed ^ len(x)) & 0xffffffff

    step = (len(x) >> DUK__STRHASH_SKIP_SHIFT) + 1
    off = len(x)
    while off >= step:
        assert(off >= 1)
        res = ((res * 33) + ord(x[off - 1])) & 0xffffffff
        off -= step

    if strhash16:
        res &= 0xffff

    return res

# Must match src-input/duk_unicode_support:duk_unicode_unvalidated_utf8_length().
def duk_unicode_unvalidated_utf8_length(x):
    assert(isinstance(x, str))
    clen = 0
    for c in x:
        t = ord(c)
        if t < 0x80 or t >= 0xc0:  # 0x80...0xbf are continuation chars, not counted
            clen += 1
    return clen
