import os, getpass, time, multiprocessing, subprocess
import threading

#Since this application doesn't need to be ultra-tight on performance,
#I'm going to threadlock *very aggressively* to remove *all possible* queue weirdness
#when interacting with the net
QueueLock = threading.Lock()
ConfFileWriteLock = threading.Lock()
QueueFileWriteLock = threading.Lock()


def MIFSubprocessSafetyDance(filename):
    #To avoid zombie processes, we assert new on all schedules in the MIF file.
    with open(filename, "r") as f:
        oldMIF = f.readlines()
        
    for i,line in enumerate(oldMIF):
        if line.strip():
            if (line.strip()[0] is not "#") and (line.strip().split()[0].lower() == "destination"):
                if not line.strip().split()[-1].lower() == "new":
                    #Yeah, we need to make "kill all" safe
                    oldMIF[i] = line.rstrip() + " new\n"

    f = open(filename, "w")
    f.write("".join(oldMIF))
    f.close() #With causes buffering errors? Fine, no with.
    
#    with open(filename + "-post", "w") as f:
#        for line in oldMIF:
#            if line.strip():
#                if line.strip()[0] is not "#" and line.strip().split()[0].lower() == "destination":
#                    if not line.strip().split()[-1].lower() == "new":
#                        f.write(line)
#                    else:
#                        #Uh-oh! "kill all" isn't safe here.
#                        #Force new copy of destination, because I said so.
#                        f.write(line.rstrip() + " new\n")
#            else:
#                f.write(line)
#            f.flush() #Really? We're having a write buffer glitch?
#    #OK. Now we're assured "kill all" is safe.
#    print "Escaped."


##########################
# FILE/PROCESS FUNCTIONS #
##########################

##
### Process Count
##

#Py2EXE caters well to Windows users, but has certain disastrous side effects on stdin/out/err that mean
#we need to falsify stdin. I'm OK with that - we weren't using it.

FALSE_STDIN = file("null-data", "a")


##
### Username Finding
##

#Getting the username used to be hard, but then I learned about GetPass.

def getUsername():
    return getpass.getuser()

##
### Path and file helpers
##

#Takes any number of strings, returns the path referenced to the cwd designated by those strings.
def localPath(*args):
    if args[0][0] == ".": #You used localpath, but you had a dot there anyway? Why?
        return os.path.sep.join(list(args))
    else:
        return os.path.sep.join(["."] + list(args))

def MIFDiveAndScoop(divestr, followLinks = False):
    foundMIFs = []
    #We were given either a directory or a MIF file.
    #Let's find out what's going on and recurse that sucker.
    if os.path.isfile(divestr) and not os.path.islink(divestr):
        if divestr[-4:] == ".mif":
            #Just a file! Send it back.
            return [divestr]
        #Else return nothing!
    else: #OK, I think it's a directory...
        for subpath in os.listdir(divestr):
            if os.path.isfile(divestr + os.path.sep + subpath) and subpath[-4:] == ".mif":
                foundMIFs.append(divestr + os.path.sep + subpath)
            elif os.path.isdir(divestr + os.path.sep + subpath) and not os.path.islink(divestr + os.path.sep + subpath):
                #TO RECURSE DIVINE
                foundMIFs.extend(MIFDiveAndScoop(divestr + os.path.sep + subpath))
            elif followLinks and os.path.islink(subpath):
                foundMIFs.extend(MIFDiveAndScoop(divestr + os.path.sep + subpath))
            #Else, misfire, or link when not following links. IGNORE!
    return foundMIFs

def filterOnExtensions(extensions, inlist):
    worklist = []
    #Sadly, it's not possible to use filter itself here without a nasty eval setup. No thanks!
    for item in inlist:
        if item.rsplit(".",1)[-1] in extensions:
            worklist.append(item)
    return worklist

def assureOutputPath(path):
    #MIF files with basenames that have subdirectories only function if those subdirectories exist! We can help.
    #If we're about to enqueue a MIF file, make *sure* any directories referenced in its basename are made.

    #Note: Inside MIF files, UNIXlike pathseps are used for all OSes
    fileLocation = path.rsplit(os.path.sep, 1)[0]

    tgt = None #Helps to have a set value to prevent a lookup exception

    with open(path, "r") as f:
        mif = f.readlines()
        for line in mif:
            if "basename" in line and not line.strip()[0] == "#":
                tgt = line.split("basename")[1].split()[0].strip() #This is as defensive as I can sanely be.
                break
    if not tgt:
        return #What? How did you supply a bad MIF file?
    #Now we have a target path... break it up on /
    dirs = tgt.split(r"/")
    # . is implicit - kill it.
    if "." in dirs:
        dirs.remove(".")

    #Next, the last element is just the target *filename* - we don't need it, and should discard it.
    dirs.pop()
    if not dirs:
        #There's no path to make - we're good.
        return

    #OK, we have to try to make a path.
    #Now, concatenate those dirs to the file's known location
    leadPath = fileLocation.split(os.path.sep)
    leadPath.extend(dirs)
    ultimatePath = os.path.sep.join(leadPath)
    if not os.path.exists(ultimatePath):
        os.makedirs(ultimatePath)

