'''create JSON/ACC files for Arbor from a set of BluePyOpt.ephys parameters'''

# pylint: disable=R0914

import io
import logging
import pathlib
from collections import ChainMap, namedtuple, OrderedDict
import re

import jinja2
import json
import shutil

from bluepyopt.ephys.acc import arbor
from bluepyopt.ephys.morphologies import ArbFileMorphology
from bluepyopt.ephys.create_hoc import \
    Location, RangeExpr, PointExpr, \
    _get_template_params, format_float

logger = logging.getLogger(__name__)

# Inhomogeneous expression for scaled parameter in Arbor
RangeIExpr = namedtuple('RangeIExpr', 'name, value, scale')


class ArbVar:
    """Definition of a Neuron to Arbor parameter conversion"""

    def __init__(self, name, conv=None):
        """Constructor

        Args:
            name (str): Arbor parameter name
            conv (): Conversion of parameter value from Neuron units
            to Arbor (defaults to identity)
        """
        self.name = name
        self.conv = conv

    def __repr__(self):
        return 'ArbVar(%s, %s)' % (self.name, self.conv)


class Nrn2ArbParamAdapter:
    """Converts a Neuron parameter to Arbor format (name and value)"""

    _mapping = dict(
        v_init=ArbVar(name='membrane-potential'),
        celsius=ArbVar(name='temperature-kelvin',
                       conv=lambda celsius: celsius + 273.15),
        Ra=ArbVar(name='axial-resistivity'),
        cm=ArbVar(name='membrane-capacitance',
                  conv=lambda cm: cm / 100.),  # NEURON: uF/cm^2, Arbor: F/m^2
        **{species + loc[0]:
            ArbVar(name='ion-%sternal-concentration \"%s\"' % (loc, species))
            for species in ['na', 'k', 'ca'] for loc in ['in', 'ex']},
        **{'e' + species:
            ArbVar(name='ion-reversal-potential \"%s\"' % species)
            for species in ['na', 'k', 'ca']}
    )

    @classmethod
    def _param_name(cls, name):
        """Neuron to Arbor parameter renaming

        Args:
            name (str): Neuron parameter name
        """
        return cls._mapping[name].name if name in cls._mapping else name

    @classmethod
    def _param_value(cls, param):
        """Neuron to Arbor units conversion for parameter values

        Args:
            param (): A Neuron parameter with a value in Neuron units
        """

        if param.name in cls._mapping and \
                cls._mapping[param.name].conv is not None:
            return format_float(
                cls._mapping[param.name].conv(float(param.value)))
        else:
            return param.value if isinstance(param.value, str) else \
                format_float(param.value)

    @classmethod
    def _conv_param(cls, param, name):
        """Convert a Neuron parameter to Arbor format (name and units)

        Args:
            param (): A Neuron parameter
            name (): Parameter name without mech prefix/suffix
        """

        if isinstance(param, Location):
            return Location(name=cls._param_name(name),
                            value=cls._param_value(param))
        elif isinstance(param, RangeExpr):
            return RangeExpr(location=param.location,
                             name=cls._param_name(name),
                             value=cls._param_value(param),
                             value_scaler=param.value_scaler)
        elif isinstance(param, PointExpr):
            return PointExpr(name=cls._param_name(name),
                             point_loc=param.point_loc,
                             value=cls._param_value(param))
        else:
            raise CreateAccException(
                'Unsupported parameter expression type %s.' % type(param))

    @classmethod
    def format(cls, param, mechs):
        """Find a parameter's mechanism and convert name to Arbor format

        Args:
            param (): A parameter in Neuron format
            mechs (): List of co-located NMODL mechanisms

        Returns:
            A tuple of mechanism name (None for a non-mechanism parameter) and
            parameter in Arbor format
        """
        if not isinstance(param, PointExpr):
            mech_matches = [i for i, mech in enumerate(mechs)
                            if param.name.endswith("_" + mech)]
        else:
            param_pprocesses = [loc.pprocess_mech for loc in param.point_loc]
            mech_matches = [i for i, mech in enumerate(mechs)
                            if mech in param_pprocesses]

        if len(mech_matches) == 0:
            return None, cls._conv_param(param, name=param.name)

        elif len(mech_matches) == 1:
            mech = mechs[mech_matches[0]]
            if not isinstance(param, PointExpr):
                name = param.name[:-(len(mech) + 1)]
            else:
                name = param.name
            return mech, cls._conv_param(param, name=name)

        else:
            raise CreateAccException("Parameter name %s matches" % param.name +
                                     " multiple mechanisms %s" %
                                     [repr(mechs[i]) for i in mech_matches])


