import shutil
import hashlib
from pathlib import Path
from typing import TextIO, BinaryIO, IO, Union
from datetime import datetime
from os.path import getmtime
from .low import ObservableDict


class Data:
    def __init__(self, data_name: str, parent, bucket,
                 protected_parent_methods: Union[None, dict] = None):
        self.__data_name__ = data_name
        self.__parent__ = parent
        self.__bucket__ = bucket
        self.__protected_parent_methods__ = protected_parent_methods
        self.__protected_parent_methods__['increase_data_count']()
        self.init_metadata()
        self.init_properties()

    @property
    def database(self):
        return self.__bucket__.db

    @property
    def db(self):
        return self.__bucket__.db

    @property
    def bucket(self):
        return self.__bucket__

    def init_metadata(self):
        if self.__data_name__ not in self.__parent__.metadata:
            self.__parent__.metadata[self.__data_name__] = dict()

    def init_properties(self):
        if self.__data_name__ not in self.__parent__.properties:
            self.__parent__.properties[self.__data_name__] = dict()

    def set_metadata(self, metadata: Union[None, dict], merge: bool = True):
        if metadata is None:
            return
        if merge:
            metadata = {**self.metadata, **metadata}
        self.__parent__.metadata[self.__data_name__] = metadata

    def set_properties(self, properties: Union[None, dict], merge: bool = True):
        if properties is None:
            return
        if merge:
            properties = {**self.properties, **properties}
        self.__parent__.properties[self.__data_name__] = properties

    @property
    def parent(self):
        return self.__parent__

    @property
    def path(self) -> Path:
        return self.__parent__.path / self.__data_name__

    @property
    def name(self) -> str:
        return self.__data_name__

    @property
    def metadata(self) -> ObservableDict:
        return self.__parent__.metadata[self.__data_name__]

    @property
    def properties(self) -> ObservableDict:
        return self.__parent__.properties[self.__data_name__]

    def rename(self, new_name: str):
        shutil.move(str(self.path), str(self.__parent__.path / new_name))
        self.__data_name__ = new_name

    def reader(self, binary: bool = False, **kwargs) -> [IO, BinaryIO, TextIO, None]:
        mode = 'r'
        mode += 'b' if binary else ''
        return open(str(self.path), mode=mode, **kwargs)

    def creator(self,
                binary: bool = False,
                confirm: bool = False,
                feedback: bool = False,
                **kwargs) -> [IO, BinaryIO, TextIO, None]:
        if confirm and not feedback:
            return None
        mode = 'x'
        mode += 'b' if binary else ''
        return open(str(self.path), mode=mode, **kwargs)

    def writer(self,
               binary: bool = False,
               append: bool = True,
               allow_overwrite: bool = False,
               confirm: bool = True,
               feedback: bool = False,
               **kwargs) -> [IO, BinaryIO, TextIO, None]:
        if not allow_overwrite and not append:
            raise PermissionError('Trying to overwrite existed data.')
        if confirm and not feedback:
            return
        mode = 'a' if append else 'w'
        mode += 'b' if binary else ''
        return open(str(self.path), mode=mode, **kwargs)

    def __repr__(self):
        return f"Data('{self.__data_name__}')"

    def import_file(self, src_path: [str, Path], allow_overwrite=False, confirm=True, feedback=False):
        if self.path.exists() and not allow_overwrite:
            return
        if confirm and not feedback:
            return
        shutil.copyfile(str(src_path), str(self.path))

    def export_file(self, dst_path: [str, Path], allow_overwrite=False):
        if Path(dst_path).exists() and not allow_overwrite:
            return
        shutil.copyfile(str(self.path), str(dst_path))

    def __calc_hash__(self, h, buffer_size: int = 131072):
        if not self.path.exists():
            return None
        with open(str(self.path), 'rb') as file_reader:
            while True:
                data = file_reader.read(buffer_size)
                if not data:
                    break
                h.update(data)
        return h.hexdigest()

    def md5(self, buffer_size: int = 131072, require_update: bool = False) -> [str, None]:
        if not self.path.exists():
            return None
        last_modified_time = getmtime(str(self.path))
        if require_update \
                or 'md5' not in self.metadata \
                or 'md5-timestamp' not in self.metadata \
                or self.metadata['md5-timestamp'] < last_modified_time:
            result = self.__calc_hash__(hashlib.md5(), buffer_size)
            self.metadata['md5'] = result
            self.metadata['md5-timestamp'] = datetime.now().timestamp()
            return result
        else:
            return self.metadata['md5']

    def sha1(self, buffer_size: int = 131072, require_update: bool = False) -> [str, None]:
        if not self.path.exists():
            return None
        last_modified_time = getmtime(str(self.path))
        if require_update \
                or 'sha1' not in self.metadata \
                or 'sha1-timestamp' not in self.metadata \
                or self.metadata['sha1-timestamp'] < last_modified_time:
            result = self.__calc_hash__(hashlib.sha1(), buffer_size)
            self.metadata['sha1'] = result
            self.metadata['sha1-timestamp'] = datetime.now().timestamp()
            return result
        else:
            return self.metadata['sha1']

    def sha256(self, buffer_size: int = 131072, require_update: bool = False) -> [str, None]:
        if not self.path.exists():
            return None
        last_modified_time = getmtime(str(self.path))
        if require_update \
                or 'sha256' not in self.metadata \
                or 'sha256-timestamp' not in self.metadata \
                or self.metadata['sha256-timestamp'] < last_modified_time:
            result = self.__calc_hash__(hashlib.sha256(), buffer_size)
            self.metadata['sha256'] = result
            self.metadata['sha256-timestamp'] = datetime.now().timestamp()
            return result
        else:
            return self.metadata['sha256']

[((144, 40, 144, 53), 'hashlib.md5', 'hashlib.md5', ({}, {}), '()', False, 'import hashlib\n'), ((159, 40, 159, 54), 'hashlib.sha1', 'hashlib.sha1', ({}, {}), '()', False, 'import hashlib\n'), ((174, 40, 174, 56), 'hashlib.sha256', 'hashlib.sha256', ({}, {}), '()', False, 'import hashlib\n'), ((121, 11, 121, 25), 'pathlib.Path', 'Path', ({(121, 16, 121, 24): 'dst_path'}, {}), '(dst_path)', False, 'from pathlib import Path\n'), ((146, 45, 146, 59), 'datetime.datetime.now', 'datetime.now', ({}, {}), '()', False, 'from datetime import datetime\n'), ((161, 46, 161, 60), 'datetime.datetime.now', 'datetime.now', ({}, {}), '()', False, 'from datetime import datetime\n'), ((176, 48, 176, 62), 'datetime.datetime.now', 'datetime.now', ({}, {}), '()', False, 'from datetime import datetime\n')]