#!/usr/bin/env python

import os, sys, getopt
import libmkdep, globals



def usage():
    print("""
Fortran dependency generator for GNU Makefiles.

Usage:

   mkdep [optional arguments see below] <filelist>

  reset          deletes temporary files created by mkdep
  -a <file name> assemble all source files (not include) into the file <file name>
  -b turn off the default behaviour, that all objects end up in the build directory.
     this is necessary if your project has files with identical name, but in different
     directories.
  -c Insert defaults for all missing optional columns in project file
  -d Turn on debugging output
  -e "iso8859-1"  switch from default utf-8 to single byte iso8859 latin 1.
  -g Insert missing default group names into the project file
  -h show this message
  -i <file>
     File is a text file with a list of directories to search for include files.
     The latter directories take precedence over the first.
     if not specified mkdep/cpp will search all directories in the project files.
  -L Generate a Makefile with support for mklib (use for large projects)
  -m write a list of modules and their source file to .mkdep_modmap
  -p <path>
     specify a path to be prepended to the links to the source files in the html tree file.
  -r <dir>
     Specify a different root dir. This path will be prepended to all filenames in <filelist>
  -s View the stripped statements just as the internal matching routines see them
  -t <title>
     Generate call tree. Off by default as this takes extra time. title will be used on the
     autogenerated html page and its file name.
  -w show warnings
  --graph print a graph at dot format to the specified file

 --fc <compiler>   specify the name of the Fortran compiler to be used in the Makefile
 --cc <compiler>   specify the name of the C compiler to be used in the Makefile
 --cpp <cpp path>  specify the full path to the c preprocessor to be used.
 --cppflags <flags>     specify the flags for cpp, e.g. all defines that are required,
                   or override include paths.
 --treetop NAME    if there is no main program, it is possible to choose a root for
                   the tree (NAME), otherwise full trees will be made for each routine.


<filelist> is a text file where lines contain the full path to a
source file in the first column. Blank lines, or lines with # at the
beginning are ignored. Columns 2-4 are optional, and may be used to
override default settings;

Column 2:
   a user defined string/label used to separate groups of object files
   that e.g. need to be compiled with special flags. default value 'opt'.

Column 3, override file type:
   free      free form fortran
   fixed     std fixed form fortran
   fixed132  fixed form fortran with extended lines
   C         C
Column 4, override file type:
   source    the file should be compiled
   include   the file is an included file

Helge Avlesen <avle@bccs.uib.no>  para//ab
""")
    sys.exit(1)



## Initialization...

use_mklib=0         # generate calls to mklib in the makefile
viewsrc=0           # print stripped src to screen; 1=on, 0=off
incfile=''          # the list of include directories
lines = 0           # count parsed lines
f=0                 # file counter
warning=0           # if duplicate modules are found, warning=1
warningmap={}       # file list of other defs.
modnamesize=14
rootdir=''          # prepend this path to all file names in the project file
f95='f90'
cc='cc'
cppcommand=""
cppflags="-P -traditional"   # the default flags to use for cpp (assume GNU cpp)

# 50 erase characters... if anyone reading this knows a prettier way do not hesitate...

erase_50 = "\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\
\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\
\x08\x08\x08\x08\x08\x08\x08\x08\x08"

# path to mkdep (including separator - implement for windows later)
i_separator = sys.argv[0].rfind("/")
MKDEPPATH = sys.argv[0][:i_separator+1]

## cleanup

if len(sys.argv)>1:
    if sys.argv[1]=="reset":
        f=open(".mkdep_objects","w")
        f.write(" ")
        f.close()
        f=open(".mkdep_dependencies","w")
        f.write(" ")
        f.close()
        f=open(".mkdep_rules","w")
        f.write(" ")
        f.close()
        try:
            os.remove(".files")
        except OSError:
            pass
        try:
            os.remove(".mkdep_includes")
        except OSError:
            pass
        try:
            os.remove(".mkdep_restart_file")
        except OSError:
            pass
        if os.access('build',os.F_OK)==0:
            os.mkdir('build')

        print("reset/initialize mkdep background data. you should now run 'make dep' or 'mkdep <project file>'")
        sys.exit()


