#
#  --------------------------------------------------------------------------
#   Gurux Ltd
#
#
#
#  Filename: $HeadURL$
#
#  Version: $Revision$,
#                   $Date$
#                   $Author$
#
#  Copyright (c) Gurux Ltd
#
# ---------------------------------------------------------------------------
#
#   DESCRIPTION
#
#  This file is a part of Gurux Device Framework.
#
#  Gurux Device Framework is Open Source software; you can redistribute it
#  and/or modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; version 2 of the License.
#  Gurux Device Framework 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 General Public License for more details.
#
#  More information of Gurux products: http://www.gurux.org
#
#  This code is licensed under the GNU General Public License v2.
#  Full text may be retrieved at http://www.gnu.org/licenses/gpl-2.0.txt
# ---------------------------------------------------------------------------
from __future__ import print_function
from .enums import (
    Priority,
    ServiceClass,
    InterfaceType,
    Authentication,
    Standard,
    HdlcFrameType,
    Conformance,
)
from .enums.DateTimeSkips import DateTimeSkips
from .ConnectionState import ConnectionState
from .objects.GXDLMSObjectCollection import GXDLMSObjectCollection
from .GXHdlcSettings import GXHdlcSettings
from .IGXCryptoNotifier import IGXCryptoNotifier
from .objects.enums.CertificateType import CertificateType
from .GXCryptoKeyParameter import GXCryptoKeyParameter
from .enums.ObjectType import ObjectType


