#!/usr/bin/env python
from __future__ import division, absolute_import, print_function

import sys, os
import fortran
import clapack_scrub

try: set
except NameError:
    from sets import Set as set

# Arguments to pass to f2c. You'll always want -A for ANSI C prototypes
# Others of interest: -a to not make variables static by default
#                     -C to check array subscripts
F2C_ARGS = '-A'

# The header to add to the top of the *_lite.c file. Note that dlamch_() calls
# will be replaced by the macros below by clapack_scrub.scrub_source()
HEADER = '''\
/*
NOTE: This is generated code. Look in Misc/lapack_lite for information on
      remaking this file.
*/
#include "Numeric/f2c.h"

#ifdef HAVE_CONFIG
#include "config.h"
#else
extern doublereal dlamch_(char *);
#define EPSILON dlamch_("Epsilon")
#define SAFEMINIMUM dlamch_("Safe minimum")
#define PRECISION dlamch_("Precision")
#define BASE dlamch_("Base")
#endif

extern doublereal dlapy2_(doublereal *x, doublereal *y);

'''

class FortranRoutine(object):
    """Wrapper for a Fortran routine in a file.
    """
    type = 'generic'
    def __init__(self, name=None, filename=None):
        self.filename = filename
        if name is None:
            root, ext = os.path.splitext(filename)
            name = root
        self.name = name
        self._dependencies = None

    def dependencies(self):
        if self._dependencies is None:
            deps = fortran.getDependencies(self.filename)
            self._dependencies = [d.lower() for d in deps]
        return self._dependencies

class UnknownFortranRoutine(FortranRoutine):
    """Wrapper for a Fortran routine for which the corresponding file
    is not known.
    """
    type = 'unknown'
    def __init__(self, name):
        FortranRoutine.__init__(self, name=name, filename='<unknown>')

    def dependencies(self):
        return []

class FortranLibrary(object):
    """Container for a bunch of Fortran routines.
    """
    def __init__(self, src_dirs):
        self._src_dirs = src_dirs
        self.names_to_routines = {}

    def _findRoutine(self, rname):
        rname = rname.lower()
        for s in self._src_dirs:
            ffilename = os.path.join(s, rname + '.f')
            if os.path.exists(ffilename):
                return self._newFortranRoutine(rname, ffilename)
        return UnknownFortranRoutine(rname)

    def _newFortranRoutine(self, rname, filename):
        return FortranRoutine(rname, filename)

    def addIgnorableRoutine(self, rname):
        """Add a routine that we don't want to consider when looking at
        dependencies.
        """
        rname = rname.lower()
        routine = UnknownFortranRoutine(rname)
        self.names_to_routines[rname] = routine

    def addRoutine(self, rname):
        """Add a routine to the library.
        """
        self.getRoutine(rname)

    def getRoutine(self, rname):
        """Get a routine from the library. Will add if it's not found.
        """
        unique = []
        rname = rname.lower()
        routine = self.names_to_routines.get(rname, unique)
        if routine is unique:
            routine = self._findRoutine(rname)
            self.names_to_routines[rname] = routine
        return routine

    def allRoutineNames(self):
        """Return the names of all the routines.
        """
        return list(self.names_to_routines.keys())

    def allRoutines(self):
        """Return all the routines.
        """
        return list(self.names_to_routines.values())

    def resolveAllDependencies(self):
        """Try to add routines to the library to satisfy all the dependencies
        for each routine in the library.

        Returns a set of routine names that have the dependencies unresolved.
        """
        done_this = set()
        last_todo = set()
        while True:
            todo = set(self.allRoutineNames()) - done_this
            if todo == last_todo:
                break
            for rn in todo:
                r = self.getRoutine(rn)
                deps = r.dependencies()
                for d in deps:
                    self.addRoutine(d)
                done_this.add(rn)
            last_todo = todo
        return todo

