import os
import time
import datetime
import glob

import tempfile

import random as librandom
choice = librandom.choice

import unicodedata

import pdb
import pprint
pp = pprint.pprint

import PIL
import numpy as np

import objc
import Foundation
import AppKit
import PyObjCTools.Conversion

from . import kgp


__all__ = (
    'grid', 'random', 'choice', 'files', 'autotext',
    '_copy_attr',
    '_copy_attrs',
    'datestring','makeunicode', 'filelist', 'imagefiles',
    'fontnames', 'fontfamilies',
    'voices', 'voiceattributes', 'anySpeakers', 'say',
    'imagepalette', 'aspectRatio', 'dithertypes', 'ditherimage',
    'sortlistfunction')


# py3 stuff
py3 = False
try:
    unicode('')
    punicode = unicode
    pstr = str
    punichr = unichr
except NameError:
    punicode = str
    pstr = bytes
    py3 = True
    punichr = chr
    long = int

def cmp_to_key(mycmp):
    'Convert a cmp= function into a key= function'
    class K:
        def __init__(self, obj, *args):
            self.obj = obj
        def __lt__(self, other):
            return mycmp(self.obj, other.obj) < 0
        def __gt__(self, other):
            return mycmp(self.obj, other.obj) > 0
        def __eq__(self, other):
            return mycmp(self.obj, other.obj) == 0
        def __le__(self, other):
            return mycmp(self.obj, other.obj) <= 0
        def __ge__(self, other):
            return mycmp(self.obj, other.obj) >= 0
        def __ne__(self, other):
            return mycmp(self.obj, other.obj) != 0
    return K


def sortlistfunction(thelist, thecompare):
    if py3:
        sortkeyfunction = cmp_to_key( thecompare )
        thelist.sort( key=sortkeyfunction )
    else:
        thelist.sort( thecompare )

g_voicetrash = []

_dithertypes = {
    'atkinson': 1,
    'floyd-steinberg': 2,
    'jarvis-judice-ninke': 3,
    'stucki': 4,
    'burkes': 5,
    'sierra-1': 6,
    'sierra-2': 7,
    'sierra-3': 8,
}

_ditherIDs = _dithertypes.values()



def makeunicode(s, srcencoding="utf-8", normalizer="NFC"):

    if type(s) not in ( pstr,
                    punicode,
                    Foundation.NSMutableAttributedString,
                    objc.pyobjc_unicode,
                    Foundation.NSMutableStringProxyForMutableAttributedString,
                    Foundation.NSString):
        s = str(s)
    if type(s) not in (
            punicode,
            #Foundation.NSMutableAttributedString,
            #objc.pyobjc_unicode,
            #Foundation.NSMutableStringProxyForMutableAttributedString
            ):
        try:
            s = punicode(s, srcencoding)
        except TypeError as err:
            
            #print() 
            #print("makeunicode(): %s" % err)
            #print(repr(s))
            #print(type(s))
            #print()
            pass
    if type(s) in ( punicode,
                    #Foundation.NSMutableAttributedString,
                    #objc.pyobjc_unicode,
                    #Foundation.NSMutableStringProxyForMutableAttributedString,
                    #Foundation.NSString
                    ):
        s = unicodedata.normalize(normalizer, s)
    return s


def datestring(dt = None, dateonly=False, nospaces=True, nocolons=True):
    """Make an ISO datestring. The defaults are good for using the result of
    'datestring()' in a filename.
    """
    if not dt:
        now = str(datetime.datetime.now())
    else:
        now = str(dt)
    if not dateonly:
        now = now[:19]
    else:
        now = now[:10]
    if nospaces:
        now = now.replace(" ", "_")
    if nocolons:
        now = now.replace(":", "")
    return now


