class ReactionKnockoutTarget(ReactionModulationTarget):
    """
    Reaction Knockout Target. Knockout a reaction present in a COBRA model.
    """

    def __init__(self, id):
        super(ReactionKnockoutTarget, self).__init__(id, 0, None)

    def apply(self, model):
        target = self.get_model_target(model)
        target.knock_out()

    def __gt__(self, other):
        if self.id == other.id:
            if isinstance(other, ReactionModulationTarget):
                if other._value == 0:
                    return True
                else:
                    raise IncompatibleTargets(self, other)
            elif isinstance(other, ReactionCofactorSwapTarget):
                raise IncompatibleTargets(self, other)
            elif isinstance(other, EnsembleTarget):
                return not other > self
            else:
                raise IncompatibleTargets(self, other)
        else:
            return self.id > other.id

    def __eq__(self, other):
        if isinstance(other, ReactionKnockoutTarget):
            return self.id == other.id
        elif isinstance(other, ReactionModulationTarget):
            return self.id == other.id and other._value == 0
        else:
            return False

    def __repr__(self):
        return "<ReactionKnockout %s>" % self.id

    def __hash__(self):
        return hash(str(self)) class ReactionModulationTarget(FluxModulationTarget):
    __gnomic_feature_type__ = "reaction"

    def __init__(self, id, value, reference_value, **kwargs):
        super(ReactionModulationTarget, self).__init__(
            id, value, reference_value, **kwargs
        )

    def get_model_target(self, model):
        return model.reactions.get_by_id(self.id)

    def __gt__(self, other):
        if self.id == other.id:
            if isinstance(other, ReactionKnockinTarget):
                if self._value == 0:
                    raise IncompatibleTargets(self, other)
                else:
                    return True
            elif isinstance(other, ReactionCofactorSwapTarget):
                return True
            elif isinstance(other, ReactionModulationTarget) and not isinstance(other, ReactionKnockoutTarget):
                return self.fold_change > other.fold_change
            elif isinstance(other, EnsembleTarget):
                return not other > self
            else:
                raise IncompatibleTargets(self, other)
        else:
            return self.id > other.id

    def __eq__(self, other):
        if isinstance(other, ReactionModulationTarget):
            return (
                (self.id == other.id) and (
                    self._value == other._value) and (
                        self._reference_value == other._reference_value)
            )
        else:
            return False

    def __hash__(self):
        return hash(str(self)) class GeneKnockoutTarget(GeneModulationTarget):
    """
    Gene Knockout Target. Knockout a gene present in a COBRA model.
    """

    def __init__(self, id, *args, **kwargs):
        super(GeneKnockoutTarget, self).__init__(id, 0, None, *args, **kwargs)

    def apply(self, model):
        target = self.get_model_target(model)
        target.knock_out()

    def __gt__(self, other):
        if self.id == other.id:
            if isinstance(other, GeneModulationTarget) and not isinstance(other, GeneKnockoutTarget):
                return self.fold_change > other.fold_change
            elif isinstance(other, GeneKnockoutTarget):
                return False
            elif isinstance(other, EnsembleTarget):
                return not other > self
            else:
                raise IncompatibleTargets(self, other)
        else:
            return self.id > other.id

    def __eq__(self, other):
        if isinstance(other, GeneKnockoutTarget):
            return self.id == other.id
        elif isinstance(other, GeneModulationTarget):
            return self.id == other.id and other._value == 0
        else:
            return False

    def __repr__(self):
        return "<GeneKnockout %s>" % self.id

    def __hash__(self):
        return hash(str(self))

    def to_gnomic(self):
        accession = Target.to_gnomic(self)
        feature = Feature(name=self.id, accession=accession)
        return Change(before=feature) class ReactionCofactorSwapTarget(Target):
    """
    Swap cofactors of a given reaction.
    """

    def __init__(self, id, swap_pairs, *args, **kwargs):
        super(ReactionCofactorSwapTarget, self).__init__(id, *args, **kwargs)
        self.swap_pairs = swap_pairs

    def apply(self, model):
        reaction = model.reactions.get_by_id(self.id)
        swap_cofactors(reaction, model, self.swap_pairs)

    @property
    def swap_str(self):
        return "+".join(str(s) for s in self.swap_pairs[0]) + "<->" + "+".join(str(s) for s in self.swap_pairs[1])

    def to_gnomic(self):
        accession = Target.to_gnomic(self)
        pairs = [(m.id for m in pair) for pair in self.swap_pairs]
        wt_feature = Feature(name=self.id, accession=accession, type='reaction',
                             variant=["cofactors=%s" % ",".join(pairs[0])])

        feature = Feature(name=self.id, accession=accession, type='reaction',
                          variant=["cofactors=%s" % ",".join(pairs[1])])
        return Change(before=wt_feature, after=feature)

    def __repr__(self):
        return "<ReactionCofactorSwap %s swap=%s>" % (self.id, self.swap_str)

    def __str__(self):
        return "%s[%s]" % (self.id, self.swap_str)

    def __gt__(self, other):
        if self.id == other.id:
            if isinstance(other, ReactionKnockoutTarget):
                raise IncompatibleTargets(self, other)
            if isinstance(other, ReactionModulationTarget):
                return False
            if isinstance(other, ReactionKnockinTarget):
                return True
            elif isinstance(other, EnsembleTarget):
                return not other > self
            else:
                raise IncompatibleTargets(self, other)
        else:
            return self.id > other.id

    def __eq__(self, other):
        if isinstance(other, ReactionCofactorSwapTarget):
            return self.id == other.id and self.swap_pairs == other.swap_pairs
        else:
            return False

    def _repr_html_(self):
        return self.id + "|" + self.swap_str.replace("<->", "&rlarr;")

    def __hash__(self):
        return hash(str(self)) class ReactionKnockinTarget(KnockinTarget):
    def __init__(self, id, value, *args, **kwargs):
        super(ReactionKnockinTarget, self).__init__(id, value, *args, **kwargs)

    def apply(self, model):
        model.add_reactions([self._value])

    def to_gnomic(self):
        accession = Target.to_gnomic(self)
        feature = Feature(accession=accession, type='reaction', name=self.id)
        return Change(after=feature)

    def __gt__(self, other):
        if self.id == other.id:
            if isinstance(other, ReactionKnockoutTarget):
                raise IncompatibleTargets(self, other)
            elif isinstance(other, ReactionModulationTarget):
                if other._value == 0:
                    raise IncompatibleTargets(self, other)
                else:
                    return False
            elif isinstance(other, ReactionKnockinTarget):
                return False
            elif isinstance(other, ReactionCofactorSwapTarget):
                return False
            elif isinstance(other, EnsembleTarget):
                return not other > self
            else:
                raise IncompatibleTargets(self, other)
        else:
            return self.id > other.id

    def __eq__(self, other):
        if isinstance(other, ReactionKnockinTarget):
            return self.id == other.id
        else:
            return False

    def __str__(self):
        return genotype_to_string(Genotype([self.to_gnomic()]))

    def __repr__(self):
        return "<ReactionKnockin %s>" % self.id

    def __hash__(self):
        return hash(str(self))

    def _repr_html_(self):
        return genotype_to_text(Genotype([self.to_gnomic()])) class EnsembleTarget(Target):
    """
    A Target resulting of merging multiple Targets.
    For example:
        1. ReactionKnockinTarget
        2. ReactionModulationTarget

    The Targets are prioritized by what should happen first in order to don't break.

    """

    def __init__(self, id, targets):
        super(EnsembleTarget, self).__init__(id)
        assert all(t.id == id for t in targets)
        self.targets = list(sorted(targets))

    def apply(self, model):
        for target in self.targets:
            target.apply(model)

    def __repr__(self):
        head = "<EnsembleTarget %s" % self.id
        body = ", ".join("%i - %s" % (i, str(target)) for i, target in enumerate(self.targets))
        end = ">"

        return head + "|" + body + "|" + end

    def to_gnomic(self):
        raise NotImplementedError

    def _repr_html_(self):
        return "|" + ";".join(target._repr_html_() for target in self.targets) + '|'

    def __gt__(self, other):
        if isinstance(other, EnsembleTarget):
            return self.id > other.id

        else:
            is_greater = False
            for t in self.targets:
                is_greater = is_greater or t >= other

    def __eq__(self, other):
        if isinstance(other, EnsembleTarget):
            if len(self.targets) == len(other.targets):
                return all(target == other.targets[i] for i, target in enumerate(self.targets))

        return False

    def __str__(self):
        return "%s[%s]" % (self.id, ", ".join(str(t) for t in self.targets))

    def __hash__(self):
        return hash(str(self)) class FluxModulationTarget(Target):
    """
    A Target that changes the flux constraints (knockouts, over-expression or down-regulation).

    See Also
    --------
    ReactionModulationTarget
    ReactionKnockoutTarget
    GeneModulationTarget
    GeneKnockoutTarget

    """
    __gnomic_feature_type__ = 'flux'

    def __init__(self, id, value, reference_value, fold_change=None, **kwargs):
        super(FluxModulationTarget, self).__init__(id, **kwargs)
        # TODO: refactor targets to make the following work
        # if value == 0:
        #     raise ValueError("Please use ReactionKnockoutTarget if flux = 0.")
        self._value = value
        self._reference_value = reference_value
        if fold_change is not None:
            self.fold_change = fold_change
        else:
            try:
                # FIXME (Moritz): Using absolute values here doesn't make sense
                #  to me.
                ref = abs(self._reference_value)
                val = abs(self._value)
                self.fold_change = (val - ref) / ref
            except TypeError:
                self.fold_change = numpy.nan
            except ZeroDivisionError:
                self.fold_change = numpy.inf

    def get_model_target(self, model):
        raise NotImplementedError

    def apply(self, model):
        """
        Applies a change to the flux. If the fold change is higher than 0 it increases the flux.
        If the target flux is zero it applies a knockout. If the fold change is lower than 0, then it
        decreases the flux.

        Parameters
        ----------
        model: cobra.Model
            A model.
        """
        target = self.get_model_target(model)

        if self._value == 0:
            target.knock_out()
        elif self.fold_change > 0:
            increase_flux(target, self._reference_value, self._value)
        elif self.fold_change < 0:
            decrease_flux(target, self._reference_value, self._value)

    def __eq__(self, other):
        if isinstance(other, FluxModulationTarget):
            return self.id == other.id and self._value == other._value
        else:
            return False

    def __str__(self):
        return genotype_to_string(Genotype([self.to_gnomic()]))

    def __repr__(self):
        if self._value == 0:
            return "<FluxModulationTarget KO-%s>" % self.id
        elif self.fold_change > 0:
            return "<FluxModulationTarget UP(%.3f)-%s>" % (self.fold_change, self.id)
        elif self.fold_change < 0:
            return "<FluxModulationTarget DOWN(%.3f)-%s>" % (self.fold_change, self.id)
        else:
            raise ValueError("fold_change shouldn't be 0")

    def __hash__(self):
        return hash(str(self))

    def _repr_html_(self):
        return genotype_to_text(Genotype([self.to_gnomic()]))

    def to_gnomic(self):
        accession = Target.to_gnomic(self)
        if self._value == 0:
            old_feature = Feature(name=self.id, accession=accession, type=self.__gnomic_feature_type__)
            new_feature = None
        elif self.fold_change != 0:
            old_feature = Feature(name=self.id, type=self.__gnomic_feature_type__,
                                  variant=tuple(["value={}".format(self._reference_value)]))
            new_feature = Feature(name=self.id, accession=accession, type=self.__gnomic_feature_type__,
                                  variant=tuple(["value={}".format(self._value)]))
        else:
            raise RuntimeError("fold_change shouldn't be 0")

        return Change(before=old_feature, after=new_feature) class ReactionInversionTarget(ReactionModulationTarget):
    def __str__(self):
        return "INV(%.3f -> %.3f)-%s" % (self._reference_value, self._value, self.id)

    def __repr__(self):
        return "<ReactionInversion %s (%.5f -> %.5f)>" % (self.id, self._reference_value, self._value)

    def _repr_html_(self):
        return "&#8645;-%s" % self.id

    def __gt__(self, other):
        if self.id == other.id:
            if isinstance(other, ReactionModulationTarget):
                if other._value == self._value:
                    return True
                else:
                    raise IncompatibleTargets(self, other)
            elif isinstance(other, ReactionKnockoutTarget):
                raise IncompatibleTargets(self, other)
            elif isinstance(other, ReactionKnockinTarget):
                return True
            else:
                raise IncompatibleTargets(self, other)
        else:
            return self.id > other.id

    def __eq__(self, other):
        if isinstance(other, ReactionInversionTarget):
            return self.id == other.id and other._value == self._value
        else:
            return False

    def apply(self, model):
        reaction = self.get_model_target(model)
        reverse_flux(reaction, self._reference_value, self._value)

    def __hash__(self):
        return hash(str(self)) class Target(object):
    """
    A Target is element of a COBRA model that will change to achieve a given phenotype.
    They are identified by StrainDesignMethod.run when identifying which manipulations
    are necessary to improve some fitness function.

    Attributes
    ----------
    id : str
        The identifier of the target. The id must be present in the COBRA model.
    accession_id : str (optional)
        An accession identifier in a database.
    accession_db : str (optional)
        The database corresponding to `accession_id`.
    """

    def __init__(self, id, accession_id=None, accession_db=None):
        self.id = id
        self.accession_id = accession_id
        self.accession_db = accession_db

    def apply(self, model):
        """Apply the modification to the target, depending on the type."""
        raise NotImplementedError

    def __eq__(self, other):
        if isinstance(other, Target):
            return other.id == self.id
        else:
            return False

    def __gt__(self, other):
        return self.id > other.id

    def to_gnomic(self):
        """
        If gnomic is available, return a Gnomic representation of the Target.

        """
        if self.accession_id:
            if self.accession_db:
                return Accession(identifier=self.accession_id, database=self.accession_db)
            else:
                return Accession(identifier=self.accession_id)
        else:
            return None

    def __repr__(self):
        return "<Target %s>" % self.id

    def __str__(self):
        return self.id

    def __hash__(self):
        return hash(str(self)) class IncompatibleTargets(Exception):
    def __init__(self, target1, target2):
        super(IncompatibleTargets, self).__init__("Incompatible targets %s and %s" % (target1, target2))
        self.target1 = target1
        self.target2 = target2
