import logging
import time
from threading import Thread, Lock
from typing import Any, List, ClassVar
from uuid import uuid4

from ._ThreadPoolParameters import ThreadPoolParameters
from ._WorkerThread import WorkerThread

logger = logging.getLogger("ZeroMqThreadPool.ZeroMqThreadPool")


class ZeroMqThreadPool(object):
    def __init__(self, context, parameters, worker_address, is_daemon, thread_class, *args, **kwargs):
        # type: (Any, ThreadPoolParameters, str, bool, ClassVar, Any, Any) -> None
        self.context = context
        self.parameters = parameters
        self.worker_address = worker_address
        self.thread_class = thread_class
        self.args = args
        self.kwargs = kwargs

        self.disposing = False
        self.disposing_lock = Lock()

        self.threads = []  # type: List[WorkerThread]
        for _ in range(0, self.parameters.initial_count, 1):
            self.create_worker_thread()

        self.monitor_thread = Thread(target=self.monitor,
                                     name="ZeroMqThreadPool_{}_MonitorThread".format(self.parameters.name))
        self.monitor_thread.daemon = is_daemon
        self.monitor_thread.start()

    def get_current_thread_count(self):
        return len(self.threads)

    def join(self):
        self.monitor_thread.join()

    def dispose(self):
        with self.disposing_lock:
            self.disposing = True
        for thread in self.threads:
            thread.stop()

        for thread in self.threads:
            thread.join()

        self.threads.clear()
        self.monitor_thread.join()

    def create_worker_thread(self):
        args = [
            self.context,
            self.worker_address,
            self.parameters.timeout,
            "{}_Worker_{}".format(self.parameters.name, str(uuid4()))
        ]
        args.extend(self.args)
        thread = self.thread_class(*args, **self.kwargs)
        thread.start()
        self.threads.append(thread)

    def monitor(self):
        while len(self.threads) > 0:
            time.sleep(self.parameters.monitor_timeout / 1000)
            busy_threads_count = 0
            ready_threads_count = 0
            idle_threads = []
            dead_threads = []
            thead_map = ""
            for thread in self.threads:
                if not thread.running:
                    dead_threads.append(thread)
                    thead_map += "{} ({}) - dead\n".format(thread.identity, thread.get_id())
                elif thread.is_working:
                    busy_threads_count += 1
                    thead_map += "{} ({}) - busy\n".format(thread.identity, thread.get_id())
                elif thread.is_idle:
                    idle_threads.append(thread)
                    thead_map += "{} ({}) - idle\n".format(thread.identity, thread.get_id())
                else:
                    ready_threads_count += 1
                    thead_map += "{} ({}) - ready\n".format(thread.identity, thread.get_id())

            if len(dead_threads) > 0:
                logger.error("Dead threads: {}".format(len(dead_threads)))
                for thread in dead_threads:
                    self.threads.remove(thread)

            logger.debug("busy: {} | ready: {} | idle: {} | total: {}".format(
                busy_threads_count,
                ready_threads_count,
                len(idle_threads),
                len(self.threads)))
            logger.debug("Thread Map:\n%s", thead_map)

            with self.disposing_lock:
                if not self.disposing and self.must_increment_threads(busy_threads_count):
                    increment = self.calculate_increment()
                    logger.debug("Incrementing {} threads".format(increment))
                    for i in range(0, increment):
                        self.create_worker_thread()

            if self.must_stop_threads(idle_threads):
                logger.debug("Stopping {} threads".format(len(idle_threads)))
                for thread in idle_threads:
                    thread.stop()
                    self.threads.remove(thread)

                    if len(self.threads) == self.parameters.initial_count:
                        break

    def must_increment_threads(self, busy_threads_count):
        return (busy_threads_count == len(self.threads) and len(self.threads) < self.parameters.max_count) or \
               len(self.threads) < self.parameters.initial_count

    def calculate_increment(self):
        if len(self.threads) + self.parameters.increment <= self.parameters.max_count:
            return self.parameters.increment
        else:
            return self.parameters.max_count - len(self.threads)

    def must_stop_threads(self, idle_threads):
        return len(self.threads) > self.parameters.initial_count and len(idle_threads) > 0
