# -*- coding: utf-8 -*-

###############################################################################
import logging
from cProfile import Profile
from json import loads, dumps
from pstats import Stats
from sqlite3 import connect, OperationalError
from datetime import datetime, timedelta
from StringIO import StringIO
from time import localtime, strftime, strptime
from unicodedata import normalize
import xml.etree.ElementTree as etree
from functools import wraps
from calendar import timegm
from os import path as os_path

import xbmc
import xbmcaddon
import xbmcgui
import xbmcvfs

from variables import DB_VIDEO_PATH, DB_MUSIC_PATH, DB_TEXTURE_PATH, \
    DB_PLEX_PATH

###############################################################################

log = logging.getLogger("PLEX."+__name__)

WINDOW = xbmcgui.Window(10000)
ADDON = xbmcaddon.Addon(id='plugin.video.plexkodiconnect')

###############################################################################
# Main methods


def window(property, value=None, clear=False, windowid=10000):
    """
    Get or set window property - thread safe!

    Returns unicode.

    Property and value may be string or unicode
    """
    if windowid != 10000:
        win = xbmcgui.Window(windowid)
    else:
        win = WINDOW

    if clear:
        win.clearProperty(property)
    elif value is not None:
        win.setProperty(tryEncode(property), tryEncode(value))
    else:
        return tryDecode(win.getProperty(property))


def pickl_window(property, value=None, clear=False, windowid=10000):
    """
    Get or set window property - thread safe! For use with Pickle
    Property and value must be string
    """
    if windowid != 10000:
        win = xbmcgui.Window(windowid)
    else:
        win = WINDOW

    if clear:
        win.clearProperty(property)
    elif value is not None:
        win.setProperty(property, value)
    else:
        return win.getProperty(property)


def settings(setting, value=None):
    """
    Get or add addon setting. Returns unicode

    setting and value can either be unicode or string
    """
    # We need to instantiate every single time to read changed variables!
    addon = xbmcaddon.Addon(id='plugin.video.plexkodiconnect')
    if value is not None:
        # Takes string or unicode by default!
        addon.setSetting(tryEncode(setting), tryEncode(value))
    else:
        # Should return unicode by default, but just in case
        return tryDecode(addon.getSetting(setting))


def language(stringid):
    # Central string retrieval
    return ADDON.getLocalizedString(stringid)


def dialog(typus, *args, **kwargs):
    """
    Displays xbmcgui Dialog. Pass a string as typus:
        'yesno', 'ok', 'notification', 'input', 'select', 'numeric'

    Icons:
        icon='{plex}'       Display Plex standard icon
        icon='{info}'       xbmcgui.NOTIFICATION_INFO
        icon='{warning}'    xbmcgui.NOTIFICATION_WARNING
        icon='{error}'      xbmcgui.NOTIFICATION_ERROR

    Input Types:
        type='{alphanum}'   xbmcgui.INPUT_ALPHANUM (standard keyboard)
        type='{numeric}'    xbmcgui.INPUT_NUMERIC (format: #)
        type='{date}'       xbmcgui.INPUT_DATE (format: DD/MM/YYYY)
        type='{time}'       xbmcgui.INPUT_TIME (format: HH:MM)
        type='{ipaddress}'  xbmcgui.INPUT_IPADDRESS (format: #.#.#.#)
        type='{password}'   xbmcgui.INPUT_PASSWORD
                            (return md5 hash of input, input is masked)
    """
    d = xbmcgui.Dialog()
    if "icon" in kwargs:
        types = {
            '{plex}': 'special://home/addons/plugin.video.plexkodiconnect/icon.png',
            '{info}': xbmcgui.NOTIFICATION_INFO,
            '{warning}': xbmcgui.NOTIFICATION_WARNING,
            '{error}': xbmcgui.NOTIFICATION_ERROR
        }
        for key, value in types.iteritems():
            kwargs['icon'] = kwargs['icon'].replace(key, value)
    if 'type' in kwargs:
        types = {
            '{alphanum}': xbmcgui.INPUT_ALPHANUM,
            '{numeric}': xbmcgui.INPUT_NUMERIC,
            '{date}': xbmcgui.INPUT_DATE,
            '{time}': xbmcgui.INPUT_TIME,
            '{ipaddress}': xbmcgui.INPUT_IPADDRESS,
            '{password}': xbmcgui.INPUT_PASSWORD
        }
        kwargs['type'] = types[kwargs['type']]
    if "heading" in kwargs:
        kwargs['heading'] = kwargs['heading'].replace("{plex}",
                                                      language(29999))
    types = {
        'yesno': d.yesno,
        'ok': d.ok,
        'notification': d.notification,
        'input': d.input,
        'select': d.select,
        'numeric': d.numeric
    }
    return types[typus](*args, **kwargs)