force_reparse=0

## this is a good place to 
## override default values with environment variables. PS Makefile variables does not count.
# tmpstring = os.environ.get('CPP_FLAGS')

    
## override the defaults with command line arguments

try:
    opts, args = getopt.getopt(sys.argv[1:], "a:bcde:ghi:Lmp:r:st:w", \
                                   ['graph=','fc=','cc=','help','cpp=','cppflags=','treetop='])
    if opts!=[]:
        for opt, value in opts:
            if opt in ("-h","--help"):
                usage()
                sys.exit(1)
            if opt == "-i":
                incfile = value
            if opt == "-a":
                libmkdep.assemblefile = value
            if opt == "-e":
                globals.encoding = value               
            if opt == "-b":
                globals.use_build_dir=0
            if opt == "-r":
                rootdir = value
            if opt == "-L":
                use_mklib=1
            if opt == "-d":
                libmkdep.debug=1
            if opt == "-p":
                libmkdep.root_path=value
            if opt == "-w":
                libmkdep.show_warnings=1
            if opt == "-t":
                libmkdep.calltree_title=value
                if libmkdep.calltree_title in ['0','False','None','none','no','NONE']: globals.storetree=0
                globals.calltree=1
            if opt == "--graph":
                libmkdep.callgraph_title=value
                globals.callgraph=1
            if opt == "-g":
                libmkdep.insert_group=1
            if opt == "-c":
                libmkdep.full_classify=1
            if opt == "-m":
                libmkdep.dump_modmap=1
            if opt == "-s":
                viewsrc=1
            if opt == "-F":
                force_reparse=1
            if opt == "--fc":
                f95=value
            if opt == "--cc":
                cc=value
            if opt == "--cppflags":
                cppflags += " " + value
            if opt == "--cpp":
                cppcommand = value
            if opt == "--treetop":
                libmkdep.treetop = value

except getopt.GetoptError:
    print("\nhm. mkdep is very confused by this argument list:")
    print(sys.argv[1:])
    print("please try something else. also see 'mkdep -h'\n")
    sys.exit(2)

if len(args)<1 or args[0]=="":
    print("\nno project file given?")
    usage()
    sys.exit(2)

if globals.callgraph and not globals.calltree:
    globals.calltree=1
    globals.storetree=0
project_file=args[0]


### Parse the project file and reload the current dependency map from .mkdep_restart_file


# should "." be a default search dir for include files? some may not always want that?
# should all directories found in the project file be used as search path for includes?

# we will not auto classify files as source/include because the
# parsing depends on the classification. files should have a good
# suffix, or given class in the project file.

# includepath is initialized to [] in classify_files, add . as default

includepath = libmkdep.classify_files(project_file, incfile, rootdir)
includepath.append(".")



if cppcommand.strip() != "type":
    cppflags += " " + globals.incpathstring


### First pass. look up definitions of e.g. modules and subprograms.

print("\nScanning source file:                                                   ", end=' ')

cppwarn=0


