# Copyright 2023 DeepMind Technologies Limited
#
# 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.
# ==============================================================================

"""Implements objects to represent problems, theorems, proofs, traceback."""

from __future__ import annotations

from collections import defaultdict
from typing import Any

import newclid.geometry as gm
import newclid.pretty as pt


def reshape(list_to_reshape: list[Any], n: int = 1) -> list[list[Any]]:
    assert len(list_to_reshape) % n == 0
    columns = [[] for i in range(n)]
    for i, x in enumerate(list_to_reshape):
        columns[i % n].append(x)
    return zip(*columns)


def isint(x: str) -> bool:
    try:
        int(x)
        return True
    except ValueError:
        return False


class Construction:
    """One predicate."""

    @classmethod
    def from_txt(cls, data: str) -> Construction:
        data = data.split(" ")
        return Construction(data[0], data[1:])

    def __init__(self, name: str, args: list[str]):
        self.name = name
        self.args = args

    def translate(self, mapping: dict[str, str]) -> Construction:
        args = [a if isint(a) else mapping[a] for a in self.args]
        return Construction(self.name, args)

    def txt(self) -> str:
        return " ".join([self.name] + list(self.args))


class Clause:
    """One construction (>= 1 predicate)."""

    @classmethod
    def from_txt(cls, data: str) -> Clause:
        if data == " =":
            return Clause([], [])
        points, constructions = data.split(" = ")
        return Clause(
            points.split(" "),
            [Construction.from_txt(c) for c in constructions.split(", ")],
        )

    def __init__(self, points: list[str], constructions: list[Construction]):
        self.points = []
        self.nums = []

        for p in points:
            num = None
            if isinstance(p, str) and "@" in p:
                p, num = p.split("@")
                x, y = num.split("_")
                num = float(x), float(y)
            self.points.append(p)
            self.nums.append(num)

        self.constructions = constructions

    def translate(self, mapping: dict[str, str]) -> Clause:
        points0 = []
        for p in self.points:
            pcount = len(mapping) + 1
            name = chr(96 + pcount)
            if name > "z":  # pcount = 26 -> name = 'z'
                name = chr(97 + (pcount - 1) % 26) + str((pcount - 1) // 26)

            p0 = mapping.get(p, name)
            mapping[p] = p0
            points0.append(p0)
        return Clause(points0, [c.translate(mapping) for c in self.constructions])

    def add(self, name: str, args: list[str]) -> None:
        self.constructions.append(Construction(name, args))

    def txt(self) -> str:
        return (
            " ".join(self.points)
            + " = "
            + ", ".join(c.txt() for c in self.constructions)
        )


def _gcd(x: int, y: int) -> int:
    while y:
        x, y = y, x % y
    return x


def simplify(n: int, d: int) -> tuple[int, int]:
    g = _gcd(n, d)
    return (n // g, d // g)


def compare_fn(dep: Dependency) -> tuple[Dependency, str]:
    return (dep, pt.pretty(dep))


def sort_deps(deps: list[Dependency]) -> list[Dependency]:
    return sorted(deps, key=compare_fn)


class Problem:
    """Describe one problem to solve."""

    @classmethod
    def from_txt_file(cls, fname: str, to_dict: bool = False, translate: bool = True):
        """Load a problem from a text file."""
        with open(fname, "r") as f:
            lines = f.read().split("\n")

        lines = [line for line in lines if line]
        data = [
            cls.from_txt(url + "\n" + problem, translate)
            for (url, problem) in reshape(lines, 2)
        ]
        if to_dict:
            return cls.to_dict(data)
        return data

    @classmethod
    def from_txt(cls, data: str, translate: bool = True) -> Problem:
        """Load a problem from a str object."""
        url = ""
        if "\n" in data:
            url, data = data.split("\n")

        if " ? " in data:
            clauses, goal = data.split(" ? ")
            goal = Construction.from_txt(goal)
        else:
            clauses, goal = data, None

        clauses = clauses.split("; ")
        problem = Problem(
            url=url, clauses=[Clause.from_txt(c) for c in clauses], goal=goal
        )
        if translate:
            return problem.translate()
        return problem

    @classmethod
    def to_dict(cls, data: list[Problem]) -> dict[str, Problem]:
        return {p.url: p for p in data}

    def __init__(self, url: str, clauses: list[Clause], goal: Construction):
        self.url = url
        self.clauses = clauses
        self.goal = goal

    def copy(self) -> Problem:
        return Problem(self.url, list(self.clauses), self.goal)

    def translate(self) -> Problem:  # to single-char point names
        """Translate point names into alphabetical."""
        mapping = {}
        clauses = []

        for clause in self.clauses:
            clauses.append(clause.translate(mapping))

        if self.goal:
            goal = self.goal.translate(mapping)
        else:
            goal = self.goal

        p = Problem(self.url, clauses, goal)
        p.mapping = mapping
        return p

    def txt(self) -> str:
        return (
            "; ".join([c.txt() for c in self.clauses]) + " ? " + self.goal.txt()
            if self.goal
            else ""
        )

    def setup_str_from_problem(self, definitions: list[Definition]) -> str:
        """Construct the <theorem_premises> string from Problem object."""
        ref = 0

        string = []
        for clause in self.clauses:
            group = {}
            p2deps = defaultdict(list)
            for c in clause.constructions:
                cdef = definitions[c.name]

                if len(c.args) != len(cdef.construction.args):
                    assert len(c.args) + len(clause.points) == len(
                        cdef.construction.args
                    )
                    c.args = clause.points + c.args

                mapping = dict(zip(cdef.construction.args, c.args))
                for points, bs in cdef.basics:
                    points = tuple([mapping[x] for x in points])
                    for p in points:
                        group[p] = points

                    for b in bs:
                        args = [mapping[a] for a in b.args]
                        name = b.name
                        if b.name in ["s_angle", "aconst"]:
                            x, y, z, v = args
                            name = "aconst"
                            v = int(v)

                            if v < 0:
                                v = -v
                                x, z = z, x

                            m, n = simplify(int(v), 180)
                            args = [y, z, y, x, f"{m}pi/{n}"]

                        p2deps[points].append(hashed_txt(name, args))

            for k, v in p2deps.items():
                p2deps[k] = sort_deps(v)

            points = clause.points
            while points:
                p = points[0]
                gr = group[p]
                points = [x for x in points if x not in gr]

                deps_str = []
                for dep in p2deps[gr]:
                    ref_str = "{:02}".format(ref)
                    dep_str = pt.pretty(dep)

                    if dep[0] == "aconst":
                        m, n = map(int, dep[-1].split("pi/"))
                        mn = f"{m}. pi / {n}."
                        dep_str = " ".join(dep_str.split()[:-1] + [mn])

                    deps_str.append(dep_str + " " + ref_str)
                    ref += 1

                string.append(" ".join(gr) + " : " + " ".join(deps_str))

        string = "{S} " + " ; ".join([s.strip() for s in string])
        goal = self.goal
        string += " ? " + pt.pretty([goal.name] + goal.args)
        return string


def parse_rely(s: str) -> dict[str, str]:
    result = {}
    if not s:
        return result
    s = [x.strip() for x in s.split(",")]
    for x in s:
        a, b = x.split(":")
        a, b = a.strip().split(), b.strip().split()
        result.update({m: b for m in a})
    return result


class Definition:
    """Definitions of construction statements."""

    @classmethod
    def from_txt_file(cls, fname: str, to_dict: bool = False) -> Definition:
        with open(fname, "r") as f:
            lines = f.read()
        return cls.from_string(lines, to_dict)

    @classmethod
    def from_string(cls, string: str, to_dict: bool = False) -> Definition:
        lines = string.split("\n")
        data = [cls.from_txt("\n".join(group)) for group in reshape(lines, 6)]
        if to_dict:
            return cls.to_dict(data)
        return data

    @classmethod
    def to_dict(cls, data: list[Definition]) -> dict[str, Definition]:
        return {d.construction.name: d for d in data}

    @classmethod
    def from_txt(cls, data: str) -> Definition:
        """Load definitions from a str object."""
        construction, rely, deps, basics, numerics, _ = data.split("\n")
        basics = [] if not basics else [b.strip() for b in basics.split(";")]

        levels = []
        for bs in basics:
            if ":" in bs:
                points, bs = bs.split(":")
                points = points.strip().split()
            else:
                points = []
            if bs.strip():
                bs = [Construction.from_txt(b.strip()) for b in bs.strip().split(",")]
            else:
                bs = []
            levels.append((points, bs))

        numerics = [] if not numerics else numerics.split(", ")

        return Definition(
            construction=Construction.from_txt(construction),
            rely=parse_rely(rely),
            deps=Clause.from_txt(deps),
            basics=levels,
            numerics=[Construction.from_txt(c) for c in numerics],
        )

    def __init__(
        self,
        construction: Construction,
        rely: dict[str, str],
        deps: Clause,
        basics: list[tuple[list[str], list[Construction]]],
        numerics: list[Construction],
    ):
        self.construction = construction
        self.rely = rely
        self.deps = deps
        self.basics = basics
        self.numerics = numerics

        args = set()
        for num in numerics:
            args.update(num.args)

        self.points = []
        self.args = []
        for p in self.construction.args:
            if p in args:
                self.args.append(p)
            else:
                self.points.append(p)


class Theorem:
    """Deduction rule."""

    @classmethod
    def from_txt_file(cls, fname: str, to_dict: bool = False) -> Theorem:
        with open(fname, "r") as f:
            theorems = f.read()
        return cls.from_string(theorems, to_dict)

    @classmethod
    def from_string(cls, string: str, to_dict: bool = False) -> Theorem:
        """Load deduction rule from a str object."""
        theorems = string.split("\n")
        theorems = [line for line in theorems if line and not line.startswith("#")]
        theorems = [cls.from_txt(line) for line in theorems]

        for i, th in enumerate(theorems):
            th.rule_name = "r{:02}".format(i)

        if to_dict:
            result = {}
            for t in theorems:
                if t.name in result:
                    t.name += "_"
                result[t.rule_name] = t

            return result

        return theorems

    @classmethod
    def from_txt(cls, data: str) -> Theorem:
        premises, conclusion = data.split(" => ")
        premises = premises.split(", ")
        conclusion = conclusion.split(", ")
        return Theorem(
            premise=[Construction.from_txt(p) for p in premises],
            conclusion=[Construction.from_txt(c) for c in conclusion],
        )

    def __init__(self, premise: list[Construction], conclusion: list[Construction]):
        if len(conclusion) != 1:
            raise ValueError("Cannot have more than one conclusion")
        self.name = "_".join([p.name for p in premise + conclusion])
        self.premise = premise
        self.conclusion = conclusion
        self.is_arg_reduce = False

        assert len(self.conclusion) == 1
        con = self.conclusion[0]

        if con.name in [
            "eqratio3",
            "midp",
            "contri",
            "simtri",
            "contri2",
            "simtri2",
            "simtri*",
            "contri*",
        ]:
            return

        prem_args = set(sum([p.args for p in self.premise], []))
        con_args = set(con.args)
        if len(prem_args) <= len(con_args):
            self.is_arg_reduce = True

    def txt(self) -> str:
        premise_txt = ", ".join([clause.txt() for clause in self.premise])
        conclusion_txt = ", ".join([clause.txt() for clause in self.conclusion])
        return f"{premise_txt} => {conclusion_txt}"

    def conclusion_name_args(
        self, mapping: dict[str, gm.Point]
    ) -> tuple[str, list[gm.Point]]:
        mapping = {arg: p for arg, p in mapping.items() if isinstance(arg, str)}
        c = self.conclusion[0]
        args = [mapping[a] for a in c.args]
        return c.name, args


def why_eqratio(
    d1: gm.Direction,
    d2: gm.Direction,
    d3: gm.Direction,
    d4: gm.Direction,
    level: int,
) -> list[Dependency]:
    """Why two ratios are equal, returns a Dependency objects."""
    all12 = list(gm.all_ratios(d1, d2, level))
    all34 = list(gm.all_ratios(d3, d4, level))

    min_why = None
    for ang12, d1s, d2s in all12:
        for ang34, d3s, d4s in all34:
            why0 = gm.why_equal(ang12, ang34, level)
            if why0 is None:
                continue
            d1_, d2_ = ang12._l
            d3_, d4_ = ang34._l
            why1 = gm.bfs_backtrack(d1, [d1_], d1s)
            why2 = gm.bfs_backtrack(d2, [d2_], d2s)
            why3 = gm.bfs_backtrack(d3, [d3_], d3s)
            why4 = gm.bfs_backtrack(d4, [d4_], d4s)
            why = why0 + why1 + why2 + why3 + why4
            if min_why is None or len(why) < len(min_why[0]):
                min_why = why, ang12, ang34, why0, why1, why2, why3, why4

    if min_why is None:
        return None

    _, ang12, ang34, why0, why1, why2, why3, why4 = min_why
    d1_, d2_ = ang12._l
    d3_, d4_ = ang34._l

    if d1 == d1_ and d2 == d2_ and d3 == d3_ and d4 == d4_:
        return why0

    (a_, b_), (c_, d_) = d1_._obj.points, d2_._obj.points
    (e_, f_), (g_, h_) = d3_._obj.points, d4_._obj.points
    deps = []
    if why0:
        dep = Dependency("eqratio", [a_, b_, c_, d_, e_, f_, g_, h_], "", level)
        dep.why = why0
        deps.append(dep)

    (a, b), (c, d) = d1._obj.points, d2._obj.points
    (e, f), (g, h) = d3._obj.points, d4._obj.points
    for why, (x, y), (x_, y_) in zip(
        [why1, why2, why3, why4],
        [(a, b), (c, d), (e, f), (g, h)],
        [(a_, b_), (c_, d_), (e_, f_), (g_, h_)],
    ):
        if why:
            dep = Dependency("cong", [x, y, x_, y_], "", level)
            dep.why = why
            deps.append(dep)

    return deps


def why_eqangle(
    d1: gm.Direction,
    d2: gm.Direction,
    d3: gm.Direction,
    d4: gm.Direction,
    level: int,
    verbose: bool = False,
) -> list[Dependency]:
    """Why two angles are equal, returns a Dependency objects."""
    all12 = list(gm.all_angles(d1, d2, level))
    all34 = list(gm.all_angles(d3, d4, level))

    min_why = None
    for ang12, d1s, d2s in all12:
        for ang34, d3s, d4s in all34:
            why0 = gm.why_equal(ang12, ang34, level)
            if why0 is None:
                continue
            d1_, d2_ = ang12._d
            d3_, d4_ = ang34._d
            why1 = gm.bfs_backtrack(d1, [d1_], d1s)
            why2 = gm.bfs_backtrack(d2, [d2_], d2s)
            why3 = gm.bfs_backtrack(d3, [d3_], d3s)
            why4 = gm.bfs_backtrack(d4, [d4_], d4s)
            why = why0 + why1 + why2 + why3 + why4
            if min_why is None or len(why) < len(min_why[0]):
                min_why = why, ang12, ang34, why0, why1, why2, why3, why4

    if min_why is None:
        return None

    _, ang12, ang34, why0, why1, why2, why3, why4 = min_why
    why0 = gm.why_equal(ang12, ang34, level)
    d1_, d2_ = ang12._d
    d3_, d4_ = ang34._d

    if d1 == d1_ and d2 == d2_ and d3 == d3_ and d4 == d4_:
        return (d1_, d2_, d3_, d4_), why0

    (a_, b_), (c_, d_) = d1_._obj.points, d2_._obj.points
    (e_, f_), (g_, h_) = d3_._obj.points, d4_._obj.points
    deps = []
    if why0:
        dep = Dependency("eqangle", [a_, b_, c_, d_, e_, f_, g_, h_], "", None)
        dep.why = why0
        deps.append(dep)

    (a, b), (c, d) = d1._obj.points, d2._obj.points
    (e, f), (g, h) = d3._obj.points, d4._obj.points
    for why, d_xy, (x, y), d_xy_, (x_, y_) in zip(
        [why1, why2, why3, why4],
        [d1, d2, d3, d4],
        [(a, b), (c, d), (e, f), (g, h)],
        [d1_, d2_, d3_, d4_],
        [(a_, b_), (c_, d_), (e_, f_), (g_, h_)],
    ):
        xy, xy_ = d_xy._obj, d_xy_._obj
        if why:
            if xy == xy_:
                name = "collx"
            else:
                name = "para"
            dep = Dependency(name, [x_, y_, x, y], "", None)
            dep.why = why
            deps.append(dep)

    return (d1_, d2_, d3_, d4_), deps


CONSTRUCTION_RULE = "c0"


class EmptyDependency:
    """Empty dependency predicate ready to get filled up."""

    def __init__(self, level: int, rule_name: str):
        self.level = level
        self.rule_name = rule_name or ""
        self.empty = True
        self.why = []
        self.trace = None

    def populate(self, name: str, args: list[gm.Point]) -> Dependency:
        dep = Dependency(name, args, self.rule_name, self.level)
        dep.trace2 = self.trace
        dep.why = list(self.why)
        return dep

    def copy(self) -> EmptyDependency:
        other = EmptyDependency(self.level, self.rule_name)
        other.why = list(self.why)
        return other

    def extend(
        self,
        g: Any,
        name0: str,
        args0: list[gm.Point],
        name: str,
        args: list[gm.Point],
    ) -> EmptyDependency:
        """Extend the dependency list by (name, args)."""
        dep0 = self.populate(name0, args0)
        deps = EmptyDependency(level=self.level, rule_name=None)
        dep = Dependency(name, args, None, deps.level)
        deps.why = [dep0, dep.why_me_or_cache(g, None)]
        return deps

    def extend_many(
        self,
        g: Any,
        name0: str,
        args0: list[gm.Point],
        name_args: list[tuple[str, list[gm.Point]]],
    ) -> EmptyDependency:
        """Extend the dependency list by many name_args."""
        if not name_args:
            return self
        dep0 = self.populate(name0, args0)
        deps = EmptyDependency(level=self.level, rule_name=None)
        deps.why = [dep0]
        for name, args in name_args:
            dep = Dependency(name, args, None, deps.level)
            deps.why += [dep.why_me_or_cache(g, None)]
        return deps


def maybe_make_equal_pairs(
    a: gm.Point,
    b: gm.Point,
    c: gm.Point,
    d: gm.Point,
    m: gm.Point,
    n: gm.Point,
    p: gm.Point,
    q: gm.Point,
    ab: gm.Line,
    mn: gm.Line,
    g: Any,
    level: int,
) -> list[Dependency]:
    """Make a-b:c-d==m-n:p-q in case a-b==m-n or c-d==p-q."""
    if ab != mn:
        return
    why = []
    eqname = "para" if isinstance(ab, gm.Line) else "cong"
    colls = [a, b, m, n]
    if len(set(colls)) > 2 and eqname == "para":
        dep = Dependency("collx", colls, None, level)
        dep.why_me(g, level)
        why += [dep]

    dep = Dependency(eqname, [c, d, p, q], None, level)
    dep.why_me(g, level)
    why += [dep]
    return why


class Dependency(Construction):
    """Dependency is a predicate that other predicates depend on."""

    def __init__(self, name: str, args: list[gm.Point], rule_name: str, level: int):
        super().__init__(name, args)
        self.rule_name = rule_name or ""
        self.level = level
        self.why = []

        self._stat = None
        self.trace = None

    def _find(self, dep_hashed: tuple[str, ...]) -> Dependency:
        for w in self.why:
            f = w._find(dep_hashed)
            if f:
                return f
            if w.hashed() == dep_hashed:
                return w

    def remove_loop(self) -> Dependency:
        f = self._find(self.hashed())
        if f:
            return f
        return self

    def copy(self) -> Dependency:
        dep = Dependency(self.name, self.args, self.rule_name, self.level)
        dep.trace = self.trace
        dep.why = list(self.why)
        return dep

    def why_me_or_cache(self, g: Any, level: int) -> Dependency:
        if self.hashed() in g.cache:
            return g.cache[self.hashed()]
        self.why_me(g, level)
        return self

    def populate(self, name: str, args: list[gm.Point]) -> Dependency:
        assert self.rule_name == CONSTRUCTION_RULE, self.rule_name
        dep = Dependency(self.name, self.args, self.rule_name, self.level)
        dep.why = list(self.why)
        return dep

    def why_me(self, g: Any, level: int) -> None:
        """Figure out the dependencies predicates of self."""
        name, args = self.name, self.args

        hashed_me = hashed(name, args)
        if hashed_me in g.cache:
            dep = g.cache[hashed_me]
            self.why = dep.why
            self.rule_name = dep.rule_name
            return

        if self.name == "para":
            a, b, c, d = self.args
            if {a, b} == {c, d}:
                self.why = []
                return

            ab = g._get_line(a, b)
            cd = g._get_line(c, d)
            if ab == cd:
                if {a, b} == {c, d}:
                    self.why = []
                    self.rule_name = ""
                    return
                dep = Dependency("coll", list({a, b, c, d}), "t??", None)
                self.why = [dep.why_me_or_cache(g, level)]
                return

            for (x, y), xy in zip([(a, b), (c, d)], [ab, cd]):
                x_, y_ = xy.points
                if {x, y} == {x_, y_}:
                    continue
                d = Dependency("collx", [x, y, x_, y_], None, level)
                self.why += [d.why_me_or_cache(g, level)]

            whypara = g.why_equal(ab, cd, None)
            self.why += whypara

        elif self.name == "midp":
            m, a, b = self.args
            ma = g._get_segment(m, a)
            mb = g._get_segment(m, b)
            dep = Dependency("coll", [m, a, b], None, None).why_me_or_cache(g, None)
            self.why = [dep] + g.why_equal(ma, mb, level)

        elif self.name == "perp":
            a, b, c, d = self.args
            ab = g._get_line(a, b)
            cd = g._get_line(c, d)
            for (x, y), xy in zip([(a, b), (c, d)], [ab, cd]):
                x_, y_ = xy.points
                if {x, y} == {x_, y_}:
                    continue
                d = Dependency("collx", [x, y, x_, y_], None, level)
                self.why += [d.why_me_or_cache(g, level)]

            _, why = why_eqangle(ab._val, cd._val, cd._val, ab._val, level)
            a, b = ab.points
            c, d = cd.points

            if hashed(self.name, [a, b, c, d]) != self.hashed():
                d = Dependency(self.name, [a, b, c, d], None, level)
                d.why = why
                why = [d]

            self.why += why

        elif self.name == "cong":
            a, b, c, d = self.args
            ab = g._get_segment(a, b)
            cd = g._get_segment(c, d)

            self.why = g.why_equal(ab, cd, level)

        elif self.name == "coll":
            _, why = gm.line_of_and_why(self.args, level)
            self.why = why

        elif self.name == "collx":
            if g.check_coll(self.args):
                args = list(set(self.args))
                hashed_me = hashed("coll", args)
                if hashed_me in g.cache:
                    dep = g.cache[hashed_me]
                    self.why = [dep]
                    self.rule_name = ""
                    return
                _, self.why = gm.line_of_and_why(args, level)
            else:
                self.name = "para"
                self.why_me(g, level)

        elif self.name == "cyclic":
            _, why = gm.circle_of_and_why(self.args, level)
            self.why = why

        elif self.name == "circle":
            o, a, b, c = self.args
            oa = g._get_segment(o, a)
            ob = g._get_segment(o, b)
            oc = g._get_segment(o, c)
            self.why = g.why_equal(oa, ob, level) + g.why_equal(oa, oc, level)

        elif self.name in ["eqangle", "eqangle6"]:
            a, b, c, d, m, n, p, q = self.args

            ab, why1 = g.get_line_thru_pair_why(a, b)
            cd, why2 = g.get_line_thru_pair_why(c, d)
            mn, why3 = g.get_line_thru_pair_why(m, n)
            pq, why4 = g.get_line_thru_pair_why(p, q)

            if ab is None or cd is None or mn is None or pq is None:
                if {a, b} == {m, n}:
                    d = Dependency("para", [c, d, p, q], None, level)
                    self.why = [d.why_me_or_cache(g, level)]
                if {a, b} == {c, d}:
                    d = Dependency("para", [p, q, m, n], None, level)
                    self.why = [d.why_me_or_cache(g, level)]
                if {c, d} == {p, q}:
                    d = Dependency("para", [a, b, m, n], None, level)
                    self.why = [d.why_me_or_cache(g, level)]
                if {p, q} == {m, n}:
                    d = Dependency("para", [a, b, c, d], None, level)
                    self.why = [d.why_me_or_cache(g, level)]
                return

            for (x, y), xy, whyxy in zip(
                [(a, b), (c, d), (m, n), (p, q)],
                [ab, cd, mn, pq],
                [why1, why2, why3, why4],
            ):
                x_, y_ = xy.points
                if {x, y} == {x_, y_}:
                    continue
                d = Dependency("collx", [x, y, x_, y_], None, level)
                d.why = whyxy
                self.why += [d]

            a, b = ab.points
            c, d = cd.points
            m, n = mn.points
            p, q = pq.points
            diff = hashed(self.name, [a, b, c, d, m, n, p, q]) != self.hashed()

            whyeqangle = None
            if ab._val and cd._val and mn._val and pq._val:
                whyeqangle = why_eqangle(ab._val, cd._val, mn._val, pq._val, level)

            if whyeqangle:
                (dab, dcd, dmn, dpq), whyeqangle = whyeqangle
                if diff:
                    d = Dependency("eqangle", [a, b, c, d, m, n, p, q], None, level)
                    d.why = whyeqangle
                    whyeqangle = [d]
                self.why += whyeqangle

            else:
                if (ab == cd and mn == pq) or (ab == mn and cd == pq):
                    self.why += []
                elif ab == mn:
                    self.why += maybe_make_equal_pairs(
                        a, b, c, d, m, n, p, q, ab, mn, g, level
                    )
                elif cd == pq:
                    self.why += maybe_make_equal_pairs(
                        c, d, a, b, p, q, m, n, cd, pq, g, level
                    )
                elif ab == cd:
                    self.why += maybe_make_equal_pairs(
                        a, b, m, n, c, d, p, q, ab, cd, g, level
                    )
                elif mn == pq:
                    self.why += maybe_make_equal_pairs(
                        m, n, a, b, p, q, c, d, mn, pq, g, level
                    )
                elif g.is_equal(ab, mn) or g.is_equal(cd, pq):
                    dep1 = Dependency("para", [a, b, m, n], None, level)
                    dep1.why_me(g, level)
                    dep2 = Dependency("para", [c, d, p, q], None, level)
                    dep2.why_me(g, level)
                    self.why += [dep1, dep2]
                elif g.is_equal(ab, cd) or g.is_equal(mn, pq):
                    dep1 = Dependency("para", [a, b, c, d], None, level)
                    dep1.why_me(g, level)
                    dep2 = Dependency("para", [m, n, p, q], None, level)
                    dep2.why_me(g, level)
                    self.why += [dep1, dep2]
                elif ab._val and cd._val and mn._val and pq._val:
                    self.why = why_eqangle(ab._val, cd._val, mn._val, pq._val, level)

        elif self.name in ["eqratio", "eqratio6"]:
            a, b, c, d, m, n, p, q = self.args
            ab = g._get_segment(a, b)
            cd = g._get_segment(c, d)
            mn = g._get_segment(m, n)
            pq = g._get_segment(p, q)

            if ab is None or cd is None or mn is None or pq is None:
                if {a, b} == {m, n}:
                    d = Dependency("cong", [c, d, p, q], None, level)
                    self.why = [d.why_me_or_cache(g, level)]
                if {a, b} == {c, d}:
                    d = Dependency("cong", [p, q, m, n], None, level)
                    self.why = [d.why_me_or_cache(g, level)]
                if {c, d} == {p, q}:
                    d = Dependency("cong", [a, b, m, n], None, level)
                    self.why = [d.why_me_or_cache(g, level)]
                if {p, q} == {m, n}:
                    d = Dependency("cong", [a, b, c, d], None, level)
                    self.why = [d.why_me_or_cache(g, level)]
                return

            if ab._val and cd._val and mn._val and pq._val:
                self.why = why_eqratio(ab._val, cd._val, mn._val, pq._val, level)

            if self.why is None:
                self.why = []
                if (ab == cd and mn == pq) or (ab == mn and cd == pq):
                    self.why = []
                elif ab == mn:
                    self.why += maybe_make_equal_pairs(
                        a, b, c, d, m, n, p, q, ab, mn, g, level
                    )
                elif cd == pq:
                    self.why += maybe_make_equal_pairs(
                        c, d, a, b, p, q, m, n, cd, pq, g, level
                    )
                elif ab == cd:
                    self.why += maybe_make_equal_pairs(
                        a, b, m, n, c, d, p, q, ab, cd, g, level
                    )
                elif mn == pq:
                    self.why += maybe_make_equal_pairs(
                        m, n, a, b, p, q, c, d, mn, pq, g, level
                    )
                elif g.is_equal(ab, mn) or g.is_equal(cd, pq):
                    dep1 = Dependency("cong", [a, b, m, n], None, level)
                    dep1.why_me(g, level)
                    dep2 = Dependency("cong", [c, d, p, q], None, level)
                    dep2.why_me(g, level)
                    self.why += [dep1, dep2]
                elif g.is_equal(ab, cd) or g.is_equal(mn, pq):
                    dep1 = Dependency("cong", [a, b, c, d], None, level)
                    dep1.why_me(g, level)
                    dep2 = Dependency("cong", [m, n, p, q], None, level)
                    dep2.why_me(g, level)
                    self.why += [dep1, dep2]
                elif ab._val and cd._val and mn._val and pq._val:
                    self.why = why_eqangle(ab._val, cd._val, mn._val, pq._val, level)

        elif self.name in ["diff", "npara", "nperp", "ncoll", "sameside"]:
            self.why = []

        elif self.name == "simtri":
            a, b, c, x, y, z = self.args
            dep1 = Dependency("eqangle", [a, b, a, c, x, y, x, z], "", level)
            dep1.why_me(g, level)
            dep2 = Dependency("eqangle", [b, a, b, c, y, x, y, z], "", level)
            dep2.why_me(g, level)
            self.rule_name = "r34"
            self.why = [dep1, dep2]

        elif self.name == "contri":
            a, b, c, x, y, z = self.args
            dep1 = Dependency("cong", [a, b, x, y], "", level)
            dep1.why_me(g, level)
            dep2 = Dependency("cong", [b, c, y, z], "", level)
            dep2.why_me(g, level)
            dep3 = Dependency("cong", [c, a, z, x], "", level)
            dep3.why_me(g, level)
            self.rule_name = "r32"
            self.why = [dep1, dep2, dep3]

        elif self.name == "ind":
            pass

        elif self.name == "aconst":
            a, b, c, d, ang0 = self.args

            measure = ang0._val

            for ang in measure.neighbors(gm.Angle):
                if ang == ang0:
                    continue
                d1, d2 = ang._d
                l1, l2 = d1._obj, d2._obj
                (a1, b1), (c1, d1) = l1.points, l2.points

                if not g.check_para_or_coll([a, b, a1, b1]) or not g.check_para_or_coll(
                    [c, d, c1, d1]
                ):
                    continue

                self.why = []
                for args in [(a, b, a1, b1), (c, d, c1, d1)]:
                    if g.check_coll(args):
                        if len(set(args)) > 2:
                            dep = Dependency("coll", args, None, None)
                            self.why.append(dep.why_me_or_cache(g, level))
                    else:
                        dep = Dependency("para", args, None, None)
                        self.why.append(dep.why_me_or_cache(g, level))

                self.why += gm.why_equal(ang, ang0)
                break

        elif self.name == "rconst":
            a, b, c, d, rat0 = self.args

            val = rat0._val

            for rat in val.neighbors(gm.Ratio):
                if rat == rat0:
                    continue
                l1, l2 = rat._l
                s1, s2 = l1._obj, l2._obj
                (a1, b1), (c1, d1) = list(s1.points), list(s2.points)

                if not g.check_cong([a, b, a1, b1]) or not g.check_cong([c, d, c1, d1]):
                    continue

                self.why = []
                for args in [(a, b, a1, b1), (c, d, c1, d1)]:
                    if len(set(args)) > 2:
                        dep = Dependency("cong", args, None, None)
                        self.why.append(dep.why_me_or_cache(g, level))

                self.why += gm.why_equal(rat, rat0)
                break

        else:
            raise ValueError("Not recognize", self.name)

    def hashed(self, rename: bool = False) -> tuple[str, ...]:
        return hashed(self.name, self.args, rename=rename)


def hashed(name: str, args: list[gm.Point], rename: bool = False) -> tuple[str, ...]:
    if name == "s_angle":
        args = [p.name if not rename else p.new_name for p in args[:-1]] + [
            str(args[-1])
        ]
    else:
        args = [p.name if not rename else p.new_name for p in args]
    return hashed_txt(name, args)


def hashed_txt(name: str, args: list[str]) -> tuple[str, ...]:
    """Return a tuple unique to name and args upto arg permutation equivariant."""

    if name in ["const", "aconst", "rconst"]:
        a, b, c, d, y = args
        a, b = sorted([a, b])
        c, d = sorted([c, d])
        return name, a, b, c, d, y

    if name in ["npara", "nperp", "para", "cong", "perp", "collx"]:
        a, b, c, d = args

        a, b = sorted([a, b])
        c, d = sorted([c, d])
        (a, b), (c, d) = sorted([(a, b), (c, d)])

        return (name, a, b, c, d)

    if name in ["midp", "midpoint"]:
        a, b, c = args
        b, c = sorted([b, c])
        return (name, a, b, c)

    if name in ["coll", "cyclic", "ncoll", "diff", "triangle"]:
        return (name,) + tuple(sorted(list(set(args))))

    if name == "circle":
        x, a, b, c = args
        return (name, x) + tuple(sorted([a, b, c]))

    if name in ["eqangle", "eqratio", "eqangle6", "eqratio6"]:
        a, b, c, d, e, f, g, h = args
        a, b = sorted([a, b])
        c, d = sorted([c, d])
        e, f = sorted([e, f])
        g, h = sorted([g, h])
        if tuple(sorted([a, b, e, f])) > tuple(sorted([c, d, g, h])):
            a, b, e, f, c, d, g, h = c, d, g, h, a, b, e, f
        if (a, b, c, d) > (e, f, g, h):
            a, b, c, d, e, f, g, h = e, f, g, h, a, b, c, d

        if name == "eqangle6":
            name = "eqangle"
        if name == "eqratio6":
            name = "eqratio"
        return (name,) + (a, b, c, d, e, f, g, h)

    if name in ["contri", "simtri", "simtri2", "contri2", "contri*", "simtri*"]:
        a, b, c, x, y, z = args
        (a, x), (b, y), (c, z) = sorted([(a, x), (b, y), (c, z)], key=sorted)
        (a, b, c), (x, y, z) = sorted([(a, b, c), (x, y, z)], key=sorted)
        return (name, a, b, c, x, y, z)

    if name in ["eqratio3"]:
        a, b, c, d, o, o = args
        (a, c), (b, d) = sorted([(a, c), (b, d)], key=sorted)
        (a, b), (c, d) = sorted([(a, b), (c, d)], key=sorted)
        return (name, a, b, c, d, o, o)

    if name in ["sameside", "s_angle"]:
        return (name,) + tuple(args)

    raise ValueError(f"Not recognize {name} to hash.")
