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


class Multiples(Iterable):
    # number of multiples allowed in mem before the OnDiskMultiples writes to disk
    on_disk_multiples_block_size = 10_000
    # boundary between using InMemMultiples and OnDiskMultiples
    multiples_factory_ondisk_inmem_cutoff = 10_000_000_000

    @staticmethod
    def factory(max_possible_value: int = 0) -> Multiples:
        return MultiplesInMem(
            max_possible_value) if max_possible_value < Multiples.multiples_factory_ondisk_inmem_cutoff else MultiplesOnDisk(
            max_possible_value, Multiples.on_disk_multiples_block_size)

    @abstractmethod
    def append(self, multiple: Multiple) -> None: pass

    @abstractmethod
    def advance(self, max_val: int) -> None: pass

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


class MultiplesInMem(Multiples):
    def __init__(self, max_possible_value) -> None:
        self.max_possible_value = max_possible_value
        self._multiples = []

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

    def __iter__(self) -> Iterable[Multiple]:
        yield from self._multiples

    def append(self, multiple: Multiple) -> None:
        if multiple.start_val < self.max_possible_value:
            self._multiples.append(multiple)

    def advance(self, max_val: int) -> None:
        for i in range(len(self._multiples)):
            self._multiples[i].advance(max_val)


class MultiplesOnDisk(Multiples):

    def __init__(self, max_possible_value: int, max_mem_buffer: int, tmp_dir: str = '/var/tmp') -> None:
        self._max_possible_value = max_possible_value
        self._max_mem_buffer = max_mem_buffer
        self._partial_path = f'{tmp_dir}/primesgenseg_multiples'
        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 __len__(self) -> int:
        return len(self.__in_mem_data) + self._max_mem_buffer * (self._block_number_loaded - 1)

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

    def advance(self, max_val: int) -> None:
        for m in self.__in_mem_data:
            m.advance(max_val)
        for block_num in range(1, self._block_number_loaded):
            block_data = self._load_block(block_num)
            for i in range(len(block_data)):
                block_data[i].advance(max_val)
            self._write_block(block_num, block_data)

    def append(self, multiple: Multiple) -> None:
        if multiple.start_val <= self._max_possible_value:
            self.__in_mem_data.append(multiple)

        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[Multiple]) -> 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[Multiple]:
        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'


class Multiple:

    def __init__(self, start_val: int, p_val: int, max_val: int = None, advance: bool = True) -> None:
        self.start_val = start_val
        self.p_val = int(p_val)
        if advance:
            self.advance(max_val)

    def advance(self, max_val: int) -> None:
        self.start_val = self._get_start_val_in_next_seg(self.start_val, self.p_val, max_val)

    @staticmethod
    def _get_start_val_in_next_seg(start_val: int, p_val: int, max_val: int) -> int:
        max_val_plus_1_seg = max_val + 1
        # change anything here to make life hell
        return int(start_val + (-max_val_plus_1_seg if start_val > max_val else (math.floor(
            ((max_val - start_val) / p_val)) + 1) * p_val - max_val_plus_1_seg))
