# Miro - an RSS based video player application
# Copyright (C) 2005-2010 Participatory Culture Foundation
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
#
# In addition, as a special exception, the copyright holders give
# permission to link the code of portions of this program with the OpenSSL
# library.
#
# You must obey the GNU General Public License in all respects for all of
# the code used other than OpenSSL. If you modify file(s) with this
# exception, you may extend this exception to your version of the file(s),
# but you are not obligated to do so. If you do not wish to do so, delete
# this exception statement from your version. If you delete this exception
# statement from all source files in the program, then also delete it here.


"""Functions to handle moving/deleting files, especially on windows
where file locking semantics can cause problems.
"""

import logging
import os
import shutil

from miro import u3info

def makedirs(path):
    path = expand_filename(path)
    return os.makedirs(path)

def isfile(path):
    if not path:
        return False
    path = expand_filename(path)
    return os.path.isfile(path)

def isdir(path):
    if not path:
        return False
    path = expand_filename(path)
    return os.path.isdir(path)

def isabs(path):
    path = expand_filename(path)
    return os.path.isabs(path)

def getctime(path):
    path = expand_filename(path)
    return os.path.getctime(path)

def getmtime(path):
    path = expand_filename(path)
    return os.path.getmtime(path)

def exists(path):
    if not path:
        return False
    path = expand_filename(path)
    return os.path.exists(path)

def remove(path):
    path = expand_filename(path)
    return os.remove(path)

def rmtree(path):
    path = expand_filename(path)
    return shutil.rmtree(path)

def listdir(path):
    path = expand_filename(path)
    return os.listdir(path)

def open_file(path, *args, **kwargs):
    path = expand_filename(path)
    return file(path, *args, **kwargs)

def access(path, *args, **kwargs):
    path = expand_filename(path)
    return os.access(path, *args, **kwargs)

def move(path):
    path = expand_filename(path)
    shutil.move (path)

def rmdir(path):
    path = expand_filename(path)
    os.rmdir (path)

def rename(src, dest):
    src = expand_filename(src)
    dest = expand_filename(dest)
    os.rename (src, dest)

def abspath(path):
    path = expand_filename(path)
    path = os.path.abspath(path)
    path = collapse_filename(path)
    return path

def is_windows_file_in_use_error(exception):
    if not isinstance(exception, Exception):
        raise TypeError("%r is not an exception" % exception)
    try:
        return exception.errno == 13 and exception.winerror == 32
    except AttributeError:
        return False

def migrate_file(source, dest, callback, retry_after=10, retry_for=60):
    """Try to migrate a file, if this works, callback is called.  If
    we fail because the file is open, we retry migrating the file
    every so often (by default every 10 seconds, stopping after 60
    seconds).  This probably only makes a difference on Windows.
    """
    import eventloop

    source = expand_filename(source)
    dest = expand_filename(dest)

    try:
        shutil.move(source, dest)
    except EnvironmentError, e:
        logging.warn("Error migrating %s to %s (Error: %s)", source, dest, e)
        try:
            os.remove(dest)
        except EnvironmentError:
            pass
        if retry_for > 0:
            if is_windows_file_in_use_error(e):
                # permission denied, assume this means it's open by
                # another process on windows.
                logging.info('Retrying migration')
                eventloop.add_timeout(retry_after, migrate_file,
                        "Migrate File Retry", args=(source, dest, callback,
                            retry_after, retry_for - retry_after))
    except TypeError, e:
        logging.warn ("Type error migrating %s (%s) to %s (%s) (Error %s)",
                source, type(source), dest, type(dest), e)
        raise
    else:
        callback()

class DeletesInProgressTracker(set):
    def __init__(self):
        self.set = set()
    def normalize(self, path):
        return os.path.abspath(os.path.normcase(path))
    def add(self, path):
        self.set.add(self.normalize(path))
    def discard(self, path):
        self.set.discard(self.normalize(path))
    def __contains__(self, path):
        return self.normalize(path) in self.set

deletes_in_progress = DeletesInProgressTracker()

def delete(path, retry_after=10, retry_for=60):
    """Try to delete a file or directory.  If this fails because the
    file is open, we retry deleting the file every so often This
    probably only makes a difference on Windows.
    """

    import eventloop
    path = expand_filename(path)
    try:
        if os.path.isfile(path):
            os.remove (path)
        elif os.path.isdir(path):
            shutil.rmtree (path)
        else:
            logging.warn("asked to delete '%s' but it's not there." % path)
    except EnvironmentError, e:
        logging.warn("Error deleting %s", path)
        if retry_for > 0 and is_windows_file_in_use_error(e):
            # permission denied, assume this means it's open by another
            # process on windows.
            deletes_in_progress.add(path)
            logging.info('Retrying delete')
            eventloop.add_timeout(retry_after, delete,
                    "Delete File Retry", args=(path, retry_after,
                        retry_for - retry_after))
    else:
        deletes_in_progress.discard(path)

