"""This module generates a fortran wrapper module to allow the interaction of
python with Fortran code via ctypes. Since Fortran allows for deferred and
assumed shape arrays, ctypes can't handle the F90 standard for code without
introducing a wrapper with known shape arrays.
"""
from fortpy.printing.formatting import present_params
from fortpy import msg
from fortpy.interop.make import makefile, get_fortpy_templates

class WrapperModule(object):
    """Represents a Fortran module coded to wrap an existing module with
    subroutines etc. that have predictable (i.e. Fortran 77 type) signatures.
    """
    def __init__(self, module, library, dirpath=None, link=None):
        """Sets up the module wrapper instance so that it can accept requests to
        build code for wrapper executables, etc.

        :arg module: an instance of fortpy.elements.Module to use for constructing
          the wrapper module.
        :arg library: the name of the library to create with ftypes.
        :arg dirpath: the full path to the directory to save the wrapper module to.
        :arg link: The path to the shared library to link with the wrapper module 
          when it is compiled.
        """
        self.module = module
        self.library = library
        self.uses = []
        """A list of executables for which valid subroutines will appear in the module."""
        from os import path
        if dirpath is not None:
            self.dirpath = path.abspath(dirpath)
        else:
            self.dirpath = path.abspath(".")
        if library is not None:
            self.dirpath = path.join(self.dirpath, library)    

        self.f90path = path.join(self.dirpath, "f90")
        """The full path to the folder to write *.f90 files to and to compile in."""            
        self.needs = []
        """A list of modules needed by the *wrapper* module to run correctly."""
        self.dependencies = self._get_dependencies()
        """A dict of modules that the original code module depends on and that need to be linked
        in to the shared library compilation.
        """
        self.link = link
        """The path to the *compiled* library to reference when linking the shared library."""

    @property
    def name(self):
        """Returns the name of the module generated by this wrapper."""
        return "{}_c".format(self.module.name)

    @property
    def libname(self):
        """Returns the name of the shared library file compiled for the wrapper
        subroutines coded in fortran.
        """
        if self.library is not None:
            return "ftypes.{}.so".format(self.library)
        else:
            return "{}.so".format(self.name)
    
    @property
    def libpath(self):
        """Returns the full path to the shared *wrapper* library created for the
        module.
        """
        from os import path
        return path.join(self.dirpath, self.libname)

    def _check_lib(self, remake, compiler, debug, profile):
        """Makes sure that the linked library with the original code exists. If it doesn't
        the library is compiled from scratch.
        """
        from os import path
        if self.link is None or not path.isfile(self.link):
            self.makelib(remake, True, compiler, debug, profile)

    @property
    def lib_exists(self):
        """Returns true if the shared library at libpath exists."""
        from os import path
        return path.isfile(self.libpath)
            
    def make(self, remake=False, dependencies=None,
             compiler="gfortran", debug=False, profile=False):
        """Generates a makefile to compile the wrapper module and link it with the shared
        library that can be compiled with self.makelib().
        """
        self._check_lib(remake, compiler, debug, profile)
        
        from os import path
        if self.link is None or not path.isfile(self.link):
            msg.err("Can't create shared library; missing compiled original code: {}".format(self.link))
            exit(0)
        
        if remake or not path.isfile(self.libpath):
            if dependencies is None:
                dependencies = ["{}_c".format(self.module.name)]
                identifier = self.name
            else:
                identifier = "ftypes.{}".format(self.library)
                #We don't need to reorder these *wrapper* modules before compilation because each of
                #them should be independent of all the others.
                #dependencies = self._process_module_needs(dependencies)

            #append the dependency for the deallocation module to be included in the shared lib.
            dependencies.append("ftypes_dealloc")
                
            makename = "Makefile.{}".format(self.library if self.library else self.name)
            makepath = path.join(self.f90path, makename)
            extralinks = list(self.dependencies.values())
            extralinks.append(self.link)
            makefile(identifier, dependencies, makepath, "ftypes.{}".format(self.name),
                     False, False, self.module.parent, "so", extralinks)
            code = self._compile(self.f90path, makename, compiler, debug, profile)
            if code == 0:
                #Copy the shared library to the main directory out of the f90 directory
                from shutil import copy
                copy(path.join(self.f90path, "{}.so".format(identifier)), self.dirpath)
            return code
        else:
            return 0

    def _compile(self, dirpath, makename, compiler, debug, profile):
        """Compiles the makefile at the specified location with 'compiler'.

        :arg dirpath: the full path to the directory where the makefile lives.
        :arg compiler: one of ['ifort', 'gfortran'].
        :arg makename: the name of the make file to compile.
        """
        from os import path
        options = ""
        if debug:
            options += " DEBUG=true"
        if profile:
            options += " GPROF=true"

        from os import system
        codestr = "cd {}; make -f '{}' F90={} FAM={}" + options
        code = system(codestr.format(dirpath, makename, compiler, compiler[0]))

        #It turns out that the compiler still returns a code of zero, even if the compile
        #failed because the actual compiler didn't fail; it did its job properly. We need to
        #check for the existence of errors in the 'compile.log' file.
        lcount = 0
        errors = []
        log = path.join(dirpath, "compile.log")
        with open(log) as f:
            for line in f:
                lcount += 1
                if lcount > 21 and lcount < 32:
                    errors.append(line)
                elif lcount > 21:
                    break

        if len(errors) > 0:
            #There are 21 lines in the compile.log file when everything runs correctly
            #Overwrite code with a bad exit value since we have some other problems.
            code = 1
            #We want to write the first couple of errors to the console and give them the
            #option to still execute if the compile only generated warnings.
            msg.warn("compile generated some errors or warnings:")
            msg.blank()
            msg.info(''.join(errors))

        return code
        
    def makelib(self, remake=False, full=True, compiler="gfortran", debug=False, profile=False):
        """Generates a makefile for the code files that reside in the same directory as the source
        that was parsed by the code parser.

        :arg full: when True, the shared library is compiled for *all* the code files in the directory
          not just the one's that are dependencies of the source module.
        """
        if self.link is None or remake:
            from os import path
            if self.library is not None:
                outpath = path.join(self.dirpath, "{}.a".format(self.library))
            else:
                outpath = path.join(self.dirpath, "{}.a".format(self.name))

            if not remake and path.isfile(outpath): #No need to recompile
                self.link = outpath
                return
                
            dependencies = self._get_lib_modules(full)
            makepath = path.join(path.dirname(self.module.filepath), "Makefile.ftypes")
            if full:
                compileid = "ftypes.all_libs"
                identifier = self.library
            else:
                compileid = "ftypes.{}_c".format(self.module.name)
                identifier = self.module.name
                
            makefile(identifier, dependencies, makepath, compileid,
                     self.module.precompile, False, self.module.parent, "a")
            code = self._compile(path.dirname(self.module.filepath), "Makefile.ftypes",
                                 compiler, debug, profile)
            if code == 0:
                self.link = path.join(path.dirname(self.module.filepath), "{}.a".format(identifier))
                self._copy_so(outpath)
                self.link = outpath
        else:
            #Just make sure it is copied over into the directory where we want to compile the wrapper
            #module for ftypes.
            self._copy_so(self.dirpath)
        
    def _copy_so(self, outpath):
        """Copies the shared library in self.so into the working directory for the wrapper
        module if it doesn't already exist.
        """
        from os import path
        if not path.isfile(outpath) and path.isfile(self.link):
            from shutil import copy
            copy(self.link, outpath)

    def _get_lib_modules(self, full):
        """Returns a list of the modules in the same folder as the one being wrapped for
        compilation as a linked library.

        :arg full: when True, all the code files in the source file's directory are considered
          as dependencies; otherwise only those explicitly needed are kept.
        """
        #The only complication with the whole process is that we need to get the list of
        #dependencies for the current module. For full lib, we compile *all* the files in
        #the directory, otherwise only those that are explicitly required.
        result = []
        if full:
            found = {}
            from os import path
            mypath = path.dirname(self.module.filepath)
            self.module.parent.scan_path(mypath, found)
            for codefile in found:
                self.module.parent.load_dependency(codefile.replace(".f90", ""), True, True, False)
                
            for modname, module in list(self.module.parent.modules.items()):
                if path.dirname(module.filepath).lower() == mypath.lower():
                    result.append(modname)
        else:
            result.extend(self.module.search_dependencies())

        return self._process_module_needs(result)
            
    def _get_dependencies(self):
        """Gets a list of the module names that should be included in the shared lib compile
        as dependencies for the current wrapper module.
        """
        #The only complication with the whole process is that we need to get the list of
        #dependencies for the current module. For full lib, we compile *all* the files in
        #the directory, otherwise only those that are explicitly required.
        from os import path
        modules = self.module.search_dependencies()
        result = {}
        for module in modules:
            self.module.parent.load_dependency(module, True, True, False)
            
        for modname, module in list(self.module.parent.modules.items()):
            pathkey = path.dirname(module.filepath).lower()
            if pathkey not in result:
                result[pathkey] = path.dirname(module.filepath)

        return result

    def _find_executables(self):
        """Finds the list of executables that pass the requirements necessary to have
        a wrapper created for them.
        """
        if len(self.needs) > 0:
            return
        
        for execname, executable in list(self.module.executables.items()):
            skip = False
            #At the moment we can't handle executables that use special derived types.
            if not execname in self.module.publics or not executable.primitive:
                msg.info("Skipping {}.{} because it is not public.".format(self.module.name, execname))
                skip = True
            #Check that all the parameters have intent specified, otherwise we won't handle them well.ha
            if any([p.direction == "" for p in executable.ordered_parameters]):
                msg.warn("Some parameters in {}.{} have no intent".format(self.module.name, execname) +
                         " specified. Can't wrap that executable.")
                skip = True
    
            if not skip:
                self.uses.append(execname)
                for depmod in executable.search_dependencies():
                    if depmod not in self.needs:
                        self.needs.append(depmod)

    def _check_dir(self):
        """Makes sure that the working directory for the wrapper modules exists.
        """
        from os import path, mkdir
        if not path.isdir(self.dirpath):
            mkdir(self.dirpath)
            #Copy the ftypes.py module shipped with fortpy to the local directory.
            ftypes = path.join(get_fortpy_templates(), "ftypes.py")
            from shutil import copy
            copy(ftypes, self.dirpath)
            #Create the __init__.py file so that the library becomes a package for
            #its module contents.
            with open(path.join(self.dirpath, "__init__.py"), 'w') as f:
                f.write("# Auto-generated for package structure by fortpy.")

        #We also need to make sure that the fortpy deallocator module is present for
        #compilation in the shared library.
        if not path.isdir(self.f90path):
            mkdir(self.f90path)
            #Copy the ftypes.py module shipped with fortpy to the local directory.
            ftypes = path.join(get_fortpy_templates(), "ftypes_dealloc.f90")
            from shutil import copy
            copy(ftypes, self.f90path)
                
    def write_py(self):
        """Writes a python module file to the current working directory for the library.
        """
        self._check_dir()
        #We make it so that the write py method can be called independently (as opposed
        #to first needing the F90 write method to be called).
        self._find_executables()
        
        lines = []
        lines.append('"""Auto-generated python module for interaction with Fortran shared library\n'
                     'via ctypes. Generated for module {}.\n'.format(self.module.name) +
                     '"""')
        lines.append("from ctypes import *")
        lines.append("from ftypes import *")
        lines.append("from numpy.ctypeslib import load_library, ndpointer")
        lines.append("from numpy import require")
        lines.append("from os import path")
        lines.append("")
        
        for execkey in self.uses:
            self._write_executable_py(execkey, lines)

        from os import path
        pypath = path.join(self.dirpath, "{}.py".format(self.module.name.lower()))
        with open(pypath, 'w') as f:
            f.write('\n'.join(lines))

    def _write_executable_py(self, execname, lines):
        """Writes the python executable definition for interacting with ctypes.

        :arg execname: the name of the executable to write.
        :arg lines: the lines of python code are appended to this list.
        """
        executable = self.module.executables[execname]
            
        #The 5 in the formatting indent comes from 1 space, 3 from the 'def'
        #and 1 from '('.
        inparams = []
        for p in executable.ordered_parameters:
            if "in" in p.direction:
                inparams.append(p.lname)
        pyparams = present_params(inparams, len(execname) + 5, linecont=", ")
        lines.append("def {}({}):".format(execname.lower(), pyparams))
        tab = "    "

        #Next, we want to append the docstring information from the XML tags
        #that were parsed by fortpy.
        docs = ['"""']
        summary = self._format_docstring(executable.summary)
        if len(summary) > 0:
            docs[0] += summary[0]
            docs.extend(summary[1::])
            docs.append("")
        else:
            docs[0] += "No XML documentation summary."
        for p in executable.ordered_parameters:
            doc = ":arg {}: {}; {}".format(p.name, p.strtype, p.summary)
            docs.extend(self._format_docstring(doc))

        lines.extend([tab + l for l in docs])
        lines.append(tab + '"""')

        pathstr = '{}libpath = path.join(path.dirname(__file__), "{}")'
        lines.append(pathstr.format(tab, self.libname))
        methparams = '"{}_c", "{}_c", libpath, True'.format(self.module.name, execname)
        lines.append("{}method = static_symbol({})".format(tab, methparams))
        argtypes = _py_ex_argtype(executable)
        spacing = len(tab) + 6 + 8 + 3
        lines.append("{}method.argtypes = [{}]".format(tab, present_params(argtypes, spacing,
                                                                           linecont=", ")))
        lines.append("")
        lparams = []
        _py_code_variables(lines, executable, lparams, tab)
        #This is the line that calls the shared library executable.
        methparams = present_params(lparams, len(tab) + 5, linecont=", ")
        lines.append("{}method({})\n".format(tab, methparams))

        #Now we clean up the output variables to turn them into numpy arrays etc.
        constructor = '{}result = FtypesResult("{}", "{}", "{}")'
        lines.append(constructor.format(tab, self.module.name, execname, type(executable).__name__))
        count = _py_code_clean(lines, tab, executable)
        
        if count > 0:
            lines.append("")
            lines.append("{}return result\n".format(tab))
        else:
            lines.append("{}return None\n".format(tab))
        
    def _format_docstring(self, text):
        """Formats the specified text for display within 90 characters. Returns a list
        of *un-joined* lines.

        :arg text: the text to format.
        """
        #The only complication we have here is that we don't want to break words up.
        words = text.split()
        result = []
        line = []
        cumline = 0
        for word in words:
            if len(word) + 1 + cumline < 90:
                line.append(word)
                cumline += len(word) + 1
            else:
                result.append(' '.join(line))
                cumline = len(word) + 1
                line = [word]

        return result
    
    def write_f90(self):
        """Writes the F90 module file to the specified directory.
        """
        from os import path
        self._check_dir()
        #Find the list of executables that we actually need to write wrappers for.
        self._find_executables()
        
        lines = []
        lines.append("!!<summary>Auto-generated Fortran module for interaction with ctypes\n"
                     "!!through python. Generated for module {}.</summary>".format(self.module.name))
        lines.append("MODULE {}_c".format(self.module.name))
        #Some of the variables and parameters will have special kinds that need to be imported.
        #Check each of the executables to find additional dependencies.
        lines.append("  use {}".format(self.module.name))
        lines.append("  use ISO_C_BINDING")
        for modname in self.needs:
            lines.append("  use {}".format(modname))        
        lines.append("  implicit none")
        lines.append("CONTAINS")
        #We want everything in these wrapper modules to be public, so we just exclude the 'private'.

        for execkey in self.uses:
            self._write_executable_f90(execkey, lines)

        lines.append("END MODULE {}_c".format(self.module.name))

        fullpath = path.join(self.f90path, "{}_c.f90".format(self.module.name))
        with open(fullpath, 'w') as f:
            f.write('\n'.join(lines))

    def _write_executable_f90(self, execname, lines):
        """Appends the Fortran code to write the wrapper executable to the specified 'lines' list.

        :arg execname: the name of the executable in the self.module.executables to write.
        """
        executable = self.module.executables[execname]
            
        #The 14 in the formatting indent comes from 4 spaces, 2 from "_c", 1 from the spacing
        #between 'subroutine' and name of the executable, 10 from subroutine, 1 from the ("
        cparams = present_params(_ctypes_ex_parameters(executable), len(execname) + 14)
        lines.append("  subroutine {}_c({}) BIND(C)".format(execname, cparams))
        #Next, we add the variables declarations, the call to the original executable and then
        #the handling of the output variables.
        lines.append("    " + "\n    ".join(_ctypes_ex_variables(executable)))
        lines.append("    " + "\n    ".join(_ctypes_ex_compatvars(executable)))

        #Add assignment/allocate statements for the intent(in*) paramaters *local* variable
        #declarations so that we can match the signature exactly.
        lines.extend(_ctypes_ex_assign(executable))
                
        if type(executable).__name__  == "Subroutine":
            prefix = "call "
        else:
            prefix = "{}_f = ".format(execname)
            
        spacing = len(list(prefix)) + len(list(execname)) + 4
        lines.append("    {}{}({})".format(prefix, execname,
                                           present_params(executable.paramorder, spacing, 90)))
        lines.append("")
        lines.extend(_ctypes_ex_clean(executable))

        lines.append("  end subroutine {}_c\n".format(execname))

    def _process_module_needs(self, modules):
        """Adds the module and its dependencies to the result list in dependency order."""
        result = list(modules)
        for i, module in enumerate(modules):
            #It is possible that the parser couldn't find it, if so
            #we can't create the executable!
            if module in self.module.parent.modules:
                modneeds = self.module.parent.modules[module].needs
                for modn in modneeds:
                    if modn not in result:
                        #Since this module depends on the other, insert the other
                        #above it in the list.
                        result.insert(i, modn)
                    else:
                        x = result.index(modn)
                        if x > i:
                            #We need to move this module higher up in the food chain
                            #because it is needed sooner.
                            result.remove(modn)
                            result.insert(i, modn)

                    newi = result.index(modn)
            else:
                raise ValueError("Unable to find module {}.".format(module))

        return result

