import os
import os.path
import posixpath
import ntpath
import random
import string

from parallels.core.utils.common import open_no_inherit


def get_dir_tree(path):
    """Retrieve tree structure of the directory with given path. Symbolic links are ignored.

    :type path: str | unicode
    :rtype: list[str | {str, list}]
    """
    if not os.path.isdir(path):
        return []

    path = path_to_binary_string(path)

    tree = []
    for item in os.listdir(path):
        item_path = os.path.join(path, item)

        if os.path.islink(item_path):
            continue

        if os.path.isfile(item_path):
            tree.append(item)
        elif os.path.isdir(item_path):
            tree.append({item: get_dir_tree(item_path)})

    return tree


def get_nested_dir_trees(dir_tree):
    return [(x.keys()[0], x.values()[0]) for x in dir_tree if isinstance(x, dict)]


def get_nested_dir_names(dir_tree):
    return [x.keys()[0] for x in dir_tree if isinstance(x, dict)]


def get_nested_file_names(dir_tree):
    return [x for x in dir_tree if isinstance(x, basestring)]


def iter_full_file_paths(directory):
    """Iterate over full paths of all files in a directory

    :type directory: str | unicode
    :rtype: list[str | unicode]
    """
    if isinstance(directory, unicode):
        # we work here with byte strings, as file names may contain not-unicode symbols
        directory = directory.encode('utf-8')

    for root, _, files in os.walk(directory):
        for name in files:
            full_path = os.path.join(root, name)
            if os.path.isfile(full_path):
                yield os.path.join(root, name)


def iter_full_dir_paths(directory):
    """Iterate over full paths of all files in a directory

    :type directory: str | unicode
    :rtype: list[str | unicode]
    """
    if isinstance(directory, unicode):
        # we work here with byte strings, as file names may contain not-unicode symbols
        directory = directory.encode('utf-8')

    for root, dirs, _ in os.walk(directory):
        for name in dirs:
            full_path = os.path.join(root, name)
            if os.path.isdir(full_path):
                yield os.path.join(root, name)


def read_file_contents(filename):
    with open_no_inherit(filename, 'rb') as item_fp:
        return item_fp.read()


def files_have_equal_contents(filename1, filename2):
    """Check whether 2 files have equal content

    :type filename1: str | unicode
    :type filename2: str | unicode
    :rtype: bool
    """
    return read_file_contents(filename1) == read_file_contents(filename2)


def is_include_path(sub_path, root_path, is_windows):
    """Check if root_path include sub_path; return True if yes

    :type sub_path: str
    :type root_path: str
    :type is_windows: bool
    :rtype: bool
    """
    sub_path_parts = get_path_parts(sub_path, is_windows)
    root_path_parts = get_path_parts(root_path, is_windows)
    if len(root_path_parts) > len(sub_path_parts):
        return False
    for i, item in enumerate(root_path_parts):
        if sub_path_parts[i] != item:
            return False
    return True


def get_relative_path(sub_path, base_path, is_windows, sub_path_is_windows=None, base_path_is_windows=None):
    """Retrieve path to sub_path related to base_path; return None if root_path not include sub_path

    :type sub_path: str
    :type base_path: str
    :type is_windows: bool
    :type sub_path_is_windows: bool | None
    :type base_path_is_windows: bool | None
    :rtype: str | None
    """
    if sub_path_is_windows is None:
        sub_path_is_windows = is_windows
    if base_path_is_windows is None:
        base_path_is_windows = is_windows
    sub_path_parts = get_path_parts(sub_path, sub_path_is_windows)
    root_path_parts = get_path_parts(base_path, base_path_is_windows)
    if len(root_path_parts) > len(sub_path_parts):
        return None
    relative_path_parts = []
    for i, item in enumerate(sub_path_parts):
        if i < len(root_path_parts):
            if root_path_parts[i] != item:
                return None
            continue
        relative_path_parts.append(item)
    if len(relative_path_parts) == 0:
        return ''
    return ntpath.join(*relative_path_parts) if is_windows else posixpath.join(*relative_path_parts)


def get_base_path(sub_path, relative_path, is_windows, sub_path_is_windows=None, relative_path_is_windows=None):
    if sub_path_is_windows is None:
        sub_path_is_windows = is_windows
    if relative_path_is_windows is None:
        relative_path_is_windows = is_windows
    sub_path_parts = get_path_parts(sub_path, sub_path_is_windows)
    relative_path_parts = get_path_parts(relative_path, relative_path_is_windows)
    base_path_parts = list(sub_path_parts)
    for relative_path_part in reversed(relative_path_parts):
        path_part = base_path_parts.pop()
        if path_part != relative_path_part:
            # relative path does not belongs to sub path
            return sub_path
    if sub_path_is_windows:
        is_absolute = ntpath.isabs(sub_path)
    else:
        is_absolute = ntpath.isabs(sub_path)
    drive = None
    if sub_path_is_windows:
        drive, _ = ntpath.splitdrive(sub_path)
    return _compose_path(base_path_parts, is_windows, is_absolute, drive)


def get_path_parts(path, is_windows):
    norm_path = ntpath.normpath(path) if is_windows else posixpath.normpath(path)
    canonical_path = ntpath.normcase(norm_path) if is_windows else posixpath.normcase(norm_path)
    parts = _split_path(canonical_path, is_windows)
    return [x for x in parts if x != '.']


def path_to_binary_string(path):
    """Convert path to binary string ('str' built-in type). Use for input file path arguments.

    File systems usually don't work in unicode, and support filenames that are not unicode sequences.
    So, in general case functions which work with files accept path as binary string.
    However, if such function received unicode path, we consider it should be converted to binary string with
    UTF-8 encoding. If we won't perform the conversion, there are issues when joining unicode string
    and binary strings that contain non-ASCII symbols, for example when using "os.path.join" function, like:
    UnicodeDecodeError: 'ascii' codec can't decode byte 0xf6 in position 2: ordinal not in range(128)

    :type path: str | unicode
    :rtype: str
    """
    if isinstance(path, unicode):
        return path.encode('utf-8')
    else:
        return path


def get_size(start_path='.'):
    """Calc size for passed path recursively

    :type start_path: str | unicode | None
    :rtype: int
    """
    total_size = 0
    for dirpath, dirnames, filenames in os.walk(start_path):
        for f in filenames:
            file_path = os.path.join(dirpath, f)
            # this check is required to skip broken soft links
            if os.path.isfile(file_path):
                total_size += os.path.getsize(file_path)
    return total_size


def _split_path(path, is_windows):
    head, tail = ntpath.split(path) if is_windows else posixpath.split(path)
    if tail:
        return _split_path(head, is_windows) + [tail]
    return []


def _compose_path(parts, is_windows, is_absolute, drive=None):
    if is_windows:
        if is_absolute:
            parts.insert(0, (drive if drive is not None else '') + '\\')
        return ntpath.join(*parts)
    else:
        if is_absolute:
            parts.insert(0, '/')
        return posixpath.join(*parts)


def temp_filename(temp_directory, prefix):
    filename = "%s-%s" % (
        prefix,
        "".join(random.choice(string.digits + string.ascii_letters) for _ in range(24))
    )
    return os.path.join(temp_directory, filename)