#!/usr/bin/python3

import os,sys,math
import subprocess

"""
Copyright Aaron M. Schinder, 2011 - MIT/BSD License

This script contains a bunch of helper functions for generating simple, imperative, hopefully
transparent build scripts using the python language (and nothing else). 

I just want the script to do the compiling and linking operations I want it to do in the order
I want it to do it in, finding every relevant source file. 

That's it. That's what I want in a build system.
"""

def flist(pth,**kwargs): 
    """
    flist - list all files in a given directory pth
    optional arguments:
       recurse - (T/F): Whether to recursively search for files in directory tree
       exts - (list): A list of file extensions to search for, otherwise all files
       normpath (T/F): whether to normalize path variables after
    filelist = flist(pth,**kwargs): 
    """
    flst = []
    if(not('recurse' in kwargs)):
        recurse_ = False
    else:
        recurse_ = kwargs['recurse']
    if(not('exts' in kwargs)):
        filterexts_ = False
    else:
        filterexts_ = True
        exts = kwargs['exts']
    if(not('normpath' in kwargs)):
        normpath_ = True
    else:
        normpath_ = kwargs['normpath']
    if(not('linuxpath' in kwargs)):
        linuxpath_ = False
    else:
        linuxpath_ = kwargs['linuxpath']
    if(not('followlinks' in kwargs)):
        followlinks_ = False
    else:
        followlinks_ = kwargs['followlinks']

    dirlist = []
    rawlist = os.listdir(pth)

    for F in rawlist:
        F2 = os.path.join(pth,F)
        if(os.path.isdir(F2)):
            b = (followlinks_) or ((not followlinks_) and not(os.path.islink(F2)))
            if(b):
                if((F2!=".")&(F2!="..")):
                    dirlist.append(F2)
        elif(os.path.isfile(F2)):
            flst.append(F2)

    #Recurse through directories
    if(recurse_):
        for D in dirlist:
            lst = flist(D,**kwargs)
            for L in lst:
                flst.append(L)

    #Postprocess:
    #Filter out all extensions except the selected ext list
    if(filterexts_):
        flst = filterexts(flst,exts)

    #Normalize filename path according to os
    if(normpath_):
        flst2 = list(flst)
        for I in range(0,len(flst2)):
            flst[I] = os.path.normpath(flst2[I])

    #If linuxpath, convert all \\ to /
    #if(linuxpath_):
    #    flst2 = list(flst)
    #    for I in range(0,len(flst2)):
    #        flst[I] = linuxpath(flst2[I])
    
    return flst


def filterexts(flst,exts):
    """
    Filters by extensions in a list of files
    flst = def filterexts(flst,exts):
    """
    flst2 = []
    if(isinstance(exts,str)):
        exts = list([exts])
    for F in flst:
        b = False
        for ext in exts:
            if(ext[0]!='.'):
                ext = '.'+ext
            F2 = os.path.splitext(F)
            if(len(F2)>=2):
                ex = F2[1]
                if(len(ex)>0):            
                    if(ex[0]!='.'):
                        ex = '.'+ex
                    if(ex==ext):
                        b = True
        if(b):
            flst2.append(F)
              
    return flst2

#Find a file fname, starting in pth and recursing
#Used for finding library files to link
def findfile(fname,pth,**kwargs):
    fullfname = ""
    flst = flist(pth,recurse=True)
    for F in flst:
        F2 = os.path.split(F)[1]
        if(F2 == fname):
            fullfname = F
    
    return fullfname

def replaceext(fname,ext):
    fname2 = ""
    if(len(ext)>0):
        if(ext[0]!='.'):
            ext = '.'+ext
        fname2 = os.path.splitext(fname)[0]+ext
    else:
        fname2 = os.path.splitext(fname)[0]
    return fname2

def replaceexts(fnamelist,ext):
    """Takes a list of filenames and returns a list with the extensions replaced by ext """
    fname2list = []
    for F in fnamelist:
        F2 = replaceext(F,ext)
        fname2list.append(F2)
    return fname2list

def except_contains(lst1,exc):
    """
    Takes a list of file names lst1, and removes filenams that match the
    list of exceptions exc. Returns a list without the exceptions.
    """
    lst2 = []
    for item in lst1:
        b = 1
        for item2 in exc:
            fsplit = os.path.split(item)
            fn = fsplit[len(fsplit)-1]
            if(fn==item2):
                b = 0
                break
        if(b==1):
            lst2.append(item)
    return lst2