for (filename,(type, is_include, compflag, mtime, deps, reparse, deleted)) in list(globals.clean_source_files2.items()):
    if not reparse and not force_reparse:
        continue

    f+=1
    print(erase_50 + filename.ljust(49), end=' ')

    out = libmkdep.getsrc(cppcommand, cppflags, filename)
    srclines = out.split('\n')

    if not is_include:
        registred=0
        for key in list(globals.object_files.keys()):
            if filename in globals.object_files[key]:
                registred=1
                if key != compflag:
                    #file excists, but has new group
                    i = globals.object_files[key].index(filename)
                    del globals.object_files[key][i]
                    if compflag in globals.object_files:
                        if not filename in globals.object_files[compflag]:
                            globals.object_files[compflag].append(filename)
                    else:
                        globals.object_files[compflag]=[filename]
        if not registred:
            if compflag in globals.object_files:
                globals.object_files[compflag].append(filename)
            else:
                globals.object_files[compflag]=[filename]

    dotpos = filename.rfind('.')
    suffix = filename[dotpos+1:]
    if suffix not in globals.suffixes:
        globals.suffixes.append(suffix)

    long_line = ""      # the line that will be sent to the splitter
    stored_line = ""    # the statement following long_line
    cline = 0           # current source line number
    file_tabs=[]        # a list of source lines on which tabs were found.
    cont_cpp=[]         # a list of line numbers with possible cpp crossing
    last_line=0         # if true, fline is the last line of a file.

    current_host=""

    for fline in srclines:
        cline += 1
        if libmkdep.debug: print("\n")

        ## the scanner. feed each line into the joiner's

        if type=="free":
            long_line, get_next = libmkdep.join_and_strip_free(fline, long_line)
        elif type in ["fixed","fixed132"]:
            last_line = cline==len(srclines)
            long_line, get_next, stored_line, warning_flag = libmkdep.join_and_strip_fixed(\
                fline, long_line, stored_line, last_line, type=type)
        else:
            long_line, get_next = fline.strip(), 0

        if get_next:
            continue

        if type!="C":
            long_line = libmkdep.strip_strings(long_line)

        ## the splitter. split each line at ;
        ## this gives us one statement per line

        for line in long_line.split(";"):
            line = line.strip()
            if line == "":
                continue
            lines += 1

            line = line.upper()

            # we are now left with very naked fortran

            fortran_names = libmkdep.match_fortran_name.findall(line)

            if viewsrc: print("pass1 %5.5d"%cline + " " + line)

            ## first look for module definitions
            name = libmkdep.match_module(fortran_names)

            if name!="":
                in_module=1
                if name in globals.module_map:
                    if filename != globals.module_map[name][0]:
                    ## this module is already defined elsewhere, give a warning
                        print("while scanning ",filename,":")
                        print("found duplicate module name:",name, ", already in file", globals.module_map[name][0], end=' ')
                        warning=1
                        if name in warningmap:
                            warningmap[name].append(('\n            '+filename.strip()))
                        else:
                            warningmap[name]=[(name + '\nfirst definition: ' + globals.module_map[name][0] +\
                                               '\nother definitions:\n            '), filename.strip()]
                #else:
                    ## use first module found
                    #libmkdep.module_map[name] = (filename, [], [])

                ## use last module found
                globals.module_map[name] = (filename, [], [])


            name = libmkdep.match_program_def(fortran_names)
            if name!="":
                if globals.calltree or globals.callgraph: in_program, current_program = libmkdep.add2dict(globals.routines, name, (filename, [], []))
                in_program, current_program = libmkdep.add2dict(globals.programs, name, (filename, [], []))
                libmkdep.register_main_program(filename)


            if globals.calltree or globals.callgraph:

                ## in this pass the main task is to find the routine
                ## names, and the mapping between routine name and the
                ## file name. we should also figure out which
                ## namespace a routine name belongs to. we have the
                ## global namespace, and namespaces for modules and
                ## routines.
                ## initially we only do f77.

                #contained = match_contained(line)
                #if contained:
                #    if in_routine:
                #        current_host=current_routine
                #    if in_module:
                #        current_host=current_module

                name = libmkdep.match_subroutine_def(fortran_names)

                # if in a module or subprog, add name to local namespace, otherwise to the global.

                if name!="":
                    in_routine, current_routine = libmkdep.add2dict(globals.routines, name, (filename, [], []))

                name = libmkdep.match_function_def(fortran_names)
                if name!="":
                    in_routine, current_routine = libmkdep.add2dict(globals.routines, name, (filename, [], []))
                    in_routine, current_routine = libmkdep.add2dict(globals.functions, name, (filename, [], []))

                name = libmkdep.match_module_end(fortran_names)
                if name != "":
                    current_host=""
                    contained=0

                name = libmkdep.match_end_subprogram(fortran_names)
                if name != "":
                    in_routine=0

                if libmkdep.match_end(fortran_names):
                    ## yeah. it is possible to have a program with no program statement.
                    ## but a single end reveals it... unless we are a subroutine or function
                    #if not in_routine:
                    #    libmkdep.register_main_program(filename)
                    current_program=""
                    in_routine=0
                    contained=0



        ## line done, reset
        long_line=""



