import copy
import csv
import logging
import os
import re
import pickle
import tempfile
from abc import ABCMeta, abstractmethod, ABC
from io import BytesIO
from typing import (
    Any,
    List,
    Union,
    Type,
    TextIO,
    Iterable,
    Tuple,
    KeysView,
    ItemsView,
    ValuesView,
)
import json

from .._requests import request
from zipfile import ZipFile, ZIP_DEFLATED

from ..display import Progress
from ..config import DATA_UNIT_MAP
from ..system import Path, mkdir
from ..array import get_cols, flatten, get_shape
from ..utils import is_sequence, clipboard
from ..system import memory_of_this
from ..utils import string_to_literal, sample, fill

logger = logging.Logger(__name__)

__all__ = ["FileIO"]


class _IFileIO(metaclass=ABCMeta):
    _size_map = DATA_UNIT_MAP
    _dont_read = [".pyc"]
    _ignore_dir = [".git"]
    _date_format = "%Y-%m-%d %H:%M:%S"
    # need implement
    _is_byte = None
    _only_read = None
    _newline = True
    _ext_allowed = None
    _need_implement = {
        "_is_byte":     """
    Define the configuration of the file in the class {class_name}:
    e.g:
    class {class_name}:
        _is_byte: bool = True # or False
    """,
        "_only_read":   """Define the configuration of the file in the class {class_name}:
    e.g:
    class {class_name}:
        _only_read: bool = True # or False

    """,
        "_ext_allowed": """Define the configuration of the file in the class {class_name}:
                       e.g:
                       class {class_name}:
                           _ext_allowed: bool = '.txt' # or ''

                       """,
    }

    def __init__(self):
        self._built = False
        self._length = None
        self._last_update = None
        self._is_deleted = False
        self._current_change = 0
        self._max_history_length = 50
        self._change_history = []
        self._data = None

    @property
    @abstractmethod
    def data(self) -> Any:
        pass

    @property
    @abstractmethod
    def path(self) -> Path:
        pass

    @property
    @abstractmethod
    def name(self):
        pass

    @property
    @abstractmethod
    def ext(self):
        pass

    @property
    @abstractmethod
    def name_without_ext(self):
        pass

    @property
    @abstractmethod
    def dir_name(self):
        pass

    @property
    @abstractmethod
    def dir_path(self):
        pass

    @property
    @abstractmethod
    def is_empty(self):
        pass

    @property
    @abstractmethod
    def updated_at(self):
        pass

    @property
    @abstractmethod
    def created_at(self):
        pass

    @property
    @abstractmethod
    def was_changed(self) -> bool:
        pass

    @property
    @abstractmethod
    def length(self):
        pass

    @property
    @abstractmethod
    def history(self):
        pass

    @property
    @abstractmethod
    def exists(self):
        pass

    @property
    @abstractmethod
    def last_access(self):
        pass

    @abstractmethod
    def _load(self, mode=None, **kwargs) -> Any:
        pass

    @abstractmethod
    def save(
            self,
            on_new_path: Union[str, Path] = None,
            exist_ok=False,
            overwrite=False,
            **kwargs,
    ):
        pass

    @abstractmethod
    def _parse_fp(self, fp: Union[TextIO, BytesIO]) -> Any:
        pass

    @abstractmethod
    def _save_fp(self, fp: Union[TextIO, BytesIO]) -> None:
        pass

    @abstractmethod
    def undo(self):
        pass

    @abstractmethod
    def redo(self):
        pass

    @abstractmethod
    def parse(self, data):
        pass

    @abstractmethod
    def add(self, data, **kwargs):
        pass

    @abstractmethod
    def size(self, unit: str = "KB"):
        pass

    @abstractmethod
    def delete(self):
        pass

    @abstractmethod
    def __iter__(self):
        pass

    @abstractmethod
    def set_path(self, p: str):
        pass


