import re


def preprocess_model_attribute_change(task, change):
    """ Process a model change

    * Compartment sizes: targets should follow the pattern ``compartments.<compartment_id>.size``
    * Function expressions: targets should follow the pattern ``functions.<function_id>.expression``
    * Initial species counts: targets should follow the pattern ``species.<species_id>.count``
    * Parameter values: targets should follow the pattern ``parameters.<parameter_id>.value``

    Args:
        task (:obj:`Task`): BioNetGen task
        change (:obj:`ModelAttributeChange`): model attribute change

    Returns:
        :obj:`dict`: processed information about the model change

    Raises:
        :obj:`ValueError`: if a target of a change is not valid
    """
    target = change.target

    compartment_size_match = re.match(r'^compartments\.([^\.]+)(\.size)?$', target)
    if compartment_size_match:
        obj_id = compartment_size_match.group(1)
        block = task.model.get('compartments', [])

        pattern = r'^' + re.escape(obj_id) + r' (\d+) ([^ ]+)( .*?)?$'
        comp_changed = False
        for i_line, line in enumerate(block):
            match = re.match(pattern, line)
            if match:
                comp_changed = True
                return {
                    'type': 'replace_line_in_block',
                    'block': block,
                    'i_line': i_line,
                    'new_line': lambda new_value: '{} {} {} {}'.format(
                        obj_id, match.group(1), new_value, (match.group(3) or '').strip()).strip(),
                }

        if not comp_changed:
            raise ValueError(('The size of compartment `{}` cannot be changed '
                              'because the model does not have a compartment with this id.').format(obj_id))

    parameter_values_match = re.match(r'^parameters\.([^\.]+)(\.value)?$', target)
    if parameter_values_match:
        return {
            'type': 'append_action',
            'action': lambda new_value: 'setParameter("{}", {})'.format(parameter_values_match.group(1), new_value),
        }

    species_counts_match = re.match(r'^species\.([^\.]+)\((.*?)\)(\.initialCount)?$', target)
    if species_counts_match:
        return {
            'type': 'append_action',
            'action': lambda new_value: 'setConcentration("{}({})", {})'.format(
                species_counts_match.group(1), species_counts_match.group(2), new_value),
        }

    functions_expression_match = re.match(r'^functions\.([^\.\(\)]+)(\.expression)?$', target)
    if functions_expression_match:
        obj_id = functions_expression_match.group(1)

        block = task.model.get('functions', [])

        func_changed = False
        pattern = r'^' + re.escape(obj_id) + r'\((.*?)\) *= *(.*?)$'
        for i_line, line in enumerate(block):
            match = re.match(pattern, line)
            if match:
                func_changed = True
                return {
                    'type': 'replace_line_in_block',
                    'block': block,
                    'i_line': i_line,
                    'new_line': lambda new_value: '{}({}) = {}'.format(obj_id, match.group(1), new_value),
                }

        if not func_changed:
            raise ValueError(('The expression of function `{}` cannot be changed '
                              'because the model does not have a function with this id.').format(obj_id))

    function_args_expression_match = re.match(r'^functions\.([^\.]+)\((.*?)\)(\.expression)?$', target)
    if function_args_expression_match:
        obj_id = function_args_expression_match.group(1)
        obj_args = function_args_expression_match.group(2)

        block = task.model.get('functions', [])

        func_changed = False
        pattern = r'^' + re.escape(obj_id) + r'\(.*?\) *= *(.*?)$'
        for i_line, line in enumerate(block):
            match = re.match(pattern, line)
            if match:
                func_changed = True
                return {
                    'type': 'replace_line_in_block',
                    'block': block,
                    'i_line': i_line,
                    'new_line': lambda new_value: '{}({}) = {}'.format(obj_id, obj_args, new_value),
                }

        if not func_changed:
            raise ValueError(('The expression of function `{}` cannot be changed '
                              'because the model does not have a function with this id.').format(obj_id))

    target_patterns = {
        'compartment size': compartment_size_match,
        'parameter value': parameter_values_match,
        'initial species count/concentration': species_counts_match,
        'function expression': functions_expression_match,
        'function arguments and expression': function_args_expression_match,
    }
    msg = '`{}` is not a valid target. The following patterns of targets are supported:\n  - {}'.format(
        target, '\n  - '.join('{}: `{}`'.format(key, target_patterns[key]) for key in sorted(target_patterns.keys())))
    raise NotImplementedError(msg)