from typing import Dict
from typing import List

from .incompatibility_cause import ConflictCause
from .incompatibility_cause import DependencyCause
from .incompatibility_cause import IncompatibilityCause
from .incompatibility_cause import NoVersionsCause
from .incompatibility_cause import PackageNotFoundCause
from .incompatibility_cause import PlatformCause
from .incompatibility_cause import PythonCause
from .incompatibility_cause import RootCause
from .term import Term


class Incompatibility:
    def __init__(
        self, terms, cause
    ):  # type: (List[Term], IncompatibilityCause) -> None
        # Remove the root package from generated incompatibilities, since it will
        # always be satisfied. This makes error reporting clearer, and may also
        # make solving more efficient.
        if (
            len(terms) != 1
            and isinstance(cause, ConflictCause)
            and any([term.is_positive() and term.dependency.is_root for term in terms])
        ):
            terms = [
                term
                for term in terms
                if not term.is_positive() or not term.dependency.is_root
            ]

        if (
            len(terms) == 1
            # Short-circuit in the common case of a two-term incompatibility with
            # two different packages (for example, a dependency).
            or len(terms) == 2
            and terms[0].dependency.name != terms[-1].dependency.name
        ):
            pass
        else:
            # Coalesce multiple terms about the same package if possible.
            by_name = {}  # type: Dict[str, Dict[str, Term]]
            for term in terms:
                if term.dependency.name not in by_name:
                    by_name[term.dependency.name] = {}

                by_ref = by_name[term.dependency.name]
                ref = term.dependency.name

                if ref in by_ref:
                    by_ref[ref] = by_ref[ref].intersect(term)

                    # If we have two terms that refer to the same package but have a null
                    # intersection, they're mutually exclusive, making this incompatibility
                    # irrelevant, since we already know that mutually exclusive version
                    # ranges are incompatible. We should never derive an irrelevant
                    # incompatibility.
                    assert by_ref[ref] is not None
                else:
                    by_ref[ref] = term

                new_terms = []
                for by_ref in by_name.values():
                    positive_terms = [
                        term for term in by_ref.values() if term.is_positive()
                    ]
                    if positive_terms:
                        new_terms += positive_terms
                        continue

                    new_terms += list(by_ref.values())

                terms = new_terms

        self._terms = terms
        self._cause = cause

    @property
    def terms(self):  # type: () -> List[Term]
        return self._terms

    @property
    def cause(self):  # type: () -> IncompatibilityCause
        return self._cause

    def is_failure(self):  # type: () -> bool
        return len(self._terms) == 0 or (
            len(self._terms) == 1 and self._terms[0].dependency.is_root
        )

    def __str__(self):
        if isinstance(self._cause, DependencyCause):
            assert len(self._terms) == 2

            depender = self._terms[0]
            dependee = self._terms[1]
            assert depender.is_positive()
            assert not dependee.is_positive()

            return "{} depends on {}".format(
                self._terse(depender, allow_every=True), self._terse(dependee)
            )
        elif isinstance(self._cause, PythonCause):
            assert len(self._terms) == 1
            assert self._terms[0].is_positive()

            cause = self._cause  # type: PythonCause
            text = "{} requires ".format(self._terse(self._terms[0], allow_every=True))
            text += "Python {}".format(cause.python_version)

            return text
        elif isinstance(self._cause, PlatformCause):
            assert len(self._terms) == 1
            assert self._terms[0].is_positive()

            cause = self._cause  # type: PlatformCause
            text = "{} requires ".format(self._terse(self._terms[0], allow_every=True))
            text += "platform {}".format(cause.platform)

            return text
        elif isinstance(self._cause, NoVersionsCause):
            assert len(self._terms) == 1
            assert self._terms[0].is_positive()

            return "no versions of {} match {}".format(
                self._terms[0].dependency.name, self._terms[0].constraint
            )
        elif isinstance(self._cause, PackageNotFoundCause):
            assert len(self._terms) == 1
            assert self._terms[0].is_positive()

            return "{} doesn't exist".format(self._terms[0].dependency.name)
        elif isinstance(self._cause, RootCause):
            assert len(self._terms) == 1
            assert not self._terms[0].is_positive()
            assert self._terms[0].dependency.is_root

            return "{} is {}".format(
                self._terms[0].dependency.name, self._terms[0].dependency.constraint
            )
        elif self.is_failure():
            return "version solving failed"

        if len(self._terms) == 1:
            term = self._terms[0]
            if term.constraint.is_any():
                return "{} is {}".format(
                    term.dependency.name,
                    "forbidden" if term.is_positive() else "required",
                )
            else:
                return "{} is {}".format(
                    term.dependency.name,
                    "forbidden" if term.is_positive() else "required",
                )

        if len(self._terms) == 2:
            term1 = self._terms[0]
            term2 = self._terms[1]

            if term1.is_positive() == term2.is_positive():
                if term1.is_positive():
                    package1 = (
                        term1.dependency.name
                        if term1.constraint.is_any()
                        else self._terse(term1)
                    )
                    package2 = (
                        term2.dependency.name
                        if term2.constraint.is_any()
                        else self._terse(term2)
                    )

                    return "{} is incompatible with {}".format(package1, package2)
                else:
                    return "either {} or {}".format(
                        self._terse(term1), self._terse(term2)
                    )

        positive = []
        negative = []

        for term in self._terms:
            if term.is_positive():
                positive.append(self._terse(term))
            else:
                negative.append(self._terse(term))

        if positive and negative:
            if len(positive) == 1:
                positive_term = [term for term in self._terms if term.is_positive()][0]

                return "{} requires {}".format(
                    self._terse(positive_term, allow_every=True), " or ".join(negative)
                )
            else:
                return "if {} then {}".format(
                    " and ".join(positive), " or ".join(negative)
                )
        elif positive:
            return "one of {} must be false".format(" or ".join(positive))
        else:
            return "one of {} must be true".format(" or ".join(negative))

    def and_to_string(
        self, other, details, this_line, other_line
    ):  # type: (Incompatibility, dict, int, int) -> str
        requires_both = self._try_requires_both(other, details, this_line, other_line)
        if requires_both is not None:
            return requires_both

        requires_through = self._try_requires_through(
            other, details, this_line, other_line
        )
        if requires_through is not None:
            return requires_through

        requires_forbidden = self._try_requires_forbidden(
            other, details, this_line, other_line
        )
        if requires_forbidden is not None:
            return requires_forbidden

        buffer = [str(self)]
        if this_line is not None:
            buffer.append(" " + this_line)

        buffer.append(" and {}".format(str(other)))

        if other_line is not None:
            buffer.append(" " + other_line)

        return "\n".join(buffer)

    def _try_requires_both(
        self, other, details, this_line, other_line
    ):  # type: (Incompatibility, dict, int, int) -> str
        if len(self._terms) == 1 or len(other.terms) == 1:
            return

        this_positive = self._single_term_where(lambda term: term.is_positive())
        if this_positive is None:
            return

        other_positive = other._single_term_where(lambda term: term.is_positive())
        if other_positive is None:
            return

        if this_positive.dependency != other_positive.dependency:
            return

        this_negatives = " or ".join(
            [self._terse(term) for term in self._terms if not term.is_positive()]
        )

        other_negatives = " or ".join(
            [self._terse(term) for term in other.terms if not term.is_positive()]
        )

        buffer = [self._terse(this_positive, allow_every=True) + " "]
        is_dependency = isinstance(self.cause, DependencyCause) and isinstance(
            other.cause, DependencyCause
        )

        if is_dependency:
            buffer.append("depends on")
        else:
            buffer.append("requires")

        buffer.append(" both {}".format(this_negatives))
        if this_line is not None:
            buffer.append(" ({})".format(this_line))

        buffer.append(" and {}".format(other_negatives))

        if other_line is not None:
            buffer.append(" ({})".format(other_line))

        return "".join(buffer)

    def _try_requires_through(
        self, other, details, this_line, other_line
    ):  # type: (Incompatibility, dict, int, int) -> str
        if len(self._terms) == 1 or len(other.terms) == 1:
            return

        this_negative = self._single_term_where(lambda term: not term.is_positive())
        other_negative = other._single_term_where(lambda term: not term.is_positive())

        if this_negative is None and other_negative is None:
            return

        this_positive = self._single_term_where(lambda term: term.is_positive())
        other_positive = self._single_term_where(lambda term: term.is_positive())

        if (
            this_negative is not None
            and other_positive is not None
            and this_negative.dependency.name == other_positive.dependency.name
            and this_negative.inverse.satisfies(other_positive)
        ):
            prior = self
            prior_negative = this_negative
            prior_line = this_line
            latter = other
            latter_line = other_line
        elif (
            other_negative is not None
            and this_positive is not None
            and other_negative.dependency.name == this_positive.dependency.name
            and other_negative.inverse.satisfies(this_positive)
        ):
            prior = other
            prior_negative = other_negative
            prior_line = other_line
            latter = self
            latter_line = this_line
        else:
            return

        prior_positives = [term for term in prior.terms if term.is_positive()]

        buffer = []
        if len(prior_positives) > 1:
            prior_string = " or ".join([self._terse(term) for term in prior_positives])
            buffer.append("if {} then ".format(prior_string))
        else:
            if isinstance(prior.cause, DependencyCause):
                verb = "depends on"
            else:
                verb = "requires"

            buffer.append(
                "{} {} ".format(self._terse(prior_positives[0], allow_every=True), verb)
            )

        buffer.append(self._terse(prior_negative))
        if prior_line is not None:
            buffer.append(" ({})".format(prior_line))

        buffer.append(" which ")

        if isinstance(latter.cause, DependencyCause):
            buffer.append("depends on ")
        else:
            buffer.append("requires ")

        buffer.append(
            " or ".join(
                [self._terse(term) for term in latter.terms if not term.is_positive()]
            )
        )

        if latter_line is not None:
            buffer.append(" ({})".format(latter_line))

        return "".join(buffer)

    def _try_requires_forbidden(
        self, other, details, this_line, other_line
    ):  # type: (Incompatibility, dict, int, int) -> str
        if len(self._terms) != 1 and len(other.terms) != 1:
            return None

        if len(self.terms) == 1:
            prior = other
            latter = self
            prior_line = other_line
            latter_line = this_line
        else:
            prior = self
            latter = other
            prior_line = this_line
            latter_line = other_line

        negative = prior._single_term_where(lambda term: not term.is_positive())
        if negative is None:
            return

        if not negative.inverse.satisfies(latter.terms[0]):
            return

        positives = [t for t in prior.terms if t.is_positive()]

        buffer = []
        if len(positives) > 1:
            prior_string = " or ".join([self._terse(term) for term in positives])
            buffer.append("if {} then ".format(prior_string))
        else:
            buffer.append(self._terse(positives[0], allow_every=True))
            if isinstance(prior.cause, DependencyCause):
                buffer.append(" depends on ")
            else:
                buffer.append(" requires ")

        buffer.append(self._terse(latter.terms[0]) + " ")
        if prior_line is not None:
            buffer.append("({}) ".format(prior_line))

        if isinstance(latter.cause, PythonCause):
            cause = latter.cause  # type: PythonCause
            buffer.append("which requires Python {}".format(cause.python_version))
        elif isinstance(latter.cause, NoVersionsCause):
            buffer.append("which doesn't match any versions")
        elif isinstance(latter.cause, PackageNotFoundCause):
            buffer.append("which doesn't exist")
        else:
            buffer.append("which is forbidden")

        if latter_line is not None:
            buffer.append(" ({})".format(latter_line))

        return "".join(buffer)

    def _terse(self, term, allow_every=False):
        if allow_every and term.constraint.is_any():
            return "every version of {}".format(term.dependency.name)

        return str(term.dependency)

    def _single_term_where(self, callable):  # type: (callable) -> Term
        found = None
        for term in self._terms:
            if not callable(term):
                continue

            if found is not None:
                return

            found = term

        return found

    def __repr__(self):
        return "<Incompatibility {}>".format(str(self))