class Nrn2ArbMechGrouper:
    """Group parameters by mechanism and convert them to Arbor format"""

    @staticmethod
    def _is_global_property(loc, param):
        """Returns if a label-specific variable is a global property in Arbor

        Args:
            loc (): An Arbor label describing the location
            param (): A parameter in Arbor format (name and units)
        """

        return loc == ArbFileMorphology.region_labels['all'] and (
            param.name in ['membrane-potential',
                           'temperature-kelvin',
                           'axial-resistivity',
                           'membrane-capacitance'] or
            param.name.split(' ')[0] in ['ion-internal-concentration',
                                         'ion-external-concentration',
                                         'ion-reversal-potential'])

    @classmethod
    def _separate_global_properties(cls, loc, mechs):
        """Separates global properties from a label-specific dict of mechanisms

        Args:
            loc (): An Arbor label describing the location
            mechs (): A mapping of mechanism name to list of parameters in
            Arbor format (None for non-mechanism parameters).

        Returns:
            A split of mechs into mechanisms without Arbor global properties
            (first component) and a dict with Arbor global properties
            (second component)
        """

        local_mechs = dict()
        global_properties = []

        for mech, params in mechs.items():

            if mech is None:
                local_properties = []
                for param in params:
                    if cls._is_global_property(loc, param):
                        global_properties.append(param)
                    else:
                        local_properties.append(param)
                local_mechs[mech] = local_properties
            else:
                local_mechs[mech] = params

        return local_mechs, {None: global_properties}

    @staticmethod
    def _format_params_and_group_by_mech(params, channels):
        """Group list of parameters by mechanism and turn them to Arbor format

        Args:
            params (): List of parameters in Neuron format
            channels (): List of co-located NMODL mechanisms

        Returns:
            Mapping of Arbor mechanism name to list of parameters in Arbor
            format
        """
        mech_params = [Nrn2ArbParamAdapter.format(
                       param, channels) for param in params]
        mechs = {mech: [] for mech, _ in mech_params}
        for mech in channels:
            if mech not in mechs:
                mechs[mech] = []
        for mech, param in mech_params:
            mechs[mech].append(param)
        return mechs

    @classmethod
    def process_global(cls, params):
        """Group global BluePyOpt params by mech, convert them to Arbor format

        Args:
            params (): List of global parameters in Neuron format

        Returns:
            A mapping of mechanism to parameters representing Arbor global
            properties. The mechanism parameters are in Arbor format
            (mechanism name is None for non-mechanism parameters).
        """
        return cls._format_params_and_group_by_mech(
            [Location(name=name, value=value)
             for name, value in params.items()],
            []  # no default mechanisms
        )

    @classmethod
    def process_local(cls, params, channels):
        """Group local BluePyOpt params by mech, convert them to Arbor format

        Args:
            params (): List of Arbor label/local parameters pairs in Neuron
            format
            channels (): Mapping of Arbor label to co-located NMODL mechanisms

        Returns:
            The return value is a tuple. In the first component, a two-level
            mapping of Arbor label to mechanism to parameters. The mechanism
            parameters are in Arbor format (mechanism name is None for
            non-mechanism parameters). In the second component, the
            Arbor global properties found are returned.
        """
        local_mechs = dict()
        global_properties = dict()
        for loc, loc_params in params:
            mechs = cls._format_params_and_group_by_mech(
                loc_params, channels[loc])

            # move Arbor global properties to global_params
            mechs, global_props = cls._separate_global_properties(loc, mechs)
            if global_props.keys() != {None}:
                raise CreateAccException(
                    'Support for Arbor default mechanisms not implemented.')
            # iterate over global_props items if above exception triggers
            global_properties[None] = \
                global_properties.get(None, []) + global_props[None]
            local_mechs[loc] = mechs
        return local_mechs, global_properties


