#!/usr/bin/env python

# Copyright (C) 2005-2007 INdT - Instituto Nokia de Tecnologia
#
# 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

__author__ = "Artur Duque de Souza"
__author_email__ = "artur.souza@openbossa.org"
__license__ = "GPL"

import re
import os
import time
import logging
import threading

from canolad.prefs import PluginPrefs
from lightmediascanner import LightMediaScanner

try:
    from pysqlite2 import dbapi2 as sqlite
except ImportError:
    from sqlite3 import dbapi2 as sqlite


class ScanPathsThread(threading.Thread):
    log = logging.getLogger("canola.daemon.ScanPathsThread")

    def __init__(self, prefs, parsers, paths, end_callback, callback_data=None):
        threading.Thread.__init__(self)
        self.prefs = prefs
        self.parsers = parsers
        self.paths = paths
        self.end_callback = end_callback
        self.callback_data = callback_data
        self.count_status = [0] * 7

    def _get_lms(self, parsers):
        lms = LightMediaScanner(self.prefs["db_path"],
                                charsets=self.prefs["charsets"],
                                slave_timeout=self.prefs["slave_timeout"],
                                commit_interval=self.prefs["commit_interval"])
        lms.set_progress_callback(self._progress)

        for p in parsers:
            try:
                lms.parser_find_and_add(p)
            except Exception, e:
                self.log.warning("Could not add parser %s: %s", p, e)
        return lms

    def _progress(self, lms, path, status):
        self.count_status[status] += 1

    def _run_scanner(self, parsers, path):
        lms = self._get_lms(parsers)
        self.log.debug("Running scanner: %s, path %r", lms, path)
        try:
            lms.process(path)
        except Exception, e:
            self.log.error("Problems with lms: %s", e)

    def run(self):
        for path in self.paths:
            self._run_scanner(self.parsers, path)

        if self.end_callback:
            if self.callback_data:
                self.end_callback(*self.callback_data)
            else:
                self.end_callback()


