# Copyright (c) 2014 The Johns Hopkins University/Applied Physics Laboratory
# All Rights Reserved.
#
# 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 logging
import six
import sys

from struct import pack, unpack
from enum import Enum

from kmip.core.enums import Types
from kmip.core.enums import Tags

from kmip.core.errors import ErrorStrings

from kmip.core import errors
from kmip.core import utils


class Base(object):
    TAG_SIZE = 3
    TYPE_SIZE = 1
    LENGTH_SIZE = 4

    def __init__(self, tag=Tags.DEFAULT, type=Types.DEFAULT):
        self.tag = tag
        self.type = type
        self.length = None

    # TODO (peter-hamilton) Convert this into a classmethod, class name can be
    #                       obtained from cls parameter that replaces self
    def is_oversized(self, stream):
        extra = len(stream.peek())
        if extra > 0:
            raise errors.StreamNotEmptyError(Base.__name__, extra)

    def read_tag(self, istream):
        # Read in the bytes for the tag
        tts = istream.read(self.TAG_SIZE)
        tag = unpack('!I', b'\x00' + tts[0:self.TAG_SIZE])[0]

        enum_tag = Tags(tag)

        # Verify that the tag matches for the current object
        if enum_tag is not self.tag:
            raise errors.ReadValueError(Base.__name__, 'tag',
                                        hex(self.tag.value), hex(tag))

    def read_type(self, istream):
        # Read in the bytes for the type
        tts = istream.read(self.TYPE_SIZE)
        num_bytes = len(tts)
        if num_bytes != self.TYPE_SIZE:
            min_bytes = 'a minimum of {0} bytes'.format(self.TYPE_SIZE)
            raise errors.ReadValueError(Base.__name__, 'type', min_bytes,
                                        '{0} bytes'.format(num_bytes))
        typ = unpack('!B', tts)[0]

        enum_typ = Types(typ)

        if enum_typ is not self.type:
            raise errors.ReadValueError(Base.__name__, 'type',
                                        self.type.value, typ)

    def read_length(self, istream):
        # Read in the bytes for the length
        lst = istream.read(self.LENGTH_SIZE)
        num_bytes = len(lst)
        if num_bytes != self.LENGTH_SIZE:
            min_bytes = 'a minimum of {0} bytes'.format(self.LENGTH_SIZE)
            raise errors.ReadValueError(Base.__name__, 'length', min_bytes,
                                        '{0} bytes'.format(num_bytes))
        self.length = unpack('!I', lst)[0]

    def read_value(self, istream):
        raise NotImplementedError()

    def read(self, istream):
        self.read_tag(istream)
        self.read_type(istream)
        self.read_length(istream)

    def write_tag(self, ostream):
        # Write the tag to the output stream
        ostream.write(pack('!I', self.tag.value)[1:])

    def write_type(self, ostream):
        if type(self.type) is not Types:
            msg = ErrorStrings.BAD_EXP_RECV
            raise TypeError(msg.format(Base.__name__, 'type',
                                       Types, type(self.type)))
        ostream.write(pack('!B', self.type.value))

    def write_length(self, ostream):
        if type(self.length) is not int:
            msg = ErrorStrings.BAD_EXP_RECV
            raise TypeError(msg.format(Base.__name__, 'length',
                                       int, type(self.length)))
        num_bytes = utils.count_bytes(self.length)
        if num_bytes > self.LENGTH_SIZE:
            raise errors.WriteOverflowError(Base.__name__, 'length',
                                            self.LENGTH_SIZE, num_bytes)
        ostream.write(pack('!I', self.length))

    def write_value(self, ostream):
        raise NotImplementedError()

    def write(self, ostream):
        self.write_tag(ostream)
        self.write_type(ostream)
        self.write_length(ostream)

    def validate(self):
        raise NotImplementedError()

    @staticmethod
    def is_tag_next(tag, stream):
        next_tag = stream.peek(Base.TAG_SIZE)
        if len(next_tag) != Base.TAG_SIZE:
            return False
        next_tag = unpack('!I', b'\x00' + next_tag)[0]
        if next_tag == tag.value:
            return True
        else:
            return False

    @staticmethod
    def is_type_next(kmip_type, stream):
        tag_type_size = Base.TAG_SIZE + Base.TYPE_SIZE
        tt = stream.peek(tag_type_size)

        if len(tt) != tag_type_size:
            return False

        typ = unpack('!B', tt[Base.TAG_SIZE:])[0]

        if typ == kmip_type.value:
            return True
        else:
            return False


