# Copyright (c) 2014  Kontron Europe GmbH
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA

from __future__ import absolute_import
from __future__ import division

import math
from . import errors

from .errors import DecodingError
from .fields import SdrTypeLengthString
from .utils import check_completion_code, ByteBuffer
from .msgs import create_request_by_name

from .helper import get_sdr_data_helper, clear_repository_helper
from .helper import get_sdr_chunk_helper
from .state import State

SDR_TYPE_FULL_SENSOR_RECORD = 0x01
SDR_TYPE_COMPACT_SENSOR_RECORD = 0x02
SDR_TYPE_EVENT_ONLY_SENSOR_RECORD = 0x03
SDR_TYPE_ENTITY_ASSOCIATION_RECORD = 0x08
SDR_TYPE_FRU_DEVICE_LOCATOR_RECORD = 0x11
SDR_TYPE_MANAGEMENT_CONTROLLER_DEVICE_LOCATOR_RECORD = 0x12
SDR_TYPE_MANAGEMENT_CONTROLLER_CONFIRMATION_RECORD = 0x13
SDR_TYPE_BMC_MESSAGE_CHANNEL_INFO_RECORD = 0x14
SDR_TYPE_OEM_SENSOR_RECORD = 0xC0

GET_INITIALIZATION_AGENT_STATUS = 0
RUN_INITIALIZATION_AGENT = 1

L_LINEAR = 0
L_LN = 1
L_LOG = 2
L_LOG2 = 3
L_E = 4
L_EXP10 = 5
L_EXP2 = 6
L_1_X = 7
L_SQR = 8
L_CUBE = 9
L_SQRT = 10
L_CUBERT = 11


class Sdr(object):
    def __init__(self):
        pass

    def get_sdr_repository_info(self):
        return SdrRepositoryInfo(
                self.send_message_with_name('GetSdrRepositoryInfo'))

    def get_sdr_repository_allocation_info(self):
        return SdrRepositoryAllocationInfo(
                self.send_message_with_name('GetSdrRepositoryAllocationInfo'))

    def reserve_sdr_repository(self):
        rsp = self.send_message_with_name('ReserveSdrRepository')
        return rsp.reservation_id

    def _get_sdr_chunk(self, reservation_id, record_id, offset, length):
        req = create_request_by_name('GetSdr')
        req.reservation_id = reservation_id
        req.record_id = record_id
        req.offset = offset
        req.bytes_to_read = length

        rsp = get_sdr_chunk_helper(self.send_message, req,
                                   self.reserve_device_sdr_repository)

        return (rsp.next_record_id, rsp.record_data)

    def get_repository_sdr(self, record_id, reservation_id=None):
        (next_id, record_data) = get_sdr_data_helper(
                self.reserve_sdr_repository, self._get_sdr_chunk,
                record_id, reservation_id)
        return SdrCommon.from_data(record_data, next_id)

    def sdr_repository_entries(self):
        """A generator that returns the SDR list.

        The Generator starts with ID=0x0000 and ends when ID=0xffff
        is returned.
        """
        reservation_id = self.reserve_sdr_repository()
        record_id = 0

        while True:
            s = self.get_repository_sdr(record_id, reservation_id)
            yield s
            if s.next_id == 0xffff:
                break
            record_id = s.next_id

    def get_repository_sdr_list(self, reservation_id=None):
        """Return the complete SDR list."""
        return list(self.sdr_repository_entries())

    def partial_add_sdr(self, reservation_id, record_id,
                        offset, progress, data):

        req = create_request_by_name('PartialAddSdr')
        req.reservation_id = reservation_id
        req.record_id = record_id
        req.offset = offset
        req.status.in_progress = progress
        req.data = data
        rsp = self.send_message(req)
        check_completion_code(rsp.completion_code)
        return rsp.record_id

    def delete_sdr(self, record_id):
        """Delete the sensor record specified by 'record_id'."""
        reservation_id = self.reserve_device_sdr_repository()
        rsp = self.send_message_with_name('DeleteSdr',
                                          reservation_id=reservation_id,
                                          record_id=record_id)
        return rsp.record_id

    def _clear_sdr_repository(self, cmd, reservation_id):
        rsp = self.send_message_with_name('ClearSdrRepository',
                                          reservation_id=reservation_id,
                                          cmd=cmd)
        return rsp.status.erase_in_progress

    def clear_sdr_repository(self, retry=5):
        clear_repository_helper(self.reserve_sdr_repository,
                                self._clear_sdr_repository, retry)

    def _run_initialization_agent(self, cmd):
        rsp = self.send_message_with_name('RunInitializationAgent', cmd=cmd)
        return rsp.status.initialization_completed

    def start_initialization_agent(self):
        self._run_initialization_agent(RUN_INITIALIZATION_AGENT)

    def get_initialization_agent_status(self):
        return self._run_initialization_agent(GET_INITIALIZATION_AGENT_STATUS)


