# Copyright 2023-2024 Blue Brain Project / EPFL

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

#     http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Module that handles the global NEURON parameters."""

import logging
from typing import Optional
from typing import NamedTuple
import neuron
from bluecellulab.circuit.config.sections import Conditions, MechanismConditions
from bluecellulab.exceptions import error_context

logger = logging.getLogger(__name__)


def set_global_condition_parameters(condition_parameters: Conditions) -> None:
    """Sets the global condition parameters in NEURON objects if GluSynapse is
    available."""
    if condition_parameters.extracellular_calcium is not None and hasattr(
        neuron.h, "cao_CR_GluSynapse"
    ):
        cao_cr_glusynapse = condition_parameters.extracellular_calcium
        with error_context("mechanism/s for cao_CR_GluSynapse need to be compiled"):
            neuron.h.cao_CR_GluSynapse = cao_cr_glusynapse

    mechanism_conditions = condition_parameters.mech_conditions
    if mechanism_conditions is not None:
        set_minis_single_vesicle_values(mechanism_conditions)
        set_init_depleted_values(mechanism_conditions)

    # Apply all mechanism variables generically (matching neurodamus behavior)
    if condition_parameters.mechanisms:
        for suffix, variables in condition_parameters.mechanisms.items():
            for var_name, value in variables.items():
                global_name = f"{var_name}_{suffix}"
                if hasattr(neuron.h, global_name):
                    setattr(neuron.h, global_name, value)
                    logger.debug("Set NEURON global %s = %s", global_name, value)


def set_init_depleted_values(mech_conditions: MechanismConditions) -> None:
    """Set the init_depleted values in NEURON."""
    with error_context("mechanism/s for init_depleted need to be compiled"):
        if (
            mech_conditions.glusynapse
            and mech_conditions.glusynapse.init_depleted is not None
        ):
            neuron.h.init_depleted_GluSynapse = mech_conditions.glusynapse.init_depleted
        if (
            mech_conditions.ampanmda
            and mech_conditions.ampanmda.init_depleted is not None
        ):
            neuron.h.init_depleted_ProbAMPANMDA_EMS = (
                mech_conditions.ampanmda.init_depleted
            )
        if mech_conditions.gabaab and mech_conditions.gabaab.init_depleted is not None:
            neuron.h.init_depleted_ProbGABAAB_EMS = mech_conditions.gabaab.init_depleted


def set_minis_single_vesicle_values(mech_conditions: MechanismConditions) -> None:
    """Set the minis_single_vesicle values in NEURON."""
    with error_context("mechanism/s for minis_single_vesicle need to be compiled"):
        if (
            mech_conditions.ampanmda
            and mech_conditions.ampanmda.minis_single_vesicle is not None
        ):
            neuron.h.minis_single_vesicle_ProbAMPANMDA_EMS = (
                mech_conditions.ampanmda.minis_single_vesicle
            )
        if (
            mech_conditions.gabaab
            and mech_conditions.gabaab.minis_single_vesicle is not None
        ):
            neuron.h.minis_single_vesicle_ProbGABAAB_EMS = (
                mech_conditions.gabaab.minis_single_vesicle
            )
        if (
            mech_conditions.glusynapse
            and mech_conditions.glusynapse.minis_single_vesicle is not None
        ):
            neuron.h.minis_single_vesicle_GluSynapse = (
                mech_conditions.glusynapse.minis_single_vesicle
            )


class NeuronGlobalParams(NamedTuple):
    temperature: float
    v_init: float


class NeuronGlobals:
    _instance = None

    def __init__(self):
        raise RuntimeError("Call get_instance() instead")

    @classmethod
    def get_instance(cls):
        if cls._instance is None:
            cls._instance = cls.__new__(cls)
            # Initialize default values
            cls._instance._temperature = 34.0  # Default temperature
            cls._instance.v_init = neuron.h.v_init
            # Set the default values in NEURON
            neuron.h.celsius = cls._instance._temperature
        return cls._instance

    @property
    def temperature(self):
        return self._temperature

    @temperature.setter
    def temperature(self, value):
        self._temperature = value
        neuron.h.celsius = value

    @property
    def v_init(self):
        return self._v_init

    @v_init.setter
    def v_init(self, value):
        self._v_init = value
        neuron.h.v_init = value

    def export_params(self) -> NeuronGlobalParams:
        return NeuronGlobalParams(self.temperature, self.v_init)

    def load_params(self, params: NeuronGlobalParams) -> None:
        self.temperature = params.temperature
        self.v_init = params.v_init


def set_neuron_globals(
    temperature: Optional[float] = 34.0, v_init: Optional[float] = -80.0
) -> None:
    """Set the global NEURON parameters."""
    if temperature is None and v_init is None:
        return
    neuron_globals = NeuronGlobals.get_instance()
    if temperature is not None:
        neuron_globals.temperature = temperature
    if v_init is not None:
        neuron_globals.v_init = v_init