def miro_listdir(directory):
    """Directory listing that's safe and convenient for finding new
    videos in a directory.

    Returns the tuple (files, directories) where both elements are a
    list of absolute pathnames.  OSErrors are silently ignored.
    Hidden files aren't returned.  Pathnames are run through
    os.path.normcase.
    """
    # FIXME - this doesn't look used anywhere

    files = []
    directories = []
    expanded_directory = expand_filename(directory)
    expanded_directory = os.path.abspath(os.path.normcase(expanded_directory))
    if expanded_directory in deletes_in_progress:
        return
    try:
        listing = os.listdir(expanded_directory)
    except OSError:
        return [], []
    for name in listing:
        if name[0] == '.' or name.lower() == 'thumbs.db':
            # thumbs.db is a windows file that speeds up thumbnails.
            # We know it's not a movie file.
            continue
        expanded_path = os.path.join(expanded_directory, os.path.normcase(name))
        path = os.path.join(directory, os.path.normcase(name))
        if expanded_path in deletes_in_progress:
            continue
        try:
            if os.path.isdir(expanded_path):
                directories.append(path)
            else:
                files.append(path)
        except OSError:
            pass
    return files, directories

def miro_allfiles(directory):
    """Directory listing that's safe and convenient for finding new
    videos in a directory.

    Returns a list of files consisting of absolute pathnames.

    OSErrors are silently ignored.  Hidden files aren't returned.
    Pathnames are run through os.path.normcase.
    """
    files = []
    expanded_directory = expand_filename(directory)
    expanded_directory = os.path.abspath(os.path.normcase(expanded_directory))
    if expanded_directory in deletes_in_progress:
        return []
    try:
        listing = os.listdir(expanded_directory)
    except OSError:
        return []
    for name in listing:
        name_lower = name.lower()
        if (name.startswith('.') or name_lower == 'thumbs.db' or
                name_lower == "incomplete downloads"):
            # thumbs.db is a windows file that speeds up thumbnails.
            # We know it's not a movie file.
            continue
        path = os.path.join(directory, os.path.normcase(name))
        expanded_path = os.path.join(expanded_directory, os.path.normcase(name))
        if expanded_path in deletes_in_progress:
            continue
        try:
            if os.path.isdir(expanded_path):
                files.extend(miro_allfiles(path))
            else:
                files.append(path)
        except OSError:
            pass
    return files

def expand_filename(filename):
    if not filename:
        return filename
    if u3info.u3_active:
        if filename.startswith(u3info.APP_DATA_PREFIX):
            filename = filename[len(u3info.APP_DATA_PREFIX):]
            while len(filename) > 0 and filename[0] in ['/', '\\']:
                filename = filename[1:]
            if len(filename) == 0:
                return u3info.app_data_path
            return os.path.join (u3info.app_data_path, filename)
        if filename.startswith(u3info.DEVICE_DOCUMENT_PREFIX):
            filename = filename[len(u3info.DEVICE_DOCUMENT_PREFIX):]
            while len(filename) > 0 and filename[0] in ['/', '\\']:
                filename = filename[1:]
            if len(filename) == 0:
                return u3info.device_document_path
            return os.path.join (u3info.device_document_path, filename)
    return filename

def collapse_filename(filename):
    if not filename:
        return filename
    if u3info.u3_active:
        if filename.startswith(u3info.app_data_path):
            filename = filename[len(u3info.app_data_path):]
            while len(filename) > 0 and filename[0] in ['/', '\\']:
                filename = filename[1:]
            if len(filename) == 0:
                return u3info.APP_DATA_PREFIX
            return u3info.APP_DATA_PREFIX + '\\' + filename

        elif filename.startswith(u3info.device_document_path):
            filename = filename[len(u3info.device_document_path):]
            while len(filename) > 0 and filename[0] in ['/', '\\']:
                filename = filename[1:]
            if len(filename) == 0:
                return u3info.DEVICE_DOCUMENT_PREFIX
            return u3info.DEVICE_DOCUMENT_PREFIX + '\\' + filename
    return filename