class _FileIO(_IFileIO, ABC):
    def __init__(
            self, path_: Path, data=None, creation_mode=False, is_byte=None, **kwargs
    ):
        super().__init__()
        if is_byte is not None:
            self._is_byte = bool(is_byte)
        if not isinstance(path_, Path):
            path_ = Path(path_)
        self._creation_mode = creation_mode
        self._path = path_
        if self._ext_allowed:
            msg_assert_err = f'{path_.suffix} != {self._ext_allowed}. Force with support_ext = ".xyz" '
            assert self._path.suffix in self._ext_allowed, msg_assert_err
        self._ext_without_point = self._path.suffix.strip(".").upper()
        self._built = (
            True  # next setattr will be saved in history to be future recovery
        )
        if creation_mode:
            self._data = self.parse(data)
        else:
            self._data = self._load(**kwargs)

    def __init_subclass__(cls, **kwargs):
        for attr in cls._need_implement:
            if getattr(cls, attr) is None:
                raise NotImplementedError(
                        cls._need_implement.get(attr).format(class_name=cls.__name__)
                )

    def __repr__(self):
        return f"File{self.__class__.__name__.replace('_', '')}({self._path.name})"

    def __setattr__(self, key, value):
        object.__setattr__(self, key, copy.copy(value))
        if (
                hasattr(self, "_change_history")
                and key
                not in (
                "_current_change",
                "_max_history_length",
                "_change_history",
                "_built",
                "_last_update",
        )
                and self._built is True and not self._only_read
        ):
            self._set_change(
                    key, copy.copy(object.__getattribute__(self, key))
            )  # append last_value of attr

    def __getitem__(self, item):
        try:
            return self.data[item]
        except TypeError:
            raise TypeError(f"'{self.__class__.__name__}' object is not subscriptable")

    def __setitem__(self, key, value):
        # TODO: save only the index that has been changed
        try:
            self._data[key] = value
            self._set_change("_data", copy.copy(self._data))
        except TypeError:
            raise TypeError(f"'{self.__class__.__name__}' object is not subscriptable")

    def _set_change(self, key, value):
        self._change_history = self._change_history[: self._current_change + 1]
        if len(self._change_history) >= self._max_history_length:
            self._change_history.pop(0)
        self._change_history.append((key, value))
        self._current_change = len(self._change_history)

    def _select_change(self, index):
        try:

            key, value = self._change_history[self._current_change + index]
            object.__setattr__(self, key, copy.copy(value))
            self._current_change += index
            logger.warning(f"You selected amendment {self._current_change + 1}")
        except IndexError:
            logger.info("It's not possible")

    @property
    def line_sep(self):
        return os.linesep

    @property
    def was_changed(self) -> bool:
        """
        Detect if file is changed by another application
        if file not exists return False

        @return: bool
        """
        if not self.exists:
            return False
        return self._last_update is not None and self.updated_at != self._last_update

    @property
    def length(self):
        return len(self._data)

    @property
    def history(self):
        return self._change_history

    @property
    def updated_at(self):
        return self._path.updated_at

    @property
    def created_at(self):
        return self._path.created_at

    @property
    def last_access(self):
        return self._path.last_access

    @property
    def name(self):
        return self._path.name

    @property
    def ext(self):
        return self._path.suffix

    @property
    def name_without_ext(self):
        return self._path.stem

    @property
    def data(self):
        return copy.copy(self._data)

    @property
    def path(self):
        return self._path

    @property
    def dir_name(self):
        return self._path.parent_name

    @property
    def dir_path(self):
        return self._path.parent

    @property
    def is_empty(self):
        return not bool(self._data)

    @property
    def exists(self):
        return self.path.exists

    @property
    def only_read(self) -> bool:
        return self._only_read

    @classmethod
    def add(cls, *args, **kwargs):
        raise NotImplementedError("it's not implemented")

    @classmethod
    def parse_ext(cls, ext: str) -> str:
        if not isinstance(ext, str):
            raise TypeError("Error during parsing ext. Send a string.")
        return "." + ext.replace(".", "")

    def _get_mode(self, mode) -> str:
        """
        if mode is 'r' return 'r+' or 'r+b' else mode is 'w' return 'w+' or 'w+b'
        """
        return f"{mode}+b" if self._is_byte else f"{mode}+"

    def size(self, unit: str = "KB"):
        """
        returns the size that the file occupies on the disk.

        :param unit: choose anyone in ('B', 'KB', 'MB', 'GB', 'TB')

        """
        assert isinstance(
                unit, str
        ), f"expected {str.__name__} not {type(unit).__name__}."

        unit = unit.upper()

        assert (
                unit in self._size_map
        ), f"{repr(unit)} is'nt valid. Choose anyone in {tuple(self._size_map)}"
        # subtracts the size of the python object
        return (
                       memory_of_this(self._data) - self._data.__class__().__sizeof__()
               ) / self._size_map[unit]

    def _load(self, mode=None, **kwargs) -> Any:
        if self._path.suffix in self._dont_read:
            logger.warning(
                    f"I can't read this file. See class attribute <{self.__class__.__name__}._dont_read>"
            )
            return
        mode = mode or self._get_mode("r")
        encoding = None if self._is_byte else "utf-8"
        assert "r" in mode, f"{mode} for read isn't valid."
        try:
            with open(self._path.path, mode=mode, encoding=encoding, **kwargs) as fp:
                parsed = self._parse_fp(fp)
                self._last_update = self.updated_at
                return parsed
        except PermissionError as err:
            logger.error(err)
            return
        except Exception as err:
            raise IOError(f"Error when trying to read the file {self._path}\n{err}")

    def save(
            self,
            on_new_path: Union[str, Path] = None,
            mode=None,
            exist_ok=False,
            overwrite=False,
            force=False,
            encoding=None,
            **kwargs,
    ):

        if not force:
            assert self._only_read is False, f"{self.name} is only read."

            if on_new_path is not None:
                self.set_path(on_new_path)

            if self.was_changed:
                assert overwrite is True, AssertionError(
                        f"File change detected (last change {self.updated_at}), if you want to "
                        + f"overwrite set overwrite=True"
                )

            assert (
                    (exist_ok or not self.path.exists) or overwrite is True or force is True
            ), FileExistsError(
                    "File exists. If you want override, please send 'exist_ok=True'"
            )

        mode = mode or self._get_mode("w")
        assert "w" in mode, f"{mode} for write isn't valid."
        encoding = None if self._is_byte else "utf-8"
        with tempfile.TemporaryDirectory() as tmpdirname:
            tmp_file = Path(tmpdirname).join(self.name)
            if not self._newline and not self._is_byte:
                kwargs.update({"newline": ""})
            try:
                with open(tmp_file, mode=mode, encoding=encoding, **kwargs) as fp:
                    self._save_fp(fp)
            except Exception as err:
                raise IOError(f"Error writing to the file system file: {err}")
            tmp_file.cp(self.path)
            self._last_update = self.updated_at

    def delete(self):
        self._path.rm()
        self._is_deleted = True

    def set_path(self, path_):
        p = Path(path_)
        assert not p.is_dir, f"{p.path} is a directory."
        self._path = p

    def undo(self):
        if self._current_change > 0:
            index = -2 if self._current_change == len(self._change_history) else -1
            self._select_change(index)

    def redo(self):
        if self._current_change < len(self._change_history):
            self._select_change(+1)

    def sample_items(self, k: int = 1, is_random=False):
        """
        Get sample of this.

        @param k: length of sample
        @param is_random: bool
        """
        assert isinstance(k, int) and k > 0, f"k = {k} isn't valid."
        return sample(self.data, k=k, is_random=is_random)

    def __len__(self):
        if self._length is None:
            return len(self._data)
        return self._length

    def __next__(self):
        for i in self._data:
            yield i

    def __iter__(self):
        return self.__next__()

    @classmethod
    def load(cls, path_, **kwargs):
        return cls(path_, creation_mode=False, **kwargs)

    @classmethod
    def create(cls, path_, data=None, **kwargs):
        return cls(path_=path_, data=data, creation_mode=True, **kwargs)