print("\n\n",lines,"statements,\n",len(globals.module_map),"modules (see .mkdep_modmap if you use -m option)")

if globals.calltree:
    print(len(globals.routines)-len(globals.functions)-len(globals.programs),"subroutines (see .mkdep_routines)")
    print(len(globals.functions),"functions")
    print(len(globals.programs),"programs")

    rfile=open('.mkdep_routines','w')
    names=list(globals.routines.keys())
    names.sort()
    for name in names:
        rfile.write( name.ljust(20) + globals.routines[name][0] + "\n" )
    rfile.close()
    if globals.storetree:
        print("\nHTML call tree stored in "+libmkdep.calltree_title+".html")
        print("see .mkdep_trees for the ascii call tree(s)")
        print("")
if globals.callgraph:
    print(len(globals.routines)-len(globals.functions)-len(globals.programs),"subroutines (see .mkdep_routines)")
    print(len(globals.functions),"functions")
    print(len(globals.programs),"programs")
    rfile=open('.mkdep_routines','w')
    names=list(globals.routines.keys())
    names.sort()
    for name in names:
        rfile.write( name.ljust(20) + globals.routines[name][0] + "\n" )
    rfile.close()
    print("\ncall graph stored in "+libmkdep.callgraph_title+".dot")
    print("see .mkdep_graph for the ascii call tree(s)")
    print("")

if 'MAIN' in globals.object_files:
    if len(globals.object_files['MAIN']) > 1:
        print("Looks like there are several files with main programs;")
        for file in globals.object_files['MAIN']:
            print("  ",file)
        print("you may want to remove all but one from the project file\n")

if warning:
    wmfile = open('.mkdep_warnings','w')
    wmfile.write(("A list of modules with multiple definitions. You should comment out/remove\n"+\
    "source files that contain the duplicate definition, from the list of source\n"+\
    "files given in '"+sys.argv[1]+"' \n\n"))
    for key in warningmap:
        wmfile.write(( "".join(warningmap[key]) + "\n" ))
    wmfile.close()

    print("\nWarning:",len(warningmap),"modules were defined in several files, see the file")
    print("         .mkdep_warnings for a list.")


print("""
We now scan files again for INCLUDE and USE statements. Lists of
object files and their dependencies for direct inclusion in a GNU
Makefile are stored in .mkdep_objects and .mkdep_dependencies
respectively.
""")

print("scanning...")
print("     ", end=' ')

lines=0
f=0