def grid(cols, rows, colSize=1, rowSize=1, shuffled=False):
    """Returns an iterator that contains coordinate tuples.
    
    The grid can be used to quickly create grid-like structures.
    A common way to use them is:
        for x, y in grid(10,10,12,12):
            rect(x,y, 10,10)
    """
    # Prefer using generators.
    rowRange = range( int(rows) )
    colRange = range( int(cols) )
    # Shuffled needs a real list, though.
    if (shuffled):
        rowRange = list(rowRange)
        colRange = list(colRange)
        librandom.shuffle(rowRange)
        librandom.shuffle(colRange)
    for y in rowRange:
        for x in colRange:
            yield (x*colSize, y*rowSize)


def random(v1=None, v2=None):
    """Returns a random value.
    
    This function does a lot of things depending on the parameters:
    - If one or more floats is given, the random value will be a float.
    - If all values are ints, the random value will be an integer.
    
    - If one value is given, random returns a value from 0 to the given value.
      This value is not inclusive.
    - If two values are given, random returns a value between the two; if two
      integers are given, the two boundaries are inclusive.
    """
    if v1 != None and v2 == None: # One value means 0 -> v1
        if isinstance(v1, float):
            return librandom.random() * v1
        else:
            return int(librandom.random() * v1)
    elif v1 != None and v2 != None: # v1 -> v2
        if isinstance(v1, float) or isinstance(v2, float):
            start = min(v1, v2)
            end = max(v1, v2)
            return start + librandom.random() * (end-start)
        else:
            start = min(v1, v2)
            end = max(v1, v2) + 1
            return int(start + librandom.random() * (end-start))
    else: # No values means 0.0 -> 1.0
        return librandom.random()


def autotext(sourceFile):
    k = kgp.KantGenerator(sourceFile)
    return k.output()


def files(path="*"):
    """Returns a list of files.
    
    You can use wildcards to specify which files to pick, e.g.
        f = files('*.gif')
    """
    f = glob.glob(path)
    f = [makeunicode(t) for t in f]
    return f


def filelist( folderpathorlist, pathonly=True ):
    """Walk a folder or a list of folders and return
    paths or ((filepath, size, lastmodified, mode) tuples..
    """

    folders = folderpathorlist
    if type(folderpathorlist) in (pstr, punicode):
        folders = [folderpathorlist]
    result = []
    for folder in folders:
        folder = os.path.expanduser( folder )
        folder = os.path.abspath( folder )
        for root, dirs, files in os.walk( folder ):
            root = makeunicode( root )

            # skip if dir starts with '.'
            _, parentfolder = os.path.split(root)
            if parentfolder and parentfolder[0] == u".":
                continue

            for thefile in files:
                thefile = makeunicode( thefile )
                basename, ext = os.path.splitext(thefile)

                # exclude dotfiles
                if thefile.startswith('.'):
                    continue

                # exclude the specials
                for item in (u'\r', u'\n', u'\t'):
                    if item in thefile:
                        continue

                filepath = os.path.join( root, thefile )

                record = filepath
                if not pathonly:
                    islink = os.path.islink( filepath )
                    if islink:
                        info = os.lstat( filepath )
                    else:
                        info = os.stat( filepath )
                    lastmodified = datetime.datetime.fromtimestamp( info.st_mtime )
                    record = (filepath, info.st_size, lastmodified,
                              oct(info.st_mode), islink )
                yield record


def imagefiles( folderpathorlist, pathonly=True ):
    """Use filelist to extract all imagefiles"""
    result = []
    filetuples = filelist( folderpathorlist, pathonly=pathonly )

    # 2017-06-23 - kw .eps dismissed
    extensions = tuple(".pdf .tif .tiff .gif .jpg .jpeg .png".split())
    for filetuple in filetuples:
        path = filetuple
        if not pathonly:
            path = filetuple[0]
        _, ext = os.path.splitext( path )
        if ext.lower() not in extensions:
            continue
        if pathonly:
            yield path
        else:
            yield filetuple


