import time
import re
import datetime
from syncplay import constants
from syncplay.messages import getMessage
import sys
import os
import itertools
import hashlib
import random
import string
import urllib
import ast
import unicodedata
import platform
import subprocess

folderSearchEnabled = True

def isWindows():
    return sys.platform.startswith(constants.OS_WINDOWS)

def isLinux():
    return sys.platform.startswith(constants.OS_LINUX)

def isMacOS():
    return sys.platform.startswith(constants.OS_MACOS)

def isBSD():
    return constants.OS_BSD in sys.platform or sys.platform.startswith(constants.OS_DRAGONFLY)

def retry(ExceptionToCheck, tries=4, delay=3, backoff=2, logger=None):
    """Retry calling the decorated function using an exponential backoff.

    http://www.saltycrane.com/blog/2009/11/trying-out-retry-decorator-python/
    original from: http://wiki.python.org/moin/PythonDecoratorLibrary#Retry

    :param ExceptionToCheck: the exception to check. may be a tuple of
        excpetions to check
    :type ExceptionToCheck: Exception or tuple
    :param tries: number of times to try (not retry) before giving up
    :type tries: int
    :param delay: initial delay between retries in seconds
    :type delay: int
    :param backoff: backoff multiplier e.g. value of 2 will double the delay
        each retry
    :type backoff: int
    :param logger: logger to use. If None, print
    :type logger: logging.Logger instance
    """
    def deco_retry(f):
        def f_retry(*args, **kwargs):
            mtries, mdelay = tries, delay
            try_one_last_time = True
            while mtries > 1:
                try:
                    #try_one_last_time = False
                    return f(*args, **kwargs)
                    break
                except ExceptionToCheck, e:
                    if logger:
                        msg = getMessage("retrying-notification").format(str(e), mdelay)
                        logger.warning(msg)
                    time.sleep(mdelay)
                    mtries -= 1
                    mdelay *= backoff
            if try_one_last_time:
                return f(*args, **kwargs)
            return
        return f_retry  # true decorator
    return deco_retry

def parseTime(timeStr):
    regex = re.compile(constants.PARSE_TIME_REGEX)
    parts = regex.match(timeStr)
    if not parts:
        return
    parts = parts.groupdict()
    time_params = {}
    for (name, param) in parts.iteritems():
        if param:
            if name == "miliseconds":
                time_params["microseconds"] = int(param) * 1000
            else:
                time_params[name] = int(param)
    return datetime.timedelta(**time_params).total_seconds()

def formatTime(timeInSeconds, weeksAsTitles=True):
    if timeInSeconds < 0:
        timeInSeconds = -timeInSeconds
        sign = '-'
    else:
        sign = ''
    timeInSeconds = round(timeInSeconds)
    weeks = timeInSeconds // 604800
    if weeksAsTitles and weeks > 0:
        title = weeks
        weeks = 0
    else:
        title = 0
    days = (timeInSeconds % 604800) // 86400
    hours = (timeInSeconds % 86400) // 3600
    minutes = (timeInSeconds % 3600) // 60
    seconds = timeInSeconds % 60
    if weeks > 0:
        formattedTime = '{0:}{1:.0f}w, {2:.0f}d, {3:02.0f}:{4:02.0f}:{5:02.0f}'.format(sign, weeks, days, hours, minutes, seconds)
    elif days > 0:
        formattedTime = '{0:}{1:.0f}d, {2:02.0f}:{3:02.0f}:{4:02.0f}'.format(sign, days, hours, minutes, seconds)
    elif hours > 0:
        formattedTime = '{0:}{1:02.0f}:{2:02.0f}:{3:02.0f}'.format(sign, hours, minutes, seconds)
    else:
        formattedTime = '{0:}{1:02.0f}:{2:02.0f}'.format(sign, minutes, seconds)
    if title > 0:
        formattedTime = "{0:} (Title {1:.0f})".format(formattedTime, title)
    return formattedTime

def formatSize (bytes, precise=False):
    if bytes == 0: # E.g. when file size privacy is enabled
        return "???"
    try:
        megabytes = int(bytes) / 1048576.0 # Technically this is a mebibyte, but whatever
        if precise:
            megabytes = round(megabytes, 1)
        else:
            megabytes = int(megabytes)
        return str(megabytes) + getMessage("megabyte-suffix")
    except: # E.g. when filesize is hashed
        return "???"

def isASCII(s):
    return all(ord(c) < 128 for c in s)

def findResourcePath(resourceName):
    if resourceName == "syncplay.lua":
        resourcePath = os.path.join(findWorkingDir(), "lua", "intf" , "resources", resourceName)
    else:
        resourcePath = os.path.join(findWorkingDir(),"resources", resourceName)
    return resourcePath

def findWorkingDir():
    frozen = getattr(sys, 'frozen', '')
    if not frozen:
        path = os.path.dirname(os.path.dirname(__file__))
    elif frozen in ('dll', 'console_exe', 'windows_exe'):
        path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
    elif frozen in ('macosx_app'):
        path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))))
    else:
        path = ""
    return path