for (filename,(type, is_include, compflag, mtime, deps, reparse, deleted)) in list(globals.clean_source_files2.items()):
    if not reparse and not force_reparse:
        continue
    f+=1

    print(erase_50 + filename.ljust(49), end=' ')


    out = libmkdep.getsrc(cppcommand, cppflags, filename)
    srclines = out.split('\n')

    deps=[]             # a list of files that the current file depend on
    long_line = ""      # the line that will be sent to the splitter
    stored_line = ""    # the statement following long_line
    cline = 0           # current source line number
    file_tabs=[]        # a list of source lines on which tabs were found.
    cont_cpp=[]         # a list of line numbers with possible cpp crossing
    last_line=0         # if true, fline is the last line of a file.

    for fline in srclines:
        cline += 1

        ## the scanner. feed each line into the joiner's

        if type=="free":
            long_line, get_next = libmkdep.join_and_strip_free(fline, long_line)

        elif type in ["fixed","fixed132"]:
            last_line = cline==len(srclines)

            long_line, get_next, stored_line, warning_flag = libmkdep.join_and_strip_fixed(\
                fline, long_line, stored_line, last_line, type=type)

            if warning_flag & 2**0: file_tabs.append(cline)
            if warning_flag & 2**1: cont_cpp.append(cline)
        else:
            long_line, get_next = fline.strip(), 0

        if get_next:
            continue


        ## lets try to find include files before we strip off strings...
        
        tmp_line = long_line
        tmp_line = tmp_line.strip()
        name = libmkdep.match_include(tmp_line, includepath)
        if name!="":
            try:
                deps.index(name)
            except ValueError:
                deps.append(name)

        ### now replace strings with A_STRING to make parsing easier
                
        if type!="C":
            long_line = libmkdep.strip_strings(long_line)
            
        ## the splitter. split each line at ;
        ## this gives us one statement per line

        for line in long_line.split(";"):
            line = line.strip()

            if line == "":
                continue
            lines+=1

            #line = libmkdep.strip_strings(line)
            line = line.upper()

            fortran_names = libmkdep.match_fortran_name.findall(line)

            if viewsrc: print("pass2 %5.5d"%cline + " " + line)

            ## now we can feed the stripped statement lines into the
            ## matchers if we would like to parse other fortran
            ## constructs this is a good place.

            #name = libmkdep.match_include(line, includepath)

            #if name<>"":
            #    try:
            #        deps.index(name)
            #    except ValueError:
            #        deps.append(name)

            name, liste = libmkdep.match_use(line, globals.module_map, filename)

            # add liste to current local namespace

            if name not in ["",filename]:
                dotpos = name.rfind(".")
                objf = name[:dotpos] + ".o"
                try:
                    deps.index(objf)
                except ValueError:
                    deps.append(objf)


            if globals.calltree:
                declaration_line = 0
                name = libmkdep.match_subroutine_def(fortran_names)
                if name!="":
                    in_routine=1
                    current_routine=name
                    declaration_line =1
                name = libmkdep.match_function_def(fortran_names)

                if name!="":
                    in_routine=1
                    current_routine=name
                    declaration_line =1
                name = libmkdep.match_program_def(fortran_names)
                if name!="":
                    in_routine=1
                    current_routine=name
                    declaration_line =1

                if libmkdep.match_end_subprogram(fortran_names):
                    current_routine=""
                    in_routine=0
                    contained=0
                    declaration_line =1

                # there is only one way to end a module def:
                #in_module = libmkdep.match_module_end(line)
                #if not in_module:
                #    current_host=""
                #    contained=0

                name = libmkdep.match_subroutine_call(fortran_names)
                if name!="":
                    if current_routine=="":
                        print("\nhm. most likely not in a routine, but found calls to subroutine ",name)
                        print("will not add call to tree. file=",filename,"line=")
                        print(line)
                    else:
                        libmkdep.add2tree(globals.routines, current_routine, [name])

                # look for function calls on all lines except beginning or end of definition
                if not declaration_line:
                    functions_called = libmkdep.match_function_call(line, fortran_names, globals.functions)
                    if len(functions_called) > 0:
                        if current_routine=="":
                            print("\nhm. not in a routine, but it looks like some function(s) are called:\n", functions_called)
                            print("call not added to the tree. file=",filename,"line=")
                            print(line)
                        else:
                            libmkdep.add2tree(globals.routines, current_routine, functions_called)



        ## line done, reset
        long_line=""

    if libmkdep.show_warnings:
        if len(file_tabs) > 0:
            print("\nWarning "+filename+" has TAB character on the line(s) " + \
                  "".join(["%d,"%s for s in file_tabs],""))
        if len(cont_cpp) > 0:
            print("\nWarning "+filename+" , line(s) " + \
                  "".join(["%d,"%s for s in cont_cpp],"") + \
                  "\npossibly continuation over cpp directive")

    do_reparse=0
    deleted=1
    globals.clean_source_files2[filename] = (type, is_include, compflag, mtime, deps, do_reparse, deleted)


if libmkdep.debug:
    print("\nDone pass2,",lines,"statements.")


libmkdep.find_uncalled()

libmkdep.save_maps()

if libmkdep.dump_modmap: libmkdep.write_modmap()

libmkdep.write_dependencies()
libmkdep.write_object_lists(globals.object_files)
libmkdep.write_makefile(globals.suffixes, use_mklib, f95, cc)

if globals.calltree and globals.storetree:
    libmkdep.write_call_trees()
    libmkdep.install_files()

if globals.callgraph:
    libmkdep.write_call_graph()
if cppwarn:
    print("\nInfo: there was a problem with cpp, so we reverted to not using cpp")
    print("if this is a problem, check the path to cpp and rerun")