def tryEncode(uniString, encoding='utf-8'):
    """
    Will try to encode uniString (in unicode) to encoding. This possibly
    fails with e.g. Android TV's Python, which does not accept arguments for
    string.encode()
    """
    if isinstance(uniString, str):
        # already encoded
        return uniString
    try:
        uniString = uniString.encode(encoding, "ignore")
    except TypeError:
        uniString = uniString.encode()
    return uniString


def tryDecode(string, encoding='utf-8'):
    """
    Will try to decode string (encoded) using encoding. This possibly
    fails with e.g. Android TV's Python, which does not accept arguments for
    string.encode()
    """
    if isinstance(string, unicode):
        # already decoded
        return string
    try:
        string = string.decode(encoding, "ignore")
    except TypeError:
        string = string.decode()
    return string


def DateToKodi(stamp):
    """
    converts a Unix time stamp (seconds passed sinceJanuary 1 1970) to a
    propper, human-readable time stamp used by Kodi

    Output: Y-m-d h:m:s = 2009-04-05 23:16:04

    None if an error was encountered
    """
    try:
        stamp = float(stamp) + float(window('kodiplextimeoffset'))
        date_time = localtime(stamp)
        localdate = strftime('%Y-%m-%d %H:%M:%S', date_time)
    except:
        localdate = None
    return localdate


def IfExists(path):
    """
    Kodi's xbmcvfs.exists is broken - it caches the results for directories.

    path: path to a directory (with a slash at the end)

    Returns True if path exists, else false
    """
    dummyfile = tryEncode(os_path.join(path, 'dummyfile.txt'))
    try:
        etree.ElementTree(etree.Element('test')).write(dummyfile)
    except:
        # folder does not exist yet
        answer = False
    else:
        # Folder exists. Delete file again.
        xbmcvfs.delete(dummyfile)
        answer = True
    return answer


def IntFromStr(string):
    """
    Returns an int from string or the int 0 if something happened
    """
    try:
        result = int(string)
    except:
        result = 0
    return result


def getUnixTimestamp(secondsIntoTheFuture=None):
    """
    Returns a Unix time stamp (seconds passed since January 1 1970) for NOW as
    an integer.

    Optionally, pass secondsIntoTheFuture: positive int's will result in a
    future timestamp, negative the past
    """
    if secondsIntoTheFuture:
        future = datetime.utcnow() + timedelta(seconds=secondsIntoTheFuture)
    else:
        future = datetime.utcnow()
    return timegm(future.timetuple())


def kodiSQL(media_type="video"):
    if media_type == "plex":
        dbPath = DB_PLEX_PATH
    elif media_type == "music":
        dbPath = DB_MUSIC_PATH
    elif media_type == "texture":
        dbPath = DB_TEXTURE_PATH
    else:
        dbPath = DB_VIDEO_PATH
    return connect(dbPath, timeout=60.0)


def create_actor_db_index():
    """
    Index the "actors" because we got a TON - speed up SELECT and WHEN
    """
    conn = kodiSQL('video')
    cursor = conn.cursor()
    try:
        cursor.execute("""
            CREATE UNIQUE INDEX index_name
            ON actor (name);
        """)
    except OperationalError:
        # Index already exists
        pass
    conn.commit()
    conn.close()


def getScreensaver():
    # Get the current screensaver value
    params = {'setting': "screensaver.mode"}
    return JSONRPC('Settings.getSettingValue').execute(params)['result']['value']


def setScreensaver(value):
    # Toggle the screensaver
    params = {'setting': "screensaver.mode", 'value': value}
    log.debug('Toggling screensaver to "%s": %s'
              % (value, JSONRPC('Settings.setSettingValue').execute(params)))