def getResourcesPath():

    if isWindows():
        return findWorkingDir() + u"\\resources\\"
    else:
        return findWorkingDir() + u"/resources/"

resourcespath = getResourcesPath()
posixresourcespath = findWorkingDir().replace(u"\\","/") + u"/resources/"

def getDefaultMonospaceFont():
    if platform.system() == "Windows":
        return constants.DEFAULT_WINDOWS_MONOSPACE_FONT
    elif platform.system() == "Darwin":
        return constants.DEFAULT_OSX_MONOSPACE_FONT
    else:
        return constants.FALLBACK_MONOSPACE_FONT

def limitedPowerset(s, minLength):
    return itertools.chain.from_iterable(itertools.combinations(s, r) for r in xrange(len(s), minLength, -1))

def blackholeStdoutForFrozenWindow():
    if getattr(sys, 'frozen', '') == "windows_exe":
        class Stderr(object):
            softspace = 0
            _file = None
            _error = None
            def write(self, text, fname='.syncplay.log'):
                if self._file is None and self._error is None:
                    if os.name <> 'nt':
                        path = os.path.join(os.getenv('HOME', '.'), fname)
                    else:
                        path = os.path.join(os.getenv('APPDATA', '.'), fname)
                    self._file = open(path, 'a')
                    #TODO: Handle errors.
                if self._file is not None:
                    self._file.write(text)
                    self._file.flush()
            def flush(self):
                if self._file is not None:
                    self._file.flush()
        sys.stderr = Stderr()
        del Stderr

        class Blackhole(object):
            softspace = 0
            def write(self, text):
                pass
            def flush(self):
                pass
        sys.stdout = Blackhole()
        del Blackhole

def truncateText(unicodeText, maxLength):
    try:
        unicodeText = unicodeText.decode('utf-8')
    except:
        pass
    try:
        return(unicode(unicodeText.encode("utf-8"), "utf-8", errors="ignore")[:maxLength])
    except:
        pass
    return ""

def splitText(unicodeText, maxLength):
    try:
        unicodeText = unicodeText.decode('utf-8')
    except:
        pass
    try:
        unicodeText = unicode(unicodeText.encode("utf-8"), "utf-8", errors="ignore")
        unicodeArray = [unicodeText[i:i + maxLength] for i in range(0, len(unicodeText), maxLength)]
        return(unicodeArray)
    except:
        pass
    return [""]

# Relate to file hashing / difference checking:

def stripfilename(filename, stripURL):
    if filename:
        try:
            filename = filename.encode('utf-8')
        except UnicodeDecodeError:
            pass
        filename = urllib.unquote(filename)
        if stripURL:
            try:
                filename = urllib.unquote(filename.split(u"/")[-1])
            except UnicodeDecodeError:
                filename = urllib.unquote(filename.split("/")[-1])
        return re.sub(constants.FILENAME_STRIP_REGEX, "", filename)
    else:
        return ""

def stripRoomName(RoomName):
    if RoomName:
        try:
            return re.sub(constants.ROOM_NAME_STRIP_REGEX, "\g<roomnamebase>", RoomName)
        except IndexError:
            return RoomName
    else:
        return ""

def hashFilename(filename, stripURL = False):
    if isURL(filename):
        stripURL = True
    strippedFilename = stripfilename(filename, stripURL)
    try:
        strippedFilename = strippedFilename.encode('utf-8')
    except UnicodeDecodeError:
        pass
    filenameHash = hashlib.sha256(strippedFilename).hexdigest()[:12]
    return filenameHash

def hashFilesize(size):
    return hashlib.sha256(str(size)).hexdigest()[:12]

def sameHashed(string1raw, string1hashed, string2raw, string2hashed):
    try:
        if string1raw.lower() == string2raw.lower():
            return True
    except AttributeError:
        pass
    if string1raw == string2raw:
        return True
    elif string1raw == string2hashed:
        return True
    elif string1hashed == string2raw:
        return True
    elif string1hashed == string2hashed:
        return True

def sameFilename (filename1, filename2):
    try:
        filename1 = filename1.encode('utf-8')
    except UnicodeDecodeError:
        pass
    try:
        filename2 = filename2.encode('utf-8')
    except UnicodeDecodeError:
        pass
    stripURL = True if isURL(filename1) ^ isURL(filename2) else False
    if filename1 == constants.PRIVACY_HIDDENFILENAME or filename2 == constants.PRIVACY_HIDDENFILENAME:
        return True
    elif sameHashed(stripfilename(filename1, stripURL), hashFilename(filename1, stripURL), stripfilename(filename2, stripURL), hashFilename(filename2, stripURL)):
        return True
    else:
        return False

def sameFilesize (filesize1, filesize2):
    if filesize1 == 0 or filesize2 == 0:
        return True
    elif sameHashed(filesize1, hashFilesize(filesize1), filesize2, hashFilesize(filesize2)):
        return True
    else:
        return False