def _py_ex_argtype(executable):
    """Returns the code to create the argtype to assign to the methods argtypes
    attribute.
    """
    result = []
    for p in executable.ordered_parameters:
        atypes = p.argtypes
        if atypes is not None:
            result.extend(p.argtypes)
        else:
            print(("No argtypes for: {}".format(p.definition())))

    if type(executable).__name__ == "Function":
        result.extend(executable.argtypes)        
            
    return result

def _py_ctype(parameter):
    """Returns the ctypes type name for the specified fortran parameter.
    """
    ctype = parameter.ctype
    if ctype is None:
        raise ValueError("Can't bind ctypes py_parameter for parameter"
                         " {}".format(parameter.definition()))
    return ctype.lower()

def _py_pytype(parameter):
    """Returns the name of the *python* type for the fortran parameter.
    """
    pytype = parameter.pytype
    if pytype is None:
        raise ValueError("Can't pre-determine type for parameter: {}".format(parameter.definition()))
    return pytype

def _py_code_clean(lines, tab, executable):
    """Appends all the code lines needed to create the result class instance and populate
    its keys with all output variables.
    """
    count = 0
    allparams = executable.ordered_parameters
    if type(executable).__name__ == "Function":
        allparams = allparams + [executable]
        
    for p in allparams:
        value = _py_clean(p, tab)
        if value is not None:
            count += 1
            lines.append(value)
            
    return count