def reset():
    # Are you sure you want to reset your local Kodi database?
    if not dialog('yesno',
                  heading='{plex} %s ' % language(30132),
                  line1=language(39600)):
        return

    # first stop any db sync
    window('plex_shouldStop', value="true")
    count = 10
    while window('plex_dbScan') == "true":
        log.debug("Sync is running, will retry: %s..." % count)
        count -= 1
        if count == 0:
            # Could not stop the database from running. Please try again later.
            dialog('ok',
                   heading='{plex} %s' % language(30132),
                   line1=language(39601))
            return
        xbmc.sleep(1000)

    # Clean up the playlists
    deletePlaylists()

    # Clean up the video nodes
    deleteNodes()

    # Wipe the kodi databases
    log.info("Resetting the Kodi video database.")
    connection = kodiSQL('video')
    cursor = connection.cursor()
    cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"')
    rows = cursor.fetchall()
    for row in rows:
        tablename = row[0]
        if tablename != "version":
            cursor.execute("DELETE FROM " + tablename)
    connection.commit()
    cursor.close()

    if settings('enableMusic') == "true":
        log.info("Resetting the Kodi music database.")
        connection = kodiSQL('music')
        cursor = connection.cursor()
        cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"')
        rows = cursor.fetchall()
        for row in rows:
            tablename = row[0]
            if tablename != "version":
                cursor.execute("DELETE FROM " + tablename)
        connection.commit()
        cursor.close()

    # Wipe the Plex database
    log.info("Resetting the Plex database.")
    connection = kodiSQL('plex')
    cursor = connection.cursor()
    cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"')
    rows = cursor.fetchall()
    for row in rows:
        tablename = row[0]
        if tablename != "version":
            cursor.execute("DELETE FROM " + tablename)
    cursor.execute('DROP table IF EXISTS plex')
    cursor.execute('DROP table IF EXISTS view')
    connection.commit()
    cursor.close()

    # Remove all cached artwork? (recommended!)
    if dialog('yesno',
              heading='{plex} %s ' % language(30132),
              line1=language(39602)):
        log.info("Resetting all cached artwork.")
        # Remove all existing textures first
        path = tryDecode(xbmc.translatePath("special://thumbnails/"))
        if xbmcvfs.exists(path):
            allDirs, allFiles = xbmcvfs.listdir(path)
            for dir in allDirs:
                allDirs, allFiles = xbmcvfs.listdir(path+dir)
                for file in allFiles:
                    if os_path.supports_unicode_filenames:
                        xbmcvfs.delete(os_path.join(
                            path + tryDecode(dir),
                            tryDecode(file)))
                    else:
                        xbmcvfs.delete(os_path.join(
                            tryEncode(path) + dir,
                            file))
        # remove all existing data from texture DB
        connection = kodiSQL('texture')
        cursor = connection.cursor()
        cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"')
        rows = cursor.fetchall()
        for row in rows:
            tableName = row[0]
            if(tableName != "version"):
                cursor.execute("DELETE FROM " + tableName)
        connection.commit()
        cursor.close()

    # reset the install run flag
    settings('SyncInstallRunDone', value="false")

    # Reset all PlexKodiConnect Addon settings? (this is usually NOT
    # recommended and unnecessary!)
    if dialog('yesno',
              heading='{plex} %s ' % language(30132),
              line1=language(39603)):
        # Delete the settings
        addon = xbmcaddon.Addon()
        addondir = tryDecode(xbmc.translatePath(addon.getAddonInfo('profile')))
        dataPath = "%ssettings.xml" % addondir
        log.info("Deleting: settings.xml")
        xbmcvfs.delete(tryEncode(dataPath))

    # Kodi will now restart to apply the changes.
    dialog('ok',
           heading='{plex} %s ' % language(30132),
           line1=language(33033))
    xbmc.executebuiltin('RestartApp')


def profiling(sortby="cumulative"):
    # Will print results to Kodi log
    def decorator(func):
        def wrapper(*args, **kwargs):

            pr = Profile()

            pr.enable()
            result = func(*args, **kwargs)
            pr.disable()

            s = StringIO()
            ps = Stats(pr, stream=s).sort_stats(sortby)
            ps.print_stats()
            log.info(s.getvalue())

            return result

        return wrapper
    return decorator

def convertdate(date):
    try:
        date = datetime.strptime(date, "%Y-%m-%dT%H:%M:%SZ")
    except TypeError:
        # TypeError: attribute of type 'NoneType' is not callable
        # Known Kodi/python error
        date = datetime(*(strptime(date, "%Y-%m-%dT%H:%M:%SZ")[0:6]))

    return date

