import os
import re
import warnings
import copy
import shutil
import subprocess
import numpy as np
import sysconfig
from collections import OrderedDict
from yggdrasil import platform, tools
from yggdrasil.drivers.CompiledModelDriver import (
    CompiledModelDriver, CompilerBase, LinkerBase, ArchiverBase)
from yggdrasil.metaschema.properties.ScalarMetaschemaProperties import (
    _valid_types)
from yggdrasil.languages import get_language_dir
from yggdrasil.config import ygg_cfg
from numpy import distutils as numpy_distutils


_default_internal_libtype = 'object'
# if platform._is_win:  # pragma: windows
#     _default_internal_libtype = 'static'


def get_OSX_SYSROOT():
    r"""Determin the path to the OSX SDK.

    Returns:
        str: Full path to the SDK directory if one is located. None
            otherwise.

    """
    fname = None
    if platform._is_mac:
        try:
            xcode_dir = subprocess.check_output(
                'echo "$(xcode-select -p)"', shell=True).decode("utf-8").strip()
        except BaseException:  # pragma: debug
            xcode_dir = None
        fname_try = []
        cfg_sdkroot = ygg_cfg.get('c', 'macos_sdkroot', None)
        if cfg_sdkroot:
            fname_try.append(cfg_sdkroot)
        if xcode_dir is not None:
            fname_base = os.path.join(xcode_dir, 'Platforms',
                                      'MacOSX.platform', 'Developer',
                                      'SDKs', 'MacOSX%s.sdk')
            fname_try += [
                fname_base % os.environ.get('MACOSX_DEPLOYMENT_TARGET', ''),
                fname_base % '',
                os.path.join(xcode_dir, 'SDKs', 'MacOSX.sdk')]
        if os.environ.get('SDKROOT', False):
            fname_try.insert(0, os.environ['SDKROOT'])
        for fcheck in fname_try:
            if os.path.isdir(fcheck):
                fname = fcheck
                break
    return fname


_osx_sysroot = get_OSX_SYSROOT()


class CCompilerBase(CompilerBase):
    r"""Base class for C compilers."""
    languages = ['c']
    default_executable_env = 'CC'
    default_flags_env = 'CFLAGS'
    default_flags = ['-g', '-Wall']
    # GCC & CLANG have similar call patterns
    linker_attributes = {'default_flags_env': 'LDFLAGS',
                         'search_path_envvar': ['LIBRARY_PATH', 'LD_LIBRARY_PATH']}
    search_path_envvar = ['C_INCLUDE_PATH']
    search_path_flags = ['-E', '-v', '-xc', '/dev/null']
    search_regex_begin = '#include "..." search starts here:'
    search_regex_end = 'End of search list.'
    search_regex = [r'(?:#include <...> search starts here:)|'
                    r'(?: ([^\n]+?)(?: \(framework directory\))?)\n']

    @staticmethod
    def before_registration(cls):
        r"""Operations that should be performed to modify class attributes prior
        to registration including things like platform dependent properties and
        checking environment variables for default settings.
        """
        if platform._is_mac:
            cls.linker_attributes = dict(cls.linker_attributes,
                                         search_path_flags=['-Xlinker', '-v'],
                                         search_regex=[r'\t([^\t\n]+)\n'],
                                         search_regex_begin='Library search paths:')
        elif platform._is_linux:
            cls.linker_attributes = dict(cls.linker_attributes,
                                         search_path_flags=['-Xlinker', '--verbose'],
                                         search_regex=[r'SEARCH_DIR\("=([^"]+)"\);'])
        CompilerBase.before_registration(cls)

    @classmethod
    def set_env(cls, *args, **kwargs):
        r"""Set environment variables required for compilation.

        Args:
            *args: Arguments are passed to the parent class's method.
            **kwargs: Keyword arguments  are passed to the parent class's
                method.

        Returns:
            dict: Environment variables for the model process.

        """
        out = super(CCompilerBase, cls).set_env(*args, **kwargs)
        if _osx_sysroot is not None:
            out['CONDA_BUILD_SYSROOT'] = _osx_sysroot
            out['SDKROOT'] = _osx_sysroot
            grp = re.search(r'MacOSX(?P<target>[0-9]+\.[0-9]+)?',
                            _osx_sysroot).groupdict()
            # This is only utilized on local installs where a
            # non-default SDK is installed in addition to the default
            if grp['target']:  # pragma: debug
                out['MACOSX_DEPLOYMENT_TARGET'] = grp['target']
        return out
    
    @classmethod
    def call(cls, args, **kwargs):
        r"""Call the compiler with the provided arguments. For |yggdrasil| C
        models will always be linked using the C++ linker since some parts of
        the interface library are written in C++."""
        if not kwargs.get('dont_link', False):
            kwargs.setdefault('linker_language', 'c++')
        return super(CCompilerBase, cls).call(args, **kwargs)
    

class GCCCompiler(CCompilerBase):
    r"""Interface class for gcc compiler/linker."""
    toolname = 'gcc'
    platforms = ['MacOS', 'Linux', 'Windows']
    default_archiver = 'ar'


class ClangCompiler(CCompilerBase):
    r"""Interface class for clang compiler/linker."""
    toolname = 'clang'
    platforms = ['MacOS', 'Linux', 'Windows']
    default_linker = 'clang'
    default_archiver = 'libtool'
    flag_options = OrderedDict(list(CCompilerBase.flag_options.items())
                               + [('sysroot', '--sysroot'),
                                  ('isysroot', {'key': '-isysroot',
                                                'prepend': True}),
                                  ('mmacosx-version-min',
                                   '-mmacosx-version-min=%s')])
    # Set to False since ClangLinker has its own class to handle
    # conflict between versions of clang and ld.
    is_linker = False


class MSVCCompiler(CCompilerBase):
    r"""Microsoft Visual Studio C Compiler."""
    toolname = 'cl'
    languages = ['c', 'c++']
    platforms = ['Windows']
    default_flags_env = ['CFLAGS', 'CXXFLAGS']
    # TODO: Currently everything compiled as C++ on windows to allow use
    # of complex types. Use '/TC' instead of '/TP' for strictly C
    default_flags = ['/W4',      # Display all errors
                     '/Zi',      # Symbolic debug in .pdb (implies debug)
                     # '/MTd',     # Use LIBCMTD.lib to create multithreaded .exe
                     # '/Z7',      # Symbolic debug in .obj (implies debug)
                     "/EHsc",    # Catch C++ exceptions only (C don't throw C++)
                     '/TP',      # Treat all files as C++
                     "/nologo",  # Suppress startup banner
                     # Don't show errors from using scanf, strcpy, etc.
                     "-D_CRT_SECURE_NO_WARNINGS"]
    output_key = '/Fo%s'
    output_first = True
    default_linker = 'LINK'
    default_archiver = 'LIB'
    linker_switch = '/link'
    search_path_envvar = 'INCLUDE'
    search_path_flags = None
    version_flags = []
    product_exts = ['.dir', '.ilk', '.pdb', '.sln', '.vcxproj', '.vcxproj.filters']
    combine_with_linker = True  # Must be explicit; linker is separate .exe
    linker_attributes = dict(GCCCompiler.linker_attributes,
                             default_executable=None,
                             default_executable_env=None,
                             default_flags_env=None,
                             output_key='/OUT:%s',
                             output_first=True,
                             output_first_library=False,
                             flag_options=OrderedDict(
                                 [('library_libs', ''),
                                  ('library_dirs', '/LIBPATH:%s')]),
                             shared_library_flag='/DLL',
                             search_path_envvar='LIB',
                             search_path_flags=None)
    
    @classmethod
    def tool_version(cls, **kwargs):  # pragma: windows
        r"""Determine the version of this tool.

        Args:
            **kwargs: Keyword arguments are passed to cls.call.

        Returns:
            str: Version of the tool.

        """
        out = cls.call(cls.version_flags, skip_flags=True,
                       allow_error=True, **kwargs)
        if 'Copyright' not in out:  # pragma: debug
            raise RuntimeError("Version call failed: %s" % out)
        return out.split('Copyright')[0]