def _arb_filter_point_proc_locs(pprocess_mechs):
    """Filter locations from point process parameters

    Args:
        pprocess_mechs (): Point process mechanisms with parameters in
        Arbor format
    """

    result = {loc: dict() for loc in pprocess_mechs}

    for loc, mechs in pprocess_mechs.items():
        for mech, point_exprs in mechs.items():
            result[loc][mech.name] = dict(
                mech=mech.suffix,
                params=[Location(point_expr.name, point_expr.value)
                        for point_expr in point_exprs])

    return result


def _arb_append_scaled_mechs(mechs, scaled_mechs):
    """Append scaled mechanism parameters to constant ones"""
    for mech, scaled_params in scaled_mechs.items():
        if mech is None and len(scaled_params) > 0:
            raise CreateAccException(
                'Non-mechanism parameters cannot have inhomogeneous'
                ' expressions in Arbor %s' % scaled_params)
        mechs[mech] = mechs.get(mech, []) + \
            [RangeIExpr(
                name=p.name,
                value=p.value,
                scale=p.value_scaler.acc_scale_iexpr(p.value))
                for p in scaled_params]


# An mechanism's NMODL GLOBAL and RANGE variables in Arbor
MechMetaData = namedtuple('MechMetaData', 'globals, ranges')


class ArbNmodlMechFormatter:
    """Loads catalogue metadata and reformats mechanism name for ACC"""

    def __init__(self, ext_catalogues):
        """Load metadata of external and Arbor's built-in mechanism catalogues

        Args:
            ext_catalogues (): Mapping of catalogue name to directory
            with NMODL files defining the mechanisms.
        """
        self.cats = self._load_mech_catalogue_meta(ext_catalogues)

    @staticmethod
    def _load_catalogue_meta(cat_dir):
        """Load mechanism catalogue metadata from NMODL files

        Args:
            cat_dir (): Path to directory with NMODL files of catalogue

        Returns:
            Mapping of name to meta data for each mechanism in the directory
        """
        # used to generate arbor_mechanisms.json on NMODL from arbor/mechanisms

        nmodl_pattern = '^\s*%s\s+((?:\w+\,\s*)*?\w+)\s*?$'  # NOQA
        suffix_pattern = nmodl_pattern % 'SUFFIX'
        globals_pattern = nmodl_pattern % 'GLOBAL'
        ranges_pattern = nmodl_pattern % 'RANGE'

        def process_nmodl(nmodl_str):
            """Extract global and range params from Arbor-conforming NMODL"""
            try:
                nrn = re.search(r'NEURON\s+{([^}]+)}', nmodl_str,
                                flags=re.MULTILINE).group(1)
                suffix_ = re.search(suffix_pattern, nrn,
                                    flags=re.MULTILINE)
                suffix_ = suffix_ if suffix_ is None else suffix_.group(1)
                globals_ = re.search(globals_pattern, nrn,
                                     flags=re.MULTILINE)
                globals_ = globals_ if globals_ is None \
                    else re.findall(r'\w+', globals_.group(1))
                ranges_ = re.search(ranges_pattern, nrn,
                                    flags=re.MULTILINE)
                ranges_ = ranges_ if ranges_ is None \
                    else re.findall(r'\w+', ranges_.group(1))
            except Exception as e:
                raise CreateAccException(
                    'NMODL-inspection for %s failed.' % nmodl_file) from e

            # skipping suffix_
            return MechMetaData(globals=globals_, ranges=ranges_)

        mechs = dict()
        cat_dir = pathlib.Path(cat_dir)
        for nmodl_file in cat_dir.glob('*.mod'):
            with open(cat_dir.joinpath(nmodl_file)) as f:
                mechs[nmodl_file.stem] = process_nmodl(f.read())

        return mechs

    @classmethod
    def _load_mech_catalogue_meta(cls, ext_catalogues):
        """Load metadata of external and Arbor's built-in mechanism catalogues

        Args:
            ext_catalogues (): Mapping of catalogue name to directory
            with NMODL files defining the mechanisms

        Returns:
            Ordered mapping of catalogue name -> mechanism name -> meta data
            for external and built-in catalogues (external ones taking
            precedence)
        """

        arb_cats = OrderedDict()

        if ext_catalogues is not None:
            for cat, cat_nmodl in ext_catalogues.items():
                arb_cats[cat] = cls._load_catalogue_meta(
                    pathlib.Path(cat_nmodl).resolve())

        builtin_catalogues = pathlib.Path(__file__).parent.joinpath(
            'static/arbor_mechanisms.json').resolve()
        with open(builtin_catalogues) as f:
            builtin_arb_cats = json.load(f)

        for cat in ['BBP', 'default', 'allen']:
            if cat not in arb_cats:
                arb_cats[cat] = {
                    mech: MechMetaData(**meta)
                    for mech, meta in builtin_arb_cats[cat].items()}

        return arb_cats

    @staticmethod
    def _mech_name(name):
        """Neuron to Arbor mechanism name conversion

        Args:
            name (): A Neuron mechanism name
        """
        if name in ['Exp2Syn', 'ExpSyn']:
            return name.lower()
        else:
            return name

    @classmethod
    def _translate_mech(cls, mech_name, mech_params, arb_cats):
        """Translate NMODL mechanism to Arbor ACC format

        Args:
            mech_name (): NMODL mechanism name (suffix)
            mech_params (): Mechanism parameters in Arbor format
            arb_cats (): Mapping of catalogue names to mechanisms
            with theirmeta data

        Returns:
            Tuple of mechanism name with NMODL GLOBAL parameters integrated and
            catalogue prefix added as well as the remaining RANGE parameters
        """

        arb_mech = None
        arb_mech_name = cls._mech_name(mech_name)

        for cat in arb_cats:  # in order of precedence
            if arb_mech_name in arb_cats[cat]:
                arb_mech = arb_cats[cat][arb_mech_name]
                mech_name = cat + '::' + arb_mech_name
                break

        if arb_mech is None:  # not Arbor built-in mech, no qualifier added
            if mech_name is not None:
                logger.warn(
                    'create_acc: Could not find Arbor mech for %s (%s).'
                    % (mech_name, mech_params))
            return (mech_name, mech_params)
        else:
            if arb_mech.globals is None:  # only Arbor range params
                for param in mech_params:
                    if param.name not in arb_mech.ranges:
                        raise CreateAccException(
                            '%s not a GLOBAL or RANGE parameter of %s' %
                            (param.name, mech_name))
                return (mech_name, mech_params)
            else:
                for param in mech_params:
                    if param.name not in arb_mech.globals and \
                            param.name not in arb_mech.ranges:
                        raise CreateAccException(
                            '%s not a GLOBAL or RANGE parameter of %s' %
                            (param.name, mech_name))
                mech_name_suffix = []
                remaining_mech_params = []
                for mech_param in mech_params:
                    if mech_param.name in arb_mech.globals:
                        mech_name_suffix.append(mech_param.name + '=' +
                                                mech_param.value)
                        if isinstance(mech_param, RangeIExpr):
                            remaining_mech_params.append(
                                RangeIExpr(name=mech_param.name,
                                           value=None,
                                           scale=mech_param.scale))
                    else:
                        remaining_mech_params.append(mech_param)
                if len(mech_name_suffix) > 0:
                    mech_name += '/' + ','.join(mech_name_suffix)
                return (mech_name, remaining_mech_params)

    def translate_density(self, mechs):
        """Translate all density mechanisms in a specific region"""
        return dict([self._translate_mech(mech, params, self.cats)
                    for mech, params in mechs.items()])

    def translate_points(self, mechs):
        """Translate all point mechanisms for a specific locset"""
        result = dict()

        for synapse_name, mech_desc in mechs.items():
            mech, params = self._translate_mech(
                mech_desc['mech'], mech_desc['params'], self.cats)
            result[synapse_name] = dict(mech=mech,
                                        params=params)

        return result


