import datetime
import json
import linecache
import math
from abc import ABC, abstractmethod
from pathlib import Path
from typing import Iterable

from src import on_disk_multiples_block_size, multiples_factory_ondisk_inmem_cutoff


def factory(max_possible_value) -> Multiples:
    return InMemMultiples(
        max_possible_value) if max_possible_value < multiples_factory_ondisk_inmem_cutoff else OnDiskMultiples(
        max_possible_value, on_disk_multiples_block_size)


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))


class Multiples(ABC):

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

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

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


class InMemMultiples(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]:
        return self._multiples.__iter__()

    def add(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 OnDiskMultiples(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._tempfile = f'{tmp_dir}/{datetime.datetime.now().strftime("primesgenseg_multipes_%Y-%m-%d_%H-%M-%S")}.dat'
        Path(self._tempfile).touch()
        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 add(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:
            to_write = json.dumps({block_num: [self.to_dict(d) for d in block_data]})
            with open(self._tempfile, 'r') as fd:
                lines = fd.readlines()
            with open(self._tempfile, 'w+') as fd:
                if len(lines) < block_num:
                    lines.append(to_write)
                elif len(lines) == block_num:
                    lines[block_num - 1] = to_write
                out = '\n'.join((line.strip('\n') for line in lines if len(line) > 0))
                fd.writelines(f'{out}')
                self._data_on_disk = True

    def _load_block(self, block_num: int) -> list[Multiple]:
        return [self.from_dict(m) for m in json.loads(linecache.getline(self._tempfile, block_num).strip('\n'))[
            f'{block_num}']] if self._data_on_disk and block_num != self._block_number_loaded else []

    @staticmethod
    def to_dict(multiple: Multiple) -> dict:
        return {"start_val": multiple.start_val, 'p_val': multiple.p_val}

    @staticmethod
    def from_dict(in_data: dict) -> Multiple:
        return Multiple(in_data['start_val'], in_data['p_val'], advance=False)