def sameFileduration (duration1, duration2):
    if not constants.SHOW_DURATION_NOTIFICATION:
        return True
    elif abs(round(duration1) - round(duration2)) < constants.DIFFERENT_DURATION_THRESHOLD:
        return True
    else:
        return False

def meetsMinVersion(version, minVersion):
    def versiontotuple(ver):
        return tuple(map(int, ver.split(".")))
    return versiontotuple(version) >= versiontotuple(minVersion)

def isURL(path):
    if path is None:
        return False
    elif "://" in path:
        return True
    else:
        return False

def getPlayerArgumentsByPathAsArray(arguments, path):
    if arguments and not isinstance(arguments, (str, unicode)) and arguments.has_key(path):
        return arguments[path]
    else:
        return None

def getPlayerArgumentsByPathAsText(arguments, path):
    argsToReturn = getPlayerArgumentsByPathAsArray(arguments, path)
    return " ".join(argsToReturn) if argsToReturn else ""

def getListAsMultilineString(pathArray):
    return u"\n".join(pathArray) if pathArray else ""

def convertMultilineStringToList(multilineString):
    return unicode.split(multilineString,u"\n") if multilineString else ""

def playlistIsValid(files):
    if len(files) > constants.PLAYLIST_MAX_ITEMS:
        return False
    elif sum(map(len, files)) > constants.PLAYLIST_MAX_CHARACTERS:
        return False
    return True

def getDomainFromURL(URL):
    try:
        URL = URL.split("//")[-1].split("/")[0]
        if URL.startswith("www."):
            URL = URL[4:]
        return URL
    except:
        return None

def open_system_file_browser(path):
    if isURL(path):
        return
    path = os.path.dirname(path)
    if platform.system() == "Windows":
        os.startfile(path)
    elif platform.system() == "Darwin":
        subprocess.Popen(["open", path])
    else:
        subprocess.Popen(["xdg-open", path])

def getListOfPublicServers():
    try:
        import urllib, syncplay, sys, messages, json
        params = urllib.urlencode({'version': syncplay.version, 'milestone': syncplay.milestone, 'release_number': syncplay.release_number,
                                   'language': messages.messages["CURRENT"]})
        f = urllib.urlopen(constants.SYNCPLAY_PUBLIC_SERVER_LIST_URL.format(params))
        response = f.read()
        response = response.replace("<p>","").replace("</p>","").replace("<br />","").replace("&#8220;","'").replace("&#8221;","'").replace(":&#8217;","'").replace("&#8217;","'").replace("&#8242;","'").replace("\n","").replace("\r","") # Fix Wordpress
        response = ast.literal_eval(response)

        if response:
            return response
        else:
            raise IOError
    except:
        raise IOError(getMessage("failed-to-load-server-list-error"))

class RoomPasswordProvider(object):
    CONTROLLED_ROOM_REGEX = re.compile("^\+(.*):(\w{12})$")
    PASSWORD_REGEX = re.compile("[A-Z]{2}-\d{3}-\d{3}")

    @staticmethod
    def isControlledRoom(roomName):
        return bool(re.match(RoomPasswordProvider.CONTROLLED_ROOM_REGEX, roomName))

    @staticmethod
    def check(roomName, password, salt):
        if not password or not re.match(RoomPasswordProvider.PASSWORD_REGEX, password):
            raise ValueError()

        if not roomName:
            raise NotControlledRoom()
        match = re.match(RoomPasswordProvider.CONTROLLED_ROOM_REGEX, roomName)
        if not match:
            raise NotControlledRoom()
        roomHash = match.group(2)
        computedHash = RoomPasswordProvider._computeRoomHash(match.group(1), password, salt)
        return roomHash == computedHash

    @staticmethod
    def getControlledRoomName(roomName, password, salt):
        return "+" + roomName + ":" + RoomPasswordProvider._computeRoomHash(roomName, password, salt)

    @staticmethod
    def _computeRoomHash(roomName, password, salt):
        roomName = roomName.encode('utf8')
        salt = hashlib.sha256(salt).hexdigest()
        provisionalHash = hashlib.sha256(roomName + salt).hexdigest()
        return hashlib.sha1(provisionalHash + salt + password).hexdigest()[:12].upper()

class RandomStringGenerator(object):
    @staticmethod
    def generate_room_password():
        parts = (
            RandomStringGenerator._get_random_letters(2),
            RandomStringGenerator._get_random_numbers(3),
            RandomStringGenerator._get_random_numbers(3)
        )
        return "{}-{}-{}".format(*parts)

    @staticmethod
    def generate_server_salt():
        parts = (
            RandomStringGenerator._get_random_letters(10),
        )
        return "{}".format(*parts)

    @staticmethod
    def _get_random_letters(quantity):
        return ''.join(random.choice(string.ascii_uppercase) for _ in xrange(quantity))

    @staticmethod
    def _get_random_numbers(quantity):
        return ''.join(random.choice(string.digits) for _ in xrange(quantity))

class NotControlledRoom(Exception):
    pass