def fontnames():
    fm = AppKit.NSFontManager.sharedFontManager()
    l = fm.availableFonts()
    result = []
    for i in l:
        # filter out the weird fontnames
        if i.startswith(u'.'):
            continue
        result.append( makeunicode(i) )
    return result


class FontRecord:
    def __init__(self, psname, familyname, style, weight, traits, traitnames):
        self.psname = psname
        self.familyname = familyname
        self.style = style
        self.weight = weight
        self.traits = traits
        self.traitnames = traitnames
    def __repr__(self):
        return (u'FontRecord( psname="%s", familyname="%s", style="%s", '
                u'weight=%.2f, traits="%s", traitnames=%s)') % (
                            self.psname, self.familyname, self.style,
                            self.weight, self.traits, self.traitnames)


def fontfamilies(flat=False):
    fm = AppKit.NSFontManager.sharedFontManager()
    l = fm.availableFontFamilies()

    def makeTraitsList( traits ):
        appleTraits = {
            0x00000001: u"italic",
            0x00000002: u"bold",
            0x00000004: u"unbold",
            0x00000008: u"nonstandardcharacterset",
            0x00000010: u"narrow",
            0x00000020: u"expanded",
            0x00000040: u"condensed",
            0x00000080: u"smallcaps",
            0x00000100: u"poster",
            0x00000200: u"compressed",
            0x00000400: u"fixedpitch",
            0x01000000: u"unitalic"}
        result = []
        keys = appleTraits.keys()
        for key in keys:
            if traits & key == key:
                result.append( appleTraits[key])
        return result

    def makeFontRecord(fnt):
        psname, styl, weight, traits = fnt
        psname = makeunicode(psname)
        styl = makeunicode(styl)
        weight = float( weight )
        traits = int(traits)
        traitNames = makeTraitsList( traits )
        return FontRecord(psname, familyName, styl, weight, traits, traitNames)
        
    if flat:
        result = []
    else:
        result = {}
    for fn in l:
        familyName = makeunicode( fn )
        if not flat:
            result[familyName] = famfonts = {}

        subs = fm.availableMembersOfFontFamily_( familyName )
        for fnt in subs:
            fontRec = makeFontRecord( fnt )
            if not flat:
                result[familyName][fontRec.style] = fontRec
            else:
                result.append( fontRec )
    return result


def voices():
    """Return a list of voice names."""
    vcs = AppKit.NSSpeechSynthesizer.availableVoices()
    vcs = [makeunicode(t) for t in vcs]
    vcs = [x.replace(u"com.apple.speech.synthesis.voice.", u"") for x in vcs]
    return vcs


def voiceattributes(voice):
    """Return a dict with attributes for voice.
    
    voice is passed without the 'com.apple.speech.synthesis.voice.' prefix, e.g.
    'Albert' or 'petra.premium'.
    """
    result = {}
    if voice and voice in voices():
        voice = u"com.apple.speech.synthesis.voice.%s" % (voice,)
        attrs = AppKit.NSSpeechSynthesizer.attributesForVoice_( voice )
        result = PyObjCTools.Conversion.pythonCollectionFromPropertyList(attrs)
        keys = result.keys()
        for key  in keys:
            result[key] = makeunicode(result[key])
    return result


def anySpeakers():
    """Return if ANY application is currently speaking."""
    global g_voicetrash

    b = bool(AppKit.NSSpeechSynthesizer.isAnyApplicationSpeaking())
    if b == False:
        # empty accumulated voices
        while len(g_voicetrash) > 0:
            f = g_voicetrash.pop()
            del f
    return b