def list_to_sss(lst):
    """List of strings to space-seperated-string"""
    lout = ""
    for I in range(0,len(lst)-1):
        lout = lout + lst[I] + " "
    if(len(lst)>0):
        lout = lout + lst[len(lst)-1]
    return lout

##########################
##System Call Procedures##
##########################

def callproc(cmd, **kwargs):
    if(not('logfile' in kwargs)):
        use_lf = False
    else:
        logfile = kwargs['logfile']
        if(logfile!=""):
            fp = open(kwargs['logfile'],'a+')
            use_lf = True
        else:
            use_lf = False
        
    if(not('echo' in kwargs)):
        echo = True
    else:
        echo = kwargs['echo']

    if(echo):
        print(cmd)

    #encoding/deconding to/from bytes is necessary to use the subprocess command
    #in python3.7
    #However, only do this in linux
    if(sys.platform!='win32'):
        cmd2 = cmd.encode(encoding='utf-8')
    else:
        cmd2 = cmd
    proc = subprocess.Popen(cmd2,stderr = subprocess.STDOUT, stdout=subprocess.PIPE, shell=True)
    (out, err) = proc.communicate()

    out = out.decode(encoding='utf-8')
    
    if(echo):
        print(out)
        #print(err);
    if(use_lf):
        fp.writelines(cmd+'\n')
        fp.writelines(out+'\n')

    if(use_lf):
        fp.close()


#############################################
## Compiler, Archive, and Linker Functions ##
#############################################

#MSVC compiler wrapper
def msvc_compile(compilername, srcfile, **kwargs):

    if(not('include' in kwargs)):
        include = ''
    else:
        include = kwargs['include']
        if(isinstance(include,list)):
            include  = list_to_sss(include)
    
    if(not('flags' in kwargs)):
        flags = ''
    else:
        flags = kwargs['flags']
        if(isinstance(flags,list)):
            flags = list_to_sss(flags)

    if(not('objext' in kwargs)):
        objext = '.obj'
    else:
        objext = kwargs['objext']
    
    if(not('srcfileflag' in kwargs)):
        srcfileflag = '/c'
    else:
        srcfileflag = kwargs['srcfileflag']

    if(not('outfileflag' in kwargs)):
        outfileflag = '/Fo:'
    else:
        outfileflag = kwargs['outfileflag']
    if(not('logfile' in kwargs)):
        logfile = ""
    else:
        logfile = kwargs['logfile']

    outfile = replaceext(srcfile,objext)
    ln = compilername+" "+flags+" "+" "+srcfileflag+" "+srcfile+" "+outfileflag+'"'+outfile+'"'
    ln = ln + " " + include

    callproc(ln,echo=True,logfile=logfile)

    return

#MSVC compiler wrapper
def msvc_compile_list(compiler,srclist,**kwargs):
    for S in srclist:
        msvc_compile(compiler,S,**kwargs)
    return

#gnu-style compiler compile: Should work with gcc, g++, gfortran
def gs_compile(compiler,srcfile,**kwargs):
    if(not('include' in kwargs)):
        include = ''
    else:
        include = kwargs['include']
        if(isinstance(include,list)):
            include  = list_to_sss(include)
    
    if(not('flags' in kwargs)):
        flags = ''
    else:
        flags = kwargs['flags']
        if(isinstance(flags,list)):
            flags = list_to_sss(flags)

    if(not('objext' in kwargs)):
        objext = '.o'
    else:
        objext = kwargs['objext']

    if(not('srcfileflag' in kwargs)):
        srcfileflag = '-c'
    else:
        srcfileflag = kwargs['srcfileflag']

    if(not('outfileflag' in kwargs)):
        outfileflag = '-o'
    else:
        outfileflag = kwargs['outfileflag']

    if(not('logfile' in kwargs)):
        logfile = ""
    else:
        logfile = kwargs['logfile']

    if(not('smartcompile' in kwargs)):
        _smartcompile = True
    else:
        _smartcompile = kwargs['smartcompile']
    
    #Do I want to make this thing this general?

    # if(not(_smartcompile) or smartcompile(srcfile,objext)):
    #     outfile = replaceext(srcfile,objext)
    #     ln = compiler+" "+flags+" " + outfileflag+" "+outfile+" "+srcfileflag+" "+srcfile
    #     ln = ln + " " + include

    #     callproc(ln,echo=True,logfile=logfile)

    outfile = replaceext(srcfile,objext)
    ln = compiler+" "+flags+" " + outfileflag+" "+outfile+" "+srcfileflag+" "+srcfile
    ln = ln + " " + include

    callproc(ln,echo=True,logfile=logfile)

    return