class LapackLibrary(FortranLibrary):
    def _newFortranRoutine(self, rname, filename):
        routine = FortranLibrary._newFortranRoutine(self, rname, filename)
        if 'BLAS' in filename:
            routine.type = 'blas'
        elif rname.startswith('z'):
            routine.type = 'zlapack'
        else:
            routine.type = 'dlapack'
        return routine

    def allRoutinesByType(self, typename):
        routines = sorted((r.name, r) for r in self.allRoutines() if r.type == typename)
        return [a[1] for a in routines]

def printRoutineNames(desc, routines):
    print(desc)
    for r in routines:
        print('\t%s' % r.name)

def getLapackRoutines(wrapped_routines, ignores, lapack_dir):
    blas_src_dir = os.path.join(lapack_dir, 'BLAS', 'SRC')
    if not os.path.exists(blas_src_dir):
        blas_src_dir = os.path.join(lapack_dir, 'blas', 'src')
    lapack_src_dir = os.path.join(lapack_dir, 'SRC')
    if not os.path.exists(lapack_src_dir):
        lapack_src_dir = os.path.join(lapack_dir, 'src')
    library = LapackLibrary([blas_src_dir, lapack_src_dir])

    for r in ignores:
        library.addIgnorableRoutine(r)

    for w in wrapped_routines:
        library.addRoutine(w)

    library.resolveAllDependencies()

    return library

def getWrappedRoutineNames(wrapped_routines_file):
    fo = open(wrapped_routines_file)
    routines = []
    ignores = []
    for line in fo:
        line = line.strip()
        if not line or line.startswith('#'):
            continue
        if line.startswith('IGNORE:'):
            line = line[7:].strip()
            ig = line.split()
            ignores.extend(ig)
        else:
            routines.append(line)
    return routines, ignores

def dumpRoutineNames(library, output_dir):
    for typename in ['unknown', 'blas', 'dlapack', 'zlapack']:
        routines = library.allRoutinesByType(typename)
        filename = os.path.join(output_dir, typename + '_routines.lst')
        fo = open(filename, 'w')
        for r in routines:
            deps = r.dependencies()
            fo.write('%s: %s\n' % (r.name, ' '.join(deps)))
        fo.close()

def concatenateRoutines(routines, output_file):
    output_fo = open(output_file, 'w')
    for r in routines:
        fo = open(r.filename, 'r')
        source = fo.read()
        fo.close()
        output_fo.write(source)
    output_fo.close()

class F2CError(Exception):
    pass

def runF2C(fortran_filename, output_dir):
    # we're assuming no funny business that needs to be quoted for the shell
    cmd = "f2c %s -d %s %s" % (F2C_ARGS, output_dir, fortran_filename)
    rc = os.system(cmd)
    if rc != 0:
        raise F2CError

def scrubF2CSource(c_file):
    fo = open(c_file, 'r')
    source = fo.read()
    fo.close()
    source = clapack_scrub.scrubSource(source, verbose=True)
    fo = open(c_file, 'w')
    fo.write(HEADER)
    fo.write(source)
    fo.close()

def main():
    if len(sys.argv) != 4:
        print('Usage: %s wrapped_routines_file lapack_dir output_dir' % \
              (sys.argv[0],))
        return
    wrapped_routines_file = sys.argv[1]
    lapack_src_dir = sys.argv[2]
    output_dir = sys.argv[3]

    wrapped_routines, ignores = getWrappedRoutineNames(wrapped_routines_file)
    library = getLapackRoutines(wrapped_routines, ignores, lapack_src_dir)

    dumpRoutineNames(library, output_dir)

    for typename in ['blas', 'dlapack', 'zlapack']:
        print('creating %s_lite.c ...'  % typename)
        routines = library.allRoutinesByType(typename)
        fortran_file = os.path.join(output_dir, typename+'_lite.f')
        c_file = fortran_file[:-2] + '.c'
        concatenateRoutines(routines, fortran_file)
        try:
            runF2C(fortran_file, output_dir)
        except F2CError:
            print('f2c failed on %s' % fortran_file)
            break
        scrubF2CSource(c_file)

if __name__ == '__main__':
    main()