class MountTable(dict):
    prefs_file = os.path.join(PluginPrefs.prefs_dir, "canolad")
    folders_file = os.path.join(PluginPrefs.prefs_dir, "scanned_folders")
    log = logging.getLogger("canola.daemon.mount_table")

    def __init__(self, cb_db_locked=None, cb_db_unlocked=None):
        dict.__init__(self)
        self.callback_db_locked = cb_db_locked
        self.callback_db_unlocked = cb_db_unlocked
        self.prefs = PluginPrefs(self.prefs_file)
        self.folders = PluginPrefs(self.folders_file)
        self.scanner = None
        self._reset_counters()

        try:
            self.con = sqlite.connect(self.prefs["db_path"])
            self.cur = self.con.cursor()
            self._locked_count = 0
        except Exception, e:
            self.log.warning(e)

        self.check_folders()

    def _reset_counters(self):
        self.count_status = [0] * 7

    def _update_counters(self, scanner):
        self.log.debug("MountTable : count %s and updated with %s" % (self.count_status, self.scanner.count_status))
        if self.scanner:
            self.count_status = [x + y for x, y in
                                 zip(self.count_status,
                                     self.scanner.count_status)]

    def _get_db_locked(self):
        if self._locked_count == 0:
            return False
        return True

    def _set_db_locked(self, locked):
        if locked:
            if self._locked_count == 0 and self.callback_db_locked:
                self.callback_db_locked()
            self._locked_count += 1
        elif self._locked_count > 0:
            self._locked_count -= 1
            if self._locked_count == 0 and self.callback_db_unlocked:
                self.callback_db_unlocked()
        else:
            self.log.error("More unlock signals than lock signals!")

    db_locked = property(_get_db_locked, _set_db_locked)

    def get_scan_status(self):
        if self.scanner:
            count_status = [x + y for x, y in
                            zip(self.count_status, self.scanner.count_status)]
            self.log.debug("Partial Scan Status : %s" % count_status)
            return count_status
        self.log.debug("Scan Status (no scanner running) : %s" % self.count_status)
        return self.count_status

    def check_folders(self):
        if not self.folders.get("audio", None):
            self.folders["audio"] = []

        if not self.folders.get("video", None):
            self.folders["video"] = []

        if not self.folders.get("photo", None):
            self.folders["photo"] = []

        self.folders.save()

    def detect_paths(self, re_comp):
        self.log.debug("Audio folders: %s", self.folders["audio"])
        self.log.debug("Video folders: %s", self.folders["video"])
        self.log.debug("Photo folders: %s", self.folders["photo"])

        audio = [x for x in self.folders["audio"] if re_comp.match(x)]
        video = [x for x in self.folders["video"] if re_comp.match(x)]
        photo = [x for x in self.folders["photo"] if re_comp.match(x)]
        self.log.debug("Paths detected: %s, %s, %s", audio, video, photo)
        return (audio, video, photo)

    def _end_callback(self):
        self.log.debug("Unlocked")
        self._update_counters(self.scanner)
        self.scanner = None
        self.db_locked = False

    def _end_video_callback(self, photo_path):
        self.log.debug("Inside END_VIDEO_CALLBACK")
        self._update_counters(self.scanner)
        if len(photo_path) > 0:
            self.scan_photo_path(photo_path, self._end_callback)
        else:
            self.log.debug("Unlocked inside END_VIDEO_CALLBACK")
            self.db_locked = False

    def _end_audio_callback(self, video_path, photo_path):
        self.log.debug("Inside END_AUDIO_CALLBACK")
        self._update_counters(self.scanner)
        if len(video_path) > 0:
            self.scan_video_path(video_path,
                                 self._end_video_callback, (photo_path,))
        elif len(photo_path) > 0:
            self.scan_photo_path(photo_path, self._end_callback)
        else:
            self.log.debug("Unlocked inside END_AUDIO_CALLBACK")
            self.db_locked = False

    def scan_paths(self, audio_path, video_path, photo_path):
        self.log.debug("Scanning all paths: %s, %s, %s",
                       audio_path, video_path, photo_path)
        self._reset_counters()
        if len(audio_path) > 0:
            self.scan_audio_path(audio_path, self._end_audio_callback,
                                 (video_path, photo_path))
        elif len(video_path) > 0:
            self.scan_video_path(video_path, self._end_video_callback,
                                 (photo_path,))
        elif len(photo_path) > 0:
            self.scan_photo_path(photo_path, self._end_callback)
        else:
            self.log.debug("Unlocked inside scan_paths")
            self.db_locked = False

        return True

    def _get_lms(self, parsers):
        lms = LightMediaScanner(self.prefs["db_path"],
                                charsets=self.prefs["charsets"],
                                slave_timeout=self.prefs["slave_timeout"],
                                commit_interval=self.prefs["commit_interval"])
        for p in parsers:
            try:
                lms.parser_find_and_add(p)
            except Exception, e:
                self.log.warning("Could not add parser %s: %s", p, e)
        return lms

    def make_db_consistent(self, path, parsers):
        if not os.path.exists(path):
            self.mark_as_invalid(path)
            return False

        lms = self._get_lms(parsers)
        self.log.debug("Running checking: %s, path %r", lms, path)
        try:
            lms.check(path)
        except Exception, e:
            self.log.error("Problems with lms: %s", e)
        return True

    def select_paths(self, scanlist, blacklist, clearlist=None):
        def create_blacklist_re():
            re_str = None
            for item in blacklist:
                if re_str != None:
                    re_str = "%s|%s/.*" % (re_str, item)
                else:
                    re_str = "%s/.*" % item
            self.log.debug("Final re for blacklist: %s", re_str)
            return re.compile(re_str)

        re_comp = create_blacklist_re()

        if clearlist and len([p for p in clearlist if re_comp.match(p)]) > 0:
            # removed father and inserted a child
            filter = False
        else:
            filter = True
        self.log.debug("Using filter: %s", filter)

        if not filter:
            # better to rescan everybody
            self.log.debug("Going to scan everybody")
            lst = scanlist
        else:
            # dont need to rescan everybody, just changed ones
            lst = [p for p in scanlist if not re_comp.match(p)]
        self.log.debug("Returning scan_path: %s", lst)
        return lst

    def check_all_paths(self, audio_path=None, video_path=None, photo_path=None):
        """ This function checks all the folders configured by Canola"""
        self.folders.reload()

        self.log.debug("Locked inside check_all_paths")
        self.log.debug("Check all paths called with audio(%s), video(%s) and photo(%s)" % (str(audio_path), str(video_path), str(photo_path)))
        self.db_locked = True

        if not audio_path:
            t_audio = self.folders["audio"]
        else:
            t_audio = audio_path

        if not video_path:
            t_video = self.folders["video"]
        else:
            t_video = video_path

        if not photo_path:
            t_photo = self.folders["photo"]
        else:
            t_photo = photo_path

        parsers = self.prefs["audio_type"]
        audio = [p for p in t_audio if self.make_db_consistent(p, parsers)]

        parsers = self.prefs["video_type"]
        video = [p for p in t_video if self.make_db_consistent(p, parsers)]

        parsers = self.prefs["photo_type"]
        photo = [p for p in t_photo if self.make_db_consistent(p, parsers)]

        self.db_locked = False
        return True

    def scan_all_paths(self, clean_database=False):
        """ This function scans all the folders configured by Canola"""
        self.folders.reload()

        self.log.debug("Locked inside scan_all_paths")
        self.db_locked = True

        t_audio = self.folders["audio"]
        t_video = self.folders["video"]
        t_photo = self.folders["photo"]

        parsers = self.prefs["audio_type"]
        audio = [p for p in t_audio if self.make_db_consistent(p, parsers)]

        parsers = self.prefs["video_type"]
        video = [p for p in t_video if self.make_db_consistent(p, parsers)]

        parsers = self.prefs["photo_type"]
        photo = [p for p in t_photo if self.make_db_consistent(p, parsers)]

        if clean_database:
            self.clean_database()
        self.scan_paths(audio, video, photo)
        return True

    def scan_paths_by_type(self, scanlist=None, type="audio"):
        """ This function scans the folders of a given type"""
        def _end_callback_by_type():
            self.folders["%s" % type] = scanlist
            self.folders.save()
            self._update_counters(self.scanner)
            self.scanner = None
            self.db_locked = False
            self.log.debug("Unlocked inside end_callback_by_type")
            return True

        self.db_locked = True
        self.log.debug("Locked inside scan_paths_by_type")
        self.folders.reload()

        if not scanlist:
            scanlist = []
        self.log.debug("Using scanlist: %s", scanlist)

        try:
            blacklist = self.folders[type]
            self.log.debug("Using blacklist: %s", blacklist)

            new_scanned_set = set(scanlist)
            scanned_set = set(blacklist)

            clearlist = list(scanned_set.difference(new_scanned_set))
            self.log.debug("Using clearlist: %s", clearlist)

            if len(blacklist) > 0:
                t_lst = self.select_paths(scanlist, blacklist, clearlist)
            else:
                t_lst = scanlist
        except KeyError:
            blacklist = None
            t_lst = self.folders[type] = []
            self.folders.save()

        if clearlist:
            for item in clearlist:
                self.remove(item, self.prefs["%s_table" % type])

        self.log.debug("Scan list: %s", t_lst)
        parsers = self.prefs["%s_type" % type]
        # scan list
        s_list = [p for p in t_lst if self.make_db_consistent(p, parsers)]
        self._reset_counters()
        self.scanner = ScanPathsThread(self.prefs, parsers, s_list,
                                       _end_callback_by_type)
        self.scanner.start()
        return True

    def scan_audio_path(self, paths, cb=None, args=None):
        def _end_callback():
            self.db_locked = False
            self.scanner = None
            self.log.debug("Unlocked inside scan_audio_path")

        if cb:
            callback = cb
        else:
            callback = _end_callback
        self.scanner = ScanPathsThread(self.prefs, self.prefs["audio_type"],
                                       paths, callback, args)
        self.scanner.start()

    def scan_video_path(self, paths, cb=None, args=None):
        def _end_callback():
            self.db_locked = False
            self.scanner = None
            self.log.debug("Unlocked inside scan_video_path")

        if cb:
            callback = cb
        else:
            callback = _end_callback
        self.scanner = ScanPathsThread(self.prefs, self.prefs["video_type"],
                                       paths, callback, args)
        self.scanner.start()

    def scan_photo_path(self, paths, cb=None, args=None):
        def _end_callback():
            self.db_locked = False
            self.scanner = None
            self.log.debug("Unlocked inside scan_photo_path")

        if cb:
            callback = cb
        else:
            callback = _end_callback
        self.scanner = ScanPathsThread(self.prefs, self.prefs["photo_type"],
                                       paths, callback, args)
        self.scanner.start()

    def clean_database(self):
        self.remove_old_invalid()
        self.vacuum_database()

    def __setitem__(self, key, value):
        if not isinstance(key, basestring):
            raise TypeError("regexp should be a string")

        self.folders.reload()
        re_str = "%s.*" % value[1]
        self.log.debug("Using regexp: %s", re_str)
        re_comp = re.compile(re_str)
        children = self.detect_paths(re_comp)
        dict.__setitem__(self, key, (value, (re_comp, children)))

    def get(self, key, d=None):
        try:
            item = self.__getitem__(key)
        except IndexError:
            return d
        return item[0]

    def mark_as_invalid(self, item):
        up_stmt = "UPDATE files SET dtime = ? WHERE path LIKE ?"

        if not item.endswith("/"):
            item = item + "/"
            self.log.debug("Added '/' to end of string")

        self.log.debug("Marking files as invalid on database: %s", item)
        try:
            self.cur.execute(up_stmt, (int(time.time()), item + "%"))
            self.con.commit()
        except Exception, e:
            self.log.warning("Mark as invalid error: %s", e)

    def vacuum_database(self):
        self.log.debug("Calling vacuum on database")
        try:
            self.cur.execute("VACUUM")
            self.con.commit()
        except Exception, e:
            self.log.warning("Vacuum database error: %s", e)

    def remove_old_invalid(self):
        max_keep_time = "1209600" # two weeks in seconds
        del_stmt = "DELETE FROM files WHERE (%s - files.dtime) > %s" % (
                   (int)(time.time()), max_keep_time)

        self.log.debug("Using del statement : %s", del_stmt)
        try:
            self.cur.execute(del_stmt)
            self.con.commit()
        except Exception, e:
            self.log.warning("Remove from database error: %s", e)

    def remove(self, item, type=None):
        if type is not None:
            del_stmt = "DELETE FROM files WHERE files.id in " \
                       "(SELECT files.id FROM files,%s WHERE files.path LIKE ? " \
                       "AND files.id == %s.id)" % (type, type)
        else:
            del_stmt = "DELETE FROM files WHERE path LIKE ?"

        if not item.endswith("/"):
            item = item + "/"
            self.log.debug("Added '/' to end of string")

        self.log.debug("Using del statement: %s", del_stmt)
        self.log.debug("Removing files with path '%s' from database", item)
        try:
            self.cur.execute(del_stmt, (item + "%",))
            self.con.commit()
        except Exception, e:
            self.con.rollback()
            self.log.warning("Remove from database error: %s", e)

    def pop(self, key, d=None):
        try:
            item = self.__getitem__(key)
            self.__delitem__(key)
        except KeyError:
            return d

        self.db_locked = True
        self.mark_as_invalid(item[0][1])
        self.db_locked = False
        return item[0]