def normalize_nodes(text):
    # For video nodes
    text = text.replace(":", "")
    text = text.replace("/", "-")
    text = text.replace("\\", "-")
    text = text.replace("<", "")
    text = text.replace(">", "")
    text = text.replace("*", "")
    text = text.replace("?", "")
    text = text.replace('|', "")
    text = text.replace('(', "")
    text = text.replace(')', "")
    text = text.strip()
    # Remove dots from the last character as windows can not have directories
    # with dots at the end
    text = text.rstrip('.')
    text = tryEncode(normalize('NFKD', unicode(text, 'utf-8')))

    return text

def normalize_string(text):
    # For theme media, do not modify unless
    # modified in TV Tunes
    text = text.replace(":", "")
    text = text.replace("/", "-")
    text = text.replace("\\", "-")
    text = text.replace("<", "")
    text = text.replace(">", "")
    text = text.replace("*", "")
    text = text.replace("?", "")
    text = text.replace('|', "")
    text = text.strip()
    # Remove dots from the last character as windows can not have directories
    # with dots at the end
    text = text.rstrip('.')
    text = tryEncode(normalize('NFKD', unicode(text, 'utf-8')))

    return text

def indent(elem, level=0):
    # Prettify xml trees
    i = "\n" + level*"  "
    if len(elem):
        if not elem.text or not elem.text.strip():
          elem.text = i + "  "
        if not elem.tail or not elem.tail.strip():
          elem.tail = i
        for elem in elem:
          indent(elem, level+1)
        if not elem.tail or not elem.tail.strip():
          elem.tail = i
    else:
        if level and (not elem.tail or not elem.tail.strip()):
          elem.tail = i


def guisettingsXML():
    """
    Returns special://userdata/guisettings.xml as an etree xml root element
    """
    path = tryDecode(xbmc.translatePath("special://profile/"))
    xmlpath = "%sguisettings.xml" % path

    try:
        xmlparse = etree.parse(xmlpath)
    except:
        # Document is blank or missing
        root = etree.Element('settings')
    else:
        root = xmlparse.getroot()
    return root


def __setXMLTag(element, tag, value, attrib=None):
    """
    Looks for an element's subelement and sets its value.
    If "subelement" does not exist, create it using attrib and value.

        element : etree element
        tag     : string/unicode for subelement
        value   : string/unicode
        attrib  : dict; will use etree attrib method

    Returns the subelement
    """
    subelement = element.find(tag)
    if subelement is None:
        # Setting does not exist yet; create it
        if attrib is None:
            etree.SubElement(element, tag).text = value
        else:
            etree.SubElement(element, tag, attrib=attrib).text = value
    else:
        subelement.text = value
    return subelement


def __setSubElement(element, subelement):
    """
    Returns an etree element's subelement. Creates one if not exist
    """
    answ = element.find(subelement)
    if answ is None:
        answ = etree.SubElement(element, subelement)
    return answ


def advancedSettingsXML():
    """
    Kodi tweaks

    Changes advancedsettings.xml, musiclibrary:
        backgroundupdate        set to "true"

    Overrides guisettings.xml in Kodi userdata folder:
        updateonstartup  : set to "false"
        usetags          : set to "false"
        findremotethumbs : set to "false"
    """
    path = tryDecode(xbmc.translatePath("special://profile/"))
    xmlpath = "%sadvancedsettings.xml" % path

    try:
        xmlparse = etree.parse(xmlpath)
    except:
        # Document is blank or missing
        root = etree.Element('advancedsettings')
    else:
        root = xmlparse.getroot()

    music = __setSubElement(root, 'musiclibrary')
    __setXMLTag(music, 'backgroundupdate', "true")
    # __setXMLTag(music, 'updateonstartup', "false")

    # Subtag 'musicfiles'
    # music = __setSubElement(root, 'musicfiles')
    # __setXMLTag(music, 'usetags', "false")
    # __setXMLTag(music, 'findremotethumbs', "false")

    # Prettify and write to file
    try:
        indent(root)
    except:
        pass
    etree.ElementTree(root).write(xmlpath)