def _arb_project_scaled_mechs(mechs):
    """Returns all (iexpr) parameters of scaled mechanisms in Arbor"""
    scaled_mechs = dict()
    for mech, params in mechs.items():
        range_iexprs = [p for p in params if isinstance(p, RangeIExpr)]
        if len(range_iexprs) > 0:
            scaled_mechs[mech] = range_iexprs
    return scaled_mechs


def _arb_populate_label_dict(local_mechs, local_scaled_mechs, pprocess_mechs):
    """Creates a dict of labels from label-specific parameters/mechanisms

    Args:
        local_mechs (): label-specific parameters/density mechanisms
        local_scaled_mechs (): label-specific iexpr parameters/density mechs
        pprocess_mechs (): label-specific point processes

    Returns:
        A dict mapping label name to ArbLabel for each label in the input
    """

    label_dict = dict()

    acc_labels = ChainMap(local_mechs, local_scaled_mechs, pprocess_mechs)

    for acc_label in acc_labels:
        if acc_label.name in label_dict and \
                acc_label != label_dict[acc_label.name]:
            raise CreateAccException(
                'Label %s already exists in' % acc_label.name +
                ' label_dict with different s-expression: '
                ' %s != %s.' % (label_dict[acc_label.name].loc,
                                acc_label.loc))
        elif acc_label.name not in label_dict:
            label_dict[acc_label.name] = acc_label

    return label_dict