class _Generic(_FileIO):
    _is_byte: bool = True
    _only_read = False
    _ext_allowed = ()
    _split_lines = False

    def _parse_fp(self, fp: BytesIO) -> List[bytes]:
        if self._split_lines or not bool(self.ext):
            data = fp.read().splitlines()
        else:
            data = fp.read()
        return self.parse(data)

    def _save_fp(self, fp: Union[TextIO, BytesIO]) -> None:
        fp.write(
                self._convert("\n").join(
                        i if isinstance(i, (str, bytes)) else str(i) for i in self._data
                )
        )

    def _convert(self, val):
        if self._is_byte and not isinstance(val, bytes):
            return str(val).encode()
        if not self._is_byte and isinstance(val, str):
            return string_to_literal(val)
        return val

    def parse(self, data) -> List[bytes]:
        if isinstance(data, (bytes, str, int, float, complex)):
            return [self._convert(data)]
        if not data:
            return [self._convert("")]
        if isinstance(data, (list, tuple)):
            return [self._convert(i) for i in data]
        raise ValueError(f"{type(data)} isn't valid.")

    def add(self, data, line=-1):
        data = self.parse(data)
        if line != -1:
            for pos, i in enumerate(data, line):
                self._data.insert(pos, i)
        else:
            self._data += data
            return
        self._set_change("_data", self._data.copy())

    def remove(self, line: Union[int, str]):
        self._data.pop(line)
        self._set_change("_data", self._data.copy())


