__author__ = "Greyscalegorilla"

# Modules import
from PySide6 import QtCore
from PySide6.QtCore import QObject, QMetaObject, Qt, Q_ARG
import time
import importlib
import socket
from queue import Queue
import threading
import errno
import json

from greyscalegorilla import material_manager as gsg_material_manager
from greyscalegorilla import protocol as gsg_protocol
from greyscalegorilla import log as gsg_log
from greyscalegorilla import version as gsg_version


MESSAGING_VERSION = "1.0"

log = gsg_log.Log()

error_codes = {
    "InvalidMetadata": 1,
    "ModelsNotFound": 2,
    "ModelsNotLoaded": 3,
    "MaterialsNotFound": 4,
    "MaterialsNotLoaded": 5,
}
class TaskManager:
    def __init__(self):
        self.queue = Queue()
        self.runners = {}

    def add(self, task_type, payload):
        task = self.create_task(task_type, payload)
        self.queue.put(task)

    def check_pending(self):
        while not self.queue.empty():
            task = self.queue.get()
            self.execute(task)
        return 1.0

    def execute(self, task):
        task_runner = self.runners[task['type']]
        if not task_runner:
            log.error(f'No task runner registered for task type: {task["type"]}')
            return
        task_runner(task['payload'])

    def register(self, task_type, runner):
        self.runners[task_type] = runner

    def create_task(self, task_type, payload):
        return {"type": task_type, "payload": payload}


protocol = gsg_protocol.Protocol()

class MainThreadHandler(QObject):
    def __init__(self):
        super().__init__()

    @QtCore.Slot(str, bool)
    def create_layer_from_path(self, in_path, in_triplanar=False):
        gsg_material_manager.create_layer_from_path(in_path, in_triplanar)

main_thread_handler = MainThreadHandler()

class GreyscalegorillaSocketsServer(threading.Thread):
    def __init__(self, port):
        threading.Thread.__init__(self)
        self.threading_event = threading.Event()
        self.host = "localhost"
        self.port = port
        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.connections = []
        self.task_manager = TaskManager()
        self.task_manager.register('load_material', self.load_material)
        self.running = False

    def load_material(self, payload):
        path = payload.get('path')
        triplanar = payload.get('triplanar')
        # execute the function in the main thread:
        QMetaObject.invokeMethod(main_thread_handler, "create_layer_from_path", Qt.QueuedConnection, Q_ARG(str, path), Q_ARG(bool, triplanar))

    def is_running(self):
        return self.running

    def run(self):
        try:
            self.socket.bind((self.host, self.port))
            self.socket.listen(5)
            log.info(f"Running on port {self.port}")
            self.running = True
            self.exit_handler = ExitHandler(self.threading_event)
            self.exit_handler.start()
            self.accept_connections()
        except socket.error as e:
            log.error('Socket error')
            win_connection_error = hasattr(errno, 'ENOTSOCK') and errno.ENOTSOCK == e.errno
            unix_connection_error = hasattr(errno, 'WSAENOTSOCK') and errno.WSAENOTSOCK == e.errno
            if win_connection_error or unix_connection_error:
                pass
        except Exception:
            log.error(
                f"Server could not start at port: {self.port}." +
                "Please make sure this is the only Substance Painter instance running " +
                "and there\'s no other applications occupying this port.")

    def accept_connections(self):
        while not self.threading_event.is_set():
            try:
                client_socket, client_address = self.socket.accept()
                log.info(f'Connection with Studio established at {client_address}.')

                self.client_handler = threading.Thread(
                    target=self.handle_client, args=(client_socket, client_address))
                self.client_handler.start()
                self.connections.append((self.client_handler, client_socket, client_address))
            except socket.error as e:
                win_connection_error = hasattr(errno, 'ENOTSOCK') and errno.ENOTSOCK == e.errno
                unix_connection_error = hasattr(errno, 'WSAENOTSOCK') and errno.WSAENOTSOCK == e.errno
                if win_connection_error or unix_connection_error:
                    break

    def handle_client(self, client_socket, client_address):
        try:
            ack_message = protocol.encode_message("greyscalegorillaconnect-connected")
            client_socket.sendall(ack_message)
            while not self.threading_event.is_set():
                messages = self.receive_messages(client_socket)
                if not messages:
                    continue
                self.handle_messages(messages, client_socket, client_address)
        except socket.error as e:
            win_connection_reset = hasattr(errno, 'ECONNRESET') and errno.ECONNRESET == e.errno
            unix_connection_reset = hasattr(errno, 'WSAECONNRESET') and errno.WSAECONNRESET == e.errno
            if win_connection_reset or unix_connection_reset:
                log.error('Sockets connection aborted.')
            else:
                log.error(f'Error with client {client_address}: {e}')
        finally:
            log.info('Studio disconnected.')
            client_socket.close()
            self.remove_connection(client_address)

    def receive_messages(self, client_socket):
        try:
            data = client_socket.recv(1024)
            return protocol.decode_buffer(data)
        except gsg_protocol.CRCException:
            self.handle_crc_exception(client_socket)
            return []

    def handle_messages(self, messages, client_socket, client_address):
        if len(messages) == 0:
            return
        message = messages.pop(0)
        # log.info(f'Received message: {message}')
        if message == "ping":
            response = "pong"
            encoded_message = protocol.encode_message(response)
            client_socket.sendall(encoded_message)
        elif message == "exit Greyscalegorilla Connect":
            self.stop()
        else:
            try:
                asset_metadata = json.loads(message)
                is_sp = asset_metadata.get('dcc') == 'SUBSTANCE_PAINTER'
                has_path_to_asset = asset_metadata.get('path_to_asset')
                has_triplanar = asset_metadata.get('triplanar') is not None
                is_valid_asset = asset_metadata.get('asset_type') == 'MATERIAL'
                if not is_valid_asset:
                    log.error('Invalid asset type, only materials are supported.')
                if is_sp and (not has_path_to_asset or not is_valid_asset):
                    raise Exception()
            except Exception:
                errors = [{
                    "error_code": error_codes['InvalidMetadata'],
                    "message": 'Invalid JSON message.',
                }]
                self.on_task_error(client_socket, errors)
                self.on_task_completed(client_socket, [])
            if asset_metadata.get('dcc') != 'SUBSTANCE_PAINTER':
                return
            if asset_metadata.get('asset_type') == 'MATERIAL' and asset_metadata.get('path_to_asset'):
                #log.info(f"Adding task manager to payload: {asset_metadata.get('path_to_asset')}")
                self.task_manager.add('load_material', {
                    "path": asset_metadata.get('path_to_asset'),
                    "triplanar": asset_metadata.get('triplanar') if has_triplanar else False,
                    "on_success": lambda completed: self.on_task_completed(client_socket, completed),
                    "on_error": lambda errors: self.on_task_error(client_socket, errors)
                })

    def remove_connection(self, client_address):
        for i, (_, _, addr) in enumerate(self.connections):
            if addr == client_address:
                del self.connections[i]
                break

    def stop(self):
        self.threading_event.set()
        # This loop uses _ as a placeholder for the first and third elements of each tuple, 
        # indicating that these elements are not used in the loop. 
        # The sock variable is used to refer to the second element of each tuple, 
        # which is the socket object.
        for _, sock, _ in self.connections:
            sock.close()
        self.socket.close()

    def on_task_completed(self, client_socket, succeses):
        #log.info('Replying...')
        response = {
            "type": "completed",
            "version": MESSAGING_VERSION,
            "completed": succeses,
        }
        encoded_message = protocol.encode_message(
            json.dumps(response, indent=4))
        client_socket.sendall(encoded_message)

    def on_task_error(self, client_socket, errors):
        for error in errors:
            if error.get('path_to_asset'):
                error['path_to_asset'] = error['path_to_asset'].replace('\\', '/')
            if error.get('message'):
                error['message'] = error['message'].replace('\\', '/')
        plus_app_response = {
            "type": "error",
            "version": MESSAGING_VERSION,
            "errors": errors,
        }
        encoded_response = protocol.encode_message(json.dumps(plus_app_response, indent=4))
        client_socket.sendall(encoded_response)