def _py_ftype(parameter, tab):
    """Returns the code to declare an Ftype object that handles memory
    deallocation for Fortran return arrays that were allocated by the
    method called in ctypes.
    """
    splice = ', '.join(["{0}_{1:d}".format(parameter.lname, i)
                        for i in range(parameter.D)])       
    return "{2}{0}_ft = Ftype({0}_o, [{1}], libpath)".format(parameter.lname, splice, tab)

def _py_clean(parameter, tab):
    """Returns the code line to clean the output value of the specified parameter.
    """
    if "out" in parameter.direction:
        if parameter.D > 0:
            if ":" in parameter.dimension and ("allocatable" in parameter.modifiers or
                                               "pointer" in parameter.modifiers):
                if parameter.D == 1:
                    clarray = "as_array({0}_o, ({0}_0.value,))".format(parameter.lname)
                else:
                    #We have to reverse the order of the indices because of the way that
                    #fortran orders the array elements. We undo this using a transpose in
                    #the ftypes.as_array() so that the elements are ordered correctly.
                    splice = ', '.join(reversed(["{0}_{1:d}.value".format(parameter.lname, i)
                                                 for i in range(parameter.D)]))       
                    clarray = "as_array({0}_o, ({1}))".format(parameter.lname, splice)

                assign = '{0}result.add("{1}", {2}, {1}_ft)'.format(tab, parameter.lname, clarray)
                fstr = _py_ftype(parameter, tab)
                    
                #For optional out parameters, we won't necessarily have anything to report.
                return _py_check_optional(parameter, tab, [fstr, assign])
            else:
                return '{1}result.add("{0}", {0}_a)'.format(parameter.lname, tab)
        else: #This handles scalar (inout) and (out) variables.
            return '{1}result.add("{0}", {0}_c.value)'.format(parameter.lname, tab)