class SdrRepositoryInfo(State):
    def __init__(self, rsp):
        if rsp:
            self._from_response(rsp)

    def _from_response(self, rsp):
        self.sdr_version = rsp.sdr_version
        self.record_count = rsp.record_count
        self.free_space = rsp.free_space
        self.most_recent_addition = rsp.most_recent_addition
        self.support_get_allocation_info = rsp.support.get_allocation_info
        self.support_reserve = rsp.support.reserve
        self.support_partial_add = rsp.support.partial_add
        self.support_delete = rsp.support.delete
        self.support_update_type = rsp.support.update_type
        self.support_overflow_flag = rsp.support.overflow_flag


class SdrRepositoryAllocationInfo(State):
    def __init__(self, rsp):
        if rsp:
            self._from_response(rsp)

    def _from_response(self, rsp):
        self.number_of_units = rsp.number_of_units
        self.unit_size = rsp.unit_size
        self.free_units = rsp.free_units
        self.largest_free_block = rsp.largest_free_block
        self.maximum_record_size = rsp.maximum_record_size


class SdrCommon(object):
    def __init__(self, data=None, next_id=None):
        if data:
            self.data = data
            self._common_header(data)

            if hasattr(self, '_from_data'):
                self._from_data(data)

        if next_id:
            self.next_id = next_id

    def __str__(self):
        if hasattr(self, 'device_id_string'):
            s = '["%s"] [%s]' % \
                 (self.device_id_string,
                  ' '.join(['%02x' % b for b in self.data]))
        else:
            s = '[%s]' % \
                 (' '.join(['%02x' % b for b in self.data]))
        return s

    def _common_header(self, data):
        buffer = ByteBuffer(data[:])
        try:
            self.id = buffer.pop_unsigned_int(2)
            self.version = buffer.pop_unsigned_int(1)
            self.type = buffer.pop_unsigned_int(1)
            self.length = buffer.pop_unsigned_int(1)
        except IndexError:
            raise DecodingError('Invalid SDR length (%d)' % len(data))

    def _common_record_key(self, buffer):
        self.owner_id = buffer.pop_unsigned_int(1)
        self.owner_lun = buffer.pop_unsigned_int(1) & 0x3
        self.number = buffer.pop_unsigned_int(1)

    def _entity(self, buffer):
        self.entity_id = buffer.pop_unsigned_int(1)
        self.entity_instance = buffer.pop_unsigned_int(1)

    def _device_id_string(self, buffer):
        self.device_id_string_type = (buffer[0] & 0xc0) >> 4
        self.device_id_string_length = buffer[0] & 0x3f
        field = SdrTypeLengthString(data=buffer[0:1+self.device_id_string_length])
        self.device_id_string = field.string
#        self.device_id_string = \
#            buffer.pop_string(self.device_id_string_length & 0x3f)

    @staticmethod
    def from_data(data, next_id=None):
        sdr_type = data[3]

        cls = {
            SDR_TYPE_FULL_SENSOR_RECORD:
                SdrFullSensorRecord,
            SDR_TYPE_COMPACT_SENSOR_RECORD:
                SdrCompactSensorRecord,
            SDR_TYPE_EVENT_ONLY_SENSOR_RECORD:
                SdrEventOnlySensorRecord,
            SDR_TYPE_FRU_DEVICE_LOCATOR_RECORD:
                SdrFruDeviceLocator,
            SDR_TYPE_MANAGEMENT_CONTROLLER_DEVICE_LOCATOR_RECORD:
                SdrManagementControllerDeviceLocator,
            SDR_TYPE_MANAGEMENT_CONTROLLER_CONFIRMATION_RECORD:
                SdrManagementControllerConfirmationRecord,
            SDR_TYPE_OEM_SENSOR_RECORD:
                SdrOEMSensorRecord,
        }.get(sdr_type, SdrUnknownSensorRecord)

        return cls(data, next_id)


