#******************************************************************************************************
#  snapConnection.py - Gbtc
#
#  Copyright © 2021, Grid Protection Alliance.  All Rights Reserved.
#
#  Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
#  the NOTICE file distributed with this work for additional information regarding copyright ownership.
#  The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this
#  file except in compliance with the License. You may obtain a copy of the License at:
#
#      http://opensource.org/licenses/MIT
#
#  Unless agreed to in writing, the subject software distributed under the License is distributed on an
#  "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
#  License for the specific language governing permissions and limitations.
#
#  Code Modification History:
#  ----------------------------------------------------------------------------------------------------
#  02/06/2021 - <NAME>
#       Generated original version of source code.
#
#******************************************************************************************************

from .databaseInfo import databaseInfo
from .encodingDefinition import encodingDefinition
from .snapClientDatabase import snapClientDatabase
from .snapTypeBase import snapTypeBase
from .enumerations import *
from . import Server
from gsf.streamEncoder import streamEncoder
from gsf.binaryStream import binaryStream
from typing import TypeVar, Generic, List, Optional
import socket
import numpy as np

TKey = TypeVar('TKey', bound=snapTypeBase)
TValue = TypeVar('TValue', bound=snapTypeBase)

class snapConnection(Generic[TKey, TValue]):
    """
    Represents a generic SNAPdb connection.
    """

    DefaultPort = 38402

    def __init__(self, hostAddress: str, key: TKey, value: TValue):
        parts = hostAddress.split(":")
        
        if len(parts) > 1:
            self.hostAddress = part[1].strip()
            self.port = int(np.uint16(part[2].strip()))
        else:
            self.hostAddress = hostAddress
            self.port = snapConnection.DefaultPort
        
        try:
            self.hostEndPoint = socket.getaddrinfo(self.hostAddress, self.port, family=socket.AF_INET, proto=socket.IPPROTO_TCP)[0][4]
        except:
            self.hostEndPoint = (self.hostAddress, self.port)

        self.instances = dict()
        self.socket : Optional[socket.socket] = None
        self.stream : Optional[binaryStream] = None
        self.instance : Optional[snapClientDatabase[TKey, TValue]] = None
        self.key = key
        self.value = value
    
    @property
    def HostAddress(self) -> str:
        """
        Gets SNABdb server address and port, e.g., "localhost:38402".
        """
        return f"{self.hostAddress}:{self.port}"

    @property
    def HostEndPoint(self) -> (str, int):
        """
        Gets SNABdb server end point, e.g., ("127.0.0.1:, "38402").
        """
        return self.hostEndPoint

    @property
    def HostIPAddress(self) -> str:
        """
        Gets SNABdb server address, e.g., "127.0.0.1"
        """
        return self.hostEndPoint[0]

    @property
    def HostPort(self) -> int:
        """
        Gets SNABdb server port, e.g., 38402.
        """
        return self.hostEndPoint[1]

    @property
    def IsConnected(self) -> bool:
        """
        Gets flag indicating if connected to SNABdb server.
        """
        return self.stream is not None
    
    @property
    def InstanceNames(self) -> List[str]:
        """
        Gets list of client database instances available in SNABdb server connection.
        """
        return list(self.instances.keys())

    def Connect(self):
        """
        Attempts connection to SNAPdb server.
        """
        try:
            self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP)
            self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)

            stream = streamEncoder((lambda length: self.socket.recv(length)), (lambda buffer: self.socket.send(buffer)))
            self.socket.connect(self.hostEndPoint)

            useSsl = False

            # Initialize SNAPdb client stream
            stream.WriteInt64(0x2BA517361121)
            stream.WriteBoolean(useSsl)

            response = stream.ReadByte()

            if response == ServerResponse.UNKNOWNPROTOCOL:
                raise RuntimeError("SNAPdb client and server cannot agree on a protocol, this is commonly because they are running incompatible versions.")

            Server.ValidateExpectedResponse(response, ServerResponse.KNOWNPROTOCOL)

            useSsl = stream.ReadBoolean()

            if not self.__tryAuthenticate(stream, useSsl):
                raise RuntimeError("SNAPdb authentication failed")

            # Establish buffered I/O for socket connected to remote binary stream
            self.stream = binaryStream(stream)

            response = Server.ReadResponse(self.stream)

            if response == ServerResponse.UNKNOWNPROTOCOL:
                raise RuntimeError("SNAPdb client and server cannot agree on a protocol, this is commonly because they are running incompatible versions.")

            Server.ValidateExpectedResponse(response, ServerResponse.CONNECTEDTOROOT)

            self.stream.WriteByte(ServerCommand.GETALLDATABASES)
            self.stream.Flush()
        
            Server.ValidateExpectedReadResponse(self.stream, ServerResponse.LISTOFDATABASES)

            self.instances = dict()
            count = self.stream.ReadInt32()

            while count > 0:
                count -= 1
                info = databaseInfo(self.stream)
                self.instances[info.DatabaseName] = info
        except:
            self.Disconnect()
            raise

    def Disconnect(self):
        """
        Disconnects from SNAPdb server.
        """
        self.CloseInstance()

        if self.stream is not None:
            self.stream.WriteByte(ServerCommand.DISCONNECT)
            self.stream.Flush()
            self.stream = None

        if self.socket is not None:
            self.socket.close()
            self.socket = None

    def InstanceExists(self, instanceName: str) -> bool:
        """
        Gets flag indicating if `instanceName` exists for list of client database
        instances available in SNABdb server connection.
        """
        return self.instances[instanceName] is not None

    def GetInstanceInfo(self, instanceName: str) -> Optional[databaseInfo]:
        """
        Gets details about SNAPdb client database instance.
        """
        return self.instances[instanceName]

    def OpenInstance(self, instanceName: str, definition: Optional[encodingDefinition] = None) -> snapClientDatabase[TKey, TValue]:
        """
        Attempts to open a connection to SNAPdb client database instance.
        If successful, connection instance will be returned and can be used to
        read and write key / value data for SNAPdb client database instance.
        """
        if self.instance is not None and not self.instance.IsDisposed:
            raise RuntimeError(f"SNAPdb instance \"{self.instance.Info.DatabaseName}\" is currently open. Only one SNAPdb instance can be open at once, call CloseInstance() API method first.")
        
        info = self.GetInstanceInfo(instanceName)

        if info is None:
            raise RuntimeError(f"Failed to find SNAPdb instance \"{instanceName}\"")

        if definition is None:
            if len(info.SupportedEncodings) == 0:
                raise RuntimeError(f"Failed to find any encoding definitions for SNAPdb instance \"{instanceName}\"")

            definition = info.SupportedEncodings[0]

        self.stream.WriteByte(ServerCommand.CONNECTTODATABASE)
        self.stream.WriteString(instanceName)
        self.stream.WriteGuid(self.key.TypeID)
        self.stream.WriteGuid(self.value.TypeID)
        self.stream.Flush()
        
        response = Server.ReadResponse(self.stream)

        if response == ServerResponse.DATABASEDOESNOTEXIST:
            raise RuntimeError(f"SNAPdb server reports instance \"{instanceName}\" does not exist")

        if response == ServerResponse.DATABASEKEYUNKNOWN:
            raise RuntimeError(f"SNAPdb server reports SNABdb key type {{{self.key.TypeID}}} does not match type defined for instance")

        if response == ServerResponse.DATABASEVALUEUNKNOWN:
            raise RuntimeError(f"SNAPdb server reports SNABdb value type {{{self.value.TypeID}}} does not match type defined for instance")

        Server.ValidateExpectedResponse(response, ServerResponse.SUCCESSFULLYCONNECTEDTODATABASE)

        self.instance = snapClientDatabase[TKey, TValue](self.stream, info, self.__clearInstanceRef, self.key, self.value)
        self.instance.SetEncodingDefinition(definition)

        return self.instance

    def __clearInstanceRef(self):
        self.instance = None

    def CloseInstance(self):
        """
        Closes last connection to SNAPdb client database instance.
        """
        if self.instance is not None:
            self.instance.Dispose()
            self.instance = None

    def __tryAuthenticate(self, stream: streamEncoder, useSsl: bool) -> bool:
        if useSsl:
            raise RuntimeError("SNAPdb instance is configured to require SSL. This version of the SNAPdb Python API does not support SSL.")

        # Future updates can attempt to try resuming an existing secure session:
        # if __tryResumeSession(self.secureStream, stream2, certSignatures):
        #     return True

        stream.WriteByte(AuthenticationMode.NONE)

        if stream.ReadBoolean():
            if stream.ReadBoolean():
                #self.resumeTicket = stream2.ReadBytes(stream2.ReadNextByte())
                #self.sessionSecret = stream2.ReadBytes(stream2.ReadNextByte())
                raise RuntimeError("Unexpected secure stream response from SNAPdb")
                
            return True
        
        return False