def _py_check_optional(parameter, tab, assign):
    """Returns the code to check for actual values when an out parameter is pure 
    out/optional and doesn't have anything changed.

    :arg assign: the actual variable assignment value if the conditions pass.
    """
    if parameter.direction == "(out)":
        result = []
        checks = ["{0}_{1:d}.value==0".format(parameter.lname, i) for i in range(parameter.D)]
        spacing = len(tab) + 2 + 3 + 3 + 2
        result.append("{}if not all([{}]):".format(tab, present_params(checks, spacing, linecont=", ")))
        for line in assign:
            if assign is not None:
                result.append("{0}{1}".format(tab, line))
        result.append("{}else:".format(tab))
        result.append('{0}{0}result.add("{1}", None)'.format(tab, parameter.lname))
        return '\n'.join(result)
    else:
        return '\n'.join(assign)
        
def _py_code_variables(lines, executable, lparams, tab):
    """Adds the variable code lines for all the parameters in the executable.

    :arg lparams: a list of the local variable declarations made so far that need to be passed
      to the executable when it is called.
    """
    allparams = executable.ordered_parameters
    if type(executable).__name__ == "Function":
        allparams = allparams + [executable]

    for p in allparams:
        _py_code_parameter(lines, p, "invar", lparams, tab)
        if p.direction == "(out)":
            #We need to reverse the order of the indices to match the fortran code
            #generation of the wrapper.
            _py_code_parameter(lines, p, "outvar", lparams, tab)
            _py_code_parameter(lines, p, "indices", lparams, tab)
        else:
            _py_code_parameter(lines, p, "indices", lparams, tab)
            _py_code_parameter(lines, p, "outvar", lparams, tab)