###
# SDR type 0x01
##################################################
class SdrFullSensorRecord(SdrCommon):
    DATA_FMT_UNSIGNED = 0
    DATA_FMT_1S_COMPLEMENT = 1
    DATA_FMT_2S_COMPLEMENT = 2
    DATA_FMT_NONE = 3

    def __init__(self, data=None, next_id=None):
        super(SdrFullSensorRecord, self).__init__(data, next_id)

    def __str__(self):
        s = '["%s"] [%s:%s] [%s]' \
                % (self.device_id_string,
                   self.entity_id,
                   self.entity_instance,
                   ' '.join(['%02x' % b for b in self.data]))
        return s

    def convert_sensor_raw_to_value(self, raw):
        if raw is None:
            return None
        fmt = self.analog_data_format
        if (fmt == self.DATA_FMT_1S_COMPLEMENT):
            if raw & 0x80:
                raw = -((raw & 0x7f) ^ 0x7f)
        elif (fmt == self.DATA_FMT_2S_COMPLEMENT):
            if raw & 0x80:
                raw = -((raw & 0x7f) ^ 0x7f) - 1
        raw = float(raw)

        return self.lin((self.m * raw + (self.b * 10**self.k1)) * 10**self.k2)

    def convert_sensor_value_to_raw(self, value):
        linearization = self.linearization & 0x7f

        if linearization is not L_LINEAR:
            raise NotImplementedError()

        raw = ((float(value) * 10**(-1 * self.k2))
               / self.m) - (self.b * 10**self.k1)

        fmt = self.analog_data_format
        if (fmt == self.DATA_FMT_1S_COMPLEMENT):
            if value < 0:
                raise NotImplementedError()
        elif (fmt == self.DATA_FMT_2S_COMPLEMENT):
            if value < 0:
                raise NotImplementedError()

        raw = int(round(raw))
        if raw > 0xff:
            raise ValueError()

        return raw

    @property
    def lin(self):
        try:
            return {
                L_LN: math.log,
                L_LOG: lambda x: math.log(x, 10),
                L_LOG2: lambda x: math.log(x, 2),
                L_E: math.exp,
                L_EXP10: lambda x: math.pow(10, x),
                L_EXP2: lambda x: math.pow(2, x),
                L_1_X: lambda x: 1.0 / x,
                L_SQR: lambda x: math.pow(x, 2),
                L_CUBE: lambda x: math.pow(x, 3),
                L_SQRT: math.sqrt,
                L_CUBERT: lambda x: math.pow(x, 1.0/3),
                L_LINEAR: lambda x: x,
            }[self.linearization & 0x7f]
        except KeyError:
            raise errors.DecodingError('unknown linearization %d' %
                                       (self.linearization & 0x7f))

    @staticmethod
    def _convert_complement(value, size):
        if (value & (1 << (size - 1))):
            value = -(1 << size) + value
        return value

    def _decode_capabilities(self, capabilities):
        self.capabilities = []

        # ignore sensor
        if capabilities & 0x80:
            self.capabilities.append('ignore_sensor')
        # sensor auto re-arm support
        if capabilities & 0x40:
            self.capabilities.append('auto_rearm')
        # sensor hysteresis support
        HYSTERESIS_MASK = 0x30
        HYSTERESIS_IS_NOT_SUPPORTED = 0x00
        HYSTERESIS_IS_READABLE = 0x10
        HYSTERESIS_IS_READ_AND_SETTABLE = 0x20
        HYSTERESIS_IS_FIXED = 0x30
        if capabilities & HYSTERESIS_MASK == HYSTERESIS_IS_NOT_SUPPORTED:
            self.capabilities.append('hysteresis_not_supported')
        elif capabilities & HYSTERESIS_MASK == HYSTERESIS_IS_READABLE:
            self.capabilities.append('hysteresis_readable')
        elif capabilities & HYSTERESIS_MASK == HYSTERESIS_IS_READ_AND_SETTABLE:
            self.capabilities.append('hysteresis_read_and_setable')
        elif capabilities & HYSTERESIS_MASK == HYSTERESIS_IS_FIXED:
            self.capabilities.append('hysteresis_fixed')
        # sensor threshold support
        THRESHOLD_MASK = 0x0C
        THRESHOLD_IS_NOT_SUPPORTED = 0x00
        THRESHOLD_IS_READABLE = 0x08
        THRESHOLD_IS_READ_AND_SETTABLE = 0x04
        THRESHOLD_IS_FIXED = 0x0C
        if capabilities & THRESHOLD_MASK == THRESHOLD_IS_NOT_SUPPORTED:
            self.capabilities.append('threshold_not_supported')
        elif capabilities & THRESHOLD_MASK == THRESHOLD_IS_READABLE:
            self.capabilities.append('threshold_readable')
        elif capabilities & THRESHOLD_MASK == THRESHOLD_IS_READ_AND_SETTABLE:
            self.capabilities.append('threshold_read_and_setable')
        elif capabilities & THRESHOLD_MASK == THRESHOLD_IS_FIXED:
            self.capabilities.append('threshold_fixed')
        # sensor event message control support
        if (capabilities & 0x03) == 0:
            pass
        if (capabilities & 0x03) == 1:
            pass
        if (capabilities & 0x03) == 2:
            pass
        if (capabilities & 0x03) == 3:
            pass

    def _from_data(self, data):
        buffer = ByteBuffer(data[5:])
        # record key bytes
        self._common_record_key(buffer.pop_slice(3))
        # record body bytes
        self._entity(buffer.pop_slice(2))

        # byte 11
        initialization = buffer.pop_unsigned_int(1)
        self.initialization = []
        if initialization & 0x40:
            self.initialization.append('scanning')
        if initialization & 0x20:
            self.initialization.append('events')
        if initialization & 0x10:
            self.initialization.append('thresholds')
        if initialization & 0x08:
            self.initialization.append('hysteresis')
        if initialization & 0x04:
            self.initialization.append('type')
        if initialization & 0x02:
            self.initialization.append('default_event_generation')
        if initialization & 0x01:
            self.initialization.append('default_scanning')

        # byte 12 - sensor capabilities
        self._decode_capabilities(buffer.pop_unsigned_int(1))

        self.sensor_type_code = buffer.pop_unsigned_int(1)
        self.event_reading_type_code = buffer.pop_unsigned_int(1)
        self.assertion_mask = buffer.pop_unsigned_int(2)
        self.deassertion_mask = buffer.pop_unsigned_int(2)
        self.discrete_reading_mask = buffer.pop_unsigned_int(2)
        # byte 21, 22, 23
        self.units_1 = buffer.pop_unsigned_int(1)
        self.units_2 = buffer.pop_unsigned_int(1)
        self.units_3 = buffer.pop_unsigned_int(1)
        self.analog_data_format = (self.units_1 >> 6) & 0x3
        self.rate_unit = (self.units_1 >> 3) >> 0x7
        self.modifier_unit = (self.units_1 >> 1) & 0x2
        self.percentage = self.units_1 & 0x1
        # byte 24
        self.linearization = buffer.pop_unsigned_int(1) & 0x7f
        # byte 25, 26
        m = buffer.pop_unsigned_int(1)
        m_tol = buffer.pop_unsigned_int(1)
        self.m = (m & 0xff) | ((m_tol & 0xc0) << 2)
        # NAC: Bug fix.  Upstream did not properly account for
        # 'M' being a twos complement value.
        self.m = self._convert_complement(self.m, 10)
        self.tolerance = (m_tol & 0x3f)

        # byte 27, 28, 29
        b = buffer.pop_unsigned_int(1)
        b_acc = buffer.pop_unsigned_int(1)
        acc_accexp = buffer.pop_unsigned_int(1)
        self.b = (b & 0xff) | ((b_acc & 0xc0) << 2)
        self.b = self._convert_complement(self.b, 10)
        self.accuracy = (b_acc & 0x3f) | ((acc_accexp & 0xf0) << 4)
        self.accuracy_exp = (acc_accexp & 0x0c) >> 2
        # byte 30
        rexp_bexp = buffer.pop_unsigned_int(1)
        self.k2 = (rexp_bexp & 0xf0) >> 4
        # convert 2s complement
        self.k2 = self._convert_complement(self.k2, 4)

        self.k1 = rexp_bexp & 0x0f
        # convert 2s complement
        self.k1 = self._convert_complement(self.k1, 4)

        # byte 31
        analog_characteristics = buffer.pop_unsigned_int(1)
        self.analog_characteristic = []
        if analog_characteristics & 0x01:
            self.analog_characteristic.append('nominal_reading')
        if analog_characteristics & 0x02:
            self.analog_characteristic.append('normal_max')
        if analog_characteristics & 0x04:
            self.analog_characteristic.append('normal_min')

        self.nominal_reading = buffer.pop_unsigned_int(1)
        self.normal_maximum = buffer.pop_unsigned_int(1)
        self.normal_minimum = buffer.pop_unsigned_int(1)
        self.sensor_maximum_reading = buffer.pop_unsigned_int(1)
        self.sensor_minimum_reading = buffer.pop_unsigned_int(1)
        self.threshold = {}
        self.threshold['unr'] = buffer.pop_unsigned_int(1)
        self.threshold['ucr'] = buffer.pop_unsigned_int(1)
        self.threshold['unc'] = buffer.pop_unsigned_int(1)
        self.threshold['lnr'] = buffer.pop_unsigned_int(1)
        self.threshold['lcr'] = buffer.pop_unsigned_int(1)
        self.threshold['lnc'] = buffer.pop_unsigned_int(1)
        self.hysteresis = {}
        self.hysteresis['positive_going'] = buffer.pop_unsigned_int(1)
        self.hysteresis['negative_going'] = buffer.pop_unsigned_int(1)
        self.reserved = buffer.pop_unsigned_int(2)
        self.oem = buffer.pop_unsigned_int(1)
        self._device_id_string(buffer)


