# -*- coding: utf-8 -*-

import socket
import ssl
import threading


class IsNotCallableException(Exception):
    pass


class SocketServer(object):


    def __init__(self):
        
        self.socket = None

        # @params: local_address, local_port
        self.onBindEvent = None

        # @params: void
        self.onWaitConnectionEvent = None

        # @params: remote_address, remote_port
        self.onClientConnectEvent = None

        # @params: remote_address, remote_port
        self.onClientCloseEvent = None

        # @params: connection, remote_address, remote_port, data
        self.onClientReceiveEvent = None


    def close(self):

        self.socket.close()


    def bindOne(self, local_address, local_port):

        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.socket.bind((str(local_address), int(local_port)))
        self.socket.listen(0)

        # User event
        if(callable(self.onBindEvent)):
            self.onBindEvent(local_address, local_port)

        # User event
        if(callable(self.onWaitConnectionEvent)):
            self.onWaitConnectionEvent()

        # Wait for new connection
        conn, client = self.socket_handler.accept()

        # Send to event
        self.onClientConnect(
            connection     = conn,
            remote_address = client[0],
            remote_port    = client[1]
        )
        
        self.close()


    def bind(self, local_address, local_port):

        self.socket_handler = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.socket_handler.bind((str(local_address), int(local_port)))
        self.socket_handler.listen(1)

        # User event
        if(callable(self.onBindEvent)):
            self.onBindEvent(local_address, local_port)

        while(True):

            # User event
            if(callable(self.onWaitConnectionEvent)):
                self.onWaitConnectionEvent()

            # Wait for new connection
            conn, client = self.socket_handler.accept()
            
            # Send to thread
            threadHandler = threading.Thread(
                target = self.onClientConnect,
                args   = (conn, str(client[0]), int(client[1]))
            )

            # End thread when end main process
            threadHandler.setDaemon(True)
            threadHandler.start()


    def onClientConnect(self, connection, remote_address, remote_port):

        # User event
        if(callable(self.onClientConnectEvent)):
            self.onClientConnectEvent(remote_address, remote_port)

        with connection:

            while(True):

                data = connection.recv(1024)

                if not data:
                    break

                if(callable(self.onClientReceiveEvent)):
                    self.onClientReceiveEvent(
                        connection,
                        remote_address,
                        remote_port,
                        data
                    )

                # connection.sendall(data)

        # User event
        if(callable(self.onClientCloseEvent)):
            self.onClientCloseEvent(remote_address, remote_port)


    def setOnBind(self, function):

        # Is callable?
        if(not callable(function)):
            raise IsNotCallableException()

        # Hook event
        self.onBindEvent = function


    def setOnWaitConnection(self, function):

        # Is callable?
        if(not callable(function)):
            raise IsNotCallableException()

        # Hook event
        self.onWaitConnectionEvent = function


    def setOnClientConnect(self, function):

        # Is callable?
        if(not callable(function)):
            raise IsNotCallableException()

        # Hook event
        self.onClientConnectEvent = function


    def setOnClientClose(self, function):

        # Is callable?
        if(not callable(function)):
            raise IsNotCallableException()

        # Hook event
        self.onClientCloseEvent = function


    def setOnClientReceive(self, function):

        # Is callable?
        if(not callable(function)):
            raise IsNotCallableException()

        # Hook event
        self.onClientReceiveEvent = function



class SocketClient(object):


    def __init__(self):

        # Socket handler
        self.socket = None

        # Line control
        self.line_part = b''

        # Satus control
        self.is_alive = False

        # @params: hostname, port, use_ssl
        self.onConnecting = None

        # @params: hostname, port, use_ssl
        self.onConnect = None

        # @params: void
        self.onClose = None

        # @params: binary_data
        self.onSent = None

        # @params: binary_data
        self.onReceive = None

        # @params: binary_line
        self.onReceiveLine = None


    def close(self):

        # Close once time
        if(not self.is_alive):
            return

        if(self.socket):
            try:
                self.socket.close()
            except Exception as ignored:
                pass

        self.is_alive = False

        # User event
        if(callable(self.onClose)):
            self.onClose()


    def connect(self, hostname, port, use_ssl=False):

        # Reset buffer line
        self.line_part = b''

        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.socket.settimeout(40)

        # User event
        if(callable(self.onConnecting)):
            self.onConnecting(str(hostname), int(port), bool(use_ssl))

        # Use ssl?
        if(bool(use_ssl)):
            self.socket = ssl.create_default_context().wrap_socket(
                self.socket,
                server_hostname=str(hostname)
            )

        self.socket.connect((str(hostname), int(port)))

        self.is_alive = True

        # User event
        if(callable(self.onConnect)):
            self.onConnect(str(hostname), int(port), bool(use_ssl))

        while(self.is_alive):

            try:
                binary_data = self.socket.recv(2048)

                if(not binary_data):
                    break

            except Exception as ignored:
                break

            # User event
            if(callable(self.onReceive)):
                self.onReceive(binary_data)

            # Buffered by line?
            if(not callable(self.onReceiveLine)):
                continue

            # Append data to buffer
            self.line_part += binary_data

            # Have a new line?
            if(not b'\n' in self.line_part):
                continue

            # Split all lines by receive and last data
            lines = self.line_part.split(b'\n')

            # Last part for continue a new line
            self.line_part = lines.pop()

            # Call user event for each line
            for line in lines:

                # Striped for drop \r
                self.onReceiveLine(line.strip())

        self.close()


    def send(self, binary_data):

        if(self.socket):
            try:
                self.socket.sendall(binary_data)

            except BrokenPipeError as e:

                # Socket is closed
                self.close()

            except Exception as e:

                # Unhandled exception
                raise e

        # User event
        if(callable(self.onSent)):
            self.onSent(binary_data)


    def setOnConnecting(self, function):

        # Is callable?
        if(not callable(function)):
            raise IsNotCallableException()

        # Hook event
        self.onConnecting = function


    def setOnConnect(self, function):

        # Is callable?
        if(not callable(function)):
            raise IsNotCallableException()

        # Hook event
        self.onConnect = function


    def setOnClose(self, function):

        # Is callable?
        if(not callable(function)):
            raise IsNotCallableException()

        # Hook event
        self.onClose = function


    def setOnSent(self, function):

        # Is callable?
        if(not callable(function)):
            raise IsNotCallableException()

        # Hook event
        self.onSent = function


    def setOnReceive(self, function):

        # Is callable?
        if(not callable(function)):
            raise IsNotCallableException()

        # Hook event
        self.onReceive = function


    def setOnReceiveLine(self, function):

        # Is callable?
        if(not callable(function)):
            raise IsNotCallableException()

        # Hook event
        self.onReceiveLine = function