class Struct(Base):

    def __init__(self, tag=Tags.DEFAULT):
        super(Struct, self).__init__(tag, type=Types.STRUCTURE)

    # NOTE (peter-hamilton) If seen, should indicate repr needs to be defined
    def __repr__(self):
        return "Struct()"


class Integer(Base):
    LENGTH = 4

    # Set for signed 32-bit integers
    MIN = -2147483648
    MAX = 2147483647

    def __init__(self, value=None, tag=Tags.DEFAULT, signed=True):
        super(Integer, self).__init__(tag, type=Types.INTEGER)

        self.value = value
        if self.value is None:
            self.value = 0

        self.length = self.LENGTH
        self.padding_length = self.LENGTH
        if signed:
            self.pack_string = '!i'
        else:
            self.pack_string = '!I'

        self.validate()

    def read_value(self, istream):
        if self.length is not self.LENGTH:
            raise errors.ReadValueError(Integer.__name__, 'length',
                                        self.LENGTH, self.length)

        self.value = unpack(self.pack_string, istream.read(self.length))[0]
        pad = unpack(self.pack_string, istream.read(self.padding_length))[0]

        if pad is not 0:
            raise errors.ReadValueError(Integer.__name__, 'pad', 0,
                                        pad)
        self.validate()

    def read(self, istream):
        super(Integer, self).read(istream)
        self.read_value(istream)

    def write_value(self, ostream):
        ostream.write(pack(self.pack_string, self.value))
        ostream.write(pack(self.pack_string, 0))

    def write(self, ostream):
        super(Integer, self).write(ostream)
        self.write_value(ostream)

    def validate(self):
        """
        Verify that the value of the Integer object is valid.

        Raises:
            TypeError: if the value is not of type int or long
            ValueError: if the value cannot be represented by a signed 32-bit
                integer
        """
        if self.value is not None:
            if type(self.value) not in six.integer_types:
                raise TypeError('expected (one of): {0}, observed: {1}'.format(
                    six.integer_types, type(self.value)))
            else:
                if self.value > Integer.MAX:
                    raise ValueError('integer value greater than accepted max')
                elif self.value < Integer.MIN:
                    raise ValueError('integer value less than accepted min')

    def __repr__(self):
        return "{0}(value={1})".format(type(self).__name__, repr(self.value))

    def __str__(self):
        return "{0}".format(repr(self.value))

    def __eq__(self, other):
        if isinstance(other, Integer):
            return self.value == other.value
        else:
            return NotImplemented

    def __ne__(self, other):
        if isinstance(other, Integer):
            return not self.__eq__(other)
        else:
            return NotImplemented


class LongInteger(Base):
    LENGTH = 8

    def __init__(self, value=None, tag=Tags.DEFAULT):
        super(LongInteger, self).__init__(tag, type=Types.LONG_INTEGER)
        self.value = value
        self.length = self.LENGTH

        self.validate()

    def read_value(self, istream):
        if self.length is not self.LENGTH:
            raise errors.ReadValueError(LongInteger.__name__, 'length',
                                        self.LENGTH, self.length)

        self.value = unpack('!q', istream.read(self.length))[0]
        self.validate()

    def read(self, istream):
        super(LongInteger, self).read(istream)
        self.read_value(istream)

    def write_value(self, ostream):
        ostream.write(pack('!q', self.value))

    def write(self, ostream):
        super(LongInteger, self).write(ostream)
        self.write_value(ostream)

    def validate(self):
        self.__validate()

    def __validate(self):
        if self.value is not None:
            data_type = type(self.value)
            if data_type not in six.integer_types:
                raise errors.StateTypeError(
                    LongInteger.__name__, "{0}".format(six.integer_types),
                    data_type)
            num_bytes = utils.count_bytes(self.value)
            if num_bytes > self.length:
                raise errors.StateOverflowError(
                    LongInteger.__name__, 'value', self.length, num_bytes)

    def __repr__(self):
        return '<Long Integer, %d>' % (self.value)


