import datetime
import glob
import os
import pickle
from abc import ABC, abstractmethod
from typing import Iterable


class Primes(ABC):
    # number of primes allowed in mem before the PrimesOnDisk writes to disk
    on_disk_primes_block_size = 20_000
    # boundary between using PrimesToReturn and PrimesOnDisk
    primes_ondisk_inmem_cutoff = 10_000_000_000

    @staticmethod
    def factory(max_value: int = 0, in_queue=None) -> Primes:
        if in_queue is not None:
            return PrimesToQueue(in_queue)
        return PrimesInMem() if max_value < Primes.primes_ondisk_inmem_cutoff is None else PrimesOnDisk(max_value,
                                                                                                        Primes.on_disk_primes_block_size)

    @abstractmethod
    def append(self, prime: int) -> None: pass

    @abstractmethod
    def __iter__(self) -> Iterable[int]: pass

    @abstractmethod
    def __len__(self) -> int: pass


class PrimesInMem(Primes):
    """
    uses a local array
    """

    def __init__(self) -> None:
        self._primes = []

    def __len__(self) -> int:
        return len(self._primes)

    def append(self, prime: int) -> None:
        self._primes.append(prime)

    def __iter__(self) -> Iterable[int]: return iter(self._primes)


class PrimesToQueue(Primes):
    """
    uses a queue
    """

    def __init__(self, in_queue) -> None:
        self._queue = in_queue
        self._queue_count = 0

    def append(self, prime: int) -> None:
        self._queue.put(prime)
        self._queue_count += 1

    def __iter__(self) -> Iterable[int]:
        self._queue.put(None)
        return iter([])

    def __len__(self) -> int:
        return self._queue_count


class PrimesOnDisk(Primes):

    def __init__(self, max_mem_buffer: int, tmp_dir: str = '/var/tmp') -> None:
        self._max_mem_buffer = max_mem_buffer
        self._partial_path = f'{tmp_dir}/primesgenseg_primes'
        for to_delete in glob.glob(f'{self._partial_path}*'):
            os.remove(to_delete)
        self._tempfile_base_name = f'{datetime.datetime.now().strftime(f"{self._partial_path}_%Y-%m-%d_%H-%M-%S")}'
        self._block_number_loaded = 1
        self.__in_mem_data = []

    def append(self, prime: int) -> None:
        self.__in_mem_data.append(prime)
        if len(self.__in_mem_data) > self._max_mem_buffer - 1:
            self._write_block(self._block_number_loaded, self.__in_mem_data)
            self.__in_mem_data.clear()
            self._block_number_loaded += 1

    def __len__(self) -> int:
        return len(self.__in_mem_data) + self._max_mem_buffer * (self._block_number_loaded - 1)

    def __iter__(self) -> Iterable[int]:
        for block_num in range(1, self._block_number_loaded):
            yield from self._load_block(block_num)
        yield from self.__in_mem_data

    def add(self, prime: int) -> None:
        self.__in_mem_data.append(prime)

        if len(self.__in_mem_data) > self._max_mem_buffer - 1:
            self._write_block(self._block_number_loaded, self.__in_mem_data)
            self.__in_mem_data.clear()
            self._block_number_loaded += 1

    def _write_block(self, block_num, block_data: list[int]) -> None:
        if len(block_data) != 0:
            with open(self._get_block_file_name(block_num), 'wb') as fd:
                pickle.dump(block_data, fd)

    def _load_block(self, block_num: int) -> list[int]:
        with open(self._get_block_file_name(block_num), 'rb') as fd:
            return pickle.load(fd)

    def _get_block_file_name(self, block_num):
        return f'{self._tempfile_base_name}-block{block_num}.dat'