# This class includes DLMS communication settings.
# pylint: disable=bad-option-value,too-many-public-methods,too-many-instance-attributes,old-style-class
class GXDLMSSettings:
    #
    # Server sender frame sequence starting number.
    #
    __SERVER_START_SENDER_FRAME_SEQUENCE = 0x1E

    #
    # Server receiver frame sequence starting number.
    #
    __SERVER_START_RECEIVER_FRAME_SEQUENCE = 0xFE

    #
    # Client sender frame sequence starting number.
    #
    __CLIENT_START_SENDER_FRAME_SEQUENCE = 0xFE

    #
    # Client receiver frame sequence starting number.
    #
    __CLIENT_START_RCEIVER_FRAME_SEQUENCE = 0xE

    #
    # DLMS version number.
    #
    __DLMS_VERSION = 6
    __MAX_RECEIVE_PDU_SIZE = 0xFFFF

    @classmethod
    def getInitialConformance(cls, useLN):
        if useLN:
            return (
                Conformance.GENERAL_PROTECTION
                | Conformance.BLOCK_TRANSFER_WITH_ACTION
                | Conformance.BLOCK_TRANSFER_WITH_SET_OR_WRITE
                | Conformance.BLOCK_TRANSFER_WITH_GET_OR_READ
                | Conformance.SET
                | Conformance.SELECTIVE_ACCESS
                | Conformance.ACTION
                | Conformance.MULTIPLE_REFERENCES
                | Conformance.GET
                | Conformance.ACCESS
            )
        return (
            Conformance.INFORMATION_REPORT
            | Conformance.READ
            | Conformance.UN_CONFIRMED_WRITE
            | Conformance.WRITE
            | Conformance.PARAMETERIZED_ACCESS
            | Conformance.MULTIPLE_REFERENCES
        )

    #
    # Constructor.
    #
    def __init__(self, isServer, parent):
        self.customChallenges = False
        self.ctoSChallenge = None
        self.stoCChallenge = None
        self.sourceSystemTitle = None
        self.invokeId = 0x1
        self.longInvokeID = 0x1
        self.priority = Priority.HIGH
        self.serviceClass = ServiceClass.CONFIRMED
        self.clientAddress = 0
        self.serverAddress = 0
        self.pushClientAddress = 0
        self.serverAddressSize = 0
        self.__useLogicalNameReferencing = True
        self.interfaceType = InterfaceType.HDLC
        self.authentication = Authentication.NONE
        self.password = None
        self.kek = None
        self.count = 0
        self.index = 0
        self.targetEphemeralKey = None
        self.dlmsVersion = self.__DLMS_VERSION
        self.connected = ConnectionState.NONE
        self.allowAnonymousAccess = False
        self.maxPduSize = self.__MAX_RECEIVE_PDU_SIZE
        self.maxServerPDUSize = self.__MAX_RECEIVE_PDU_SIZE
        self.startingPacketIndex = 1
        # Gets current block index.
        self.blockIndex = 1
        self.cipher = None
        self.blockNumberAck = 0
        self.protocolVersion = None
        self.isServer = isServer
        self.objects = GXDLMSObjectCollection()
        self.hdlc = GXHdlcSettings()
        self.gateway = None
        self.proposedConformance = GXDLMSSettings.getInitialConformance(
            self.__useLogicalNameReferencing
        )
        self.receiverFrame = 0
        self.senderFrame = 0
        self.resetFrameSequence()
        self.gbtWindowSize = 1
        self.userId = -1
        # Quality of service.
        self.qualityOfService = 0
        self.useUtc2NormalTime = False
        self.increaseInvocationCounterForGMacAuthentication = False
        self.dateTimeSkips = DateTimeSkips.NONE
        self.standard = Standard.DLMS
        self.negotiatedConformance = Conformance.NONE
        self.invokeID = 0
        self.command = 0
        self.commandType = 0
        self.useCustomChallenge = False
        self.preEstablishedSystemTitle = None
        # Assigned association for the server.
        self.__assignedAssociation = None

        ###Event listeners.
        self.__listeners = []
        if isinstance(parent, IGXCryptoNotifier):
            self.__listeners.append(parent)

    #
    # Client to Server challenge.
    #
    def getCtoSChallenge(self):
        return self.ctoSChallenge

    #
    # Notify client from decrypted PDU.
    #
    def _onPduEventHandler(self, complete, data):
        for it in self.__listeners:
            it.onPduEventHandler(it, complete, data)

    #
    # @param value
    # Client to Server challenge.
    #
    def setCtoSChallenge(self, value):
        if not self.customChallenges or self.ctoSChallenge is None:
            self.ctoSChallenge = value

    #
    # Server to Client challenge.
    #
    def getStoCChallenge(self):
        return self.stoCChallenge

    #
    # @param value
    # Server to Client challenge.
    #
    def setStoCChallenge(self, value):
        if not self.customChallenges or self.stoCChallenge is None:
            self.stoCChallenge = value

    #
    # Is connection accepted.
    #
    def acceptConnection(self):
        return (
            self.connected != ConnectionState.NONE
            or self.allowAnonymousAccess
            or (self.cipher and self.cipher.sharedSecret)
        )

    #
    # Reset frame sequence.
    #
    def resetFrameSequence(self):
        if self.isServer:
            self.senderFrame = self.__SERVER_START_SENDER_FRAME_SEQUENCE
            self.receiverFrame = self.__SERVER_START_RECEIVER_FRAME_SEQUENCE
        else:
            self.senderFrame = self.__CLIENT_START_SENDER_FRAME_SEQUENCE
            self.receiverFrame = self.__CLIENT_START_RCEIVER_FRAME_SEQUENCE

    # pylint: disable=too-many-return-statements
    def checkFrame(self, frame_, xml):
        #  If notify
        if frame_ == 0x13:
            return True

        #  If U frame.
        if (frame_ & HdlcFrameType.U_FRAME) == HdlcFrameType.U_FRAME:
            if frame_ == 0x93:
                isEcho = (
                    not self.isServer
                    and frame_ == 0x93
                    and self.senderFrame in (0x10, 0xFE)
                    and self.receiverFrame == 0xE
                )
                self.resetFrameSequence()
                return not isEcho
            if frame_ == 0x73 and not self.isServer:
                return self.senderFrame == 0xFE and self.receiverFrame == 0xE
            return True
        #  If S -frame.
        if (frame_ & HdlcFrameType.S_FRAME) == HdlcFrameType.S_FRAME:
            # If echo.
            if frame_ == (self.senderFrame & 0xF1):
                return False
            self.receiverFrame = self.increaseReceiverSequence(self.receiverFrame)
            return True
        #  Handle I-frame.
        if (self.senderFrame & 0x1) == 0:
            expected = GXDLMSSettings.increaseReceiverSequence(
                GXDLMSSettings.increaseSendSequence(self.receiverFrame)
            )
            if frame_ == expected:
                self.receiverFrame = frame_
                return True
            # If the final bit is not set.
            if frame_ == (expected & ~0x10) and self.hdlc.windowSizeRX != 1:
                self.receiverFrame = frame_
                return True
            # If Final bit is not set for the previous message.
            if (self.receiverFrame & 0x10) == 0 and self.hdlc.windowSizeRX != 1:
                expected = 0x10 | self.increaseSendSequence(self.receiverFrame)
                if frame_ == expected:
                    self.receiverFrame = frame_
                    return True
                # If the final bit is not set.
                if frame_ == (expected & ~0x10):
                    ReceiverFrame = frame_
                    return True
        else:
            expected = self.increaseSendSequence(self.receiverFrame) & 0xFF
            #  If answer for RR.
            if frame_ == expected:
                self.receiverFrame = frame_
                return True
            if frame_ == (expected & ~0x10):
                self.receiverFrame = frame_
                return True
            if self.hdlc.windowSizeRX != 1:
                # If HDLC window size is bigger than one.
                expected = self.increaseReceiverSequence(
                    self.increaseSendSequence(ReceiverFrame)
                )
                if frame_ == expected:
                    ReceiverFrame = frame_
                    return True

        # If try to find data from bytestream and not real communicating.
        if xml and (
            (not self.isServer and self.receiverFrame == 0xE)
            or (self.isServer and self.receiverFrame == 0xEE)
        ):
            ReceiverFrame = frame_
            return True
        print("Invalid HDLC Frame: " + hex(frame_) + " Expected: " + hex(expected))
        return False

    #
    # Increase receiver sequence.
    #
    # @param value
    # Frame value.
    # Increased receiver frame sequence.
    #
    @classmethod
    def increaseReceiverSequence(cls, value):
        return ((value & 0xFF) + 0x20 | 0x10 | value & 0xE) & 0xFF

    #
    # Increase sender sequence.
    #
    # @param value
    # Frame value.
    # Increased sender frame sequence.
    #
    @classmethod
    def increaseSendSequence(cls, value):
        return ((value & 0xF0 | (value + 0x2) & 0xE) & 0xFF) & 0xFF

    #
    # Generates I-frame.
    # @param first
    # Is this first packet.
    # Generated I-frame
    #
    def getNextSend(self, first):
        if first:
            self.senderFrame = self.increaseReceiverSequence(
                self.increaseSendSequence(int(self.senderFrame))
            )
        else:
            self.senderFrame = self.increaseSendSequence(int(self.senderFrame))
        return self.senderFrame & 0xFF

    #
    # Generates Receiver Ready S-frame.
    #
    def getReceiverReady(self):
        self.senderFrame = self.increaseReceiverSequence((self.senderFrame | 1))
        return self.senderFrame & 0xF1

    #
    # Generates Keep Alive S-frame.
    #
    def getKeepAlive(self):
        self.senderFrame = self.senderFrame | 1
        return self.senderFrame & 0xF1

    #
    # Gets starting block index in HDLC framing.  Default is One based,
    #      but some
    # meters use Zero based value.  Usually this is not used.
    #
    # Current block index.
    #
    def getStartingPacketIndex(self):
        return self.startingPacketIndex

    #
    # Set starting block index in HDLC framing.  Default is One based,
    #      but some
    # meters use Zero based value.  Usually this is not used.
    #
    # @param value
    # Zero based starting index.
    #
    def setStartingPacketIndex(self, value):
        self.startingPacketIndex = value
        self.resetBlockIndex()

    #
    # Sets current block index.
    #
    # @param value
    # Block index.
    #
    def setBlockIndex(self, value):
        self.blockIndex = value

    #
    # Block number acknowledged in GBT.
    #
    def getBlockNumberAck(self):
        return self.blockNumberAck

    #
    # @param value
    # Block number acknowledged in GBT.
    #
    def setBlockNumberAck(self, value):
        self.blockNumberAck = value

    #
    # Resets block index to default value.
    #
    def resetBlockIndex(self):
        self.blockIndex = self.startingPacketIndex
        self.blockNumberAck = 0

    #
    # Increases block index.
    #
    def increaseBlockIndex(self):
        self.blockIndex += 1

    # Is Logical Name Referencing used.
    # Don't use property. For some reason Python 2.7 doesn't call it in Rasbperry PI.
    def getUseLogicalNameReferencing(self):
        return self.__useLogicalNameReferencing

    # Is Logical Name Referencing used.
    # Don't use property. For some reason Python 2.7 doesn't call it in Rasbperry PI.
    def setUseLogicalNameReferencing(self, value):
        if self.__useLogicalNameReferencing != value:
            self.__useLogicalNameReferencing = value
            self.proposedConformance = GXDLMSSettings.getInitialConformance(
                self.__useLogicalNameReferencing
            )

    #
    # Invoke ID.
    #
    def getInvokeID(self):
        return self.invokeID

    #
    # @param value
    # update invoke ID.
    #
    def updateInvokeId(self, value):
        # pylint: disable=bad-option-value,redefined-variable-type
        if (value & 0x80) != 0:
            self.priority = Priority.HIGH
        else:
            self.priority = Priority.NORMAL
        if (value & 0x40) != 0:
            self.serviceClass = ServiceClass.CONFIRMED
        else:
            self.serviceClass = ServiceClass.UN_CONFIRMED
        self.invokeID = int((value & 0xF))

    #
    # @param value
    # Invoke ID.
    #
    def setInvokeID(self, value):
        if value > 0xF:
            raise ValueError("Invalid InvokeID")
        self.invokeID = int(value)

    #
    # Invoke ID.
    #
    def getLongInvokeID(self):
        return self.longInvokeID

    #
    # @param value
    # Invoke ID.
    #
    def setLongInvokeID(self, value):
        if value > 0xFFFFFFFF:
            raise ValueError("Invalid InvokeID")
        self.longInvokeID = value

    #
    # Source system title.
    #
    def getSourceSystemTitle(self):
        return self.sourceSystemTitle

    #
    # @param value
    # Source system title.
    #
    def setSourceSystemTitle(self, value):
        if not value or len(value) != 8:
            raise ValueError("Invalid client system title.")
        self.sourceSystemTitle = value

    #
    # Long data count.
    #
    def getCount(self):
        return self.count

    #
    # @param value
    # Data count.
    #
    def setCount(self, value):
        if value < 0:
            raise ValueError("Invalid count.")
        self.count = value

    #
    # Long data index.
    #
    def getIndex(self):
        return self.index

    #
    # @param value
    # Long data index
    #
    def setIndex(self, value):
        if value < 0:
            raise ValueError("Invalid Index.")
        self.index = value

    # Encrypt or decrypt the data using external Hardware Security Module.
    def crypt(self, certificateType, Data, encrypt, keyType):
        for it in self.__listeners:
            if isinstance(it, IGXCryptoNotifier):
                args = GXCryptoKeyParameter()
                args.keyType = keyType
                args.encrypt = encrypt
                args.systemTitle = self.cipher.systemTitle
                args.recipientSystemTitle = self.sourceSystemTitle
                args.certificateType = certificateType
                args.invocationCounter = self.cipher.invocationCounter
                args.securitySuite = self.cipher.securitySuite
                args.securityPolicy = self.cipher.securityPolicy
                if encrypt:
                    args.plainText = Data
                else:
                    args.encrypted = Data
                args.authenticationKey = self.cipher.authenticationKey
                if (
                    len(self.cipher.dedicatedKey) == 16
                    and (self.connected & ConnectionState.DLMS) != 0
                ):
                    args.blockCipherKey = self.cipher.dedicatedKey
                else:
                    args.blockCipherKey = self.cipher.blockCipherKey
                it.crypto(it, args)
                if encrypt:
                    return args.encrypted
                return args.plainText
            return None

    # pylint: disable=import-outside-toplevel, too-many-nested-blocks
    def getKey(self, certificateType, systemTitle, encrypt):
        from .asn.enums.KeyUsage import KeyUsage
        from .objects.GXDLMSAssociationLogicalName import GXDLMSAssociationLogicalName

        for it in self.__listeners:
            if isinstance(it, IGXCryptoNotifier):
                if certificateType == CertificateType.DIGITAL_SIGNATURE:
                    if encrypt:
                        if self.cipher.signingKeyPair and self.cipher.signingKeyPair[1]:
                            return self.cipher.signingKeyPair[1]
                    elif self.cipher.signingKeyPair and self.cipher.signingKeyPair[0]:
                        return self.cipher.signingKeyPair[0]
                elif certificateType == CertificateType.KEY_AGREEMENT:
                    if encrypt:
                        if (
                            self.cipher.keyAgreementKeyPair
                            and self.cipher.keyAgreementKeyPair[1]
                        ):
                            return self.cipher.KeyAgreementKeyPair[1]
                    elif (
                        self.cipher.keyAgreementKeyPair
                        and self.cipher.keyAgreementKeyPair[0]
                    ):
                        return self.cipher.keyAgreementKeyPair[0]
                # Find keys from security setup object.
                if (
                    isinstance(self.__assignedAssociation, GXDLMSAssociationLogicalName)
                    and systemTitle
                ):
                    ss = self.__assignedAssociation.objectList.findByLN(
                        ObjectType.SECURITY_SETUP,
                        self.__assignedAssociation.securitySetupReference,
                    )
                    if ss:
                        if certificateType == CertificateType.DIGITAL_SIGNATURE:
                            cert = ss.ServerCertificates.FindBySystemTitle(
                                systemTitle, KeyUsage.DIGITAL_SIGNATURE
                            )
                            if cert and ss.signingKey:
                                if encrypt:
                                    if ss.signingKey[1]:
                                        return ss.signingKey[1]
                                elif cert.publicKey:
                                    return cert.publicKey
                        elif certificateType == CertificateType.KEY_AGREEMENT:
                            cert = ss.ServerCertificates.FindBySystemTitle(
                                systemTitle, KeyUsage.KEY_AGREEMENT
                            )
                            if cert and ss.keyAgreementKey:
                                if encrypt:
                                    if ss.keyAgreementKey[1]:
                                        return ss.KeyAgreementKey[1]
                                elif cert.publicKey:
                                    return cert.publicKey

                args = GXCryptoKeyParameter()
                args.encrypt = encrypt
                args.securitySuite = self.cipher.securitySuite
                args.certificateType = certificateType
                args.systemTitle = systemTitle
                if it.onKey:
                    it.onKey(it, args)
                if encrypt:
                    return args.privateKey
                return args.publicKey
        return None