class BigInteger(Base):
    BLOCK_SIZE = 8
    SHIFT_SIZE = 64

    def __init__(self, value=None, tag=Tags.DEFAULT):
        super(BigInteger, self).__init__(tag, type=Types.BIG_INTEGER)
        self.value = value

        if self.value is not None:
            self.real_length = utils.count_bytes(self.value)
            self.padding_length = self.BLOCK_SIZE - (self.length %
                                                     self.BLOCK_SIZE)
            if self.padding_length == self.BLOCK_SIZE:
                self.padding_length = 0
        else:
            self.length = None
            self.padding_length = None

        self.validate()

    def read_value(self, istream):
        if (self.length < self.BLOCK_SIZE) or (self.length % self.BLOCK_SIZE):
            raise errors.InvalidLengthError(BigInteger.__name__,
                                            ('multiple'
                                             'of {0}'.format(self.BLOCK_SIZE)),
                                            self.length)
        self.value = 0
        num_blocks = self.length / self.BLOCK_SIZE

        # Read first block as signed data
        self.value = unpack('!q', str(istream.read(self.BLOCK_SIZE)))[0]

        # Shift current value and add on next unsigned block
        for _ in range(num_blocks - 1):
            self.value = self.value << self.SHIFT_SIZE
            stream_data = istream.read(self.BLOCK_SIZE)
            self.value += unpack('!Q', stream_data)[0]

        self.validate()

    def read(self, istream):
        super(BigInteger, self).read(istream)
        self.read_value(istream)

    def write_value(self, ostream):
        # 1. Determine the sign of the value (+/-); save it.
        # 2. Extend hex of value with 0s until encoding is right size (8x).
        # 3. Write out each block of the encoding as signed, 2s complement:
        #    pack('!q', sign * block)

        # Determine sign for padding
        pad_byte = 0x00
        pad_nybl = 0x0

        if self.value < 0:
            pad_byte = 0xff
            pad_nybl = 0xf

        # Compose padding bytes
        pad = ''
        for _ in range(self.padding_length):
            pad += hex(pad_byte)[2:]

        str_rep = hex(self.value).rstrip("Ll")[2:]
        if len(str_rep) % 2:
            pad += hex(pad_nybl)[2]

        # Compose value for block-based write
        str_rep = pad + str_rep
        num_blocks = len(str_rep) / self.BLOCK_SIZE

        # Write first block as signed data
        block = int(str_rep[0:self.BLOCK_SIZE], 16)
        ostream.write(pack('!q', block))

        # Write remaining blocks as unsigned data
        for i in range(1, num_blocks):
            block = str_rep[(self.BLOCK_SIZE * i):(self.BLOCK_SIZE * (i + 1))]
            block = int(block, 16)
            ostream.write(pack('!Q', block))

    def write(self, ostream):
        super(BigInteger, self).write(ostream)
        self.write_value(ostream)

    def validate(self):
        self.__validate()

    def __validate(self):
        if self.value is not None:
            data_type = type(self.value)
            if data_type not in six.integer_types:
                raise errors.StateTypeError(
                    BigInteger.__name__, "{0}".format(six.integer_types),
                    data_type)
            num_bytes = utils.count_bytes(self.length)
            if num_bytes > self.LENGTH_SIZE:
                raise errors.StateOverflowError(
                    BigInteger.__name__, 'length', self.LENGTH_SIZE,
                    num_bytes)


class Enumeration(Integer):
    ENUM_TYPE = None

    def __init__(self, value=None, tag=Tags.DEFAULT):
        self.enum = value
        self.validate()

        if self.enum is None:
            super(Enumeration, self).__init__(None, tag, False)
        else:
            super(Enumeration, self).__init__(self.enum.value, tag, False)
        self.type = Types.ENUMERATION

    def read(self, istream):
        super(Enumeration, self).read(istream)
        self.enum = self.ENUM_TYPE(self.value)
        self.validate()

    def write(self, ostream):
        super(Enumeration, self).write(ostream)

    def validate(self):
        self.__validate()

    def __validate(self):
        if self.enum is not None:
            if not isinstance(self.enum, Enum):
                raise TypeError("expected {0}, observed {1}".format(
                    type(self.enum), Enum))

    def __repr__(self):
        return "{0}(value={1})".format(type(self).__name__, self.enum)

    def __str__(self):
        return "{0}.{1}".format(type(self.enum).__name__, self.enum.name)