def say(txt, voice=None, outfile=None, wait=True):
    """Say txt with a voice. Write AIFF file to outfile if parent(outfile) exists.
    defer return if wait is True.
    """
    global g_voicetrash
    if voice and voice in voices():
        voice = u"com.apple.speech.synthesis.voice.%s" % (voice,)
    else:
        voice = AppKit.NSSpeechSynthesizer.defaultVoice()
    
    # outfile is a path to an AIFF file to be exported to
    # if the containing folder does not exist, abort
    path = url = None
    if outfile:
        path = os.path.abspath( makeunicode(outfile) )
        folder, filename = os.path.split( path )
        if not os.path.exists( folder ):
            path = None

    if path:
        url = Foundation.NSURL.fileURLWithPath_isDirectory_( path, False )
    speaker = AppKit.NSSpeechSynthesizer.alloc().initWithVoice_(voice)

    if speaker and url:
        g_voicetrash.append( speaker )
        speaker.startSpeakingString_toURL_(txt, url)
        return speaker

    if speaker:
        if wait:
            while anySpeakers():
                time.sleep(0.1)
        # it is importatnt that speaker gets added AFTER anySpeakers()
        # it does garbage collection
        g_voicetrash.append( speaker )
        speaker.startSpeakingString_(txt)
        return speaker


def aspectRatio(size, maxsize=None, maxw=None, maxh=None):
    """scale a size tuple (w,h) to 
        - maxsize (max w or h)
        - or max width maxw
        - or max height maxh."""
    w, h = size
    denom = maxcurrent = 1

    if maxsize:
        maxcurrent = max(size)
        denom = maxsize
    elif maxw:
        maxcurrent = w
        denom = maxw
    elif maxh:
        maxcurrent = h
        denom = maxh

    if maxcurrent == denom:
        return size
    elif maxsize == 0:
        return size

    ratio = maxcurrent / float(denom)

    neww = int(round(w / ratio))
    newh = int(round(h / ratio))
    return neww, newh


def palette(pilimage, mask):
    """
    Return palette in descending order of frequency
    """
    result = []
    arr = np.asarray(pilimage)
    if mask != None:
        if 0 <= mask <= 255:
            arr = arr & int(mask)
    palette, index = np.unique(asvoid(arr).ravel(), return_inverse=True)
    palette = palette.view(arr.dtype).reshape(-1, arr.shape[-1])
    count = np.bincount(index)
    order = np.argsort(count)
    
    p = palette[order[::-1]]

    for col in p:
        r,g,b = col
        
        result.append( (r / 255.0, g / 255.0, b / 255.0) )
    return result


def asvoid(arr):
    """View the array as dtype np.void (bytes)
    This collapses ND-arrays to 1D-arrays, so you can perform 1D operations on them.
    http://stackoverflow.com/a/16216866/190597 (Jaime)
    http://stackoverflow.com/a/16840350/190597 (Jaime)
    Warning:
    >>> asvoid([-0.]) == asvoid([0.])
    array([False], dtype=bool)
    """
    arr = np.ascontiguousarray(arr)
    result = arr.view(np.dtype((np.void, arr.dtype.itemsize * arr.shape[-1])))
    return result


def imagepalette( pathOrPILimgage, mask=None ):
    t = type(pathOrPILimgage)
    result = []
    if t in (pstr, punicode):
        f = PIL.Image.open( pathOrPILimgage )
        f = f.convert("RGB")
        result = palette( f, mask )
    else:
        try:
            result = palette( pathOrPILimgage, mask )
        except Exception as err:
            pass
    return result


def tempimagepath(mode='w+b', suffix='.png'):
    """Create a temporary file with mode and suffix.
    Returns pathstring."""
    fob = tempfile.NamedTemporaryFile(mode=mode, suffix=suffix, delete=False)
    fname = fob.name
    fob.close()
    return fname


def dithertypes():
    """Return names of all supported dither types."""
    return list(_dithertypes.keys())


def ditherimage(pathOrPILimgage, dithertype, threshhold):
    # argh, a circular import. Dang!
    from nodebox.geo import dither

    t = type(pathOrPILimgage)

    if dithertype in list(_dithertypes):
        dithername = dithertype
        ditherid = _dithertypes.get( dithertype )
    elif dithertype in _ditherIDs:
        ditherid = dithertype
        dithername = _dithertypes.get( dithertype )
        # pass
    else:
        ditherid = 0