# Exit handler of the socket server
#
class ExitHandler(threading.Thread):
    def __init__(self, exit_event):
        self.exit_event = exit_event
        threading.Thread.__init__(self)

    def run(self):
        try:
            run_checker = True
            port_number = gsg_version.port_number
            while run_checker and not self.exit_event.is_set():
                time.sleep(1)
                for i in threading.enumerate():
                    if (i.getName() == "MainThread" and not i.is_alive()):
                        host, port = 'localhost', port_number
                        s = socket.socket()
                        s.connect((host, port))
                        data = "exit Greyscalegorilla Connect"
                        s.send(protocol.encode_message(data))
                        #log.info('Sending exit message')
                        s.close()
                        run_checker = False
                        break
        except Exception as e:
            log.error(f'Greyscalegorilla Connect - Error initializing ExitHandler. Error: str(e)')

# Painter does not have an even loop callback, so we need a class for it.
# Inherits from threading.Thread so it can be started without blocking the main thread.
#
class Scheduler(threading.Thread):
    def __init__(self, in_interval=1):
        super().__init__()
        self.running = False
        self.interval = in_interval

    def run(self):
        self.running = True

        try:
            port_number = gsg_version.port_number
            self.socket_server = GreyscalegorillaSocketsServer(port_number)
            if not self.socket_server.is_running():
                self.socket_server.start()
                #log.info('Socket server started')
        except Exception as e:
            log.error(f'Can not start Socket server. Error: {str(e)}')

        while self.running:
            time.sleep(self.interval)
            self.socket_server.task_manager.check_pending()

    def stop(self):
        self.running = False
        if self.socket_server:
            self.socket_server.stop()

scheduler = Scheduler()

def start_plugin():
    log.debug('Starting plugin...')
    scheduler.start()
    log.debug('Plugin started')

def close_plugin():
    log.debug('Closing plugin')
    if scheduler.running:
        scheduler.stop()

def reload_plugin():
    importlib.reload(gsg_material_manager)