# C Linkers
class LDLinker(LinkerBase):
    r"""Linker class for ld tool."""
    toolname = 'ld'
    # Languages disabled for ld by default to prevent it being
    # selected instead of the default which seems to be happening
    # on the CI
    languages = ['c']  # ['c', 'c++', 'fortran']
    default_executable_env = 'LD'
    default_flags_env = 'LDFLAGS'
    version_flags = ['-v']
    search_path_envvar = ['LIBRARY_PATH', 'LD_LIBRARY_PATH']

    @classmethod
    def tool_version(cls, **kwargs):
        r"""Determine the version of this tool.

        Args:
            **kwargs: Keyword arguments are passed to cls.call.

        Returns:
            str: Version of the tool.

        """
        out = cls.call(cls.version_flags, skip_flags=True,
                       allow_error=True, **kwargs)
        if platform._is_mac:
            regex = r'PROJECT:ld64-(?P<version>\d+(?:\.\d+)?)'
        else:
            regex = (r'GNU ld \(GNU Binutils for (?P<os>.+)\) '
                     r'(?P<version>\d+(?:\.\d+){0,2})')
        match = re.search(regex, out)
        if match is None:  # pragma: debug
            raise RuntimeError("Could not locate version in string: %s" % out)
        return match.group('version')


class ClangLinker(LDLinker):
    r"""Interface class for clang linker (calls to ld)."""
    toolname = ClangCompiler.toolname
    aliases = ClangCompiler.aliases
    languages = ClangCompiler.languages
    platforms = ClangCompiler.platforms
    default_executable = ClangCompiler.default_executable
    default_executable_env = ClangCompiler.default_executable_env
    search_path_flags = ['-Xlinker', '-v']
    search_regex = [r'\t([^\t\n]+)\n']
    search_regex_begin = 'Library search paths:'
    flag_options = OrderedDict(LDLinker.flag_options,
                               **{'linker-version': '-mlinker-version=%s'})

    @classmethod
    def get_flags(cls, *args, **kwargs):
        r"""Get a list of linker flags."""
        # Handle case where clang (10.0.0) is trying to pass
        # -platform_version to a version of ld64 that dosn't support
        # it (<520).
        # https://bugs.llvm.org/show_bug.cgi?id=44813
        # https://reviews.llvm.org/D71579
        # https://reviews.llvm.org/D74784
        out = cls.call(cls.version_flags, skip_flags=True, allow_error=True)
        regex = r'clang version (?P<version>\d+)\.\d+\.\d+'
        match = re.search(regex, out)
        if (match is not None) and (int(match.group('version')) >= 10):
            ld_version = LDLinker.tool_version()
            if float(ld_version) < 520:
                kwargs['linker-version'] = ld_version
        out = super(ClangLinker, cls).get_flags(*args, **kwargs)
        if '-lstdc++' not in out:
            out.append('-lstdc++')
        return out


# C Archivers
class ARArchiver(ArchiverBase):
    r"""Archiver class for ar tool."""
    toolname = 'ar'
    languages = ['c', 'c++']
    default_executable_env = 'AR'
    default_flags_env = None
    static_library_flag = 'rcs'
    output_key = ''
    output_first_library = True


class LibtoolArchiver(ArchiverBase):
    r"""Archiver class for libtool tool."""
    toolname = 'libtool'
    languages = ['c', 'c++']
    default_executable_env = 'LIBTOOL'
    static_library_flag = '-static'  # This is the default
    

class MSVCArchiver(ArchiverBase):
    r"""Microsoft Visual Studio C Archiver."""
    toolname = 'LIB'
    languages = ['c', 'c++']
    platforms = ['Windows']
    static_library_flag = None
    output_key = '/OUT:%s'
    

_top_lang_dir = get_language_dir('c')
_incl_interface = _top_lang_dir
_incl_seri = os.path.join(_top_lang_dir, 'serialize')
_incl_comm = os.path.join(_top_lang_dir, 'communication')
_python_inc = ygg_cfg.get('c', 'python_include', None)
if (_python_inc is None) or (not os.path.isfile(_python_inc)):  # pragma: no cover
    _python_inc = sysconfig.get_paths()['include']
else:
    _python_inc = os.path.dirname(_python_inc)
try:
    _python_lib = ygg_cfg.get('c', 'python_shared',
                              ygg_cfg.get('c', 'python_static', None))
    if (_python_lib is None) or (not os.path.isfile(_python_lib)):  # pragma: no cover
        _python_lib = tools.get_python_c_library(allow_failure=False)
except BaseException:  # pragma: debug
    warnings.warn("ERROR LOCATING PYTHON LIBRARY")
    _python_lib = None
_numpy_inc = numpy_distutils.misc_util.get_numpy_include_dirs()
_numpy_lib = None