class Boolean(Base):
    """
    An encodeable object representing a boolean value.

    A Boolean is one of the KMIP primitive object types. It is encoded as an
    unsigned, big-endian, 8-byte value, capable of taking the values True (1)
    or False (0). For more information, see Section 9.1 of the KMIP 1.1
    specification.
    """
    LENGTH = 8

    def __init__(self, value=True, tag=Tags.DEFAULT):
        """
        Create a Boolean object.

        Args:
            value (bool): The value of the Boolean. Optional, defaults to True.
            tag (Tags): An enumeration defining the tag of the Boolean object.
                Optional, defaults to Tags.DEFAULT.
        """
        super(Boolean, self).__init__(tag, type=Types.BOOLEAN)
        self.logger = logging.getLogger(__name__)
        self.value = value
        self.length = self.LENGTH

        self.validate()

    def read_value(self, istream):
        """
        Read the value of the Boolean object from the input stream.

        Args:
            istream (Stream): A buffer containing the encoded bytes of the
                value of a Boolean object. Usually a BytearrayStream object.
                Required.

        Raises:
            ValueError: if the read boolean value is not a 0 or 1.
        """
        try:
            value = unpack('!Q', istream.read(self.LENGTH))[0]
        except:
            self.logger.error("Error reading boolean value from buffer")
            raise

        if value == 1:
            self.value = True
        elif value == 0:
            self.value = False
        else:
            raise ValueError("expected: 0 or 1, observed: {0}".format(value))

        self.validate()

    def read(self, istream):
        """
        Read the encoding of the Boolean object from the input stream.

        Args:
            istream (Stream): A buffer containing the encoded bytes of a
                Boolean object. Usually a BytearrayStream object. Required.
        """
        super(Boolean, self).read(istream)
        self.read_value(istream)

    def write_value(self, ostream):
        """
        Write the value of the Boolean object to the output stream.

        Args:
            ostream (Stream): A buffer to contain the encoded bytes of the
                value of a Boolean object. Usually a BytearrayStream object.
                Required.
        """
        try:
            ostream.write(pack('!Q', self.value))
        except:
            self.logger.error("Error writing boolean value to buffer")
            raise

    def write(self, ostream):
        """
        Write the encoding of the Boolean object to the output stream.

        Args:
            ostream (Stream): A buffer to contain the encoded bytes of a
                Boolean object. Usually a BytearrayStream object. Required.
        """
        super(Boolean, self).write(ostream)
        self.write_value(ostream)

    def validate(self):
        """
        Verify that the value of the Boolean object is valid.

        Raises:
            TypeError: if the value is not of type bool.
        """
        if self.value:
            if not isinstance(self.value, bool):
                raise TypeError("expected: {0}, observed: {1}".format(
                    bool, type(self.value)))

    def __repr__(self):
        return "{0}(value={1})".format(type(self).__name__, repr(self.value))

    def __str__(self):
        return "{0}".format(repr(self.value))

    def __eq__(self, other):
        if isinstance(other, Boolean):
            return self.value == other.value
        else:
            return NotImplemented

    def __ne__(self, other):
        if isinstance(other, Boolean):
            return not self.__eq__(other)
        else:
            return NotImplemented