class _TxtIO(_Generic):
    _is_byte: bool = False
    _only_read = False
    _ext_allowed = ()
    _split_lines = True


class _JsonIO(_FileIO):
    _is_byte: bool = False
    _only_read = False
    _ext_allowed = (".json",)
    indent = False
    ensure_ascii = False

    def __init__(self, path_: Path, indent=False, string_eval=True, **kwargs):
        self.indent = indent
        self.string_eval = string_eval
        if "ensure_ascii" in kwargs:
            self.ensure_ascii = kwargs.pop("ensure_ascii")
        super().__init__(path_, **kwargs)

    @staticmethod
    def _object_hook(obj):
        return {string_to_literal(k): string_to_literal(v) for k, v in obj.items()}

    def _parse_fp(self, fp: TextIO) -> dict:
        return json.load(
                fp, object_hook=self._object_hook if self.string_eval else None
        )

    def _save_fp(self, fp):
        json.dump(
                self._data,
                fp,
                indent=4 if self.indent else None,
                ensure_ascii=self.ensure_ascii,
        )

    def items(self) -> ItemsView:
        return self._data.items()

    def keys(self) -> KeysView:
        return self._data.keys()

    def values(self) -> ValuesView:
        return self._data.values()

    def parse(self, data) -> Union[dict, list]:
        if isinstance(data, (dict, list)):
            return data
        elif isinstance(data, (str, bytes)):
            return json.loads(
                    data, object_hook=self._object_hook if self.string_eval else None
            )
        else:
            raise TypeError(f"{type(data)} isn't valid.")

    def _add(self, k, v):
        self._data[k] = v

    def add(self, key, value):
        self._add(key, value)

    def remove(self, key):
        self._data.pop(key)

    def get(self, _key):
        return self._data.get(_key)

    def __getitem__(self, item):
        try:
            return self.data[item]
        except KeyError:
            raise KeyError(f"{item} not found.")

    def __setitem__(self, key, value):
        self.add(key, value)

    def __iter__(self):
        for i in self.data:
            yield i


class _Mp4IO(_FileIO, ABC):
    _is_byte: bool = True
    _only_read = True
    _ext_allowed = (".mp4",)

    def _save_fp(self, fp):
        fp.write(self._data)

    def _parse_fp(self, fp: BytesIO) -> bytes:
        return fp.read()

    def parse(self, data):
        return self._data