def _py_code_parameter(lines, parameter, position, lparams, tab):
    """Appends the code to produce the parameter at the specified position in the executable.

    :arg position: one of ['invar', 'outvar', 'indices'].
    :arg lparams: a list of the local variable declarations made so far that need to be passed
      to the executable when it is called.
    """
    mdict = {
        "invar": _py_invar,
        "outvar": _py_outvar,
        "indices": _py_indices
    }
    line = mdict[position](parameter, lparams, tab)
    if line is not None:
        value, blank = line
        lines.append(tab + value)
        if blank:
            lines.append("")

def _py_invar(parameter, lparams, tab):
    """Returns the code to create the local input parameter that is coerced to have the
    correct type for ctypes interaction.
    """
    if ("in" in parameter.direction and parameter.D > 0):
        if parameter.direction == "(inout)" and ":" not in parameter.dimension:
            wstr = ", writeable"
        else:
            wstr = ""
        pytype = _py_pytype(parameter)
        lparams.append("{}_a".format(parameter.lname))
        return ('{0}_a = require({0}, {1}, "F{2}")'.format(parameter.lname, pytype, wstr), False)
    elif parameter.D == 0:
        #Even for scalar outvars, we need to initialize a variable to pass by reference.
        lparams.append("byref({}_c)".format(parameter.lname))
        if parameter.direction == "(out)":
            initval = parameter.py_initval
            return ("{0}_c = {1}({2})".format(parameter.lname, _py_ctype(parameter), initval), False)
        else:
            return ("{0}_c = {1}({0})".format(parameter.lname, _py_ctype(parameter)), False)

