import errno
import os

from . import _errors as error
from . import _generate as generate
from . import _stream as stream


class find_torrent_files:
    """Iterator over ``(torrent_file, torrent_file_counter, exception)`` tuples"""

    def __init__(self, *paths, max_file_size=float('inf')):
        self._paths = paths
        self._counter = 0
        self._max_file_size = max_file_size

    def __iter__(self):
        """
        Find torrent files recursively beneath each path in `paths`

        Each list item is a 4-tuple that contains the torrent file path or
        ``None``, a counter that increments for each torrent file and an
        exception or ``None``.
        """
        for path in self._paths:
            yield from self._find(path)

    def _find(self, path):
        if os.path.isdir(path):
            try:
                for name in os.listdir(path):
                    subpath = os.sep.join((str(path), name))
                    yield from self._find(subpath)
            except OSError as e:
                yield None, self._counter, error.ReadError(e.errno, str(path))

        elif os.path.basename(path).lower().endswith('.torrent'):
            try:
                file_size = os.path.getsize(path)
            except OSError:
                self._counter += 1
                yield path, self._counter, error.ReadError(errno.ENOENT, str(path))
            else:
                if file_size <= self._max_file_size:
                    self._counter += 1
                    yield path, self._counter, None

        elif not os.path.exists(path):
            yield None, self._counter, error.ReadError(errno.ENOENT, str(path))

    @property
    def total(self):
        """Total number of torrents beneath all paths"""
        # Get a sequence of all torrents without changing self._counter.
        items = tuple(type(self)(*self._paths, max_file_size=self._max_file_size))
        if items:
            # Last item should contain the number of torrents found.
            return items[-1][1]
        else:
            return 0


def is_file_match(torrent, candidate):
    """
    Whether `torrent` contains the same files as `candidate`

    Both arugments are :class:`~.Torrent` objects.

    The torrents match if they both share the same ``name`` and ``files`` or
    ``name`` and ``length`` fields in their :attr:`~.Torrent.metainfo`.
    `candidate`'s :attr:`~.Torrent.piece_size` of must also not exceed
    `torrent`'s :attr:`~.Torrent.piece_size_max`.

    This is a quick check that doesn't require any system calls.
    """
    # Compare relative file paths and file sizes.
    # Order of files is important.
    torrent_info, candidate_info = torrent.metainfo['info'], candidate.metainfo['info']

    # Don't bother doing anything else if the names are different
    if torrent_info['name'] != candidate_info['name']:
        return False

    torrent_id = _get_filepaths_and_sizes(torrent_info)
    candidate_id = _get_filepaths_and_sizes(candidate_info)
    if torrent_id == candidate_id:
        if torrent.piece_size_min <= candidate.piece_size <= torrent.piece_size_max:
            return True

    return False

def _get_filepaths_and_sizes(info):
    name = info['name']

    # Singlefile torrent
    length = info.get('length', None)
    if length:
        return [(name, length)]

    # Multifile torrent
    files = info.get('files', None)
    if files:
        files_and_sizes = []
        for file in files:
            files_and_sizes.append((
                os.sep.join((name, *file['path'])),
                file['length'],
            ))
        return sorted(files_and_sizes)

    else:
        raise RuntimeError(f'Unable to find files: {info!r}')


def is_content_match(torrent, candidate):
    """
    Whether `torrent` contains the same files as `candidate`

    Both arugments are :class:`~.Torrent` objects.

    If a `candidate` matches, a few piece hashes from each file are compared to
    the corresponding hashes from `candidate` to detect files name/size
    collisions.

    This is relatively slow and should only be used after :func:`is_file_match`
    returned `True`.
    """
    if not torrent.path:
        raise RuntimeError(f'Torrent does not have a file system path: {torrent!r}')

    # Compare some piece hashes for each file
    with stream.TorrentFileStream(candidate, content_path=torrent.path) as tfs:
        check_piece_indexes = set()
        for file in torrent.files:
            all_file_piece_indexes = tfs.get_piece_indexes_of_file(file)
            middle_piece_index = int(len(all_file_piece_indexes) / 2)
            some_file_piece_indexes = (
                all_file_piece_indexes[:1]
                + [middle_piece_index]
                + all_file_piece_indexes[-1:]
            )
            check_piece_indexes.update(some_file_piece_indexes)

        for piece_index in sorted(check_piece_indexes):
            if not tfs.verify_piece(piece_index):
                return False
    return True


def copy(from_torrent, to_torrent):
    """
    Copy ``pieces``, ``piece length`` and ``files`` from `from_torrent` to
    `to_torrent`
    """
    source_info = from_torrent.metainfo['info']
    to_torrent.metainfo['info']['pieces'] = source_info['pieces']
    to_torrent.metainfo['info']['piece length'] = source_info['piece length']
    if 'files' in from_torrent.metainfo['info']:
        # Confirm both file lists are identical while ignoring order
        def make_sortable(files):
            return [tuple(f.items()) for f in files]

        # Only include "length" and "files" fields
        source_files = [
            {'length': file['length'], 'path': file['path']}
            for file in source_info['files']
        ]

        assert sorted(make_sortable(to_torrent.metainfo['info']['files'])) \
            == sorted(make_sortable(source_files))

        # Copy file order from `source_info`
        to_torrent.metainfo['info']['files'] = source_files


class ReuseCallback(generate._IntervaledCallback):
    def __init__(self, *args, torrent, torrent_files_total, **kwargs):
        super().__init__(*args, **kwargs)
        self._torrent = torrent
        self._torrent_files_total = torrent_files_total

    def __call__(self, torrent_filepath, torrent_files_done, is_match, exception):
        if self._callback:
            force = bool(
                # Call callback if there is an error, e.g. "Permission denied"
                exception
                # Call callback if we found a match of if we are verifying file contents
                or is_match in (True, None)
                # Call callback if this is the last torrent file
                or torrent_files_done >= self._torrent_files_total
            )
            return super().__call__(
                self._torrent,
                torrent_filepath,
                torrent_files_done,
                self._torrent_files_total,
                is_match,
                exception,
                force=force,
            )
        elif exception:
            raise exception