class _CsvIO(_FileIO):
    _is_byte: bool = False
    _only_read = False
    _newline = False
    _ext_allowed = (".csv",)

    def __init__(
            self,
            *args,
            cols: Union[Tuple[str], List[str]] = (),
            fill_with=None,
            str_to_literal=True,
            has_col=False,
            **kwargs,
    ):
        """
        :param data: expected row or list of rows with `n` elements equal to `k` cols, if not it will be fill with
        `fill_with` value.
        :param fill_with: fill empty
        :param has_col: bool
        :param literal_values: Convert `str` on literal types e.g `"'1'"` (str) -> `1` (int)
        """
        self._cols = cols
        self._str_to_literal = str_to_literal
        self._fill_with = fill_with
        self._n_values = 0
        self._has_col = has_col
        super().__init__(*args, **kwargs)

    @property
    def cols(self):
        return tuple(
                value if value is not None else f"unamed_col_{i}"
                for i, value in enumerate(self._cols)
        )

    @property
    def n_cols(self):
        return len(self.cols)

    @property
    def n_values(self):
        return self._n_values

    @property
    def rows(self):
        """
        generator for each row

        format -> {col_name: value, col_name2: value, ...}

        :return: rows with column. Is a dict type
        """
        for row in self.data:
            yield dict(zip(self.cols, row))

    def _parse_fp(self, fp: TextIO, **kwargs) -> List[List[Any]]:
        reader = csv.reader(fp)
        return self.parse(reader)

    def _save_fp(self, fp: TextIO):
        writer = csv.DictWriter(fp, fieldnames=self.cols)
        writer.writeheader()
        writer.writerows(self.rows)

    def _normalize_row(self, row, fill_with=None, literal_values=True):
        if not is_sequence(row):
            row = [row]
        if self._cols:
            row = (
                fill(value=list(row), max_size=len(self.cols), with_=fill_with)
                if len(row) < len(self.cols)
                else row
            )
        return [
            string_to_literal(item) if isinstance(item, str) else item
            for item in row
            if literal_values
        ]

    def parse(
            self,
            data: Union[Iterable[Iterable[Any]], str, int, float, complex],
            fill_with=None,
    ) -> List[List[Any]]:
        if fill_with is None:
            fill_with = self._fill_with
        normalized_data = []
        if isinstance(data, dict):
            self._cols = tuple(data)
            data = data.values()
        if not is_sequence(data):
            data = [data]
        for row in data:
            if not self._cols:
                # set cols on first iter
                self._cols = fill(value=list(row), max_size=len(row), with_=fill_with)
                continue
            row_normalized = self._normalize_row(
                    row, fill_with=fill_with, literal_values=self._str_to_literal
            )
            self._n_values += len(row_normalized)
            normalized_data.append(row_normalized)
        return normalized_data

    def to_dict(self):
        return dict(zip(self.cols, get_cols(self.data)))

    def flatten(self):
        return flatten(self._data)

    def shape(self):
        return get_shape(self._data)

    def _add(self, row: List[Any], index, fill_with=None):
        row = self.parse([row], fill_with=fill_with)
        if index != -1:
            for pos, i in enumerate(row, index):
                self._data.insert(pos, i)
        else:
            self._data += row

    def add(self, row: List[Any], index=-1, fill_with=None):
        """
        Add row is similar to list.append

        :param row: expected row with n elements equal to k cols, if not it will be fill with `fill_with` value.
        :param index: position that will be added
        :param fill_with: None
        """
        self._add(row=row, index=index, fill_with=fill_with)

    def __getitem__(self, item):
        if isinstance(item, str):
            if item not in self.cols:
                raise ValueError(f"Column name <{item}> not in {self.cols}")
            return get_cols(self.data)[self.cols.index(item)]
        if isinstance(item, tuple):
            lines, col = item
            if isinstance(col, tuple):
                raise ValueError("Isn't Possible.")
            assert col <= self.n_cols - 1, ValueError(
                    f"Invalid Column. Choice available index {list(range(self.n_cols))}"
            )
            return get_cols(self.data)[col][lines]
        return super().__getitem__(item)


class _PyObj(_FileIO):
    _is_byte: bool = True
    _only_read = False
    _newline = False
    _ext_allowed = (".pkl",)

    def _parse_fp(self, fp: TextIO, **kwargs) -> List[List[Any]]:
        loaded_obj = pickle.load(fp, **kwargs)
        return self.parse(loaded_obj)

    def _save_fp(self, fp: TextIO):
        pickle.dump(self.data, fp)

    def parse(self, data):
        return data


class _ZipFileIO(_FileIO):
    _is_byte: bool = False
    _only_read = False
    _newline = False
    _ext_allowed = (".zip",)

    def __init__(self, path_: Path, **kwargs):

        super().__init__(path_, **kwargs)

    def add(self, data: Union[str, list, tuple]):
        parsed = self.parse(data)
        self._data += parsed

    def _parse_fp(self, fp: Union[TextIO, BytesIO]) -> Any:
        return self.unzip(fp.name)

    @classmethod
    def unzip(
            cls,
            file_path,
            save_on: str = None,
            members: List = None,
            k=None,
            is_random=False,
    ):
        with ZipFile(file_path, mode="r") as myzip:
            members = members or sample(myzip.namelist(), k, is_random)
            if save_on:
                mkdir(save_on)
            myzip.extractall(
                    save_on, members=Progress.prog(list(members), name="Extracting")
            )

    def _save_fp(self, fp: Union[TextIO, BytesIO]) -> None:
        raise NotImplementedError

    @classmethod
    def load(cls, path_, load_on_memory=False, **kwargs):
        return cls(
                path_=path_, creation_mode=False, load_on_memory=load_on_memory, **kwargs
        )

    def save(
            self,
            on_new_path: Union[str, Path] = None,
            exist_ok=False,
            overwrite=False,
            force=False,
            **kwargs,
    ):
        with tempfile.TemporaryDirectory() as tmpdirname:
            tmp_file = Path(tmpdirname).join(self.name)
            try:
                with ZipFile(
                        tmp_file.path, mode="w", compression=ZIP_DEFLATED
                ) as myzip:
                    for i in self.data:
                        myzip.write(i)
            except Exception as err:
                raise IOError(f"Error writing to the file system file: {err}")
            tmp_file.cp(self.path)
            self._last_update = self.updated_at

    def parse(self, data):
        if not data:
            return []
        if isinstance(data, (str, Path)):
            data = [data]

        if not is_sequence(data):
            raise ValueError("Send list of paths invalid.")
        res = []

        for p in data:
            p = Path(p)
            if not p.exists:
                raise ValueError(f"{p.path} not Found.")
            res.append(p)
        return res