def _py_outvar(parameter, lparams, tab):
    """Returns the code to produce a ctypes output variable for interacting with fortran.
    """
    if ("out" in parameter.direction and parameter.D > 0 and ":" in parameter.dimension and
        ("allocatable" in parameter.modifiers or "pointer" in parameter.modifiers)):
        lparams.append("byref({}_o)".format(parameter.lname))
        blank = True if parameter.direction == "(inout)" else False
        return ("{0}_o = POINTER({1})()".format(parameter.lname, _py_ctype(parameter)), blank)

def _py_indices(parameter, lparams, tab):
    """Returns the code to produce py ctypes for the indices of the specified parameter
    that has D > 0.
    """
    if parameter.D > 0 and ":" in parameter.dimension:
        if "in" in parameter.direction:
            indexstr = "{0}_{1:d} = c_int({0}_a.shape[{1:d}])"
        else:
            indexstr = "{0}_{1:d} = c_int(0)"
        lparams.extend(["byref({}_{})".format(parameter.lname, i) for i in range(parameter.D)])
        joinstr = "\n" + tab
        blank = True if parameter.direction == "(out)" else False
        return (joinstr.join([indexstr.format(parameter.lname, i)
                              for i in range(parameter.D)]), blank)

def _ctypes_code_parameter(lines, parameter, position):
    """Returns the code for the specified parameter being written into a subroutine wrapper.

    :arg position: one of ['indices', 'variable', 'out', 'regular', 'saved', 'assign', 'clean']
    """
    mdict = {
        'indices': _ctypes_indices,
        'variable': _ctypes_variables,
        'out': _ctypes_out,
        'regular': _ctypes_regular,
        'saved': _ctypes_saved,
        'assign': _ctypes_assign,
        'clean': _ctypes_clean
    }
    line = mdict[position](parameter)
    if line is not None:
        value, blank = line
        lines.append(value)
        if blank:
            lines.append("")

def _ctypes_dtype(parameter):
    """Determines the ISO_C data type to use for describing the specified parameter.
    """
    ctype = parameter.ctype
    if ctype is None:
        if parameter.kind is not None:
            return "{}({})".format(parameter.dtype, parameter.kind)
        else:
            return parameter.dtype
    else:
        return"{}({})".format(parameter.dtype, ctype)

