#
# compressedColumn.py
#
# This source file is part of the FoundationDB open source project
#
# Copyright 2013-2018 Apple Inc. and the FoundationDB project authors
# 
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# 
#     http://www.apache.org/licenses/LICENSE-2.0
# 
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

import fdb
import fdb.tuple
import struct

_unpackedPrefix = 'unpacked'
_packedPrefix = 'packed'

fdb.api_version(16)

class _MergedData:
    def __init__(self):
        self.results = []
        self.finishedPack = False
        self.finishedUnpack = False
        self.packedIndex = 0
        pass

class Column:
    def __init__(self, columnName):
        self.columnName = columnName
        self.packFetchCount = 10
        self.targetChunkSize = 5000
        self.maxChunkSize = 10000
        #self.mergeChunkSize = 2500

    def _getSubKeyTuple(self, key):
        return fdb.tuple.unpack(key)[2:]

    def _getSubKey(self, key):
        return fdb.tuple.unpack(key)[2]

    def _isPackedKey(self, key):
        return str(key).startswith(fdb.tuple.pack((self.columnName, _packedPrefix)))

    #This results in slight inefficiencies when the key being searched for comes before the first packed segment with strictRange=False
    def _getPackedData(self, key, packedRange, requireKey = True, strictRange = True):
        found = False
        keyRange = None
        packedKeyRange = None
        packedData = None

        for k,v in packedRange:
            #print 'Searching ' + k + ' for ' + key
            if self._isPackedKey(k):
                if found:
                    endRange = self._getSubKeyTuple(k)
                    keyRange = (keyRange[0], endRange[0])
                    break

                keyRange = self._getSubKeyTuple(k)
                packedKeyRange = keyRange
                #print str(keyRange)
                if (not requireKey or key >= keyRange[0]) and key <= keyRange[1]:
                    if strictRange:
                        packedData = _PackedData(v)
                        break
                    else:
                        found = True
                        keyRange = (keyRange[0], keyRange[1] + chr(0))
                        packedData = _PackedData(v)

            elif found:
                keyRange = (keyRange[0], '\xff')

        if strictRange:
            return [keyRange, packedData]
        else:
            return [packedKeyRange, keyRange, packedData]

    def _getPackedRange(self, tr, key):
        return tr.get_range(fdb.KeySelector.last_less_than(fdb.tuple.pack((self.columnName, _packedPrefix, key + chr(0)))), fdb.tuple.pack((self.columnName, _packedPrefix + chr(0))), 2)

    def _getUnpackedData(self, tr, key):
        return tr[fdb.tuple.pack((self.columnName, _unpackedPrefix, key))]

    def _getUnpackedRange(self, tr, keyBegin, keyEnd, limit):
        return tr.get_range(fdb.tuple.pack((self.columnName, _unpackedPrefix, keyBegin)), fdb.tuple.pack((self.columnName, _unpackedPrefix, keyEnd)), limit)

    def _mergeResults(self, packed, unpacked, totalUnpacked, packedIndex = 0, minPackedKey = '', maxKey = None):
        data = _MergedData()
        if packed is None:
            #print 'No merge necessary'
            data.finishedUnpack = True
            data.finishedPack = True
            data.packedIndex = 0

            if maxKey is None:
                data.results = [fdb.KeyValue(self._getSubKey(k), v) for k,v in unpacked]
            else:
                for k,v in unpacked:
                    if k < maxKey:
                        data.results.append(fdb.KeyValue(self._getSubKey(k), v))
                    else:
                        data.finishedUnpack = False
                        break
        else:
            #print 'Merging packed'
            unpackedCount = 0
            for k,v in unpacked:
                subKey = self._getSubKey(k)
                #print 'Unpacked: ' + subKey
                if maxKey is not None and subKey >= maxKey:
                    #print 'subKey >= maxKey %s, %s' % (subKey, maxKey)
                    break

                exactMatch = False
                while packedIndex < len(packed.rows) and packed.rows[packedIndex].key <= subKey and (maxKey is None or packed.rows[packedIndex].key < maxKey):
                    exactMatch = packed.rows[packedIndex].key == subKey
                    if packed.rows[packedIndex].key < subKey and packed.rows[packedIndex].key >= minPackedKey:
                        data.results.append(packed.rows[packedIndex])

                    packedIndex += 1

                if maxKey is None and packedIndex == len(packed.rows):
                    if exactMatch:
                        data.results.append(fdb.KeyValue(self._getSubKey(k), v))

                    #print 'packedIndex == len(packed.rows)'
                    break

                data.results.append(fdb.KeyValue(self._getSubKey(k), v))
                unpackedCount += 1

            #print "Packed index: %d, Unpacked: %d, total: %d" % (packedIndex, unpackedCount, totalUnpacked)
            if unpackedCount < totalUnpacked:
                while packedIndex < len(packed.rows) and (maxKey is None or packed.rows[packedIndex].key < maxKey):
                    if packed.rows[packedIndex].key >= minPackedKey:
                        data.results.append(packed.rows[packedIndex])

                    packedIndex += 1

            data.finishedPack = packedIndex == len(packed.rows)
            data.finishedUnpack = unpackedCount == totalUnpacked
            data.packedIndex = packedIndex

            #print str(data.results)
            #print 'Num Results: %d' % len(data.results)

        return data

    @fdb.transactional
    def setRow(self, tr, rowName, value):
        tr[fdb.tuple.pack((self.columnName, _unpackedPrefix, rowName))] = value

    @fdb.transactional
    def getRow(self, tr, rowName):
        unpacked = self._getUnpackedData(tr, rowName)
        packedRange = self._getPackedRange(tr, rowName)

        if unpacked.present():
            return unpacked
        else:
            packedData = self._getPackedData(rowName, packedRange)[1]
            if packedData is None:
                return None

            return packedData.getRow(rowName)

        return None

    @fdb.transactional
    def delete(self, tr):
        tr.clear_range_startswith(self.columnName)

    def getColumnStream(self, db, startRow = ''):
        return _ColumnStream(db, self, startRow)

    #This function is not fully transactional.  Each compressed block will be created in a transaction
    def pack(self, db, startRow = '', endRow = '\xff'):
        currentRow = startRow
        numFetched = self.packFetchCount

        while numFetched == self.packFetchCount:
            #print 'outer: \'' + repr(currentRow) + '\''
            try:
                tr = db.create_transaction()
                packedIndex = 0
                packedData = None
                lastRow = currentRow
                newPack = _PackedData()
                oldRows = []

                while 1:
                    #print 'inner: \'' + repr(currentRow) + '\''
                    unpacked = list(self._getUnpackedRange(tr, lastRow, endRow, self.packFetchCount))

                    unpackedCount = len(unpacked)
                    if len(unpacked) == 0:
                        break

                    if packedData is None:
                        subKey = self._getSubKey(unpacked[0].key)
                        packedRange = self._getPackedRange(tr, subKey)
                        [packedKeyRange, keyRange, packedData] = self._getPackedData(subKey, packedRange, False, False)
                        if packedKeyRange is not None:
                            #print 'Deleting old rows'
                            oldRows.append(fdb.tuple.pack((self.columnName, _packedPrefix, packedKeyRange[0], packedKeyRange[1])))

                    maxKey = None
                    if keyRange is not None:
                        maxKey = keyRange[1]

                    merged = self._mergeResults(packedData, unpacked, self.packFetchCount, packedIndex, lastRow, maxKey)
                    for row in merged.results:
                        oldRows.append(fdb.tuple.pack((self.columnName, _unpackedPrefix, row.key)))
                        newPack.addRow(row)
                        lastRow = row.key
                        #print 'Set lastRow = \'' + repr(lastRow) + '\''

                    lastRow = lastRow + chr(0)
                    if (maxKey is not None and merged.finishedPack) or (maxKey is None and newPack.bytes > self.targetChunkSize):
                        break

                #print 'Deleting rows'
                for row in oldRows:
                    #print 'Deleting row ' + repr(row)
                    del tr[row]

                for k,v in newPack.getPackedKeyValues(self, self.targetChunkSize, self.maxChunkSize):
                    tr[k] = v

                tr.commit().wait()
                currentRow = lastRow
                numFetched = unpackedCount
            except fdb.FDBError as e:
                if e.code == 1007: #past_version
                    pass
                    #FIXME: Unpack the overlapping packed block and try again
                tr.on_error(e.code).wait()

