from __future__ import division

import os
from collections import namedtuple
from functools import partial
from pathlib import Path
from threading import Lock
from typing import Callable, List, Optional

from .util import CallbackDispatcher

FileChunk = namedtuple("FileChunk", ["index", "size", "read"])


def build_chunks(
    read_bytes: Callable[[int, int], bytes], file_size: int, chunk_size: int
) -> List[FileChunk]:
    chunks = []

    index = 0
    start = 0

    while start < file_size:
        end = min(start + chunk_size, file_size)
        size = end - start

        chunk = FileChunk(index, size, partial(read_bytes, start, size))
        chunks.append(chunk)

        index += 1
        start += chunk_size

    return chunks


class ResumableFile:
    """A file to be uploaded in a resumable session.

    Parameters
    ----------
    path : str or pathlib.Path
        The path of the file
    relative_path : pathlib.Path or None
        The relative path of the file
    chunk_size : int
        The size, in bytes, of chunks uploaded in a single request

    Attributes
    ----------
    completed : resumable.util.CallbackDispatcher
        Triggered when all chunks of the file have been uploaded
    chunk_completed : resumable.util.CallbackDispatcher
        Triggered when a chunks of the file has been uploaded, passing the
        chunk
    """

    def __init__(
        self,
        path: Path,
        relative_path: Optional[Path],
        chunk_size: int,
        generate_unique_identifier: Callable[[Path, Path], str],
    ) -> None:

        self.path = path
        if relative_path is not None:
            self.relative_path = relative_path
        else:
            self.relative_path = path
        self.unique_identifier = generate_unique_identifier(
            self.path, self.relative_path
        )
        self.chunk_size = chunk_size
        self.size = os.path.getsize(self.path)

        self._fp = open(self.path, "rb")
        self._fp_lock = Lock()

        self.chunks = build_chunks(self._read_bytes, self.size, chunk_size)
        self._chunk_done = {chunk: False for chunk in self.chunks}

        self.completed = CallbackDispatcher()
        self.chunk_completed = CallbackDispatcher()

    def close(self) -> None:
        """Close the file."""
        self._fp.close()

    def _read_bytes(self, start: int, num_bytes: int) -> bytes:
        """Read a byte range from the file."""
        with self._fp_lock:
            self._fp.seek(start)
            return self._fp.read(num_bytes)

    @property
    def is_completed(self) -> bool:
        """Indicates if all chunks of this file have been uploaded."""
        return all(self._chunk_done.values())

    @property
    def fraction_completed(self) -> float:
        """The fraction of the file that has been completed."""
        return sum(self._chunk_done.values()) / len(self.chunks)

    def mark_chunk_completed(self, chunk: FileChunk) -> None:
        """Mark a chunk of this file as having been successfully uploaded.

        If all chunks have been completed, this will trigger the `completed`
        callback of this file.

        Parameters
        ----------
        chunk : resumable.chunk.FileChunk
            The chunk to mark as completed
        """
        self._chunk_done[chunk] = True
        if self.is_completed:
            self.completed.trigger()
            self.close()
        self.chunk_completed.trigger(chunk)