def gs_compile_list(compiler,srclist,**kwargs):
    for S in srclist:
        gs_compile(compiler,S,**kwargs)
    return

def gs_compile_all(compiler,srcdir,srcexts,**kwargs):
    if(not('recurse' in kwargs)):
        recurse = True
    else:
        recurse = kwargs['recurse']

    srcfils = flist(srcdir,exts=srcexts,recurse=recurse)
    
    for S in srcfils:
        gs_compile(compiler,S,**kwargs)

    return

def gs_link_all(linker,srcpath,target,**kwargs):

    if(not('objext' in kwargs)):
        objext = '.o'
    else:
        objext = kwargs['objext']

    if(not('recurse' in kwargs)):
        recurse = True
    else:
        recurse = kwargs['recurse']


    objfils = flist(srcpath,exts=objext,recurse=recurse)
    oflst = list_to_sss(objfils)
    
    gs_link_list(linker,oflst,target,**kwargs)
    
    return

def gs_link_list(linker,objlist,target,**kwargs):

    if(not('objext' in kwargs)):
        objext = '.o'
    else:
        objext = kwargs['objext']

    if(not('libdir' in kwargs)):
        libdir = ''
    else:
        libdir = kwargs['libdir']

    if(not('staticlibs' in kwargs)):
        staticlibs = ''
    else:
        staticlibs = kwargs['staticlibs']

    if(not('libflags' in kwargs)):
        libflags = ''
    else:
        libflags = kwargs['libflags']

    if(not('linkerflags' in kwargs)):
        linkerflags = ''
    else:
        linkerflags = kwargs['linkerflags']

    if(not('recurse' in kwargs)):
        recurse = True
    else:
        recurse = kwargs['recurse']

    if(not('logfile' in kwargs)):
        logfile = ''
    else:
        logfile = kwargs['logfile']

    ln = linker+" -o "+target+" "+libdir
    ln = ln+" "+objlist+" "+staticlibs+" "+libflags+" "+linkerflags
    
    callproc(ln,logfile=logfile)
    return

def msvc_link_list(objlist,target,**kwargs):

    linker = 'link'

    if(not('objext' in kwargs)):
        objext = '.obj'
    else:
        objext = kwargs['objext']

    if(not('libdir' in kwargs)):
        libdir = ''
    else:
        libdir = kwargs['libdir']

    if(not('staticlibs' in kwargs)):
        staticlibs = ''
    else:
        staticlibs = kwargs['staticlibs']

    if(not('libflags' in kwargs)):
        libflags = ''
    else:
        libflags = kwargs['libflags']

    if(not('linkerflags' in kwargs)):
        linkerflags = ''
    else:
        linkerflags = kwargs['linkerflags']

    if(not('recurse' in kwargs)):
        recurse = True
    else:
        recurse = kwargs['recurse']

    if(not('logfile' in kwargs)):
        logfile = ''
    else:
        logfile = kwargs['logfile']

    ln = linker+" "+libdir
    ln = ln+" "+objlist+" "+staticlibs+" "+linkerflags
    ln = ln+" /out:"+target+" "+libflags
    
    callproc(ln,logfile=logfile)

    return

def ar_all(srcpath,arname,**kwargs):
    if(not('recurse' in kwargs)):
        recurse = True
    else:
        recurse = kwargs['recurse']
    if(not('objext' in kwargs)):
        objext = '.o'
    else:
        objext = kwargs['objext']
    
    objlist = flist(srcpath,exts=objext,recurse=recurse)
    ar_list(objlist,arname,**kwargs)
    
    return

def msvc_lib_list(objlist,arname,**kwargs):
    objlist2 = list_to_sss(objlist)
    
    ln = "lib "+objlist2+" /out:"+arname
    callproc(ln)

    return

def ar_list(objlist,arname,**kwargs):
    objlist2 = list_to_sss(objlist)

    ln = "ar cr "+ arname+" "+objlist2
    callproc(ln)
    
    return