class CModelDriver(CompiledModelDriver):
    r"""Class for running C models."""

    _schema_subtype_description = ('Model is written in C.')
    language = 'c'
    language_ext = ['.c', '.h']
    interface_library = 'ygg'
    supported_comms = ['ipc', 'zmq']
    supported_comm_options = {
        'ipc': {'platforms': ['MacOS', 'Linux']},
        'zmq': {'libraries': ['zmq', 'czmq']}}
    interface_dependencies = ['rapidjson']
    interface_directories = [_incl_interface]
    external_libraries = {
        'rapidjson': {'include': os.path.join(os.path.dirname(tools.__file__),
                                              'rapidjson', 'include',
                                              'rapidjson', 'rapidjson.h'),
                      'libtype': 'header_only',
                      'language': 'c'},
        'zmq': {'include': 'zmq.h',
                'libtype': 'shared',
                'language': 'c'},
        'czmq': {'include': 'czmq.h',
                 'libtype': 'shared',
                 'language': 'c'},
        'numpy': {'include': os.path.join(_numpy_inc[0], 'numpy',
                                          'arrayobject.h'),
                  'libtype': 'header_only',
                  'language': 'c'},
        'python': {'include': os.path.join(_python_inc, 'Python.h'),
                   'language': 'c'}}
    internal_libraries = {
        'ygg': {'source': os.path.join(_incl_interface, 'YggInterface.c'),
                'linker_language': 'c++',  # Some dependencies are C++
                'internal_dependencies': ['regex', 'datatypes'],
                'external_dependencies': ['rapidjson',
                                          'python', 'numpy'],
                'include_dirs': [_incl_comm, _incl_seri],
                'compiler_flags': []},
        'regex_win32': {'source': 'regex_win32.cpp',
                        'directory': os.path.join(_top_lang_dir, 'regex'),
                        'language': 'c++',
                        'libtype': _default_internal_libtype,
                        'internal_dependencies': [],
                        'external_dependencies': []},
        'regex_posix': {'source': 'regex_posix.h',
                        'directory': os.path.join(_top_lang_dir, 'regex'),
                        'language': 'c',
                        'libtype': 'header_only',
                        'internal_dependencies': [],
                        'external_dependencies': []},
        'datatypes': {'directory': os.path.join(_top_lang_dir, 'datatypes'),
                      'language': 'c++',
                      'libtype': _default_internal_libtype,
                      'internal_dependencies': ['regex'],
                      'external_dependencies': ['rapidjson',
                                                'python', 'numpy'],
                      'include_dirs': []}}
    type_map = {
        'int': 'intX_t',
        'float': 'double',
        'string': 'string_t',
        'array': 'json_array_t',
        'object': 'json_object_t',
        'boolean': 'bool',
        'null': 'void*',
        'uint': 'uintX_t',
        'complex': 'complex_X',
        'bytes': 'char*',
        'unicode': 'unicode_t',
        '1darray': '*',
        'ndarray': '*',
        'ply': 'ply_t',
        'obj': 'obj_t',
        'schema': 'schema_t',
        'flag': 'int',
        'class': 'python_class_t',
        'function': 'python_function_t',
        'instance': 'python_instance_t',
        'any': 'generic_t'}
    function_param = {
        'import': '#include \"{filename}\"',
        'index': '{variable}[{index}]',
        'interface': '#include \"{interface_library}\"',
        'input': ('yggInput_t {channel} = yggInputType('
                  '\"{channel_name}\", {channel_type});'),
        'output': ('yggOutput_t {channel} = yggOutputType('
                   '\"{channel_name}\", {channel_type});'),
        'recv_heap': 'yggRecvRealloc',
        'recv_stack': 'yggRecv',
        'recv_function': 'yggRecvRealloc',
        'send_function': 'yggSend',
        'not_flag_cond': '{flag_var} < 0',
        'flag_cond': '{flag_var} >= 0',
        'declare': '{type_name} {variable};',
        'init_array': 'init_json_array()',
        'init_object': 'init_json_object()',
        'init_schema': 'init_schema()',
        'init_ply': 'init_ply()',
        'init_obj': 'init_obj()',
        'init_class': 'init_python()',
        'init_function': 'init_python()',
        'init_instance': 'init_generic()',
        'init_any': 'init_generic()',
        'copy_array': '{name} = copy_json_array({value});',
        'copy_object': '{name} = copy_json_object({value});',
        'copy_schema': '{name} = copy_schema({value});',
        'copy_ply': '{name} = copy_ply({value});',
        'copy_obj': '{name} = copy_obj({value});',
        'copy_class': '{name} = copy_python({value});',
        'copy_function': '{name} = copy_python({value});',
        'copy_instance': '{name} = copy_generic({value});',
        'copy_any': '{name} = copy_generic({value});',
        'free_array': 'free_json_array({variable});',
        'free_object': 'free_json_object({variable});',
        'free_schema': 'free_schema({variable});',
        'free_ply': 'free_ply({variable});',
        'free_obj': 'free_obj({variable});',
        'free_class': 'destroy_python({variable});',
        'free_function': 'destroy_python({variable});',
        'free_instance': 'free_generic({variable});',
        'free_any': 'free_generic({variable});',
        'print_float': 'printf("%f\\n", {object});',
        'print_int': 'printf("%i\\n", {object});',
        'print_uint': 'printf("%u\\n", {object});',
        'print_string': 'printf("%s\\n", {object});',
        'print_unicode': 'printf("%s\\n", {object});',
        'print_bytes': 'printf("%s\\n", {object});',
        'print_complex': 'print_complex({object});',
        'print_array': 'display_json_array({object});',
        'print_object': 'display_json_object({object});',
        'print_schema': 'display_schema({object});',
        'print_ply': 'display_ply({object});',
        'print_obj': 'display_obj({object});',
        'print_class': 'display_python({object});',
        'print_function': 'display_python({object});',
        'print_instance': 'display_generic({object});',
        'print_any': 'display_generic({object});',
        'assign': '{name} = {value};',
        'assign_copy': 'memcpy({name}, {value}, {N}*sizeof({native_type}));',
        'comment': '//',
        'true': '1',
        'false': '0',
        'not': '!',
        'and': '&&',
        'indent': 2 * ' ',
        'quote': '\"',
        'print': 'printf(\"{message}\\n\");',
        'fprintf': 'printf(\"{message}\\n\", {variables});',
        'error': 'printf(\"{error_msg}\\n\"); return -1;',
        'block_end': '}',
        'line_end': ';',
        'if_begin': 'if ({cond}) {{',
        'if_elif': '}} else if ({cond}) {{',
        'if_else': '}} else {{',
        'for_begin': ('for ({iter_var} = {iter_begin}; {iter_var} < {iter_end}; '
                      '{iter_var}++) {{'),
        'while_begin': 'while ({cond}) {{',
        'break': 'break;',
        'exec_begin': 'int main() {',
        'exec_end': '  return 0;\n}',
        'exec_prefix': '#include <stdbool.h>',
        'free': 'if ({variable} != NULL) {{ free({variable}); {variable} = NULL; }}',
        'function_def_begin': '{output_type} {function_name}({input_var}) {{',
        'return': 'return {output_var};',
        'function_def_regex': (
            r'(?P<flag_type>.+?)\s*{function_name}\s*'
            r'\((?P<inputs>(?:[^{{])*?)\)\s*\{{'
            r'(?P<body>(?:.*?\n?)*?)'
            r'(?:(?:return *(?P<flag_var>.+?)?;(?:.*?\n?)*?\}})'
            r'|(?:\}}))'),
        'inputs_def_regex': (
            r'\s*(?P<native_type>(?:[^\s\*])+(\s+)?'
            r'(?P<ptr>\*+)?)(?(ptr)(?(1)(?:\s*)|(?:\s+)))'
            r'(\((?P<name_ptr>\*+)?)?(?P<name>.+?)(?(4)(?:\)))'
            r'(?P<shape>(?:\[.+?\])+)?\s*(?:,|$)(?:\n)?'),
        'outputs_def_regex': (
            r'\s*(?P<native_type>(?:[^\s\*])+(\s+)?'
            r'(?P<ptr>\*+)?)(?(ptr)(?(1)(?:\s*)|(?:\s+)))'
            r'(?P<name>.+?)(?P<shape>(?:\[.+?\])+)?\s*(?:,|$)(?:\n)?')}
    outputs_in_inputs = True
    include_channel_obj = True
    is_typed = True
    brackets = (r'{', r'}')

    @staticmethod
    def after_registration(cls, **kwargs):
        r"""Operations that should be performed to modify class attributes after
        registration."""
        if cls.default_compiler is None:
            if platform._is_linux:
                cls.default_compiler = 'gcc'
            elif platform._is_mac:
                cls.default_compiler = 'clang'
            elif platform._is_win:  # pragma: windows
                cls.default_compiler = 'cl'
        CompiledModelDriver.after_registration(cls, **kwargs)
        if kwargs.get('second_pass', False):
            return
        if _python_lib:
            if _python_lib.endswith(('.lib', '.a')):
                cls.external_libraries['python']['libtype'] = 'static'
                cls.external_libraries['python']['static'] = _python_lib
            else:
                cls.external_libraries['python']['libtype'] = 'shared'
                cls.external_libraries['python']['shared'] = _python_lib
        for x in ['zmq', 'czmq']:
            if x in cls.external_libraries:
                if platform._is_win:  # pragma: windows
                    cls.external_libraries[x]['libtype'] = 'static'
        # Platform specific regex internal library
        if platform._is_win:  # pragma: windows
            regex_lib = cls.internal_libraries['regex_win32']
        else:
            regex_lib = cls.internal_libraries['regex_posix']
        cls.internal_libraries['regex'] = regex_lib
        # Platform specific internal library options
        cls.internal_libraries['ygg']['include_dirs'] += [_top_lang_dir]
        if platform._is_win:  # pragma: windows
            stdint_win = os.path.join(_top_lang_dir, 'windows_stdint.h')
            assert(os.path.isfile(stdint_win))
            shutil.copy(stdint_win, os.path.join(_top_lang_dir, 'stdint.h'))
            cls.internal_libraries['datatypes']['include_dirs'] += [_top_lang_dir]
        if platform._is_linux:
            for x in ['ygg', 'datatypes']:
                if 'compiler_flags' not in cls.internal_libraries[x]:
                    cls.internal_libraries[x]['compiler_flags'] = []
                if '-fPIC' not in cls.internal_libraries[x]['compiler_flags']:
                    cls.internal_libraries[x]['compiler_flags'].append('-fPIC')
        
    @classmethod
    def configure(cls, cfg, macos_sdkroot=None):
        r"""Add configuration options for this language. This includes locating
        any required external libraries and setting option defaults.

        Args:
            cfg (YggConfigParser): Config class that options should be set for.
            macos_sdkroot (str, optional): Full path to the root directory for
                the MacOS SDK that should be used. Defaults to None and is
                ignored.

        Returns:
            list: Section, option, description tuples for options that could not
                be set.

        """
        # Call __func__ to avoid direct invoking of class which dosn't exist
        # in after_registration where this is called
        out = CompiledModelDriver.configure.__func__(cls, cfg)
        # Change configuration to be directory containing include files
        rjlib = cfg.get(cls._language, 'rapidjson_include', None)
        if (rjlib is not None) and os.path.isfile(rjlib):
            cfg.set(cls._language, 'rapidjson_include',
                    os.path.dirname(os.path.dirname(rjlib)))
        nplib = cfg.get(cls._language, 'numpy_include', None)
        if (nplib is not None) and os.path.isfile(nplib):
            cfg.set(cls._language, 'numpy_include',
                    os.path.dirname(os.path.dirname(nplib)))
        if macos_sdkroot is None:
            macos_sdkroot = _osx_sysroot
        if macos_sdkroot is not None:
            if not os.path.isdir(macos_sdkroot):  # pragma: debug
                raise ValueError("Path to MacOS SDK root directory "
                                 "does not exist: %s." % macos_sdkroot)
            cfg.set(cls._language, 'macos_sdkroot', macos_sdkroot)
        return out

    @classmethod
    def call_linker(cls, obj, language=None, **kwargs):
        r"""Link several object files to create an executable or library (shared
        or static), checking for errors.

        Args:
            obj (list): Object files that should be linked.
            language (str, optional): Language that should be used to link
                the files. Defaults to None and the language of the current
                driver is used.
            **kwargs: Additional keyword arguments are passed to run_executable.

        Returns:
            str: Full path to compiled source.

        """
        if (((cls.language == 'c') and (language is None)
             and kwargs.get('for_model', False)
             and (not kwargs.get('skip_interface_flags', False)))):
            language = 'c++'
            kwargs.update(cls.update_linker_kwargs(**kwargs))
            kwargs['skip_interface_flags'] = True
        return super(CModelDriver, cls).call_linker(obj, language=language,
                                                    **kwargs)
        
    @classmethod
    def update_ld_library_path(cls, env, paths_to_add=None, add_to_front=False):
        r"""Update provided dictionary of environment variables so that
        LD_LIBRARY_PATH includes the interface directory containing the interface
        libraries.

        Args:
            env (dict): Dictionary of enviroment variables to be updated.
            paths_to_add (list, optional): Paths that should be added. If not
                provided, defaults to [cls.get_language_dir()].
            add_to_front (bool, optional): If True, new paths are added to the
                front, rather than the end. Defaults to False.

        Returns:
            dict: Updated dictionary of environment variables.

        """
        if paths_to_add is None:
            paths_to_add = [cls.get_language_dir()]
        if platform._is_linux:
            path_list = []
            prev_path = env.pop('LD_LIBRARY_PATH', '')
            if prev_path:
                path_list.append(prev_path)
            for x in paths_to_add:
                if x not in prev_path:
                    if add_to_front:
                        path_list.insert(0, x)
                    else:
                        path_list.append(x)
            if path_list:
                env['LD_LIBRARY_PATH'] = os.pathsep.join(path_list)
        return env

    def set_env(self, **kwargs):
        r"""Get environment variables that should be set for the model process.

        Args:
            **kwargs: Additional keyword arguments are passed to the parent
                class's method.

        Returns:
            dict: Environment variables for the model process.

        """
        out = super(CModelDriver, self).set_env(**kwargs)
        out = self.update_ld_library_path(out)
        if platform._is_win:  # pragma: windows
            out.setdefault('PYTHONHOME', sysconfig.get_config_var('prefix'))
            out.setdefault('PYTHONPATH', os.pathsep.join([
                sysconfig.get_path('stdlib'), sysconfig.get_path('purelib'),
                os.path.join(sysconfig.get_config_var('prefix'), 'DLLs')]))
        return out
    
    @classmethod
    def parse_var_definition(cls, io, value, **kwargs):
        r"""Extract information about input/output variables from a
        string definition.

        Args:
            io (str): Description of variables contained in the provided
                string. Must be 'inputs' or 'outputs'.
            value (str): String containing one or more variable definitions.
            **kwargs: Additional keyword arguments are passed to the
                parent class's method.

        Returns:
            list: List of information about the variables contained in
                the provided string.

        Raises:
            AssertionError: If io is not 'inputs' or 'outputs'.
            NotImplementedError: If the def_regex for the specified
                io is not defined.

        """
        out = super(CModelDriver, cls).parse_var_definition(io, value, **kwargs)
        io_map = {x['name']: x for x in out}
        for i, x in enumerate(out):
            if (x['name'] + '_length') in io_map:
                x['length_var'] = x['name'] + '_length'
            elif ('length_' + x['name']) in io_map:
                x['length_var'] = 'length_' + x['name']
            elif (((x['name'] + '_ndim') in io_map)
                  and ((x['name'] + '_shape') in io_map)):
                x['ndim_var'] = x['name'] + '_ndim'
                x['shape_var'] = x['name'] + '_shape'
                x['datatype']['type'] = 'ndarray'
            elif ((('ndim_' + x['name']) in io_map)
                  and (('shape_' + x['name']) in io_map)):
                x['ndim_var'] = 'ndim_' + x['name']
                x['shape_var'] = 'shape_' + x['name']
                x['datatype']['type'] = 'ndarray'
            elif 'shape' in x:
                x['datatype']['shape'] = [
                    int(float(s.strip('[]')))
                    for s in x.pop('shape').split('][')]
                assert(x['datatype']['subtype'] in _valid_types)
                if len(x['datatype']['shape']) == 1:
                    x['datatype']['length'] = x['datatype'].pop(
                        'shape')[0]
                    x['datatype']['type'] = '1darray'
                else:
                    x['datatype']['type'] = 'ndarray'
        return out
        
    @classmethod
    def update_io_from_function(cls, model_file, model_function,
                                inputs=[], outputs=[], contents=None,
                                outputs_in_inputs=None):
        r"""Update inputs/outputs from the function definition.

        Args:
            model_file (str): Full path to the file containing the model
                function's declaration.
            model_function (str): Name of the model function.
            inputs (list, optional): List of model inputs including types.
                Defaults to [].
            outputs (list, optional): List of model outputs including types.
                Defaults to [].
            contents (str, optional): Contents of file to parse rather than
                re-reading the file. Defaults to None and is ignored.
            outputs_in_inputs (bool, optional): If True, the outputs are
                presented in the function definition as inputs. Defaults
                to False.

        Returns:
            dict, None: Flag variable used by the model. If None, the
                model does not use a flag variable.

        """
        flag_var = super(CModelDriver, cls).update_io_from_function(
            model_file, model_function, inputs=inputs,
            outputs=outputs, contents=contents,
            outputs_in_inputs=outputs_in_inputs)
        # Add length_vars if missing for use by yggdrasil
        for x in inputs:
            for v in x['vars']:
                if cls.requires_length_var(v) and (not v.get('length_var', False)):
                    v['length_var'] = {'name': v['name'] + '_length',
                                       'datatype': {'type': 'uint',
                                                    'precision': 64},
                                       'is_length_var': True,
                                       'dependent': True}
                elif cls.requires_shape_var(v):
                    if not (v.get('ndim_var', False)
                            and v.get('shape_var', False)):  # pragma: debug
                        raise RuntimeError("Uncomment logic that follows.")
                    # if not v.get('ndim_var', False):
                    #     v['ndim_var'] = {
                    #         'name': v['name'] + '_ndim',
                    #         'datatype': {'type': 'uint',
                    #                      'precision': 64},
                    #         'is_length_var': True,
                    #         'dependent': True}
                    # if not v.get('shape_var', False):
                    #     v['shape_var'] = {
                    #         'name': v['name'] + '_ndim',
                    #         'datatype': {'type': '1darray',
                    #                      'subtype': 'uint',
                    #                      'precision': 64},
                    #         'is_length_var': True,
                    #         'dependent': True}
        for x in outputs:
            for v in x['vars']:
                if cls.requires_length_var(v) and (not v.get('length_var', False)):
                    if v['datatype']['type'] in ['1darray', 'ndarray']:  # pragma: debug
                        raise RuntimeError("Length must be defined for arrays.")
                    elif v['datatype'].get('subtype', v['datatype']['type']) == 'bytes':
                        v['length_var'] = 'strlen(%s)' % v['name']
                    else:
                        v['length_var'] = 'strlen4(%s)' % v['name']
                elif (cls.requires_shape_var(v)
                      and not (v.get('ndim_var', False)
                               and v.get('shape_var', False))):  # pragma: debug
                    raise RuntimeError("Shape must be defined for ND arrays.")
        # Flag input variables for reallocation
        for x in inputs:
            allows_realloc = [cls.allows_realloc(v) for v in x['vars']]
            if all(allows_realloc):
                for v in x['vars']:
                    if (((v['native_type'] not in ['char*', 'string_t',
                                                   'bytes_t', 'unicode_t'])
                         and (not v.get('is_length_var', False))
                         and (v['datatype']['type'] not in
                              ['any', 'object', 'array', 'schema',
                               'instance', '1darray', 'ndarray'])
                         and (cls.function_param['recv_function']
                              == cls.function_param['recv_heap']))):
                        v['allow_realloc'] = True
        for x in inputs + outputs:
            if x['datatype']['type'] == 'array':
                nvars_items = len(x['datatype'].get('items', []))
                nvars = sum([(not ix.get('is_length_var', False))
                             for ix in x['vars']])
                if nvars_items == nvars:
                    x['use_generic'] = False
                else:
                    x['use_generic'] = True
        return flag_var
        
    @classmethod
    def input2output(cls, var):
        r"""Perform conversion necessary to turn a variable extracted from a
        function definition from an input to an output.

        Args:
            var (dict): Variable definition.

        Returns:
            dict: Updated variable definition.

        """
        out = super(CModelDriver, cls).input2output(var)
        if out.get('ptr', ''):
            assert(out['native_type'].endswith('*'))
            out['ptr'] = out['ptr'][:-1]
            out['native_type'] = out['native_type'][:-1]
            out['datatype'] = cls.get_json_type(out['native_type'])
            if (((out['datatype']['type'] == '1darray')
                 and var.get('ndim_var', False)
                 and var.get('shape_var', False))):
                out['datatype']['type'] = 'ndarray'
        return out

    @classmethod
    def output2input(cls, var, in_definition=True):
        r"""Perform conversion necessary to turn an output variable
        into an corresponding input that can be used to format a
        function definition.

        Args:
            var (dict): Variable definition.
            in_definition (bool, optional): If True, the returned
                dictionary corresponds to an input variable in a
                function definition. If False, the returned value
                will correspond to an input to a function. Defaults to
                True.

        Returns:
            dict: Updated variable definition.

        """
        out = super(CModelDriver, cls).output2input(var)
        if isinstance(var, dict):
            if in_definition:
                out = dict(out, name='*' + out['name'])
                if ((('shape' in out.get('datatype', {}))
                     or ('length' in out.get('datatype', {})))):
                    out['name'] = '(%s)' % out['name']
            else:
                out = dict(out, name='&' + out['name'])
                if ('shape' in out.get('datatype', {})) and (not platform._is_win):
                    out['name'] += len(out['datatype']['shape']) * '[0]'
        return out
        
    @classmethod
    def allows_realloc(cls, var):
        r"""Determine if a variable allows the receive call to perform
        realloc.

        Args:
            var (dict): Dictionary of variable properties.

        Returns:
            bool: True if the variable allows realloc, False otherwise.

        """
        if isinstance(var, dict):
            datatype = var.get('datatype', var)
            if ('shape' in datatype) or ('length' in datatype):
                return False
        return True
        
    @classmethod
    def requires_length_var(cls, var):
        r"""Determine if a variable requires a separate length variable.

        Args:
            var (dict): Dictionary of variable properties.

        Returns:
            bool: True if a length variable is required, False otherwise.

        """
        if ((isinstance(var, dict)
             and ((cls.get_native_type(**var) in ['char*', 'string_t',
                                                  'bytes_t', 'unicode_t'])
                  or var.get('datatype', {}).get(
                      'type', var.get('type', None)) in ['1darray'])
             and (not var.get('is_length_var', False))
             and ('length' not in var.get('datatype', {})))):
            return True
        return False
    
    @classmethod
    def requires_shape_var(cls, var):
        r"""Determine if a variable requires a separate shape variable.

        Args:
            var (dict): Dictionary of variable properties.

        Returns:
            bool: True if a shape variable is required, False otherwise.

        """
        if ((isinstance(var, dict)
             and (var.get('datatype', {}).get(
                 'type', var.get('type', None)) == 'ndarray')
             and (not var.get('is_length_var', False))
             and ('shape' not in var.get('datatype', {})))):
            return True
        return False
              
    @classmethod
    def get_native_type(cls, **kwargs):
        r"""Get the native type.

        Args:
            type (str, optional): Name of |yggdrasil| extended JSON
                type or JSONSchema dictionary defining a datatype.
            **kwargs: Additional keyword arguments may be used in determining
                the precise declaration that should be used.

        Returns:
            str: The native type.

        """
        out = super(CModelDriver, cls).get_native_type(**kwargs)
        if not ((out == '*') or ('X' in out) or (out == 'double')):
            return out
        from yggdrasil.metaschema.datatypes import get_type_class
        json_type = kwargs.get('datatype', kwargs.get('type', 'bytes'))
        if isinstance(json_type, str):
            json_type = {'type': json_type}
        assert(isinstance(json_type, dict))
        json_type = get_type_class(json_type['type']).normalize_definition(
            json_type)
        if out == '*':
            json_subtype = copy.deepcopy(json_type)
            json_subtype['type'] = json_subtype.pop('subtype')
            out = cls.get_native_type(datatype=json_subtype)
            if ('length' not in json_type) and ('shape' not in json_type):
                out += '*'
        elif 'X' in out:
            precision = json_type['precision']
            if json_type['type'] == 'complex':
                precision_map = {64: 'float',
                                 128: 'double',
                                 256: 'long_double'}
                if precision in precision_map:
                    out = out.replace('X', precision_map[precision])
                else:  # pragma: debug
                    raise ValueError("Unsupported precision for complex types: %d"
                                     % precision)
            else:
                out = out.replace('X', str(precision))
        elif out == 'double':
            if json_type['precision'] == 32:
                out = 'float'
        return out.replace(' ', '')
        
    @classmethod
    def get_json_type(cls, native_type):
        r"""Get the JSON type from the native language type.

        Args:
            native_type (str): The native language type.

        Returns:
            str, dict: The JSON type.

        """
        out = {}
        regex_var = r'(?P<type>.+?(?P<precision>\d*)(?:_t)?)\s*(?P<pointer>\**)'
        grp = re.fullmatch(regex_var, native_type).groupdict()
        if grp.get('precision', False):
            out['precision'] = int(grp['precision'])
            grp['type'] = grp['type'].replace(grp['precision'], 'X')
        if grp['type'] == 'char':
            out['type'] = 'bytes'
            out['precision'] = 0
        elif grp['type'] == 'void':
            out['type'] = 'null'
        elif grp['type'].startswith('complex'):
            out['type'] = 'complex'
            precision_map = {'long_double': 256,
                             'double': 128,
                             'float': 64}
            prec_str = grp['type'].split('complex_')[-1]
            if prec_str in precision_map:
                out['precision'] = precision_map[prec_str]
            else:  # pragma: debug
                raise ValueError("Cannot determine precision for complex type '%s'"
                                 % grp['type'])
        else:
            if grp['type'] == 'double':
                out['precision'] = 8 * 8
            elif grp['type'] == 'float':
                grp['type'] = 'double'
                out['precision'] = 4 * 8
            elif grp['type'] in ['int', 'uint']:
                grp['type'] += 'X_t'
                out['precision'] = 8 * np.dtype('intc').itemsize
            elif grp['type'] in ['bytes_t', 'string_t', 'unicode_t']:
                out['precision'] = 0
            out['type'] = super(CModelDriver, cls).get_json_type(grp['type'])
        if grp.get('pointer', False):
            nptr = len(grp['pointer'])
            if grp['type'] in ['char', 'void']:
                nptr -= 1
            if nptr > 0:
                out['subtype'] = out['type']
                out['type'] = '1darray'
        if out['type'] in _valid_types:
            out['subtype'] = out['type']
            out['type'] = 'scalar'
        return out
        
    @classmethod
    def format_function_param(cls, key, default=None, **kwargs):
        r"""Return the formatted version of the specified key.

        Args:
            key (str): Key in cls.function_param mapping that should be
                formatted.
            default (str, optional): Format that should be returned if key
                is not in cls.function_param. Defaults to None.
            **kwargs: Additional keyword arguments are used in formatting the
                request function parameter.

        Returns:
            str: Formatted string.

        Raises:
            NotImplementedError: If key is not in cls.function_param and default
                is not set.

        """
        if (key == 'import') and ('filename' in kwargs):
            kwargs['filename'] = os.path.basename(kwargs['filename'])
        elif (key == 'interface') and ('interface_library' in kwargs):
            kwargs['interface_library'] = os.path.basename(
                kwargs['interface_library']).replace('.c', '.h')
        kwargs['default'] = default
        return super(CModelDriver, cls).format_function_param(key, **kwargs)
    
    @classmethod
    def write_model_function_call(cls, model_function, flag_var,
                                  inputs, outputs, **kwargs):
        r"""Write lines necessary to call the model function.

        Args:
            model_function (str): Handle of the model function that should be
                called.
            flag_var (str): Name of variable that should be used as a flag.
            inputs (list): List of dictionaries describing inputs to the model.
            outputs (list): List of dictionaries describing outputs from the model.
            **kwargs: Additional keyword arguments are passed to the parent
                class's method.

        Returns:
            list: Lines required to carry out a call to a model function in
                this language.

        """
        new_inputs = copy.deepcopy(inputs)
        for x in new_inputs:
            for v in x['vars']:
                if v.get('allow_realloc', False):
                    v['name'] = '*' + v['name']
        return super(CModelDriver, cls).write_model_function_call(
            model_function, flag_var, new_inputs, outputs, **kwargs)
        
    @classmethod
    def write_model_recv(cls, channel, recv_var, **kwargs):
        r"""Write a model receive call include checking the return flag.

        Args:
            channel (str): Name of variable that the channel being received from
                was stored in.
            recv_var (dict, list): Information of one or more variables that
                receieved information should be stored in.
            **kwargs: Additional keyword arguments are passed to the parent
                class's method.

        Returns:
            list: Lines required to carry out a receive call in this language.

        """
        recv_var_str = recv_var
        if not isinstance(recv_var, str):
            recv_var_par = cls.channels2vars(recv_var)
            allows_realloc = [cls.allows_realloc(v)
                              for v in recv_var_par]
            if all(allows_realloc):
                kwargs['alt_recv_function'] = cls.function_param['recv_heap']
            else:
                kwargs['alt_recv_function'] = cls.function_param['recv_stack']
            recv_var_str = cls.prepare_output_variables(
                recv_var_par, in_inputs=cls.outputs_in_inputs,
                for_yggdrasil=True)
        return super(CModelDriver, cls).write_model_recv(channel, recv_var_str, **kwargs)
            
    @classmethod
    def write_declaration(cls, var, **kwargs):
        r"""Return the lines required to declare a variable with a certain
        type.

        Args:
            var (dict, str): Name or information dictionary for the variable
                being declared.
            **kwargs: Addition keyword arguments are passed to the parent
                class's method.

        Returns:
            list: The lines declaring the variable.

        """
        if isinstance(var, str):  # pragma: no cover
            var = {'name': var}
        type_name = cls.get_native_type(**var)
        if var.get('allow_realloc', False):
            type_name += '*'
            var = dict(var, native_type=type_name)
        if ((type_name.endswith('*')
             or (type_name in ['bytes_t', 'string_t', 'unicode_t']))):
            kwargs.get('requires_freeing', []).append(var)
            kwargs.setdefault('value', 'NULL')
        elif var.get('is_length_var', False):
            kwargs.setdefault('value', '0')
        var = dict(var, name=cls.get_name_declare(var))
        out = super(CModelDriver, cls).write_declaration(var, **kwargs)
        for k in ['length', 'ndim', 'shape']:
            if ((isinstance(var.get(k + '_var', None), dict)
                 and var[k + '_var'].get('dependent', False))):
                out += cls.write_declaration(var[k + '_var'])
        return out

    @classmethod
    def get_name_declare(cls, var):
        r"""Determine the name that should be used for declaration.

        Args:
            var (str, dict): Name of variable or dictionary of information.

        Returns:
            str: Modified name for declaration.

        """
        if isinstance(var, str):  # pragma: no cover
            return var
        assert(isinstance(var, dict))
        out = var['name']
        if 'length' in var.get('datatype', {}):
            out += '[%d]' % var['datatype']['length']
        elif 'shape' in var.get('datatype', {}):
            for s in var['datatype']['shape']:
                out += '[%d]' % s
        return out
        
    @classmethod
    def write_free(cls, var, **kwargs):
        r"""Return the lines required to free a variable with a certain type.

        Args:
            var (dict, str): Name or information dictionary for the variable
                being declared.
            **kwargs: Additional keyword arguments are passed to the parent
                class's method.

        Returns:
            list: The lines freeing the variable.

        """
        out = []
        if isinstance(var, str):
            var = {'name': var}
        if ((isinstance(var.get('datatype', False), dict)
             and (('free_%s' % var['datatype']['type'])
                  in cls.function_param))):
            if var.get('allow_realloc', False):
                out += super(CModelDriver, cls).write_free(
                    var, **kwargs)
                var = {'name': var['name']}
            else:
                var = dict(var, name=('&' + var['name']))
        out += super(CModelDriver, cls).write_free(var, **kwargs)
        return out
        
    @classmethod
    def prepare_variables(cls, vars_list, in_definition=False,
                          for_yggdrasil=False):
        r"""Concatenate a set of input variables such that it can be passed as a
        single string to the function_call parameter.

        Args:
            vars_list (list): List of variable dictionaries containing info
                (e.g. names) that should be used to prepare a string representing
                input/output to/from a function call.
            in_definition (bool, optional): If True, the returned sequence
                will be of the format required for specifying variables
                in a function definition. Defaults to False.
            for_yggdrasil (bool, optional): If True, the variables will be
                prepared in the formated expected by calls to yggdarsil
                send/recv methods. Defaults to False.

        Returns:
            str: Concatentated variables list.

        """
        if not isinstance(vars_list, list):
            vars_list = [vars_list]
        new_vars_list = []
        for x in vars_list:
            if isinstance(x, str):
                new_vars_list.append(x)
            else:
                assert(isinstance(x, dict))
                if for_yggdrasil and x.get('is_length_var', False):
                    continue
                new_vars_list.append(x)
                if for_yggdrasil:
                    for k in ['length', 'ndim', 'shape']:
                        kvar = k + '_var'
                        if x.get(kvar, False):
                            if ((x['name'].startswith('*')
                                 or x['name'].startswith('&'))):
                                new_vars_list.append(
                                    dict(x[kvar],
                                         name=x['name'][0] + x[kvar]['name']))
                            else:
                                new_vars_list.append(x[kvar])
        if in_definition:
            new_vars_list2 = []
            for x in new_vars_list:
                if x['name'].startswith('*'):
                    name = '%s%s* %s' % tuple(
                        [cls.get_native_type(**x)]
                        + x['name'].rsplit('*', 1))
                else:
                    name = '%s %s' % (cls.get_native_type(**x), x['name'])
                new_var = dict(x, name=name)
                new_var['name'] = cls.get_name_declare(new_var)
                new_vars_list2.append(new_var)
            new_vars_list = new_vars_list2
        return super(CModelDriver, cls).prepare_variables(
            new_vars_list, in_definition=in_definition,
            for_yggdrasil=for_yggdrasil)
    
    @classmethod
    def prepare_output_variables(cls, vars_list, in_definition=False,
                                 in_inputs=False, for_yggdrasil=False):
        r"""Concatenate a set of output variables such that it can be passed as
        a single string to the function_call parameter.

        Args:
            vars_list (list): List of variable names to concatenate as output
                from a function call.
            in_definition (bool, optional): If True, the returned sequence
                will be of the format required for specifying output
                variables in a function definition. Defaults to False.
            in_inputs (bool, optional): If True, the output variables should
                be formated to be included as input variables. Defaults to
                False.
            for_yggdrasil (bool, optional): If True, the variables will be
                prepared in the formated expected by calls to yggdarsil
                send/recv methods. Defaults to False.

        Returns:
            str: Concatentated variables list.

        """
        if not in_inputs:
            # If the output is a True output and not passed as an input
            # parameter, then the output should not include the type
            # information that is added if in_definition is True.
            in_definition = False
        return super(CModelDriver, cls).prepare_output_variables(
            vars_list, in_definition=in_definition, in_inputs=in_inputs,
            for_yggdrasil=for_yggdrasil)

    @classmethod
    def write_print_output_var(cls, var, in_inputs=False, **kwargs):
        r"""Get the lines necessary to print an output variable in this
        language.

        Args:
            var (dict): Variable information.
            in_inputs (bool, optional): If True, the output variable
                is passed in as an input variable to be populated.
                Defaults to False.
            **kwargs: Additional keyword arguments are passed to write_print_var.

        Returns:
            list: Lines printing the specified variable.

        """
        if in_inputs and (cls.language != 'c++'):
            if isinstance(var, dict):
                var = dict(var, name='%s[0]' % var['name'])
            else:
                var = '%s[0]' % var
        return super(CModelDriver, cls).write_print_output_var(
            var, in_inputs=in_inputs, **kwargs)
        
    @classmethod
    def write_function_def(cls, function_name, dont_add_lengths=False,
                           use_length_prefix=False, **kwargs):
        r"""Write a function definition.

        Args:
            function_name (str): Name fo the function being defined.
            dont_add_lengths (bool, optional): If True, length variables
                are not added for arrays. Defaults to False.
            use_length_prefix (bool, optional): If True and length variables
                are added, they will be named using prefixes. Otherwise,
                suffixes will be used. Defaults to False.
            **kwargs: Additional keyword arguments are passed to the
                parent class's method.

        Returns:
            list: Lines completing the function call.

        Raises:
            ValueError: If outputs_in_inputs is not True and more than
                one output variable is specified.

        """
        if not dont_add_lengths:
            for io in ['input', 'output']:
                if io + '_var' in kwargs:
                    io_var = cls.parse_var_definition(
                        io + 's', kwargs.pop(io + '_var'))
                else:
                    io_var = kwargs.get(io + 's', [])
                for x in io_var:
                    if use_length_prefix:
                        v_length = 'length_' + x['name']
                        v_ndim = 'ndim_' + x['name']
                        v_shape = 'shape_' + x['name']
                    else:
                        v_length = x['name'] + '_length'
                        v_ndim = x['name'] + '_ndim'
                        v_shape = x['name'] + '_shape'
                    if x.get('is_length_var', False):
                        continue
                    if cls.requires_length_var(x):
                        if not x.get('length_var', False):
                            x['length_var'] = {
                                'name': v_length,
                                'datatype': {'type': 'uint',
                                             'precision': 64},
                                'is_length_var': True}
                            io_var.append(x['length_var'])
                    elif cls.requires_shape_var(x):
                        if not x.get('ndim_var', False):
                            x['ndim_var'] = {
                                'name': v_ndim,
                                'datatype': {'type': 'uint',
                                             'precision': 64},
                                'is_length_var': True}
                            io_var.append(x['ndim_var'])
                        if not x.get('shape_var', False):
                            x['shape_var'] = {
                                'name': v_shape,
                                'datatype': {'type': '1darray',
                                             'subtype': 'uint',
                                             'precision': 64},
                                'is_length_var': True}
                            io_var.append(x['shape_var'])
                        length_var = {
                            'name': v_length,
                            'datatype': {'type': 'uint',
                                         'precision': 64},
                            'is_length_var': True}
                        kwargs['function_contents'] = (
                            cls.write_declaration(length_var)
                            + kwargs.get('function_contents', []))
                kwargs[io + 's'] = io_var
        output_type = None
        if kwargs.get('outputs_in_inputs', False):
            output_type = cls.get_native_type(datatype='flag')
        else:
            if 'output_var' in kwargs:
                kwargs['outputs'] = cls.parse_var_definition(
                    'outputs', kwargs.pop('output_var'))
            outputs = kwargs.get('outputs', [])
            nout = len(outputs)
            if nout == 0:
                output_type = 'void'
            elif nout == 1:
                output_type = cls.get_native_type(**(outputs[0]))
            else:  # pragma: debug
                raise ValueError("C does not support more than one "
                                 "output variable.")
        kwargs['output_type'] = output_type
        return super(CModelDriver, cls).write_function_def(
            function_name, **kwargs)
        
    @classmethod
    def write_native_type_definition(cls, name, datatype, name_base=None,
                                     requires_freeing=None, no_decl=False,
                                     use_generic=False):
        r"""Get lines declaring the data type within the language.

        Args:
            name (str): Name of variable that definition should be stored in.
            datatype (dict): Type definition.
            requires_freeing (list, optional): List that variables requiring
                freeing should be appended to. Defaults to None.
            no_decl (bool, optional): If True, the variable is defined without
                declaring it (assumes that variable has already been declared).
                Defaults to False.
            use_generic (bool, optional): If True variables serialized
                and/or deserialized by the type will be assumed to be
                generic objects. Defaults to False.

        Returns:
            list: Lines required to define a type definition.

        """
        out = []
        fmt = None
        keys = {}
        if use_generic:
            keys['use_generic'] = 'true'
        else:
            keys['use_generic'] = 'false'
        typename = datatype['type']
        if name_base is None:
            name_base = name
        if datatype['type'] == 'array':
            if 'items' in datatype:
                assert(isinstance(datatype['items'], list))
                keys['nitems'] = len(datatype['items'])
                keys['items'] = '%s_items' % name_base
                fmt = ('create_dtype_json_array({nitems}, {items}, '
                       '{use_generic})')
                out += [('dtype_t** %s = '
                         '(dtype_t**)malloc(%d*sizeof(dtype_t*));')
                        % (keys['items'], keys['nitems'])]
                for i, x in enumerate(datatype['items']):
                    # Prevent recusion
                    x_copy = copy.deepcopy(x)
                    x_copy.pop('items', None)
                    x_copy.pop('properties', None)
                    out += cls.write_native_type_definition(
                        '%s[%d]' % (keys['items'], i), x_copy,
                        name_base=('%s_item%d' % (name_base, i)),
                        requires_freeing=requires_freeing, no_decl=True,
                        use_generic=use_generic)
                assert(isinstance(requires_freeing, list))
                requires_freeing += [keys['items']]
            else:
                keys['use_generic'] = 'true'
                fmt = ('create_dtype_json_array(0, NULL, '
                       '{use_generic})')
        elif datatype['type'] == 'object':
            keys['use_generic'] = 'true'
            if 'properties' in datatype:
                assert(isinstance(datatype['properties'], dict))
                keys['nitems'] = len(datatype['properties'])
                keys['keys'] = '%s_keys' % name_base
                keys['values'] = '%s_vals' % name_base
                fmt = ('create_dtype_json_object({nitems}, {keys}, '
                       '{values}, {use_generic})')
                out += [('dtype_t** %s = '
                         '(dtype_t**)malloc(%d*sizeof(dtype_t*));')
                        % (keys['values'], keys['nitems']),
                        ('char** %s = (char**)malloc(%d*sizeof(char*));')
                        % (keys['keys'], keys['nitems'])]
                for i, (k, v) in enumerate(datatype['properties'].items()):
                    # Prevent recusion
                    v_copy = copy.deepcopy(v)
                    v_copy.pop('items', None)
                    v_copy.pop('properties', None)
                    out += ['%s[%d] = \"%s\";' % (keys['keys'], i, k)]
                    out += cls.write_native_type_definition(
                        '%s[%d]' % (keys['values'], i), v_copy,
                        name_base=('%s_prop%d' % (name_base, i)),
                        requires_freeing=requires_freeing, no_decl=True,
                        use_generic=use_generic)
                assert(isinstance(requires_freeing, list))
                requires_freeing += [keys['values'], keys['keys']]
            else:
                fmt = ('create_dtype_json_object(0, NULL, NULL, '
                       '{use_generic})')
        elif datatype['type'] in ['ply', 'obj']:
            fmt = 'create_dtype_%s({use_generic})' % datatype['type']
        elif datatype['type'] == '1darray':
            fmt = ('create_dtype_1darray(\"{subtype}\", {precision}, {length}, '
                   '\"{units}\", {use_generic})')
            for k in ['subtype', 'precision']:
                keys[k] = datatype[k]
            keys['length'] = datatype.get('length', '0')
            keys['units'] = datatype.get('units', '')
        elif datatype['type'] == 'ndarray':
            fmt = ('create_dtype_ndarray(\"{subtype}\", {precision},'
                   ' {ndim}, {shape}, \"{units}\", {use_generic})')
            for k in ['subtype', 'precision']:
                keys[k] = datatype[k]
            if 'shape' in datatype:
                shape_var = '%s_shape' % name_base
                out += ['size_t %s[%d] = {%s};' % (
                    shape_var, len(datatype['shape']),
                    ', '.join([str(s) for s in datatype['shape']]))]
                keys['ndim'] = len(datatype['shape'])
                keys['shape'] = shape_var
                fmt = fmt.replace('create_dtype_ndarray',
                                  'create_dtype_ndarray_arr')
            else:
                keys['ndim'] = 0
                keys['shape'] = 'NULL'
            keys['units'] = datatype.get('units', '')
        elif (typename == 'scalar') or (typename in _valid_types):
            fmt = ('create_dtype_scalar(\"{subtype}\", {precision}, '
                   '\"{units}\", {use_generic})')
            keys['subtype'] = datatype.get('subtype', datatype['type'])
            keys['units'] = datatype.get('units', '')
            if keys['subtype'] in ['bytes', 'string', 'unicode']:
                keys['precision'] = datatype.get('precision', 0)
            else:
                keys['precision'] = datatype['precision']
            typename = 'scalar'
        elif datatype['type'] in ['boolean', 'null', 'number',
                                  'integer', 'string']:
            fmt = 'create_dtype_default(\"{type}\", {use_generic})'
            keys['type'] = datatype['type']
        elif (typename in ['class', 'function']):
            fmt = 'create_dtype_pyobj(\"{type}\", {use_generic})'
            keys['type'] = typename
        elif typename == 'instance':
            keys['use_generic'] = 'true'
            # fmt = 'create_dtype_pyinst(NULL, NULL)'
            fmt = 'create_dtype_empty({use_generic})'
        elif typename == 'schema':
            keys['use_generic'] = 'true'
            fmt = 'create_dtype_schema({use_generic})'
        elif typename == 'any':
            keys['use_generic'] = 'true'
            fmt = 'create_dtype_empty({use_generic})'
        else:  # pragma: debug
            raise ValueError("Cannot create C version of type '%s'"
                             % typename)
        def_line = '%s = %s;' % (name, fmt.format(**keys))
        if not no_decl:
            def_line = 'dtype_t* ' + def_line
        out.append(def_line)
        return out

    @classmethod
    def write_channel_def(cls, key, datatype=None, requires_freeing=None,
                          use_generic=False, **kwargs):
        r"""Write an channel declaration/definition.

        Args:
            key (str): Entry in cls.function_param that should be used.
            datatype (dict, optional): Data type associated with the channel.
                Defaults to None and is ignored.
            requires_freeing (list, optional): List that variables requiring
                freeing should be appended to. Defaults to None.
            use_generic (bool, optional): If True variables serialized
                and/or deserialized by the channel will be assumed to be
                generic objects. Defaults to False.
            **kwargs: Additional keyword arguments are passed as parameters
                to format_function_param.

        Returns:
            list: Lines required to declare and define an output channel.

        """
        out = []
        if (datatype is not None) and ('{channel_type}' in cls.function_param[key]):
            kwargs['channel_type'] = '%s_type' % kwargs['channel']
            out += cls.write_native_type_definition(
                kwargs['channel_type'], datatype,
                requires_freeing=requires_freeing,
                use_generic=use_generic)
        out += super(CModelDriver, cls).write_channel_def(key, datatype=datatype,
                                                          **kwargs)
        return out

    @classmethod
    def write_assign_to_output(cls, dst_var, src_var,
                               outputs_in_inputs=False,
                               dont_add_lengths=False,
                               use_length_prefix=False, **kwargs):
        r"""Write lines assigning a value to an output variable.

        Args:
            dst_var (str, dict): Name or information dictionary for
                variable being assigned to.
            src_var (str, dict): Name or information dictionary for
                value being assigned to dst_var.
            outputs_in_inputs (bool, optional): If True, outputs are passed
                as input parameters. In some languages, this means that a
                pointer or reference is passed (e.g. C) and so the assignment
                should be to the memory indicated rather than the variable.
                Defaults to False.
            dont_add_lengths (bool, optional): If True, length variables
                are not added for arrays. Defaults to False.
            use_length_prefix (bool, optional): If True and length variables
                are added, they will be named using prefixes. Otherwise,
                suffixes will be used. Defaults to False.
            **kwargs: Additional keyword arguments are passed to the parent
                class's method.

        Returns:
            list: Lines achieving assignment.

        """
        out = []
        if cls.requires_length_var(dst_var):
            src_var_length = None
            dst_var_length = None
            if isinstance(src_var, dict):
                src_var_length = src_var.get('length_var', None)
            if isinstance(dst_var, dict):
                dst_var_length = dst_var.get('length_var', None)
            if not dont_add_lengths:
                if src_var_length is None:
                    if use_length_prefix:
                        src_var_length = 'length_' + src_var['name']
                    else:
                        src_var_length = src_var['name'] + '_length'
                if dst_var_length is None:
                    if use_length_prefix:
                        dst_var_length = 'length_' + dst_var['name']
                    else:
                        dst_var_length = dst_var['name'] + '_length'
                out += cls.write_assign_to_output(
                    dst_var_length, src_var_length,
                    outputs_in_inputs=outputs_in_inputs)
            elif src_var_length is None:
                if ((dst_var['datatype']['type']
                     in ['1darray', 'ndarray'])):  # pragma: debug
                    raise RuntimeError("Length must be set in order "
                                       "to write array assignments.")
                elif (dst_var['datatype'].get('subtype', dst_var['datatype']['type'])
                      in ['bytes']):
                    src_var_length = '(strlen(%s)+1)' % src_var['name']
                else:
                    src_var_length = '(strlen4(%s)+1)' % src_var['name']
            src_var_dtype = cls.get_native_type(**src_var)
            if src_var_dtype in ['bytes_t', 'unicode_t', 'string_t']:
                src_var_dtype = 'char*'
            src_var_dtype = src_var_dtype.rsplit('*', 1)[0]
            out += cls.write_assign_to_output(
                dst_var['name'], 'value',
                outputs_in_inputs=outputs_in_inputs,
                replacement=('{name} = ({native_type}*)realloc({name}, '
                             '{N}*sizeof({native_type}));'),
                native_type=src_var_dtype, N=src_var_length)
            kwargs.update(copy=True, native_type=src_var_dtype,
                          N=src_var_length)
        elif cls.requires_shape_var(dst_var):
            if dont_add_lengths:  # pragma: debug
                raise RuntimeError("Shape must be set in order "
                                   "to write ND array assignments.")
            # Dimensions
            src_var_ndim = None
            dst_var_ndim = None
            if isinstance(src_var, dict):
                src_var_ndim = src_var.get('ndim_var', None)
            if isinstance(dst_var, dict):
                dst_var_ndim = dst_var.get('ndim_var', None)
            if src_var_ndim is None:
                if use_length_prefix:
                    src_var_ndim = 'ndim_' + src_var['name']
                else:
                    src_var_ndim = src_var['name'] + '_ndim'
            if dst_var_ndim is None:
                if use_length_prefix:
                    dst_var_ndim = 'ndim_' + dst_var['name']
                else:
                    dst_var_ndim = dst_var['name'] + '_ndim'
            if isinstance(src_var_ndim, str):
                src_var_ndim = {'name': src_var_ndim,
                                'datatype': {'type': 'uint',
                                             'precision': 64}}
            if isinstance(dst_var_ndim, str):
                dst_var_ndim = {'name': dst_var_ndim,
                                'datatype': {'type': 'uint',
                                             'precision': 64}}

            out += cls.write_assign_to_output(
                dst_var_ndim, src_var_ndim,
                outputs_in_inputs=outputs_in_inputs)
            # Shape
            src_var_shape = None
            dst_var_shape = None
            if isinstance(src_var, dict):
                src_var_shape = src_var.get('shape_var', None)
            if isinstance(dst_var, dict):
                dst_var_shape = dst_var.get('shape_var', None)
            if src_var_shape is None:
                if use_length_prefix:
                    src_var_shape = 'shape_' + src_var['name']
                else:
                    src_var_shape = src_var['name'] + '_shape'
            if dst_var_shape is None:
                if use_length_prefix:
                    dst_var_shape = 'shape_' + dst_var['name']
                else:
                    dst_var_shape = dst_var['name'] + '_shape'
            if isinstance(src_var_shape, str):
                src_var_shape = {'name': src_var_shape,
                                 'datatype': {'type': '1darray',
                                              'subtype': 'uint',
                                              'precision': 64},
                                 'length_var': src_var_ndim['name']}
            if isinstance(dst_var_shape, str):
                dst_var_shape = {'name': dst_var_shape,
                                 'datatype': {'type': '1darray',
                                              'subtype': 'uint',
                                              'precision': 64},
                                 'length_var': dst_var_ndim['name']}
            out += cls.write_assign_to_output(
                dst_var_shape, src_var_shape,
                outputs_in_inputs=outputs_in_inputs,
                dont_add_lengths=True)
            src_var_dtype = cls.get_native_type(**src_var).rsplit('*', 1)[0]
            if use_length_prefix:
                src_var_length = 'length_' + src_var['name']
            else:
                src_var_length = src_var['name'] + '_length'
            out += (('{length} = 1;\n'
                     'size_t cdim;\n'
                     'for (cdim = 0; cdim < {ndim}; cdim++) {{\n'
                     '  {length} = {length}*{shape}[cdim];\n'
                     '}}\n').format(length=src_var_length,
                                    ndim=src_var_ndim['name'],
                                    shape=src_var_shape['name'])).splitlines()
            out += cls.write_assign_to_output(
                dst_var['name'], 'value',
                outputs_in_inputs=outputs_in_inputs,
                replacement=('{name} = ({native_type}*)realloc({name}, '
                             '{N}*sizeof({native_type}));'),
                native_type=src_var_dtype, N=src_var_length)
            kwargs.update(copy=True, native_type=src_var_dtype,
                          N=src_var_length)
        elif isinstance(dst_var, dict):
            if 'shape' in dst_var.get('datatype', {}):
                nele = 1
                for s in dst_var['datatype']['shape']:
                    nele *= s
                kwargs.update(copy=True, N=nele,
                              native_type=dst_var['datatype']['subtype'])
            elif 'length' in dst_var.get('datatype', {}):
                kwargs.update(copy=True, N=dst_var['datatype']['length'],
                              native_type=dst_var['datatype']['subtype'])
        if outputs_in_inputs and (cls.language != 'c++'):
            if isinstance(dst_var, dict):
                dst_var = dict(dst_var,
                               name='%s[0]' % dst_var['name'])
            else:
                dst_var = '%s[0]' % dst_var
        if ((outputs_in_inputs and isinstance(dst_var, dict)
             and isinstance(dst_var['datatype'], dict)
             and ('copy_' + dst_var['datatype']['type']
                  in cls.function_param))):
            kwargs['copy'] = True
        out += super(CModelDriver, cls).write_assign_to_output(
            dst_var, src_var, outputs_in_inputs=outputs_in_inputs,
            **kwargs)
        return out