class _ColumnStream:

    def __init__(self, db, column, startKey):
        self.column = column
        self.db = db
        self.currentKey = startKey
        self.results = []
        self.resultsIndex = 0
        self.firstRead = True
        self.fetchCount = 5
        self.packedData = None
        self.packedIndex = 0

    def __iter__(self):
        return self

    def next(self):
        value = self._readNextRow(self.db)
        if value is None:
            raise StopIteration
        else:
            return value

    def _readNextRow(self, db):
        #print 'Reading next row'
        if self.resultsIndex >= len(self.results):
            #print 'Fetching rows'
            self._fetchRows(db)

        if self.resultsIndex >= len(self.results):
            #print 'Finished iterating: (%d/%d)' % (self.resultsIndex, len(self.results))
            return None

        else:
            self.currentKey = self.results[self.resultsIndex].key
            value = self.results[self.resultsIndex].value
            self.resultsIndex += 1
            #print 'Returning value (%s, %s)' % (self.currentKey, value)
            return (self.currentKey, value)

    @fdb.transactional
    def _fetchRows(self, tr):

        if self.firstRead:
            #print 'First fetch'
            startKey = self.currentKey
        else:
            #print 'Subsequent fetch %s' % self.currentKey
            startKey = self.currentKey + chr(0)

        #print 'Using start key %s' % startKey

        #Read next packed and unpacked entries
        #FIXME: Should we read unpacked after getting the result of the packed data?  If we do, then we can more accurately limit the number
        #of results that we get back
        unpacked = self.column._getUnpackedRange(tr, startKey, '\xff', self.fetchCount)

        if self.packedData is None:
            packedRange = self.column._getPackedRange(tr, startKey)
            [keyRange, self.packedData] = self.column._getPackedData(startKey, packedRange, False)

        merged = self.column._mergeResults(self.packedData, unpacked, self.fetchCount, self.packedIndex, startKey)

        if merged.finishedPack:
            #print 'reset packed'
            self.packedData = None
            self.packedIndex = 0
        else:
            #print 'more packed %d' % merged.packedIndex
            self.packedIndex = merged.packedIndex

        self.results = merged.results

        #print 'Getting range %s - %s (%d)' % (startKey, fdb.tuple.pack((self.column.columnName + chr(0))), self.fetchCount)
        self.resultsIndex = 0
        self.firstRead = False