class TextString(Base):
    PADDING_SIZE = 8
    BYTE_FORMAT = '!c'

    def __init__(self, value=None, tag=Tags.DEFAULT):
        super(TextString, self).__init__(tag, type=Types.TEXT_STRING)

        if value is None:
            self.value = ''
        else:
            self.value = value

        self.validate()

        if self.value is not None:
            self.length = len(self.value)
            self.padding_length = self.PADDING_SIZE - (self.length %
                                                       self.PADDING_SIZE)
            if self.padding_length == self.PADDING_SIZE:
                self.padding_length = 0
        else:
            self.length = None
            self.padding_length = None

    def read_value(self, istream):
        # Read string text
        self.value = ''
        for _ in range(self.length):
            c = unpack(self.BYTE_FORMAT, istream.read(1))[0]
            if sys.version >= '3':
                c = c.decode()
            self.value += c

        # Read padding and check content
        self.padding_length = self.PADDING_SIZE - (self.length %
                                                   self.PADDING_SIZE)
        if self.padding_length < self.PADDING_SIZE:
            for _ in range(self.padding_length):
                pad = unpack('!B', istream.read(1))[0]
                if pad is not 0:
                    raise errors.ReadValueError(TextString.__name__, 'pad', 0,
                                                pad)

    def read(self, istream):
        super(TextString, self).read(istream)
        self.read_value(istream)
        self.validate()

    def write_value(self, ostream):
        # Write string to stream
        for char in self.value:
            if sys.version < '3':
                c = char
            else:
                c = char.encode()
            ostream.write(pack(self.BYTE_FORMAT, c))

        # Write padding to stream
        for _ in range(self.padding_length):
            ostream.write(pack('!B', 0))

    def write(self, ostream):
        super(TextString, self).write(ostream)
        self.write_value(ostream)

    def validate(self):
        self.__validate()

    def __validate(self):
        if self.value is not None:
            data_type = type(self.value)
            if data_type is not str:
                msg = ErrorStrings.BAD_EXP_RECV
                raise TypeError(msg.format('TextString', 'value', str,
                                           data_type))

    def __repr__(self):
        return "{0}(value={1})".format(type(self).__name__, repr(self.value))

    def __str__(self):
        return "{0}".format(repr(self.value))

    def __eq__(self, other):
        if isinstance(other, TextString):
            return self.value == other.value
        else:
            return NotImplemented

    def __ne__(self, other):
        if isinstance(other, TextString):
            return not (self == other)
        else:
            return NotImplemented


class ByteString(Base):
    PADDING_SIZE = 8
    BYTE_FORMAT = '!B'

    def __init__(self, value=None, tag=Tags.DEFAULT):
        super(ByteString, self).__init__(tag, type=Types.BYTE_STRING)

        if value is None:
            self.value = bytes()
        else:
            self.value = bytes(value)

        self.validate()

        if self.value is not None:
            self.length = len(self.value)
            self.padding_length = self.PADDING_SIZE - (self.length %
                                                       self.PADDING_SIZE)
            if self.padding_length == self.PADDING_SIZE:
                self.padding_length = 0
        else:
            self.length = None
            self.padding_length = None

    def read_value(self, istream):
        # Read bytes into bytearray
        data = bytearray()
        for _ in range(self.length):
            data.append(istream.read(1)[0])
        self.value = bytes(data)

        # Read padding and check content
        self.padding_length = self.PADDING_SIZE - (self.length %
                                                   self.PADDING_SIZE)
        if self.padding_length == self.PADDING_SIZE:
            self.padding_length = 0

        if self.padding_length < self.PADDING_SIZE:
            for _ in range(self.padding_length):
                pad = unpack('!B', istream.read(1))[0]
                if pad is not 0:
                    raise errors.ReadValueError(TextString.__name__, 'pad', 0,
                                                pad)

    def read(self, istream):
        super(ByteString, self).read(istream)
        self.read_value(istream)

    def write_value(self, ostream):
        # Write bytes to stream
        data = bytearray(self.value)
        for byte in data:
            ostream.write(pack(self.BYTE_FORMAT, byte))

        # Write padding to stream
        for _ in range(self.padding_length):
            ostream.write(pack('!B', 0))

    def write(self, ostream):
        super(ByteString, self).write(ostream)
        self.write_value(ostream)

    def validate(self):
        self.__validate()

    def __validate(self):
        # TODO (peter-hamilton) Test is pointless, value is always bytes. Fix.
        if self.value is not None:
            data_type = type(self.value)
            if data_type is not bytes:
                msg = ErrorStrings.BAD_EXP_RECV
                raise TypeError(msg.format('ByteString', 'value', bytes,
                                           data_type))

    def __repr__(self):
        return "{0}(value={1})".format(type(self).__name__, repr(self.value))

    def __str__(self):
        return "{0}".format(str(self.value))

    def __eq__(self, other):
        if isinstance(other, ByteString):
            return self.value == other.value
        else:
            return NotImplemented

    def __ne__(self, other):
        if isinstance(other, ByteString):
            return not (self == other)
        else:
            return NotImplemented


class DateTime(LongInteger):

    def __init__(self, value=None, tag=Tags.DEFAULT):
        super(DateTime, self).__init__(value, tag)
        self.type = Types.DATE_TIME


class Interval(Integer):

    def __init__(self, value=None, tag=Tags.DEFAULT):
        super(Interval, self).__init__(value, tag)
        self.type = Types.INTERVAL