##
### Time helpers
##

def secsToDuration(now, then):
    #Given two times in seconds, make something display-friendly.
    #This is probably in datetime somewhere, but I want a quick hack
    delta = int(now-then) #Round to the nearest second, honestly.
    secs = str(delta%60).rjust(2,"0")
    mins = str((delta//60)%60).rjust(2,"0")
    hours = str((delta//3600)%24).rjust(2,"0")
    days = str(delta//86400)

    #I am not proud of this.

    if delta < 3600:
        return "%s:%s" % (mins, secs)
    elif delta < 86400:
        return "%s:%s:%s" % (hours, mins, secs)
    else:
        return "%s:%s:%s:%s" % (days, hours, mins, secs)

def secsToDisplayTime(secs):
    strct = time.localtime(secs)
    return time.strftime("%H:%M %a %b %d %Y",strct)


##
### Simple list element transposition helper
##

def transpose(lst, a, b):
    #Transpose two elements in place
    foo = lst[a]
    lst[a] = lst[b]
    lst[b] = foo

#######################################
# KEEP THE DATA SEPARATE FROM THE GUI #
#######################################

class OOMMFSim(object):
    def __init__(self, **kwargs):
        self.username = kwargs["username"]
        self.path = kwargs["path"]
        self.startTime = None
        self.endTime = None
        self.subprocess = None

    #Make nice data for sending, ignoring features we don't need.
    #We *certainly* don't want to be shipping subprocesses
    def tuplize(self):
        return (self.username, self.path, self.startTime, self.endTime)

class _OOMMFq(object):
    def __init__(self):
        #Before construction, pick up configuration options to properly display them in the GUI.
        #These may all be defaults - and that's OK!
        self.conf = self.getConfig()

        self.runningSims = []
        self.queuedSims = []
        self.doneSims = []
        self.erroredSims = []
        self.loadQueue()

        #Should we just go ahead then?
        if int(self.conf["autoStart"]):
            self.startMoreSims()

    def checkSimsForCompletion(self):
        doRemove = set()
        #Does exactly what it says on the tin.
        with QueueLock:
            for sim in self.runningSims:
                if sim.subprocess and not sim.subprocess.poll() == None:
                    #Hey, a sim is done! Mark for deletion - DO NOT DELETE WHILE ITERATING OVER LIST
                    doRemove.add(sim)

            if doRemove:
                #Clean these up and start new ones.
                for sim in doRemove:
                    self.finishSim(sim)
                self.startMoreSims()
                return True #True marks that some sims completed.
            else:
                return False #False marks that nothing has changed.

    def clearErrors(self):
        self.erroredSims = []

    def startMoreSims(self):
        #Launch sims from the queue until we're saturated.
        while self.queuedSims and len(self.runningSims) < int(self.conf["maxSims"]):
            self.runFromQueue(self.queuedSims[0])

    def runFromQueue(self, item):
        #YOU HAVE the QueueLock. DO NOT REAQCUIRE.
        #Move a sim from queued to running, and launch the actual OOMMF process.
        try:
            self.queuedSims.remove(item)
        except:
            print "WARNING: Tried to run an item from the queue that wasn't *in* the queue!"
            print item

        #Let's give it a time and move it to running.
        item.startTime = time.time()
        self.runningSims.append(item)

        #start the party
        #$TCLSH $PATH boxsi -kill all -nice 1 -restart 2 $FILENAME
        command = self.conf['tclshCall'] + ' "' + self.conf["oommfPath"] + '" boxsi -kill all -nice 1 -restart 2 "' + item.path + '"'
        #Duck STDIN altogether - no reason to do the fancy py2exe-avoidance checks if, frankly, we're never going to touch it.
        item.subprocess = subprocess.Popen(command, shell=True, stdin = FALSE_STDIN, stdout=subprocess.PIPE,  stderr=subprocess.STDOUT)
        #And now we watch for it to be done...

    def finishSim(self, item):
        #We are already inside QueueLock. DO NOT REACQUIRE
        #Remove a sim from the running list.
        try:
            self.runningSims.remove(item)
            if item.subprocess.returncode > 0:
                self.erroredSims.append(item)
            item.endTime = time.time()
        except:
            print "WARNING: Tried to finish a sim that wasn't in the list of running sims."
            print "This is probably the programmer's fault; email him."

        #It's finally save to save the queue with this sim missing. We're done.
        self.doneSims.append(item)
        self.saveQueue()

    def enqueueSim(self, filename, username = None):
        if not username:
            username = self.conf["consoleUsername"]
        assureOutputPath(filename) #Make paths to match basename
        with QueueLock:
            MIFSubprocessSafetyDance(filename)
            self.queuedSims.append(OOMMFSim(username=username, path=filename))
            self.saveQueue()
        if len(self.runningSims) < self.conf["maxSims"]:
            self.startMoreSims()


    def loadQueue(self):
        if self.runningSims or self.queuedSims:
            print "WARNING: Attempted to reload sim queue from file while running and populated."
            print "It is possible you meant to do this, but I'm not going to help you."
        #Load in username, file, starttime from the tab-separated queue file.
        if os.path.exists(localPath("oommfq.queue")):
            with open(localPath("oommfq.queue"), "r") as f:
                for line in f.readlines():
                    if not line.strip(): #Dodge spurious blank lines, in case people have been hand-frobbing.
                        continue
                    parsed = line.split('\t')
                    if len(parsed) > 2:
                        print "WARNING: Too many tabs in the queue file. Please check."
                        continue
                    elif len(parsed) <2:
                        print "WARNING: Malformed line (no tab)"
                        print parsed
                        continue
                    username = parsed[0].strip()
                    path = parsed[1].strip()
                    with QueueLock:
                        self.queuedSims.append(OOMMFSim(username=username,path=path))

    def prepareDirectoryForRemote(self, listpath):
        try:
            #Concatenate items and use localPath to generate the path.
            #os.makedirs will traverse - and if the directory exists, we'll
            #return the problem so that the end-user can be alerted.
            path = localPath(*tuple(["remote"] + listpath))
            os.makedirs(path)
            return path
        except Exception as e:
            return False

    def saveQueue(self):
        with QueueFileWriteLock:
            #Write username, file, starttime to a tab-separated queue file.
            #We like tab separation, since neither usernames nor paths may contain tabs.
            with open(localPath("oommfq.queue"), "w") as f:
                for sim in self.runningSims + self.queuedSims:
                    f.write(sim.username+"\t"+sim.path+"\n")

    def clearQueueItems(self, indices):
        #Pull the actual items, and then lookup-kill them. It's slow,
        #but it's clean!
        with QueueLock:
            victims = [self.queuedSims[i] for i in indices]
            for item in victims:
                self.queuedSims.remove(item)

    def listbump(self, items, delta):
        maxlen = len(self.queuedSims)
        #Now, see, here's the thing: We *also* don't want to shift-up the farthest-up brick, or down the
        #farthest-down brick, is those are selected. So we need to catch those.
        preserve = [] #Let's save the post-transformation IDs to keep the highlight!
        if delta < 0: #catch top brick
            mincatch = 0
            while mincatch in items:
                items.remove(mincatch)
                preserve.append(mincatch)
                mincatch += 1
        elif delta > 0:
            maxcatch = len(self.queuedSims)-1
            while maxcatch in items:
                items.remove(maxcatch)
                preserve.append(maxcatch)
                maxcatch -= 1
        #OK. Any unshiftable blocks have been removed from the itemlist - we can shift the remaining.
        with QueueLock:
            for item in items:
                if item + delta >= 0 and item + delta <= maxlen: #Guarantees the trade exists - SHOULDN'T be necessary for single-shift.
                    #Do the transpose, and finally, life the lock.
                    transpose(self.queuedSims, item, item+delta)
                    preserve.append(item+delta)

        return preserve

    def confUpdate(self, key, val, lazy=False):
        self.conf[key] = val
        if not lazy: #For some reason, we don't think this is worth writing right now
            self.writeConf()

    def getConfig(self):
        #Returns a dictionary - set default values now
        #Yes, initialize everything to strings - ensure code is adequately defensive.
        #It's what you'd get from the file, and what we'll be dealing with in wx
        #You must check to numberize later.
        confValues = {"oommfPath" : "No OOMMF Path Specified!",
                      "tclshCall" : "tclsh",
                      "diveSimlinks" : "0",
                      "consoleUsername" : getUsername(),
                      "port" : "16101",
                      "autoServe" : "0",
                      "maxSims" : str(max((multiprocessing.cpu_count()/2),1)),
                      "autoStart" : "0",
                      "refreshTime" : "1000",
                      "simRecheckTime" : "5000"}

        #Try to open the file; if not, use default values.
        if os.path.exists(localPath("oommfq.conf")):
            with open(localPath("oommfq.conf"), "r") as f:
                for line in f.readlines():
                    #Assign the key-value pairs. Hope the user doesn't hurt you too much.
                    if len(line.split(" ", 1)) == 2:
                        #Looks well-formed
                        key, val = line.strip().split(" ",1)
                        confValues[key] = val
                    else:
                        #Ignore this conf value
                        continue

        #Sanitize unreasonable values
        if int(confValues["maxSims"]) > 64:
            confValues["maxSims"] = "64"

        return confValues

    def writeConf(self):
        with ConfFileWriteLock:
            #Write the dictionary to a file
            with open("." + os.path.sep + "oommfq.conf", "w") as f:
                for key, val in self.conf.iteritems():
                    f.write(key + " " + val + "\n")

    def goDown(self):
        for item in self.runningSims:
            if item.subprocess:
                item.subprocess.terminate()


###################
# IMPORT-TIME FUN #
###################

#Make the singleton data manager
OOMMFq = _OOMMFq()

#Finally, some debug!
# TODO: REMOVE DEBUG

if __name__=="__main__":
    #assureOutputPath(r"G:\Work\OOMMFqTests\rhomboid.mif")
    #print MIFDiveAndScoop(r"C:\Python26")
    pass