def _read_templates(template_dir, template_filename):
    """Expand Jinja2 template filepath with glob and
     return dict of target filename -> parsed template"""
    if template_dir is None:
        template_dir = \
            pathlib.Path(__file__).parent.joinpath('templates').resolve()

    template_paths = pathlib.Path(template_dir).glob(template_filename)

    templates = dict()
    for template_path in template_paths:
        with open(template_path) as template_file:
            template = template_file.read()
            name = template_path.name
            if name.endswith('.jinja2'):
                name = name[:-7]
            if name.endswith('_template'):
                name = name[:-9]
            if '_' in name:
                name = '.'.join(name.rsplit('_', 1))
            templates[name] = jinja2.Template(template)

    if templates == {}:
        raise FileNotFoundError(
            f'No templates found for JSON/ACC-export in {template_dir}')

    return templates


def _arb_loc_desc(location, param_or_mech):
    """Generate Arbor location description for label dict and decor"""
    return location.acc_label()


def create_acc(mechs,
               parameters,
               morphology=None,
               morphology_dir=None,
               ext_catalogues=None,
               ignored_globals=(),
               replace_axon=None,
               create_mod_morph=False,
               template_name='CCell',
               template_filename='acc/*_template.jinja2',
               disable_banner=None,
               template_dir=None,
               custom_jinja_params=None):
    '''return a dict with strings containing the rendered JSON/ACC templates

    Args:
        mechs (): All the mechs for the decor template
        parameters (): All the parameters in the decor/label-dict template
        morphology (str): Name of morphology
        morphology_dir (str): Directory of morphology
        ext_catalogues (): Name to path mapping of non-Arbor built-in
        NMODL mechanism catalogues compiled with modcc
        ignored_globals (iterable str): Skipped NrnGlobalParameter in decor
        replace_axon (): Axon replacement morphology
        create_mod_morph (): Create ACC morphology with axon replacement
        template_filename (str): file path of the cell.json , decor.acc and
        label_dict.acc jinja2 templates (with wildcards expanded by glob)
        template_dir (str): dir name of the jinja2 templates
        custom_jinja_params (dict): dict of additional jinja2 params in case
        of a custom template
    '''

    if custom_jinja_params is None:
        custom_jinja_params = {}

    if pathlib.Path(morphology).suffix.lower() not in ['.swc', '.asc']:
        raise CreateAccException("Morphology file %s not supported in Arbor "
                                 " (only supported types are .swc and .asc)."
                                 % morphology)

    if replace_axon is not None:
        if not hasattr(arbor.segment_tree, 'tag_roots'):
            raise NotImplementedError("Need a newer version of Arbor"
                                      " for axon replacement.")
        logger.debug("Obtain axon replacement by applying "
                     "ArbFileMorphology.replace_axon after loading "
                     "morphology in Arbor.")
        replace_axon_path = \
            pathlib.Path(morphology).stem + '_axon_replacement.acc'
        replace_axon_acc = io.StringIO()
        arbor.write_component(replace_axon, replace_axon_acc)
        replace_axon_acc.seek(0)

        if create_mod_morph:
            modified_morphology_path = \
                pathlib.Path(morphology).stem + '_modified.acc'
            modified_morpho = ArbFileMorphology.load(
                pathlib.Path(morphology_dir).joinpath(morphology),
                replace_axon_acc)
            replace_axon_acc.seek(0)
            modified_morphology_acc = io.StringIO()
            arbor.write_component(
                modified_morpho, modified_morphology_acc)
            modified_morphology_acc.seek(0)
            modified_morphology_acc = modified_morphology_acc.read()
        else:
            modified_morphology_path = None
            modified_morphology_acc = None

        replace_axon_acc = replace_axon_acc.read()
    else:
        replace_axon_path = None
        modified_morphology_path = None

    templates = _read_templates(template_dir, template_filename)

    default_location_order = list(ArbFileMorphology.region_labels.values())

    template_params = _get_template_params(mechs,
                                           parameters,
                                           ignored_globals,
                                           disable_banner,
                                           default_location_order,
                                           _arb_loc_desc)

    filenames = {
        name: template_name + (name if name.startswith('.') else "_" + name)
        for name in templates.keys()}

    # postprocess template parameters for Arbor
    channels = template_params['channels']
    point_channels = template_params['point_channels']
    banner = template_params['banner']

    # global_mechs refer to default density mechs/params in Arbor
    # [mech -> param] (params under mech == None)
    global_mechs = \
        Nrn2ArbMechGrouper.process_global(
            template_params['global_params'])

    # local_mechs refer to locally painted density mechs/params in Arbor
    # [label -> mech -> param.name/.value] (params under mech == None)
    local_mechs, additional_global_mechs = \
        Nrn2ArbMechGrouper.process_local(
            template_params['section_params'], channels)
    for mech, params in additional_global_mechs.items():
        global_mechs[mech] = \
            global_mechs.get(mech, []) + params

    # scaled_mechs refer to iexpr params of scaled density mechs in Arbor
    # [label -> mech -> param.location/.name/.value/.value_scaler]
    range_params = {loc: [] for loc in default_location_order}
    for param in template_params['range_params']:
        range_params[param.location].append(param)
    range_params = list(range_params.items())

    local_scaled_mechs, global_scaled_mechs = \
        Nrn2ArbMechGrouper.process_local(
            range_params, channels)

    # join each mech's constant params with inhomogeneous ones on mechanisms
    _arb_append_scaled_mechs(global_mechs, global_scaled_mechs)
    for loc in local_scaled_mechs:
        _arb_append_scaled_mechs(local_mechs[loc], local_scaled_mechs[loc])

    # pprocess_mechs refer to locally placed mechs/params in Arbor
    # [label -> mech -> param.name/.value]
    pprocess_mechs, global_pprocess_mechs = \
        Nrn2ArbMechGrouper.process_local(
            template_params['pprocess_params'], point_channels)
    if any(len(params) > 0 for params in global_pprocess_mechs.values()):
        raise CreateAccException('Point process mechanisms cannot be'
                                 ' placed globally in Arbor.')

    # Evaluate synapse locations
    # (no new labels introduced, but locations explicitly defined)
    pprocess_mechs = _arb_filter_point_proc_locs(pprocess_mechs)

    # NMODL formatter loads metadata of external and Arbor's built-in
    # mech catalogues
    nmodl_formatter = ArbNmodlMechFormatter(ext_catalogues)

    # translate mechs to Arbor's nomenclature
    global_mechs = nmodl_formatter.translate_density(global_mechs)
    local_mechs = {
        loc: nmodl_formatter.translate_density(mechs)
        for loc, mechs in local_mechs.items()}
    pprocess_mechs = {
        loc: nmodl_formatter.translate_points(mechs)
        for loc, mechs in pprocess_mechs.items()}

    # get iexpr parameters of scaled density mechs
    global_scaled_mechs = _arb_project_scaled_mechs(global_mechs)
    local_scaled_mechs = {loc: _arb_project_scaled_mechs(mechs)
                          for loc, mechs in local_mechs.items()}

    # populate label dict
    label_dict = _arb_populate_label_dict(local_mechs,
                                          local_scaled_mechs,
                                          pprocess_mechs)

    ret = {filenames[name]:
           template.render(template_name=template_name,
                           banner=banner,
                           morphology=morphology,
                           replace_axon=replace_axon_path,
                           modified_morphology=modified_morphology_path,
                           filenames=filenames,
                           label_dict=label_dict,
                           global_mechs=global_mechs,
                           global_scaled_mechs=global_scaled_mechs,
                           local_mechs=local_mechs,
                           local_scaled_mechs=local_scaled_mechs,
                           pprocess_mechs=pprocess_mechs,
                           **custom_jinja_params)
           for name, template in templates.items()}

    if replace_axon is not None:
        ret[replace_axon_path] = replace_axon_acc
        if modified_morphology_path is not None:
            ret[modified_morphology_path] = modified_morphology_acc

    return ret