###
# SDR type 0x02
##################################################
class SdrCompactSensorRecord(SdrCommon):
    def __init__(self, data=None, next_id=None):
        super(SdrCompactSensorRecord, self).__init__(data, next_id)

    def __str__(self):
        s = '["%s"] [%s]' \
            % (self.device_id_string,
               ' '.join(['%02x' % b for b in self.data]))
        return s

    def _from_data(self, data):
        buffer = ByteBuffer(data[5:])

        # record key bytes
        self._common_record_key(buffer.pop_slice(3))

        # record body bytes
        self._entity(buffer.pop_slice(2))

        self.sensor_initialization = buffer.pop_unsigned_int(1)
        self.capabilities = buffer.pop_unsigned_int(1)
        self.sensor_type_code = buffer.pop_unsigned_int(1)
        self.event_reading_type_code = buffer.pop_unsigned_int(1)
        self.assertion_mask = buffer.pop_unsigned_int(2)
        self.deassertion_mask = buffer.pop_unsigned_int(2)
        self.discrete_reading_mask = buffer.pop_unsigned_int(2)
        self.units_1 = buffer.pop_unsigned_int(1)
        self.units_2 = buffer.pop_unsigned_int(1)
        self.units_3 = buffer.pop_unsigned_int(1)
        self.record_sharing = buffer.pop_unsigned_int(2)
        self.positive_going_hysteresis = buffer.pop_unsigned_int(1)
        self.negative_going_hysteresis = buffer.pop_unsigned_int(1)
        self.reserved = buffer.pop_unsigned_int(3)
        self.oem = buffer.pop_unsigned_int(1)
        self._device_id_string(buffer)