def _ctypes_splice(parameter):
    """Returns a list of variable names that define the size of each dimension.
    """
    params = parameter.ctypes_parameter()
    if parameter.direction == "(inout)" and ("allocatable" in parameter.modifiers or
                                             "pointer" in parameter.modifiers):
        return ', '.join(params[1:-1])
    else:
        return ', '.join(params[1::])
     
def _ctypes_indices(parameter):
    """Returns code for parameter variable declarations specifying the size of each
    dimension in the specified parameter.
    """
    if (parameter.dimension is not None and ":" in parameter.dimension):
        splice = _ctypes_splice(parameter)
        if "out" in parameter.direction:
            #Even for pure out variables, the ctypes pointer is passed in and will
            #have a value already.
            return ("integer, intent(inout) :: {}".format(splice), False)
        else:
            return ("integer, intent(in) :: {}".format(splice), False)

def _ctypes_variables(parameter):
    """Returns the local parameter definition for implementing a Fortran wrapper subroutine
    for this parameter's parent executable.
    """
    if parameter.dimension is not None and ":" in parameter.dimension:
        #For arrays that provide input (including 'inout'), we pass the output separately
        #through a c-pointer. They will always be input arrays. For pure (out) parameters
        #we use *only* a c-ptr, but it needs intent (inout) since it is created by ctypes.
        if "in" in parameter.direction or parameter.direction == "":
            #We have to get the ctypes-compatible data type for the parameters.
            #stype = _ctypes_dtype(parameter)
            splice = _ctypes_splice(parameter)
            name = parameter.ctypes_parameter()[0]
            if parameter.direction == "(inout)" and not ("allocatable" in parameter.modifiers or
                                                         "pointer" in parameter.modifiers):
                return (parameter.definition(optionals=False, customdim=splice), False)
            else:
                return ("{}, intent(in) :: {}({})".format(parameter.strtype, name, splice), False)
    else:
        if parameter.dtype == "logical":
            stype = _ctypes_dtype(parameter)
            suffix = "_c"
            modifiers = None
        elif hasattr(parameter, "parameters"):
            stype = None
            suffix = "_f"
            modifiers = ["intent(out)"]
        else:
            stype = None
            suffix = ""
            modifiers = None
            
        return (parameter.definition(ctype=stype, optionals=False, suffix=suffix,
                                     modifiers=modifiers), False)

def _ctypes_out(parameter):
    """Returns a parameter variable declaration for an output variable for the specified
    parameter.
    """
    if (parameter.dimension is not None and ":" in parameter.dimension
        and "out" in parameter.direction and ("allocatable" in parameter.modifiers or
                                              "pointer" in parameter.modifiers)):
        if parameter.direction == "(inout)":
            return ("type(C_PTR), intent(inout) :: {}_o".format(parameter.name), True)
        else: #self.direction == "(out)" since that is the only other option.
            return ("type(C_PTR), intent(inout) :: {}_c".format(parameter.name), True)

def _ctypes_regular(parameter):
    """Returns the code lines to define a *local* variable with the fortran types
    that has a matching signature to the wrapped executable.
    """
    if ("pointer" in parameter.modifiers or "allocatable" in parameter.modifiers
        or "target" in parameter.modifiers or parameter.dtype == "logical"):
        return (parameter.definition(local=True), False)
        
def _ctypes_saved(parameter):
    """Returns local variable declarations that create allocatable variables that can persist
    long enough to return their value through ctypes.
    """
    if ("out" in parameter.direction and parameter.dimension is not None and
        ":" in parameter.dimension and ("allocatable" in parameter.modifiers or
                                        "pointer" in parameter.modifiers)):
        stype = _ctypes_dtype(parameter)
        name = "{}_t({})".format(parameter.name, parameter.dimension)
        return ("{}, allocatable, target, save :: {}".format(stype, name), True)

def _ctypes_assign(parameter):
    """Returns the Fortran code lines to allocate and assign values to the *original*
    parameter ValueElement that exists now only as a local variable so that the
    signatures match for the compiler.
    """
    #If the array is set to only have intent(out), we don't want to allocate or assign
    #a value to it; otherwise we do.
    if (parameter.direction != "(out)" and ("allocate" in parameter.modifiers
                                            or "pointer" in parameter.modifiers
                                            or "target" in parameter.modifiers)):
        if "in" in parameter.direction:
            splice = _ctypes_splice(parameter)
            result = []
            result.append("    allocate({}({}))".format(parameter.name, splice))
            result.append("    {0} = {0}_c".format(parameter.name))
            return ('\n'.join(result), True)
    elif parameter.dtype == "logical" and "in" in parameter.direction:
        return ("    {0} = {0}_c".format(parameter.name), False)
       