def sourcesXML():
    # To make Master lock compatible
    path = tryDecode(xbmc.translatePath("special://profile/"))
    xmlpath = "%ssources.xml" % path

    try:
        xmlparse = etree.parse(xmlpath)
    except: # Document is blank or missing
        root = etree.Element('sources')
    else:
        root = xmlparse.getroot()


    video = root.find('video')
    if video is None:
        video = etree.SubElement(root, 'video')
        etree.SubElement(video, 'default', attrib={'pathversion': "1"})

    # Add elements
    count = 2
    for source in root.findall('.//path'):
        if source.text == "smb://":
            count -= 1

        if count == 0:
            # sources already set
            break
    else:
        # Missing smb:// occurences, re-add.
        for i in range(0, count):
            source = etree.SubElement(video, 'source')
            etree.SubElement(source, 'name').text = "Plex"
            etree.SubElement(source, 'path', attrib={'pathversion': "1"}).text = "smb://"
            etree.SubElement(source, 'allowsharing').text = "true"
    # Prettify and write to file
    try:
        indent(root)
    except: pass
    etree.ElementTree(root).write(xmlpath)


def passwordsXML():
    # To add network credentials
    path = tryDecode(xbmc.translatePath("special://userdata/"))
    xmlpath = "%spasswords.xml" % path

    try:
        xmlparse = etree.parse(xmlpath)
    except: # Document is blank or missing
        root = etree.Element('passwords')
        skipFind = True
    else:
        root = xmlparse.getroot()
        skipFind = False

    dialog = xbmcgui.Dialog()
    credentials = settings('networkCreds')
    if credentials:
        # Present user with options
        option = dialog.select(
            "Modify/Remove network credentials", ["Modify", "Remove"])

        if option < 0:
            # User cancelled dialog
            return

        elif option == 1:
            # User selected remove
            for paths in root.getiterator('passwords'):
                for path in paths:
                    if path.find('.//from').text == "smb://%s/" % credentials:
                        paths.remove(path)
                        log.info("Successfully removed credentials for: %s"
                                 % credentials)
                        etree.ElementTree(root).write(xmlpath)
                        break
            else:
                log.error("Failed to find saved server: %s in passwords.xml"
                          % credentials)

            settings('networkCreds', value="")
            xbmcgui.Dialog().notification(
                heading='PlexKodiConnect',
                message="%s removed from passwords.xml" % credentials,
                icon="special://home/addons/plugin.video.plexkodiconnect/icon.png",
                time=1000,
                sound=False)
            return

        elif option == 0:
            # User selected to modify
            server = dialog.input("Modify the computer name or ip address", credentials)
            if not server:
                return
    else:
        # No credentials added
        dialog.ok(
            heading="Network credentials",
            line1= (
                "Input the server name or IP address as indicated in your plex library paths. "
                'For example, the server name: \\\\SERVER-PC\\path\\ or smb://SERVER-PC/path is "SERVER-PC".'))
        server = dialog.input("Enter the server name or IP address")
        if not server:
            return

    # Network username
    user = dialog.input("Enter the network username")
    if not user:
        return
    # Network password
    password = dialog.input("Enter the network password",
                            '',  # Default input
                            xbmcgui.INPUT_ALPHANUM,
                            xbmcgui.ALPHANUM_HIDE_INPUT)
    # Need to url-encode the password
    from urllib import quote_plus
    password = quote_plus(password)
    # Add elements. Annoying etree bug where findall hangs forever
    if skipFind is False:
        skipFind = True
        for path in root.findall('.//path'):
            if path.find('.//from').text.lower() == "smb://%s/" % server.lower():
                # Found the server, rewrite credentials
                path.find('.//to').text = "smb://%s:%s@%s/" % (user, password, server)
                skipFind = False
                break
    if skipFind:
        # Server not found, add it.
        path = etree.SubElement(root, 'path')
        etree.SubElement(path, 'from', attrib={'pathversion': "1"}).text = "smb://%s/" % server
        topath = "smb://%s:%s@%s/" % (user, password, server)
        etree.SubElement(path, 'to', attrib={'pathversion': "1"}).text = topath
        # Force Kodi to see the credentials without restarting
        xbmcvfs.exists(topath)

    # Add credentials
    settings('networkCreds', value="%s" % server)
    log.info("Added server: %s to passwords.xml" % server)
    # Prettify and write to file
    try:
        indent(root)
    except: pass
    etree.ElementTree(root).write(xmlpath)

    # dialog.notification(
    #     heading="PlexKodiConnect",
    #     message="Added to passwords.xml",
    #     icon="special://home/addons/plugin.video.plexkodiconnect/icon.png",
    #     time=5000,
    #     sound=False)

