import os
import sys
import typing
from decimal import Decimal

from knowit import VIDEO_EXTENSIONS

if sys.version_info < (3, 8):
    OS_FAMILY = str
else:
    OS_FAMILY = typing.Literal['windows', 'macos', 'unix']

OPTION_MAP = typing.Dict[str, typing.Tuple[str]]


def recurse_paths(
        paths: typing.Union[str, typing.Iterable[str]]
) -> typing.List[str]:
    """Return a list of video files."""
    enc_paths = []

    if isinstance(paths, str):
        paths = [p.strip() for p in paths.split(',')] if ',' in paths else paths.split()

    for path in paths:
        if os.path.isfile(path):
            enc_paths.append(path)
        if os.path.isdir(path):
            for root, directories, filenames in os.walk(path):
                for filename in filenames:
                    if os.path.splitext(filename)[1] in VIDEO_EXTENSIONS:
                        full_path = os.path.join(root, filename)
                        enc_paths.append(full_path)

    # Lets remove any dupes since mediainfo is rather slow.
    unique_paths = dict.fromkeys(enc_paths)
    return list(unique_paths)


def to_dict(
        obj: typing.Any,
        classkey: typing.Optional[typing.Type] = None
) -> typing.Union[str, dict, list]:
    """Transform an object to dict."""
    if isinstance(obj, str):
        return obj
    elif isinstance(obj, dict):
        data = {}
        for (k, v) in obj.items():
            data[k] = to_dict(v, classkey)
        return data
    elif hasattr(obj, '_ast'):
        return to_dict(obj._ast())
    elif hasattr(obj, '__iter__'):
        return [to_dict(v, classkey) for v in obj]
    elif hasattr(obj, '__dict__'):
        values = [(key, to_dict(value, classkey))
                  for key, value in obj.__dict__.items() if not callable(value) and not key.startswith('_')]
        data = {k: v for k, v in values if v is not None}
        if classkey is not None and hasattr(obj, '__class__'):
            data[classkey] = obj.__class__.__name__
        return data
    return obj


def detect_os() -> OS_FAMILY:
    """Detect os family: windows, macos or unix."""
    if os.name in ('nt', 'dos', 'os2', 'ce'):
        return 'windows'
    if sys.platform == 'darwin':
        return 'macos'
    return 'unix'


def define_candidate(
        locations: OPTION_MAP,
        names: OPTION_MAP,
        os_family: typing.Optional[OS_FAMILY] = None,
        suggested_path: typing.Optional[str] = None,
) -> typing.Generator[str, None, None]:
    """Select family-specific options and generate possible candidates."""
    os_family = os_family or detect_os()
    family_names = names[os_family]
    all_locations = (suggested_path, ) + locations[os_family]
    yield from build_candidates(all_locations, family_names)


def build_candidates(
        locations: typing.Iterable[typing.Optional[str]],
        names: typing.Iterable[str],
) -> typing.Generator[str, None, None]:
    """Build candidate names."""
    for location in locations:
        if not location:
            continue
        if location == '__PATH__':
            yield from build_path_candidates(names)
        elif os.path.isfile(location):
            yield location
        elif os.path.isdir(location):
            for name in names:
                cmd = os.path.join(location, name)
                if os.path.isfile(cmd):
                    yield cmd


def build_path_candidates(
    names: typing.Iterable[str],
    os_family: typing.Optional[OS_FAMILY] = None,
) -> typing.Generator[str, None, None]:
    """Build candidate names on environment PATH."""
    os_family = os_family or detect_os()
    if os_family != 'windows':
        yield from names
    else:
        paths = os.environ['PATH'].split(';')
        yield from (
            os.path.join(path, name)
            for path in paths
            for name in names
        )


def round_decimal(value: Decimal, min_digits=0, max_digits: typing.Optional[int] = None):
    exponent = int(value.normalize().as_tuple().exponent)
    if exponent >= 0:
        return round(value, min_digits)

    decimal_places = abs(exponent)
    if decimal_places <= min_digits:
        return round(value, min_digits)
    if max_digits:
        return round(value, min(max_digits, decimal_places))
    return value