import pytest
from gnomic.genotype import Genotype
from gnomic.types import Accession, Feature, Change
from gnomic.utils import genotype_to_string, genotype_to_text
from cobra import Metabolite, Reaction
from cameo.core.target import (ReactionKnockoutTarget, ReactionModulationTarget, GeneKnockoutTarget,
                               ReactionCofactorSwapTarget, ReactionKnockinTarget)
from cameo.core.target import (EnsembleTarget, FluxModulationTarget,
                               ReactionInversionTarget, Target)
from cameo.exceptions import IncompatibleTargets
    def test_invalid_reaction_modulation_target(self, model):
        reaction_id = "PGI_XY"
        ref_val = 4.86
        value = 4

        down_reg_target = ReactionModulationTarget(reaction_id, value, ref_val)

        with pytest.raises(KeyError):
            down_reg_target.apply(model)

        reaction_id = "RPI_Z"
        ref_val = -2.28150
        value = -2.0

        down_reg_target = ReactionModulationTarget(reaction_id, value, ref_val)

        with pytest.raises(KeyError):
            down_reg_target.apply(model)

    def test_invalid_reaction_cofactor_swap_target(self, model):
        cofactor_id_swaps = [("nad_c", "nadh_c"), ("nadp_c", "nadph_c")]

        swap_pairs = ([model.metabolites.get_by_id(m) for m in cofactor_id_swaps[0]],
                      [model.metabolites.get_by_id(m) for m in cofactor_id_swaps[1]])

        swap_target = ReactionCofactorSwapTarget("GAPD_124", swap_pairs)

        with pytest.raises(KeyError):
            swap_target.apply(model)

        swap_target = ReactionCofactorSwapTarget("ACKr", swap_pairs)

        with pytest.raises(ValueError):
            swap_target.apply(model)

    def test_gene_knockout_target(self, model):
        gene = "b4025"
        knockout_target = GeneKnockoutTarget(gene, accession_id=gene, accession_db='bigg')
        knockout_target_gnomic = knockout_target.to_gnomic()
        assert genotype_to_string(Genotype([knockout_target_gnomic])) == "-b4025#bigg:b4025"
        with model:
            knockout_target.apply(model)
            assert model.reactions.PGI.lower_bound == 0
            assert model.reactions.PGI.upper_bound == 0
            assert abs(model.slim_optimize() - 0.8631) < 0.0001

        assert model.reactions.PGI.lower_bound == -1000
        assert model.reactions.PGI.upper_bound == 1000

    @pytest.mark.skip("Gene Overexpression not implemented yet")
    def test_gene_over_express_target(self):
        raise NotImplementedError

    @pytest.mark.skip("Gene Downregulation not implemented yet")
    def test_gene_down_regulation_target(self):
        raise NotImplementedError

    def test_gnomic_integration_Target(self, model):
        abstract_target = Target("test")
        abstract_target_gnomic = abstract_target.to_gnomic()
        assert abstract_target_gnomic is None
        abstract_target = Target("test", accession_id='test', accession_db='test')
        abstract_target_gnomic = abstract_target.to_gnomic()
        assert isinstance(abstract_target_gnomic, Accession)
        assert abstract_target_gnomic.identifier == abstract_target.id

    def test_gnomic_integration_FluxModulationTarget(self, model):
        # with pytest.raises(ValueError):
        #     FluxModulationTarget("test", 0, 0)  # TODO: this should really not be possible
        flux_modulation_target = FluxModulationTarget("test", 1, 0)
        flux_modulation_target_gnomic = flux_modulation_target.to_gnomic()
        expected = "flux.test(value=1)"
        assert genotype_to_string(Genotype([flux_modulation_target_gnomic])) == expected

        flux_modulation_target = FluxModulationTarget("PGK", 0.5, 1, accession_id="PGK", accession_db="bigg")
        flux_modulation_target_gnomic = flux_modulation_target.to_gnomic()
        expected = "flux.PGK#bigg:PGK(value=0.5)"
        assert genotype_to_string(Genotype([flux_modulation_target_gnomic])) == expected

    def test_gnomic_integration_ReactionKnockinTarget(self, model):
        reaction = Reaction(id="atpzase", name="Cosmic ATP generator")
        atp_z = Metabolite(id="atp_z", name="Cosmic ATP", compartment="c")

        reaction.add_metabolites({model.metabolites.atp_c: 1, atp_z: -1})
        knockin_target = ReactionKnockinTarget("atpzase", reaction)
        knockin_target_gnomic = knockin_target.to_gnomic()
        assert genotype_to_string(Genotype([knockin_target_gnomic])) == "+reaction.atpzase"

        reaction.add_metabolites({model.metabolites.atp_c: 1, atp_z: -1})
        knockin_target = ReactionKnockinTarget("atpzase", reaction, accession_id='atpzase', accession_db='unicorn')
        knockin_target_gnomic = knockin_target.to_gnomic()
        assert genotype_to_string(Genotype([knockin_target_gnomic])) == "+reaction.atpzase#unicorn:atpzase"

    def test_gnomic_integration_ReactionCofactorSwapTarget(self, model):

        cofactor_id_swaps = [("nad_c", "nadh_c"), ("nadp_c", "nadph_c")]

        swap_pairs = ([model.metabolites.get_by_id(m) for m in cofactor_id_swaps[0]],
                      [model.metabolites.get_by_id(m) for m in cofactor_id_swaps[1]])

        swap_target = ReactionCofactorSwapTarget("GAPD", swap_pairs)
        swap_target_gnomic = swap_target.to_gnomic()
        expected = "reaction.GAPD(cofactors=nadp_c,nadph_c)"
        assert genotype_to_string(Genotype([swap_target_gnomic])) == expected


class TestEnsembleTargets:
    def test_compatible_targets(self):
        modulation_target = ReactionModulationTarget("a", 1, 0)
        ki_target = ReactionKnockinTarget("a", None)

        ensemble_1 = EnsembleTarget("a", [modulation_target, ki_target])
        ensemble_2 = EnsembleTarget("a", [ki_target, modulation_target])

        assert ensemble_1.targets == ensemble_2.targets

        modulation_target = ReactionModulationTarget("b", 1, 0)
        swap_target = ReactionCofactorSwapTarget("b", [("nad_c", "nadh_c"), ("nadp_c", "nadph_c")])

        ensemble_1 = EnsembleTarget("b", [modulation_target, swap_target])

        ensemble_2 = EnsembleTarget("b", [swap_target, modulation_target])