def write_acc(output_dir, cell, parameters,
              template_filename='acc/*_template.jinja2',
              ext_catalogues=None,
              create_mod_morph=False,
              sim=None):
    '''Output mixed JSON/ACC format for Arbor cable cell to files

    Args:
        output_dir (str): Output directory. If not exists, will be created
        cell (): Cell model to output
        parameters (): Values for mechanism parameters, etc.
        template_filename (str): file path of the cell.json , decor.acc and
        label_dict.acc jinja2 templates (with wildcards expanded by glob)
        ext_catalogues (): Name to path mapping of non-Arbor built-in
        NMODL mechanism catalogues compiled with modcc
        create_mod_morph (str): Output ACC with axon replacement
        sim (): Neuron simulator instance (only used used with axon
        replacement if morphology has not yet been instantiated)
    '''
    output = cell.create_acc(parameters, template=template_filename,
                             ext_catalogues=ext_catalogues,
                             create_mod_morph=create_mod_morph,
                             sim=sim)

    cell_json = [comp_rendered
                 for comp, comp_rendered in output.items()
                 if pathlib.Path(comp).suffix == '.json']
    if len(cell_json) != 1:
        raise CreateAccException(
            'JSON file from create_acc is non-unique: %s' % cell_json)

    cell_json = json.loads(cell_json[0])

    output_dir = pathlib.Path(output_dir)
    if not output_dir.exists():
        output_dir.mkdir()
    for comp, comp_rendered in output.items():
        comp_filename = output_dir.joinpath(comp)
        if comp_filename.exists():
            raise CreateAccException("%s already exists!" % comp_filename)
        with open(output_dir.joinpath(comp), 'w') as f:
            f.write(comp_rendered)

    morpho_filename = output_dir.joinpath(cell_json['morphology']['original'])
    if morpho_filename.exists():
        raise CreateAccException("%s already exists!" % morpho_filename)
    shutil.copy2(cell.morphology.morphology_path, morpho_filename)


# Read the mixed JSON/ACC-output, to be moved to Arbor in future release
def read_acc(cell_json_filename):
    '''Return constituents to build an Arbor cable cell from create_acc-export

    Args:
        cell_json_filename (str): The path to the JSON file containing
        meta-information on morphology, label-dict and decor of exported cell
    '''

    with open(cell_json_filename) as cell_json_file:
        cell_json = json.load(cell_json_file)

    cell_json_dir = pathlib.Path(cell_json_filename).parent

    morpho_filename = cell_json_dir.joinpath(
        cell_json['morphology']['original'])
    replace_axon = cell_json['morphology'].get('replace_axon', None)
    if replace_axon is not None:
        replace_axon = cell_json_dir.joinpath(replace_axon)
    morpho = ArbFileMorphology.load(morpho_filename, replace_axon)

    decor = arbor.load_component(
        cell_json_dir.joinpath(cell_json['decor'])).component
    labels = arbor.load_component(
        cell_json_dir.joinpath(cell_json['label_dict'])).component

    return cell_json, morpho, decor, labels


class CreateAccException(Exception):

    """Exceptions generated by create_acc module"""

    def __init__(self, message):
        """Constructor"""

        super(CreateAccException, self).__init__(message)