def playlistXSP(mediatype, tagname, viewid, viewtype="", delete=False):
    """
    Feed with tagname as unicode
    """
    path = tryDecode(xbmc.translatePath("special://profile/playlists/video/"))
    if viewtype == "mixed":
        plname = "%s - %s" % (tagname, mediatype)
        xsppath = "%sPlex %s - %s.xsp" % (path, viewid, mediatype)
    else:
        plname = tagname
        xsppath = "%sPlex %s.xsp" % (path, viewid)

    # Create the playlist directory
    if not xbmcvfs.exists(tryEncode(path)):
        log.info("Creating directory: %s" % path)
        xbmcvfs.mkdirs(tryEncode(path))

    # Only add the playlist if it doesn't already exists
    if xbmcvfs.exists(tryEncode(xsppath)):
        log.info('Path %s does exist' % xsppath)
        if delete:
            xbmcvfs.delete(tryEncode(xsppath))
            log.info("Successfully removed playlist: %s." % tagname)

        return

    # Using write process since there's no guarantee the xml declaration works with etree
    itemtypes = {
        'homevideos': 'movies',
        'movie': 'movies',
        'show': 'tvshows'
    }
    log.info("Writing playlist file to: %s" % xsppath)
    try:
        f = xbmcvfs.File(tryEncode(xsppath), 'wb')
    except:
        log.error("Failed to create playlist: %s" % xsppath)
        return
    else:
        f.write(tryEncode(
            '<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>\n'
            '<smartplaylist type="%s">\n\t'
                '<name>Plex %s</name>\n\t'
                '<match>all</match>\n\t'
                '<rule field="tag" operator="is">\n\t\t'
                    '<value>%s</value>\n\t'
                '</rule>\n'
            '</smartplaylist>\n'
            % (itemtypes.get(mediatype, mediatype), plname, tagname)))
        f.close()
    log.info("Successfully added playlist: %s" % tagname)

def deletePlaylists():

    # Clean up the playlists
    path = tryDecode(xbmc.translatePath("special://profile/playlists/video/"))
    dirs, files = xbmcvfs.listdir(tryEncode(path))
    for file in files:
        if tryDecode(file).startswith('Plex'):
            xbmcvfs.delete(tryEncode("%s%s" % (path, tryDecode(file))))

def deleteNodes():

    # Clean up video nodes
    import shutil
    path = tryDecode(xbmc.translatePath("special://profile/library/video/"))
    dirs, files = xbmcvfs.listdir(tryEncode(path))
    for dir in dirs:
        if tryDecode(dir).startswith('Plex'):
            try:
                shutil.rmtree("%s%s" % (path, tryDecode(dir)))
            except:
                log.error("Failed to delete directory: %s" % tryDecode(dir))
    for file in files:
        if tryDecode(file).startswith('plex'):
            try:
                xbmcvfs.delete(tryEncode("%s%s" % (path, tryDecode(file))))
            except:
                log.error("Failed to file: %s" % tryDecode(file))


###############################################################################
# WRAPPERS