###
# SDR type 0x03
##################################################
class SdrEventOnlySensorRecord(SdrCommon):
    def __init__(self, data=None, next_id=None):
        super(SdrEventOnlySensorRecord, self).__init__(data, next_id)

    def __str__(self):
        return 'Not supported yet.'

    def _from_data(self, data):
        buffer = ByteBuffer(data[5:])

        # record key bytes
        self._common_record_key(buffer.pop_slice(3))

        # record body bytes
        self._entity(buffer.pop_slice(2))

        self.sensor_type = buffer.pop_unsigned_int(1)
        self.event_reading_type_code = buffer.pop_unsigned_int(1)
        self.record_sharing = buffer.pop_unsigned_int(2)
        self.reserved = buffer.pop_unsigned_int(1)
        self.oem = buffer.pop_unsigned_int(1)
        self._device_id_string(buffer)


###
# SDR type 0x11
##################################################
class SdrFruDeviceLocator(SdrCommon):
    def __init__(self, data=None, next_id=None):
        super(SdrFruDeviceLocator, self).__init__(data, next_id)

    def __str__(self):
        s = '["%s"] [%s]' \
            % (self.device_id_string,
               ' '.join(['%02x' % b for b in self.data]))
        return s

    def _from_data(self, data):
        buffer = ByteBuffer(data[5:])
        self.device_access_address = buffer.pop_unsigned_int(1) >> 1
        self.fru_device_id = buffer.pop_unsigned_int(1)
        self.logical_physical = buffer.pop_unsigned_int(1)
        self.channel_number = buffer.pop_unsigned_int(1)
        self.reserved = buffer.pop_unsigned_int(1)
        self.device_type = buffer.pop_unsigned_int(1)
        self.device_type_modifier = buffer.pop_unsigned_int(1)
        self._entity(buffer.pop_slice(2))
        self.oem = buffer.pop_unsigned_int(1)
        self._device_id_string(buffer)