def ar_add_list(objlist,arname,**kwargs):
    objlist2 = list_to_sss(objlist)

    ln = "ar t "+arname+" "+objlist2
    callproc(ln)
    return

#####################################
## Incremental Compilation Library ##
#####################################

#silently read lines from a text file if exists
def readtextlines(fname):
    txtlns = []

    if(not os.path.isfile(fname)):
        return txtlns

    try:
        fp = open(fname,"r")
    except:
        return txtlns

    ln = " "
    while(ln!=""):
        ln = fp.readline()
        txtlns.append(ln)

    fp.close()

    return txtlns

def getincludefnfrage(includeline):

    fnfrag = ""
    I1 = -1
    I2 = -1

    for I in range(0,len(includeline)):
        if(I1<0 and (includeline[I]=='<' or includeline[I]=='"')):
            I1 = I
        if(I1>=0 and (includeline[I]=='>' or includeline[I]=='"')):
            I2 = I
            break
    if(I1>=0 and I2>=0):
        fnfrag = includeline[I1+1:I2]

    return fnfrag

#Returns the name of the source file fname (if it exists)
#and all included filenames
def getsrcandincludes(fname, incdirs):

    flist = []
    if(os.path.isfile(fname)):
        flist.append(fname)

        Ilist = 0
        while(Ilist<len(flist)):
            #recurse through files
            f1 = flist[Ilist]
            lns = readtextlines(f1)
            for J in range(0,len(lns)):
                if(lns[J].find("#include")>=0):
                    fnfrag = getincludefnfrage(lns[J])
                    for K in range(0,len(incdirs)):
                        tfn = os.path.join(incdirs[K],fnfrag)
                        if(os.path.isfile(tfn)):
                            flist.append(tfn)
                            break

            Ilist = Ilist + 1

    return flist

#Returns the name of the object file associated with the source file
#within the object store folder (if it exists)
def getobjfile(fname,objstore,objext = ".o"):

    fret = ""
    f1 = os.path.split(fname)[1]
    f2 = f1
    while(os.path.splitext(f2)[1]!=""):
        f2 = os.path.splitext(f2)[0]
    objext = objext.strip('.')
    f3 = os.path.join(objstore,"{}.{}".format(f2,objext))
    if(os.path.exists(f3)):
        fret = f3

    return fret

def getsrctimes(fname, incdirs):

    ftimes = []
    flst = getsrcandincludes(fname, incdirs)
    for I in range(0,len(flst)):
        f = flst[I]
        mt = os.path.getmtime(f)
        ftimes.append(mt)
    
    return ftimes

def getobjtime(fname,objstore,objext=".o"):
    ret = -1
    fret = getobjfile(fname,objstore,objext)
    if(fret!=""):
        ret = os.path.getmtime(fret)

    return ret

#Decide whether or not to compile source file
def decidecompile(fname,**kwargs):
    ret = True

    if(not os.path.isfile(fname)):
        ret = False
        return ret
    
    ##unpack kwargs
    if("searchincdirs" in kwargs):
        incdirs = kwargs["searchincdirs"]
    else:
        incdirs = ["./include"]
    
    if("objext" in kwargs):
        objext = kwargs["objext"]
    else:
        objext = ".o"
    if("objstore" in kwargs):
        objstore = kwargs["objstore"]
    else:
        objstore = "./objstore"
    
    
    srclist = getsrcandincludes(fname,incdirs)
    srctlist = getsrctimes(fname,incdirs)
    obj = getobjfile(fname,objstore,objext)
    objt = getobjtime(fname,objstore,objext)
    
    if(obj!=""):
        ret = False
        for I in range(0,len(srctlist)):
            if(srctlist[I]>objt):
                ret = True
                break

    return ret

