# Copyright (c) 2014 Dark Secret Software Inc.
#
# 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 struct


class InvalidVersion(Exception):
    pass


class OutOfSync(Exception):
    pass


class EndOfFile(Exception):
    pass


BOR_MAGIC_NUMBER = 0x69867884


class Version0(object):
    # Preamble ... same for all versions.
    # i = 0x69867884  (EVNT)
    # h = version

    def __init__(self):
        self.preamble_schema = "ih"
        self.preamble_size = struct.calcsize(self.preamble_schema)

    def make_preamble(self, version):
        return struct.pack(self.preamble_schema, BOR_MAGIC_NUMBER, version)

    def _check_eof(self, expected, actual):
        if actual < expected:
            raise EndOfFile()

    def load_preamble(self, file_handle):
        raw = file_handle.read(self.preamble_size)
        self._check_eof(self.preamble_size, len(raw))
        header = struct.unpack(self.preamble_schema, raw)
        if header[0] != BOR_MAGIC_NUMBER:
            raise OutOfSync("Expected Beginning of Record marker")
        return header[1]


class Version1(Version0):
    # Version 1 SCHEMA
    # ----------------
    # i = metadata block length
    # i = raw notification block length
    # i = 0x00000000 EOR

    # Metadata dict block
    # i = number of strings (N) - key/value = 2 strings
    # N * i = length of key followed by length of value
    # N * (*s) = key followed by value

    # Raw notification block
    # i = length of raw data block
    # *s = raw data

    # EXAMPLE
    # --------
    # With above Event and Metadata
    #
    # Header schema: "iii"
    # Metadata length: 119
    # Raw notification length: 201
    # Metadata = 6 strings (3 key-value pairs)
    # Metadata schema: "iiiiiii6s14s10s31s10s20s"
    #                                     ------ key/value
    #                               ------ key/value
    #                          ----- key/value
    #                    ------ length of the 6 strings
    #                   - 12 entries (6 string sizes + 6 strings)
    # Raw notification: "i197s"
    #                     ---- json notification
    #                    - 197

    def __init__(self):
        super(Version1, self).__init__()
        self.header_schema = "iii"
        self.header_size = struct.calcsize(self.header_schema)

    def _encode(self, s):
        if isinstance(s, unicode):
            return s.encode('utf-8')
        return s

    def pack(self, notification, metadata):
        nsize = len(notification)
        raw_block_schema = "i%ds" % nsize
        raw_block = struct.pack(raw_block_schema, nsize, notification)

        metadata_items = ["i"]  # appended with N "%ds"'s
        metadata_values = [len(metadata) * 4]  # [n]=key, [n+1]=value
        for key, value in metadata.iteritems():
            key = self._encode(key)
            value = self._encode(value)
            metadata_items.append("i")
            metadata_items.append("i")
            metadata_values.append(len(key))
            metadata_values.append(len(value))

        for key, value in metadata.iteritems():
            key = self._encode(key)
            value = self._encode(value)
            metadata_items.append("%ds" % len(key))
            metadata_values.append(key)
            metadata_items.append("%ds" % len(value))
            metadata_values.append(value)
        metadata_schema = "".join(metadata_items)

        metadata = struct.pack(metadata_schema, *metadata_values)

        header = struct.pack(self.header_schema,
                             struct.calcsize(metadata_schema),
                             struct.calcsize(raw_block_schema), 0)

        preamble = self.make_preamble(1)
        return (preamble, header, metadata, raw_block)

    def unpack(self, file_handle):
        header_bytes = file_handle.read(self.header_size)
        self._check_eof(self.header_size, len(header_bytes))
        header = struct.unpack(self.header_schema, header_bytes)

        if header[2] != 0:
            raise OutOfSync("Didn't find 0 EOR marker.")

        metadata_bytes = file_handle.read(header[0])
        self._check_eof(header[0], len(metadata_bytes))
        num_strings = struct.unpack_from("i", metadata_bytes)
        offset = struct.calcsize("i")
        lengths = num_strings[0] / 2
        lengths_schema = "i" * lengths
        key_value_sizes = struct.unpack_from(lengths_schema, metadata_bytes,
                                             offset=offset)
        key_value_schema_list = ["%ds" % sz for sz in key_value_sizes]
        key_value_schema = "".join(key_value_schema_list)
        offset += struct.calcsize(lengths_schema)
        key_values = struct.unpack_from(key_value_schema, metadata_bytes,
                                        offset=offset)
        metadata = dict((key_values[n], key_values[n + 1])
                        for n in range(len(key_values))[::2])

        raw = file_handle.read(header[1])
        self._check_eof(header[1], len(raw))
        raw_len = struct.unpack_from("i", raw)
        offset = struct.calcsize("i")
        jnot = struct.unpack_from("%ds" % raw_len[0], raw, offset=offset)
        return (metadata, jnot[0])


VERSIONS = {1: Version1()}
CURRENT_VERSION = 1


def get_version_handler(version=CURRENT_VERSION):
    global VERSIONS

    version_handler = VERSIONS.get(version)
    if not version_handler:
        raise InvalidVersion()
    return version_handler


def pack_notification(notification, metadata, version=CURRENT_VERSION):
    version_handler = get_version_handler(version)
    return version_handler.pack(notification, metadata)


def unpack_notification(file_handle):
    v0 = Version0()
    version = v0.load_preamble(file_handle)
    version_handler = get_version_handler(version)
    return version_handler.unpack(file_handle)