###
# SDR type 0x12
##################################################
class SdrManagementControllerDeviceLocator(SdrCommon):
    def __init__(self, data=None, next_id=None):
        super(SdrManagementControllerDeviceLocator, self).__init__(
                data, next_id)

    def __str__(self):
        s = '["%s"] [%s]' \
            % (self.device_id_string,
               ' '.join(['%02x' % b for b in self.data]))
        return s

    def _from_data(self, data):
        buffer = ByteBuffer(data[5:])
        self.device_slave_address = buffer.pop_unsigned_int(1) >> 1
        self.channel_number = buffer.pop_unsigned_int(1) & 0xf
        self.power_state_notification = buffer.pop_unsigned_int(1)
        self.global_initialization = 0
        self.device_capabilities = buffer.pop_unsigned_int(1)
        self.reserved = buffer.pop_unsigned_int(3)
        self._entity(buffer.pop_slice(2))
        self.oem = buffer.pop_unsigned_int(1)
        self._device_id_string(buffer)


###
# SDR type 0x13
##################################################
class SdrManagementControllerConfirmationRecord(SdrCommon):
    def __init__(self, data=None, next_id=None):
        super(SdrManagementControllerConfirmationRecord, self).__init__(
                data, next_id)

    def _from_data(self, data):
        buffer = ByteBuffer(data[5:])
        self.device_slave_address = buffer.pop_unsigned_int(1) >> 1
        self.device_id = buffer.pop_unsigned_int(1)
        self.channel_number = buffer.pop_unsigned_int(1)
        self.firmware_revision_1 = buffer.pop_unsigned_int(1)
        self.firmware_revision_2 = buffer.pop_unsigned_int(1)
        self.ipmi_version = buffer.pop_unsigned_int(1)
        self.manufacturer_id = buffer.pop_unsigned_int(3) & 0xfffff
        self.product_id = buffer.pop_unsigned_int(2)
        self.device_guid = buffer.pop_unsigned_int(16)


###
# SDR type 0xC0
##################################################
class SdrOEMSensorRecord(SdrCommon):
    def __init__(self, data=None, next_id=None):
        super(SdrOEMSensorRecord, self).__init__(data, next_id)

    def __str__(self):
        return 'Not supported yet.'

    def _from_data(self, data):
        buffer = ByteBuffer(data[5:])

        # record key bytes
        self._common_record_key(buffer.pop_slice(3))


# Any SDR type not known or not implemented
class SdrUnknownSensorRecord(SdrCommon):
    def __init__(self, data=None, next_id=None):
        super(SdrUnknownSensorRecord, self).__init__(data, next_id)

    def __str__(self):
        return 'Not supported yet.'