#******************************************************************************************************
#  snapConnection.py - Gbtc
#
#  Copyright © 2021, Grid Protection Alliance.  All Rights Reserved.
#
#  Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
#  the NOTICE file distributed with this work for additional information regarding copyright ownership.
#  The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this
#  file except in compliance with the License. You may obtain a copy of the License at:
#
#      http://opensource.org/licenses/MIT
#
#  Unless agreed to in writing, the subject software distributed under the License is distributed on an
#  "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
#  License for the specific language governing permissions and limitations.
#
#  Code Modification History:
#  ----------------------------------------------------------------------------------------------------
#  02/06/2021 - <NAME>
#       Generated original version of source code.
#
#******************************************************************************************************

from .databaseInfo import databaseInfo
from .encodingDefinition import encodingDefinition
from .snapClientDatabase import snapClientDatabase
from .snapTypeBase import snapTypeBase
from .enumerations import *
from . import Server
from gsf.streamEncoder import streamEncoder
from gsf.binaryStream import binaryStream
from typing import TypeVar, Generic, List, Optional
import socket
import numpy as np

TKey = TypeVar('TKey', bound=snapTypeBase)
TValue = TypeVar('TValue', bound=snapTypeBase)

class snapConnection(Generic[TKey, TValue]):
    """
    Represents a generic SNAPdb connection.
    """

    DefaultPort = 38402

    def __init__(self, hostAddress: str, key: TKey, value: TValue):
        parts = hostAddress.split(":")
        
        if len(parts) > 1:
            self.hostAddress = part[1].strip()
            self.port = int(np.uint16(part[2].strip()))
        else:
            self.hostAddress = hostAddress
            self.port = snapConnection.DefaultPort
        
        try:
            self.hostEndPoint = socket.getaddrinfo(self.hostAddress, self.port, family=socket.AF_INET, proto=socket.IPPROTO_TCP)[0][4]
        except:
            self.hostEndPoint = (self.hostAddress, self.port)

        self.instances = dict()
        self.socket : Optional[socket.socket] = None
        self.stream : Optional[binaryStream] = None
        self.instance : Optional[snapClientDatabase[TKey, TValue]] = None
        self.key = key
        self.value = value
    
    @property
    def HostAddress(self) -> str:
        """
        Gets SNABdb server address and port, e.g., "localhost:38402".
        """
        return f"{self.hostAddress}:{self.port}"

    @property
    def HostEndPoint(self) -> (str, int):
        """
        Gets SNABdb server end point, e.g., ("127.0.0.1:, "38402").
        """
        return self.hostEndPoint

    @property
    def HostIPAddress(self) -> str:
        """
        Gets SNABdb server address, e.g., "127.0.0.1"
        """
        return self.hostEndPoint[0]

    @property
    def HostPort(self) -> int:
        """
        Gets SNABdb server port, e.g., 38402.
        """
        return self.hostEndPoint[1]

    @property
    def IsConnected(self) -> bool:
        """
        Gets flag indicating if connected to SNABdb server.
        """
        return self.stream is not None
    
    @property
    def InstanceNames(self) -> List[str]:
        """
        Gets list of client database instances available in SNABdb server connection.
        """
        return list(self.instances.keys())

    def Connect(self):
        """
        Attempts connection to SNAPdb server.
        """
        try:
            self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP)
            self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)

            stream = streamEncoder((lambda length: self.socket.recv(length)), (lambda buffer: self.socket.send(buffer)))
            self.socket.connect(self.hostEndPoint)

            useSsl = False

            # Initialize SNAPdb client stream
            stream.WriteInt64(0x2BA517361121)
            stream.WriteBoolean(useSsl)

            response = stream.ReadByte()

            if response == ServerResponse.UNKNOWNPROTOCOL:
                raise RuntimeError("SNAPdb client and server cannot agree on a protocol, this is commonly because they are running incompatible versions.")

            Server.ValidateExpectedResponse(response, ServerResponse.KNOWNPROTOCOL)

            useSsl = stream.ReadBoolean()

            if not self.__tryAuthenticate(stream, useSsl):
                raise RuntimeError("SNAPdb authentication failed")

            # Establish buffered I/O for socket connected to remote binary stream
            self.stream = binaryStream(stream)

            response = Server.ReadResponse(self.stream)

            if response == ServerResponse.UNKNOWNPROTOCOL:
                raise RuntimeError("SNAPdb client and server cannot agree on a protocol, this is commonly because they are running incompatible versions.")

            Server.ValidateExpectedResponse(response, ServerResponse.CONNECTEDTOROOT)

            self.stream.WriteByte(ServerCommand.GETALLDATABASES)
            self.stream.Flush()
        
            Server.ValidateExpectedReadResponse(self.stream, ServerResponse.LISTOFDATABASES)

            self.instances = dict()
            count = self.stream.ReadInt32()

            while count > 0:
                count -= 1
                info = databaseInfo(self.stream)
                self.instances[info.DatabaseName] = info
        except:
            self.Disconnect()
            raise

    def Disconnect(self):
        """
        Disconnects from SNAPdb server.
        """
        self.CloseInstance()

        if self.stream is not None:
            self.stream.WriteByte(ServerCommand.DISCONNECT)
            self.stream.Flush()
            self.stream = None

        if self.socket is not None:
            self.socket.close()
            self.socket = None

    def InstanceExists(self, instanceName: str) -> bool:
        """
        Gets flag indicating if `instanceName` exists for list of client database
        instances available in SNABdb server connection.
        """
        return self.instances[instanceName] is not None

    def GetInstanceInfo(self, instanceName: str) -> Optional[databaseInfo]:
        """
        Gets details about SNAPdb client database instance.
        """
        return self.instances[instanceName]

    def OpenInstance(self, instanceName: str, definition: Optional[encodingDefinition] = None) -> snapClientDatabase[TKey, TValue]:
        """
        Attempts to open a connection to SNAPdb client database instance.
        If successful, connection instance will be returned and can be used to
        read and write key / value data for SNAPdb client database instance.
        """
        if self.instance is not None and not self.instance.IsDisposed:
            raise RuntimeError(f"SNAPdb instance \"{self.instance.Info.DatabaseName}\" is currently open. Only one SNAPdb instance can be open at once, call CloseInstance() API method first.")
        
        info = self.GetInstanceInfo(instanceName)

        if info is None:
            raise RuntimeError(f"Failed to find SNAPdb instance \"{instanceName}\"")

        if definition is None:
            if len(info.SupportedEncodings) == 0:
                raise RuntimeError(f"Failed to find any encoding definitions for SNAPdb instance \"{instanceName}\"")

            definition = info.SupportedEncodings[0]

        self.stream.WriteByte(ServerCommand.CONNECTTODATABASE)
        self.stream.WriteString(instanceName)
        self.stream.WriteGuid(self.key.TypeID)
        self.stream.WriteGuid(self.value.TypeID)
        self.stream.Flush()
        
        response = Server.ReadResponse(self.stream)

        if response == ServerResponse.DATABASEDOESNOTEXIST:
            raise RuntimeError(f"SNAPdb server reports instance \"{instanceName}\" does not exist")

        if response == ServerResponse.DATABASEKEYUNKNOWN:
            raise RuntimeError(f"SNAPdb server reports SNABdb key type {{{self.key.TypeID}}} does not match type defined for instance")

        if response == ServerResponse.DATABASEVALUEUNKNOWN:
            raise RuntimeError(f"SNAPdb server reports SNABdb value type {{{self.value.TypeID}}} does not match type defined for instance")

        Server.ValidateExpectedResponse(response, ServerResponse.SUCCESSFULLYCONNECTEDTODATABASE)

        self.instance = snapClientDatabase[TKey, TValue](self.stream, info, self.__clearInstanceRef, self.key, self.value)
        self.instance.SetEncodingDefinition(definition)

        return self.instance

    def __clearInstanceRef(self):
        self.instance = None

    def CloseInstance(self):
        """
        Closes last connection to SNAPdb client database instance.
        """
        if self.instance is not None:
            self.instance.Dispose()
            self.instance = None

    def __tryAuthenticate(self, stream: streamEncoder, useSsl: bool) -> bool:
        if useSsl:
            raise RuntimeError("SNAPdb instance is configured to require SSL. This version of the SNAPdb Python API does not support SSL.")

        # Future updates can attempt to try resuming an existing secure session:
        # if __tryResumeSession(self.secureStream, stream2, certSignatures):
        #     return True

        stream.WriteByte(AuthenticationMode.NONE)

        if stream.ReadBoolean():
            if stream.ReadBoolean():
                #self.resumeTicket = stream2.ReadBytes(stream2.ReadNextByte())
                #self.sessionSecret = stream2.ReadBytes(stream2.ReadNextByte())
                raise RuntimeError("Unexpected secure stream response from SNAPdb")
                
            return True
        
        return False

