from abc import abstractmethod
from typing import Generator, Iterable, Optional

from cerulean.file_system import FileSystem
from cerulean.path import AbstractPath, EntryType, Path, Permission


class FileSystemImpl(FileSystem):
    """Abstract class for file system implementations.

    This class exists to document the (internal!) interface and to
    help keep it consistent between the different kinds of file
    systems, as well as to implement some shared convenience functions.

    You will want to use a  concrete file system, like LocalFileSystem
    or SftpFileSystem.
    """

    # Feature support

    _features = ['symlinks', 'permissions', 'devices']

    @abstractmethod
    def _supports(self, feature: str) -> bool:
        """Whether the file system supports the given feature.

        See _features for supported features.

        Args:
            feature: The feature to check.

        Returns:
            Whether it is supported.

        """
        pass

    # Existence and contents

    @abstractmethod
    def _exists(self, path: AbstractPath) -> bool:
        pass

    @abstractmethod
    def _mkdir(
            self, path: AbstractPath, mode: Optional[int] = None,
            parents: bool = False, exists_ok: bool = False) -> None:
        pass

    @abstractmethod
    def _iterdir(
            self, path: AbstractPath) -> Generator[AbstractPath, None, None]:
        pass

    @abstractmethod
    def _rmdir(self, path: AbstractPath, recursive: bool = False) -> None:
        pass

    @abstractmethod
    def _touch(self, path: AbstractPath) -> None:
        pass

    @abstractmethod
    def _streaming_read(
            self, path: AbstractPath) -> Generator[bytes, None, None]:
        pass

    @abstractmethod
    def _streaming_write(
            self, path: AbstractPath, data: Iterable[bytes]) -> None:
        pass

    # Convenience functions are implemented here, on top of the internal API
    def _read_bytes(self, path: AbstractPath) -> bytes:
        data = bytearray()
        for chunk in self._streaming_read(path):
            data += chunk
        return bytes(data)

    def _read_text(
            self, path: AbstractPath, encoding: str = 'utf-8', errors: str = 'strict'
            ) -> str:
        return self._read_bytes(path).decode(encoding, errors)

    def _write_bytes(self, path: AbstractPath, data: bytes) -> None:
        self._streaming_write(path, [data])

    def _write_text(
            self, path: AbstractPath, data: str, encoding: str = 'utf-8',
            errors: str = 'strict') -> None:
        self._write_bytes(path, data.encode(encoding, errors))

    @abstractmethod
    def _rename(self, path: AbstractPath, target: AbstractPath) -> None:
        pass

    @abstractmethod
    def _unlink(self, path: AbstractPath) -> None:
        pass

    # File type and size

    @abstractmethod
    def _is_dir(self, path: AbstractPath) -> bool:
        pass

    @abstractmethod
    def _is_file(self, path: AbstractPath) -> bool:
        pass

    @abstractmethod
    def _is_symlink(self, path: AbstractPath) -> bool:
        pass

    @abstractmethod
    def _entry_type(self, path: AbstractPath) -> EntryType:
        pass

    @abstractmethod
    def _size(self, path: AbstractPath) -> int:
        pass

    # Permissions

    @abstractmethod
    def _uid(self, path: AbstractPath) -> int:
        pass

    @abstractmethod
    def _gid(self, path: AbstractPath) -> int:
        pass

    @abstractmethod
    def _has_permission(
            self, path: AbstractPath, permission: Permission) -> bool:
        pass

    @abstractmethod
    def _set_permission(
            self, path: AbstractPath, permission: Permission,
            value: bool = True) -> None:
        pass

    @abstractmethod
    def _chmod(self, path: AbstractPath, mode: int) -> None:
        pass

    @abstractmethod
    def _symlink_to(self, path: AbstractPath, target: AbstractPath) -> None:
        pass

    @abstractmethod
    def _readlink(self, path: AbstractPath, recursive: bool) -> Path:
        pass