def _ctypes_clean(parameter):
    """Returns the lines of code required to assign the correct values to the integers
    representing the indices of the output arrays in the ctypes wrapper subroutine.
    """
    if ("out" in parameter.direction and parameter.dimension is not None
        and ":" in parameter.dimension and ("pointer" in parameter.modifiers or
                                            "allocatable" in parameter.modifiers)):
        params = parameter.ctypes_parameter()
        result = []
        spacer = "    "
        #We get segfaults if we try to assign c-pointers to unallocated pointers or arrays.
        if "optional" in parameter.modifiers:
            if "pointer" in parameter.modifiers:
                result.append("{}if (associated({})) then".format(spacer, parameter.name))
                spacer += "  "
            elif "allocatable" in parameter.modifiers:
                result.append("{}if (allocated({})) then".format(spacer, parameter.name))
                spacer += "  "

        #We have to copy the values or at least update the pointer to access the
        #variables with 'target' and 'save' attributes.
        result.append("{0}{1}_t = {1}".format(spacer, parameter.name))
            
        if parameter.direction == "(inout)":
            result.append("{1}{0}_o = C_LOC({0}_t)".format(parameter.name, spacer))
            #Update the index variables for the dimensions of the output variable.
            for i, p in enumerate(params[1:-1]):
                result.append("{}{} = size({}, {})".format(spacer, p, parameter.name, i+1))
        else:
            #This is the case for intent(out)
            result.append("{1}{0}_c = C_LOC({0}_t)".format(parameter.name, spacer))
            for i, p in enumerate(params[1::]):
                result.append("{}{} = size({}, {})".format(spacer, p, parameter.name, i+1))

        if ("optional" in parameter.modifiers and
            ("pointer" in parameter.modifiers or "allocatable" in parameter.modifiers)):
            result.append("    end if")

        return ('\n'.join(result), True)
    elif parameter.dtype == "logical" and "out" in parameter.direction:
        return ("    {0}_c = {0}".format(parameter.name), False)

def _ctypes_ex_parameters(executable):
    """Returns a list of the parameters to form the wrapper executable to interact
    with ctypes.
    """
    result = []
    for p in executable.ordered_parameters:
        result.extend(p.ctypes_parameter())

    if type(executable).__name__ == "Function":
        result.extend(executable.ctypes_parameter()) #This method inherits from ValueElement.
        
    return result

def _ctypes_ex_variables(executable):
    """Returns a list of the local variable definitions required to construct the
    ctypes interop wrapper.
    """
    result = []
    for p in executable.ordered_parameters:
        _ctypes_code_parameter(result, p, "indices")
        _ctypes_code_parameter(result, p, "variable")
        _ctypes_code_parameter(result, p, "out")

    if type(executable).__name__ == "Function":
        #For functions, we still create a subroutine-type interface and then just add an extra
        #output-type parameter for the function's return type.
        _ctypes_code_parameter(result, executable, "indices")
        _ctypes_code_parameter(result, executable, "variable")
        _ctypes_code_parameter(result, executable, "out")
        
    return result

def _ctypes_ex_compatvars(executable):
    """Returns a list of code lines for signature matching variables, and 
    pointer saving variables.
    """
    result = []
    for p in executable.ordered_parameters:
        _ctypes_code_parameter(result, p, "regular")
        _ctypes_code_parameter(result, p, "saved")

    if type(executable).__name__ == "Function":
        _ctypes_code_parameter(result, executable, "regular")
        _ctypes_code_parameter(result, executable, "saved")

    return result    

def _ctypes_ex_assign(executable):
    """Return a list of code lines to allocate and assign the local parameter definitions
    that match those in the signature of the wrapped executable.
    """
    result = []
    for p in executable.ordered_parameters:
        _ctypes_code_parameter(result, p, "assign")

    if type(executable).__name__ == "Function":
        _ctypes_code_parameter(result, executable, "assign")

    return result

def _ctypes_ex_clean(executable):
    """Return a list of code lines to take care of assigning pointers for output variables
    interacting with ctypes.
    """
    result = []
    for p in executable.ordered_parameters:
        _ctypes_code_parameter(result, p, "clean")
        
    if type(executable).__name__ == "Function":
        _ctypes_code_parameter(result, executable, "clean")
        
    return result