def gs_incremental_compile(compiler,srcfile,**kwargs):

    if(not('include' in kwargs)):
        include = ''
    else:
        include = kwargs['include']
        if(isinstance(include,list)):
            include  = list_to_sss(include)
    if(not('flags' in kwargs)):
        flags = ''
    else:
        flags = kwargs['flags']
        if(isinstance(flags,list)):
            flags = list_to_sss(flags)
    if(not('objext' in kwargs)):
        objext = '.o'
    else:
        objext = kwargs['objext']
    if(not('srcfileflag' in kwargs)):
        srcfileflag = '-c'
    else:
        srcfileflag = kwargs['srcfileflag']
    if(not('outfileflag' in kwargs)):
        outfileflag = '-o'
    else:
        outfileflag = kwargs['outfileflag']

    if(not('logfile' in kwargs)):
        logfile = ""
    else:
        logfile = kwargs['logfile']
    if(not('smartcompile' in kwargs)):
        _smartcompile = True
    else:
        _smartcompile = kwargs['smartcompile']

    #incrementalcompile
    if("searchincdirs" in kwargs):
        incdirs = kwargs["searchincdirs"]
    else:
        incdirs = ["./include"]
    
    if("objstore" in kwargs):
        objstore = kwargs["objstore"]
    else:
        objstore = "./objstore"
    
    #Do I want to make this thing this general?

    docompile = decidecompile(srcfile,**kwargs)

    if(docompile):
        f1 = os.path.split(srcfile)[1]
        f2 = f1
        while(os.path.splitext(f2)[1]!=""):
            f2 = os.path.splitext(f2)[0]
        outfile = os.path.join(objstore,"{}{}".format(f2,objext))

        ln = compiler+" "+flags+" " + outfileflag+" "+outfile+" "+srcfileflag+" "+srcfile
        ln = ln + " " + include

        callproc(ln,echo=True,logfile=logfile)

    return

def msvc_incremental_compile(compiler,srcfile,**kwargs):

    if(not('include' in kwargs)):
        include = ''
    else:
        include = kwargs['include']
        if(isinstance(include,list)):
            include  = list_to_sss(include)
    if(not('flags' in kwargs)):
        flags = ''
    else:
        flags = kwargs['flags']
        if(isinstance(flags,list)):
            flags = list_to_sss(flags)
    if(not('objext' in kwargs)):
        objext = '.obj'
    else:
        objext = kwargs['objext']
    if(not('srcfileflag' in kwargs)):
        srcfileflag = '/c'
    else:
        srcfileflag = kwargs['srcfileflag']
    if(not('outfileflag' in kwargs)):
        outfileflag = '/Fo'
    else:
        outfileflag = kwargs['outfileflag']

    if(not('logfile' in kwargs)):
        logfile = ""
    else:
        logfile = kwargs['logfile']

    #incrementalcompile
    if("searchincdirs" in kwargs):
        incdirs = kwargs["searchincdirs"]
    else:
        incdirs = ["./include"]
    
    if("objstore" in kwargs):
        objstore = kwargs["objstore"]
    else:
        objstore = "./objstore"
    
    #Do I want to make this thing this general?

    docompile = decidecompile(srcfile,**kwargs)

    if(docompile):
        f1 = os.path.split(srcfile)[1]
        f2 = f1
        while(os.path.splitext(f2)[1]!=""):
            f2 = os.path.splitext(f2)[0]
        outfile = os.path.join(objstore,"{}{}".format(f2,objext))
        outfile = os.path.normpath(outfile)

        ln = compiler+" "+flags+" "+srcfileflag+" "+srcfile+" "+ outfileflag+'"'+outfile+'"'
        ln = ln + " " + include

        callproc(ln,echo=True,logfile=logfile)

    return


def gs_incremental_compile_list(compiler,srclist,**kwargs):

    for s in srclist:
        gs_incremental_compile(compiler,s,**kwargs)

    return

def msvc_incremental_compile_list(compiler,srclist,**kwargs):

    for s in srclist:
        msvc_incremental_compile(compiler,s,**kwargs)

    return

#######################
## Main Script Tests ##
#######################

def testtimes(args):
    if(len(args)>=2):
        flist = getsrcandincludes(args[1],["./include"])
        ftlist = getsrctimes(args[1],["./include"])
        for I in range(0,len(flist)):
            print("{}\t\t{}".format(flist[I],ftlist[I]))
        
        print("associated obj file:")
        fobj = getobjfile(args[1],"./objstore")
        ftobj = getobjtime(args[1],"./objstore")
        if(fobj!=""):
            print("{}\t\t{}".format(fobj,ftobj))
        else:
            print("none found")

        cflag = decidecompile(args[1])
        print("compile? : {}".format(cflag))

    
    return

# if(__name__ == "__main__"):

#     args = sys.argv
#     testtimes(args)
    
        

        