# -*- coding: utf-8 -*-
from __future__ import annotations

from functools import wraps
from pathlib import Path
from typing import Callable, IO, Iterable, Mapping, NamedTuple, TypeVar

from .typedefs import BytesLike, FilePath, KT, T, VT
from .typeutils import isfilepath, tobytes, verify_fileobj

__all__ = [
    'BINARIES_ROOTDIR',
    'bytestrxor',
    'getattribute',
    'proberfuncfactory'
]

BINARIES_ROOTDIR = Path(__file__).parent / 'binaries'


def getattribute(obj: object,
                 name: str,
                 *default: T,
                 follow_callable: bool = False,
                 callable_args: Iterable = None,
                 callable_kwargs: Mapping | Iterable[tuple[KT, VT]] = None
                 ) -> T:
    """用法：``getattribute(object, name[, default][, follow_callable=...][, callable_args=...][, callable_kwargs=...]) -> value``

    ``getattribute()`` 的用法与 ``getattr()`` 基本一致。

    返回对象 ``obj`` 具名属性 ``name`` 的值。``name`` 必须是字符串。
    如果该字符串是对象的属性之一，则返回该属性的值。

    例如，``getattribute(x, 'foobar')`` 等同于 ``x.foobar``。

    如果指定的属性不存在，且提供了 ``default`` 值，则返回它，否则触发 ``AttributeError``。

    如果向 ``getattribute()`` 提供了关键字参数 ``follow_callable=True``，而且
    ``x.foobar`` 存在并且是一个可调对象（例如目标对象的一个方法），那么 ``getattribute()``
    会尝试获取该属性的返回值，作为 ``getattribute()`` 的返回值。

    Args:
        obj: 目标对象
        name: 目标属性的名字
        default: 可选，在目标对象不存在目标属性时，返回的默认值
        follow_callable: 若为真值，则在目标属性为可调对象时，返回调用该属性后的返回值
        callable_args: 可选，若目标属性为可调对象且
            follow_callable 为真值，调用该属性时传入的位置参数
        callable_kwargs: 可选，若目标属性为可调对象且
            follow_callable 为真值，调用该属性时传入的关键字参数
    """
    if len(default) > 1:
        raise TypeError(f'getattribute expected at most 3 arguments, got {2 + len(default)}')
    else:
        attr = getattr(obj, name, *default)

    if callable_args is None:
        callable_args = ()
    else:
        callable_args = tuple(callable_kwargs)
    if callable_kwargs is None:
        callable_kwargs = {}
    else:
        callable_kwargs = dict(callable_kwargs)

    if callable(attr) and bool(follow_callable):
        return attr(*callable_args, **callable_kwargs)
    return attr


def bytestrxor(term1: BytesLike, term2: BytesLike, /) -> bytes:
    """用法：``bytestrxor(term1, term2) -> xored_bytes``

    返回两个字节对象或类字节对象 ``term1`` 和 ``term2`` 经过异或之后的结果。

    ``term1`` 和 ``term2`` 在转换为 ``bytes`` 之后的长度必须相等，否则会触发
    ``ValueError``。
    """
    bytestring1 = tobytes(term1)
    bytestring2 = tobytes(term2)

    if len(bytestring1) != len(bytestring2):
        raise ValueError(
            'only byte strings of equal length can be xored: '
            f'term 1 ({len(bytestring1)}) != term 2 ({len(bytestring2)})'
        )

    return bytes(b1 ^ b2 for b1, b2 in zip(bytestring1, bytestring2))


_FileInfo = TypeVar('_FileInfo', bound=NamedTuple)


def proberfuncfactory(docstring_from=None):
    """本装饰器是一个工厂函数，被装饰的函数必须具备以下行为：

    - 至少接受一个参数（位于第一个），并将此参数视为文件对象进行操作
    - 内含探测文件结构的所有逻辑
    - 如果探测到受支持的文件，返回一个 NamedTuple 对象，该对象包含探测到的信息；否则返回 `None`

    具备以上行为的函数在被本装饰器装饰后，将会变成一个文件结构探测函数，具备以下行为：

    - 第一个纯位置参数为文件路径或文件对象，随后的任何参数都会被传递给被装饰的函数
    - 返回一个 2 元组：

        - 第一个元素是接受的文件路径或文件对象
        - 第二个元素是探测结果，具体内容见上文
    """

    def prober(
            operation: Callable[[IO[bytes], ...], _FileInfo | None]
    ) -> Callable[[FilePath, ...], tuple[FilePath | IO[bytes], _FileInfo | None]]:
        @wraps(operation)
        def wrapper(filething: FilePath | IO[bytes], /, *args, **kwargs) -> tuple[FilePath | IO[bytes], _FileInfo | None]:
            if isfilepath(filething):
                with open(filething, mode='rb') as fileobj:
                    return Path(filething), operation(fileobj, *args, **kwargs)
            else:
                fileobj = verify_fileobj(filething, 'binary',
                                         verify_readable=True,
                                         verify_seekable=True
                                         )
                fileobj_origpos = fileobj.tell()
                prs = operation(fileobj, *args, **kwargs)
                fileobj.seek(fileobj_origpos, 0)

                return fileobj, prs

        if isinstance(docstring_from, str):
            wrapper.__doc__ = str(docstring_from)
        elif docstring_from is not None:
            docstring = getattr(docstring_from, '__doc__', None)
            if docstring is not None:
                wrapper.__doc__ = docstring

        return wrapper

    if callable(docstring_from):
        return prober(docstring_from)
    return prober