def CatchExceptions(warnuser=False):
    """
    Decorator for methods to catch exceptions and log them. Useful for e.g.
    librarysync threads using itemtypes.py, because otherwise we would not
    get informed of crashes

    warnuser=True:      sets the window flag 'plex_scancrashed' to true
                        which will trigger a Kodi infobox to inform user
    """
    def decorate(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            try:
                return func(*args, **kwargs)
            except Exception as e:
                log.error('%s has crashed. Error: %s' % (func.__name__, e))
                import traceback
                log.error("Traceback:\n%s" % traceback.format_exc())
                if warnuser:
                    window('plex_scancrashed', value='true')
                return
        return wrapper
    return decorate


def LogTime(func):
    """
    Decorator for functions and methods to log the time it took to run the code
    """
    @wraps(func)
    def wrapper(*args, **kwargs):
        starttotal = datetime.now()
        result = func(*args, **kwargs)
        elapsedtotal = datetime.now() - starttotal
        log.info('It took %s to run the function %s'
                  % (elapsedtotal, func.__name__))
        return result
    return wrapper


def ThreadMethodsAdditionalStop(windowAttribute):
    """
    Decorator to replace stopThread method to include the Kodi windowAttribute

    Use with any sync threads. @ThreadMethods still required FIRST
    """
    def wrapper(cls):
        def threadStopped(self):
            return (self._threadStopped or
                    (window('plex_terminateNow') == "true") or
                    window(windowAttribute) == "true")
        cls.threadStopped = threadStopped
        return cls
    return wrapper


def ThreadMethodsAdditionalSuspend(windowAttribute):
    """
    Decorator to replace threadSuspended(): thread now also suspends if a
    Kodi windowAttribute is set to 'true', e.g. 'suspend_LibraryThread'

    Use with any library sync threads. @ThreadMethods still required FIRST
    """
    def wrapper(cls):
        def threadSuspended(self):
            return (self._threadSuspended or
                    window(windowAttribute) == 'true')
        cls.threadSuspended = threadSuspended
        return cls
    return wrapper


def ThreadMethods(cls):
    """
    Decorator to add the following methods to a threading class:

    suspendThread():    pauses the thread
    resumeThread():     resumes the thread
    stopThread():       stopps/kills the thread

    threadSuspended():  returns True if thread is suspend_thread
    threadStopped():    returns True if thread is stopped (or should stop ;-))
                        ALSO stops if Kodi is exited

    Also adds the following class attributes:
        _threadStopped
        _threadSuspended
    """
    # Attach new attributes to class
    cls._threadStopped = False
    cls._threadSuspended = False

    # Define new class methods and attach them to class
    def stopThread(self):
        self._threadStopped = True
    cls.stopThread = stopThread

    def suspendThread(self):
        self._threadSuspended = True
    cls.suspendThread = suspendThread

    def resumeThread(self):
        self._threadSuspended = False
    cls.resumeThread = resumeThread

    def threadSuspended(self):
        return self._threadSuspended
    cls.threadSuspended = threadSuspended

    def threadStopped(self):
        return self._threadStopped or (window('plex_terminateNow') == 'true')
    cls.threadStopped = threadStopped

    # Return class to render this a decorator
    return cls


class Lock_Function:
    """
    Decorator for class methods and functions to lock them with lock.

    Initialize this class first
    lockfunction = Lock_Function(lock), where lock is a threading.Lock() object

    To then lock a function or method:

    @lockfunction.lockthis
    def some_function(args, kwargs)
    """
    def __init__(self, lock):
        self.lock = lock

    def lockthis(self, func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            with self.lock:
                result = func(*args, **kwargs)
            return result
        return wrapper

###############################################################################
# UNUSED METHODS


def changePlayState(itemType, kodiId, playCount, lastplayed):
    """
    YET UNUSED

    kodiId: int or str
    playCount: int or str
    lastplayed: str or int unix timestamp
    """
    lastplayed = DateToKodi(lastplayed)

    kodiId = int(kodiId)
    playCount = int(playCount)
    method = {
        'movie': ' VideoLibrary.SetMovieDetails',
        'episode': 'VideoLibrary.SetEpisodeDetails',
        'musicvideo': ' VideoLibrary.SetMusicVideoDetails',  # TODO
        'show': 'VideoLibrary.SetTVShowDetails',  # TODO
        '': 'AudioLibrary.SetAlbumDetails',  # TODO
        '': 'AudioLibrary.SetArtistDetails',  # TODO
        'track': 'AudioLibrary.SetSongDetails'
    }
    params = {
        'movie': {
            'movieid': kodiId,
            'playcount': playCount,
            'lastplayed': lastplayed
        },
        'episode': {
            'episodeid': kodiId,
            'playcount': playCount,
            'lastplayed': lastplayed
        }
    }
    query = {
        "jsonrpc": "2.0",
        "id": 1,
    }
    query['method'] = method[itemType]
    query['params'] = params[itemType]
    result = xbmc.executeJSONRPC(dumps(query))
    result = loads(result)
    result = result.get('result')
    log.debug("JSON result was: %s" % result)


class JSONRPC(object):
    id_ = 1
    jsonrpc = "2.0"

    def __init__(self, method, **kwargs):
        self.method = method
        for arg in kwargs:  # id_(int), jsonrpc(str)
            self.arg = arg

    def _query(self):
        query = {
            'jsonrpc': self.jsonrpc,
            'id': self.id_,
            'method': self.method,
        }
        if self.params is not None:
            query['params'] = self.params
        return dumps(query)

    def execute(self, params=None):
        self.params = params
        return loads(xbmc.executeJSONRPC(self._query()))