#****************************************************************************************************** #  snapConnection.py - Gbtc # #  Copyright © 2021, Grid Protection Alliance.  All Rights Reserved. # #  Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See #  the NOTICE file distributed with this work for additional information regarding copyright ownership. #  The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this #  file except in compliance with the License. You may obtain a copy of the License at: # #      http://opensource.org/licenses/MIT # #  Unless agreed to in writing, the subject software distributed under the License is distributed on an #  "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the #  License for the specific language governing permissions and limitations. # #  Code Modification History: #  ---------------------------------------------------------------------------------------------------- #  02/06/2021 - <NAME> #       Generated original version of source code. # #****************************************************************************************************** Represents a generic SNAPdb connection. Gets SNABdb server address and port, e.g., "localhost:38402". Gets SNABdb server end point, e.g., ("127.0.0.1:, "38402"). Gets SNABdb server address, e.g., "127.0.0.1" Gets SNABdb server port, e.g., 38402. Gets flag indicating if connected to SNABdb server. Gets list of client database instances available in SNABdb server connection. Attempts connection to SNAPdb server. # Initialize SNAPdb client stream # Establish buffered I/O for socket connected to remote binary stream Disconnects from SNAPdb server. Gets flag indicating if `instanceName` exists for list of client database         instances available in SNABdb server connection. Gets details about SNAPdb client database instance. Attempts to open a connection to SNAPdb client database instance.         If successful, connection instance will be returned and can be used to         read and write key / value data for SNAPdb client database instance. Closes last connection to SNAPdb client database instance. # Future updates can attempt to try resuming an existing secure session: # if __tryResumeSession(self.secureStream, stream2, certSignatures): #     return True #self.resumeTicket = stream2.ReadBytes(stream2.ReadNextByte()) #self.sessionSecret = stream2.ReadBytes(stream2.ReadNextByte())