class _SrtFile(_TxtIO):
    _only_read = False
    _ext_allowed = (".srt", ".vtt")

    @classmethod
    def is_block_start(cls, line) -> bool:
        return bool(
                re.match(r"\d{2}:\d{2}:\d{2}[.,]\d+\s-->\s\d{2}:\d{2}:\d{2}[.,]\d+", line)
        )

    def parse(self, data):
        data = super().parse(data)
        blocks = []
        current_block = 0
        block = []
        block_started = False
        for i in data:
            if self.is_block_start(i):
                if block_started:
                    blocks.append(self.Block(current_block, *block))
                block_started = True
                current_block += 1
                block = []
            if block_started and not i.isdigit():
                block.append(i)
        if block:
            blocks.append(self.Block(current_block, *block))

        return blocks

    @property
    def string(self):
        return "\n".join(
                [str(block_item) for block in self.data for block_item in block]
        )

    def add(self, data, line=-1):
        raise NotImplementedError

    @classmethod
    def from_url(cls, url: str, *args, **kwargs):
        # TODO: need improves
        resp = request.get(url, *args, **kwargs)
        if resp.code == 200:
            return cls(Path("./subtitle.srt"), resp.data, creation_mode=True)

    @property
    def text(self):
        return [
            content.strip()
            for block in self.data
            for content in block.content
            if content
        ]

    class Block:
        def __init__(self, number, *content):
            self.number = number
            time_, *content = content

            self.time = time_
            start, time_separator, end = time_.split()
            self._start = start
            self._end = end
            self.content = content
            self._time_separator = time_separator

        @property
        def start(self):
            return self._start

        @property
        def end(self):
            return self._end

        @property
        def _values(self):
            return (self.number, self.time, *self.content)

        def __getitem__(self, item):
            return self._values[item]

        def __repr__(self):
            return f"{self._values}"


class FileIO:
    txt = _TxtIO
    json = _JsonIO
    mp4 = _Mp4IO
    csv = _CsvIO
    pkl = _PyObj
    zip = _ZipFileIO
    srt = _SrtFile
    _generic = _Generic

    def __new__(cls, path_, data=None, **kwargs):
        raise Exception(
                f"Cannot instantiate {cls.__name__}, use the methods ['create', 'load', 'load_files']"
        )

    @classmethod
    def create(cls, path_: Union[Type[Path], str], data: Any, **kwargs) -> _FileIO:
        path_ = Path(path_)
        return cls.lookup(path_.suffix).create(path_=path_, data=data, **kwargs)

    @classmethod
    def lookup(cls, ext: str) -> Type[_FileIO]:
        ext = ext.replace(".", "")
        if hasattr(cls, ext):
            return getattr(cls, ext)
        return cls._generic

    @classmethod
    def load(cls, path_: Union[str, Path], **kwargs) -> _FileIO:
        path_ = Path(path_)
        if not path_.exists:
            raise FileNotFoundError(f"{path_.path} not found.")
        return cls.lookup(path_.suffix).load(path_=path_, **kwargs)

    @classmethod
    def load_from_clipboard(cls):
        return cls.load(Path(clipboard()))

    @classmethod
    def load_files(
            cls,
            path_,
            ext=None,
            contains_in_name: List = (),
            not_contains_in_name=(),
            take_empty=True,
            recursive=False,
            ignore_dirs=(),
    ):

        ext = ext or ""
        f_paths = Path(path_).list_files(
                ext=ext,
                contains_in_name=contains_in_name,
                not_contains_in_name=not_contains_in_name,
                recursive=recursive,
                ignore_dirs=ignore_dirs,
        )
        loaded = []
        for p in f_paths:
            file_ = cls.load(path_=p)
            if file_ is None:
                continue
            if take_empty is True and file_.is_empty:
                continue
            loaded.append(file_)
        return loaded


FileType = Type[_Generic]
FileTxtType = Type[_TxtIO]
FileJsonType = Type[_JsonIO]
FileCsvType = Type[_CsvIO]
FilePythonObj = Type[_PyObj]