class _PackedData:
    def __init__(self, packedValue = None):
        self.rows = []
        self.bytes = 0

        if packedValue is not None:
            self._unpack(packedValue)

    def addRow(self, row):
        #print 'adding row %s' % row.key
        self.rows.append(row)
        self.bytes += len(row.key) + len(row.value) + 12

    def getRow(self, rowName):
        for row in self.rows:
            if row.key == rowName:
                return row.value

        return None

    def getPackedKeyValues(self, column, targetSize, maxSize):
        rowIndex = 0
        currentByte = 0

        results = []

        while currentByte < self.bytes:
            headerItems = []
            size = targetSize
            if self.bytes - currentByte < maxSize:
                size = maxSize

            startRowIndex = rowIndex
            startKey = None
            endKey = None
            bodyLength = 0
            packBytes = 0
            while rowIndex < len(self.rows) and packBytes < size:
                row = self.rows[rowIndex]
                headerItems.append(struct.pack('iii', len(row.key), len(row.value), bodyLength) + row.key)
                bodyLength += len(row.value)
                packBytes += len(row.key) + len(row.value) + 12
                rowIndex += 1

                if startKey is None:
                    startKey = row.key
                endKey = row.key

            header = ''.join(headerItems)
            body = ''.join(row.value for row in self.rows[startRowIndex:rowIndex])

            results.append(fdb.KeyValue(fdb.tuple.pack((column.columnName, _packedPrefix, startKey, endKey)), struct.pack('i', len(header)) + header + body))
            currentByte += packBytes

        return results

    def _unpack(self, str):
        self.bytes = len(str)
        headerLength = struct.unpack('i', str[0:4])[0]
        header = str[4:4+headerLength]
        body = str[4+headerLength:]

        index = 0
        while index < headerLength:
            #print 'header length: %d, %d' % (len(self.header), index)
            (keyLength, valueLength, valueOffset) = struct.unpack('iii', header[index:index+12])
            key = header[index+12:index+12+keyLength]
            index = index + 12 + keyLength
            value = body[valueOffset:valueOffset+valueLength]
            self.rows.append(fdb.KeyValue(key, value))
