# 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 the graph representation of the proof state."""

from __future__ import annotations

from collections import defaultdict
from typing import Callable, Generator, Optional, Type, Union
import logging

import newclid.ar as ar
import newclid.geometry as gm
from newclid.geometry import Angle, Direction, Length, Ratio
from newclid.geometry import Circle, Line, Point, Segment
from newclid.geometry import Measure, Value
import newclid.graph_utils as utils
import newclid.numericals as nm
import newclid.problem as problem
from newclid.problem import Dependency, EmptyDependency


np = nm.np


FREE = [
    "free",
    "segment",
    "r_triangle",
    "risos",
    "triangle",
    "triangle12",
    "ieq_triangle",
    "eq_quadrangle",
    "eq_trapezoid",
    "eqdia_quadrangle",
    "quadrangle",
    "r_trapezoid",
    "rectangle",
    "isquare",
    "trapezoid",
    "pentagon",
    "iso_triangle",
]

INTERSECT = [
    "angle_bisector",
    "angle_mirror",
    "eqdistance",
    "lc_tangent",
    "on_aline",
    "on_bline",
    "on_circle",
    "on_line",
    "on_pline",
    "on_tline",
    "on_dia",
    "s_angle",
    "on_opline",
    "eqangle3",
]


class DepCheckFailError(Exception):
    pass


class PointTooCloseError(Exception):
    pass


class PointTooFarError(Exception):
    pass


class Graph:
    """Graph data structure representing proof state."""

    def __init__(self):
        self.type2nodes = {
            Point: [],
            Line: [],
            Segment: [],
            Circle: [],
            Direction: [],
            Length: [],
            Angle: [],
            Ratio: [],
            Measure: [],
            Value: [],
        }
        self._name2point = {}
        self._name2node = {}

        self.rconst = {}  # contains all constant ratios
        self.aconst = {}  # contains all constant angles.

        self.halfpi, _ = self.get_or_create_const_ang(1, 2)
        self.vhalfpi = self.halfpi.val

        self.atable = ar.AngleTable()
        self.dtable = ar.DistanceTable()
        self.rtable = ar.RatioTable()

        # to quick access deps.
        self.cache = {}

        self._pair2line = {}
        self._triplet2circle = {}

    def copy(self) -> Graph:
        """Make a copy of self."""
        p, definitions = self.build_def

        p = p.copy()
        for clause in p.clauses:
            clause.nums = []
            for pname in clause.points:
                clause.nums.append(self._name2node[pname].num)

        g, _ = Graph.build_problem(p, definitions, verbose=False, init_copy=False)

        g.build_clauses = list(getattr(self, "build_clauses", []))
        return g

    def _create_const_ang(self, n: int, d: int) -> None:
        n, d = ar.simplify(n, d)
        ang = self.aconst[(n, d)] = self.new_node(Angle, f"{n}pi/{d}")
        ang.set_directions(None, None)
        self.connect_val(ang, deps=None)

    def _create_const_rat(self, n: int, d: int) -> None:
        n, d = ar.simplify(n, d)
        rat = self.rconst[(n, d)] = self.new_node(Ratio, f"{n}/{d}")
        rat.set_lengths(None, None)
        self.connect_val(rat, deps=None)

    def get_or_create_const_ang(self, n: int, d: int) -> None:
        n, d = ar.simplify(n, d)
        if (n, d) not in self.aconst:
            self._create_const_ang(n, d)
        ang1 = self.aconst[(n, d)]

        n, d = ar.simplify(d - n, d)
        if (n, d) not in self.aconst:
            self._create_const_ang(n, d)
        ang2 = self.aconst[(n, d)]
        return ang1, ang2

    def get_or_create_const_rat(self, n: int, d: int) -> None:
        n, d = ar.simplify(n, d)
        if (n, d) not in self.rconst:
            self._create_const_rat(n, d)
        rat1 = self.rconst[(n, d)]

        if (d, n) not in self.rconst:
            self._create_const_rat(d, n)
        rat2 = self.rconst[(d, n)]
        return rat1, rat2

    def add_algebra(self, dep: Dependency, level: int) -> None:
        """Add new algebraic predicates."""
        _ = level
        if dep.name not in [
            "para",
            "perp",
            "eqangle",
            "eqratio",
            "aconst",
            "rconst",
            "cong",
        ]:
            return

        name, args = dep.name, dep.args

        if name == "para":
            ab, cd = dep.algebra
            self.atable.add_para(ab, cd, dep)

        if name == "perp":
            ab, cd = dep.algebra
            self.atable.add_const_angle(ab, cd, 90, dep)

        if name == "eqangle":
            ab, cd, mn, pq = dep.algebra
            if (ab, cd) == (pq, mn):
                self.atable.add_const_angle(ab, cd, 90, dep)
            else:
                self.atable.add_eqangle(ab, cd, mn, pq, dep)

        if name == "eqratio":
            ab, cd, mn, pq = dep.algebra
            if (ab, cd) == (pq, mn):
                self.rtable.add_eq(ab, cd, dep)
            else:
                self.rtable.add_eqratio(ab, cd, mn, pq, dep)

        if name == "aconst":
            bx, ab, y = dep.algebra
            self.atable.add_const_angle(bx, ab, y, dep)

        if name == "rconst":
            l1, l2, m, n = dep.algebra
            self.rtable.add_const_ratio(l1, l2, m, n, dep)

        if name == "cong":
            a, b, c, d = args
            ab, _ = self.get_line_thru_pair_why(a, b)
            cd, _ = self.get_line_thru_pair_why(c, d)
            self.dtable.add_cong(ab, cd, a, b, c, d, dep)

            ab, cd = dep.algebra
            self.rtable.add_eq(ab, cd, dep)

    def add_eqrat_const(
        self, args: list[Point], deps: EmptyDependency
    ) -> list[Dependency]:
        """Add new algebraic predicates of type eqratio-constant."""
        a, b, c, d, num, den = args
        nd, dn = self.get_or_create_const_rat(num, den)

        if num == den:
            return self.add_cong([a, b, c, d], deps)

        ab = self._get_or_create_segment(a, b, deps=None)
        cd = self._get_or_create_segment(c, d, deps=None)

        self.connect_val(ab, deps=None)
        self.connect_val(cd, deps=None)

        if ab.val == cd.val:
            raise ValueError(f"{ab.name} and {cd.name} cannot be equal")

        args = [a, b, c, d, nd]
        i = 0
        for x, y, xy in [(a, b, ab), (c, d, cd)]:
            i += 1
            x_, y_ = list(xy._val._obj.points)
            if {x, y} == {x_, y_}:
                continue
            if deps:
                deps = deps.extend(self, "rconst", list(args), "cong", [x, y, x_, y_])
            args[2 * i - 2] = x_
            args[2 * i - 1] = y_

        ab_cd, cd_ab, why = self._get_or_create_ratio(ab, cd, deps=None)
        if why:
            dep0 = deps.populate("rconst", [a, b, c, d, nd])
            deps = EmptyDependency(level=deps.level, rule_name=None)
            deps.why = [dep0] + why

        lab, lcd = ab_cd._l
        a, b = list(lab._obj.points)
        c, d = list(lcd._obj.points)

        add = []
        if not self.is_equal(ab_cd, nd):
            args = [a, b, c, d, nd]
            dep1 = deps.populate("rconst", args)
            dep1.algebra = ab._val, cd._val, num, den
            self.make_equal(nd, ab_cd, deps=dep1)
            self.cache_dep("rconst", [a, b, c, d, nd], dep1)
            add += [dep1]

        if not self.is_equal(cd_ab, dn):
            args = [c, d, a, b, dn]
            dep2 = deps.populate("rconst", args)
            dep2.algebra = cd._val, ab._val, num, den
            self.make_equal(dn, cd_ab, deps=dep2)
            self.cache_dep("rconst", [c, d, a, b, dn], dep2)
            add += [dep2]

        return add

    def do_algebra(self, name: str, args: list[Point]) -> list[Dependency]:
        """Derive (but not add) new algebraic predicates."""
        if name == "para":
            a, b, dep = args
            if gm.is_equiv(a, b):
                return []
            (x, y), (m, n) = a._obj.points, b._obj.points
            return self.add_para([x, y, m, n], dep)

        if name == "aconst":
            a, b, n, d, dep = args
            ab, ba, why = self.get_or_create_angle_d(a, b, deps=None)
            nd, dn = self.get_or_create_const_ang(n, d)

            (x, y), (m, n) = a._obj.points, b._obj.points

            if why:
                dep0 = dep.populate("aconst", [x, y, m, n, nd])
                dep = EmptyDependency(level=dep.level, rule_name=None)
                dep.why = [dep0] + why

            a, b = ab._d
            (x, y), (m, n) = a._obj.points, b._obj.points

            added = []
            if not self.is_equal(ab, nd):
                if nd == self.halfpi:
                    added += self.add_perp([x, y, m, n], dep)
                # else:
                name = "aconst"
                args = [x, y, m, n, nd]
                dep1 = dep.populate(name, args)
                self.cache_dep(name, args, dep1)
                self.make_equal(nd, ab, deps=dep1)
                added += [dep1]

            if not self.is_equal(ba, dn):
                if dn == self.halfpi:
                    added += self.add_perp([m, n, x, y], dep)
                name = "aconst"
                args = [m, n, x, y, dn]
                dep2 = dep.populate(name, args)
                self.cache_dep(name, args, dep2)
                self.make_equal(dn, ba, deps=dep2)
                added += [dep2]
            return added

        if name == "rconst":
            a, b, c, d, num, den, dep = args
            return self.add_eqrat_const([a, b, c, d, num, den], dep)

        if name == "eqangle":
            d1, d2, d3, d4, dep = args
            a, b = d1._obj.points
            c, d = d2._obj.points
            e, f = d3._obj.points
            g, h = d4._obj.points

            return self.add_eqangle([a, b, c, d, e, f, g, h], dep)

        if name == "eqratio":
            d1, d2, d3, d4, dep = args
            a, b = d1._obj.points
            c, d = d2._obj.points
            e, f = d3._obj.points
            g, h = d4._obj.points

            return self.add_eqratio([a, b, c, d, e, f, g, h], dep)

        if name in ["cong", "cong2"]:
            a, b, c, d, dep = args
            if not (a != b and c != d and (a != c or b != d)):
                return []
            return self.add_cong([a, b, c, d], dep)

        return []

    def derive_algebra(
        self, level: int, verbose: bool = False
    ) -> tuple[dict[str, list[tuple[Point, ...]]], dict[str, list[tuple[Point, ...]]]]:
        """Derive new algebraic predicates."""
        derives = {}
        ang_derives = self.derive_angle_algebra(level, verbose=verbose)
        dist_derives = self.derive_distance_algebra(level, verbose=verbose)
        rat_derives = self.derive_ratio_algebra(level, verbose=verbose)

        derives.update(ang_derives)
        derives.update(dist_derives)
        derives.update(rat_derives)

        # Separate eqangle and eqratio derivations
        # As they are too numerous => slow down DD+AR.
        # & reserve them only for last effort.
        eqs = {"eqangle": derives.pop("eqangle"), "eqratio": derives.pop("eqratio")}
        return derives, eqs

    def derive_ratio_algebra(
        self, level: int, verbose: bool = False
    ) -> dict[str, list[tuple[Point, ...]]]:
        """Derive new eqratio predicates."""
        added = {"cong2": [], "eqratio": []}

        for x in self.rtable.get_all_eqs_and_why():
            x, why = x[:-1], x[-1]
            dep = EmptyDependency(level=level, rule_name="a01")
            dep.why = why

            if len(x) == 2:
                a, b = x
                if gm.is_equiv(a, b):
                    continue

                (m, n), (p, q) = a._obj.points, b._obj.points
                added["cong2"].append((m, n, p, q, dep))

            if len(x) == 4:
                a, b, c, d = x
                added["eqratio"].append((a, b, c, d, dep))

        return added

    def derive_angle_algebra(
        self, level: int, verbose: bool = False
    ) -> dict[str, list[tuple[Point, ...]]]:
        """Derive new eqangles predicates."""
        added = {"eqangle": [], "aconst": [], "para": []}

        for x in self.atable.get_all_eqs_and_why():
            x, why = x[:-1], x[-1]
            dep = EmptyDependency(level=level, rule_name="a02")
            dep.why = why

            if len(x) == 2:
                a, b = x
                if gm.is_equiv(a, b):
                    continue

                (e, f), (p, q) = a._obj.points, b._obj.points
                if not nm.check("para", [e, f, p, q]):
                    continue

                added["para"].append((a, b, dep))

            if len(x) == 3:
                a, b, (n, d) = x

                (e, f), (p, q) = a._obj.points, b._obj.points
                if not nm.check("aconst", [e, f, p, q, n, d]):
                    continue

                added["aconst"].append((a, b, n, d, dep))

            if len(x) == 4:
                a, b, c, d = x
                added["eqangle"].append((a, b, c, d, dep))

        return added

    def derive_distance_algebra(
        self, level: int, verbose: bool = False
    ) -> dict[str, list[tuple[Point, ...]]]:
        """Derive new cong predicates."""
        added = {"inci": [], "cong": [], "rconst": []}
        for x in self.dtable.get_all_eqs_and_why():
            x, why = x[:-1], x[-1]
            dep = EmptyDependency(level=level, rule_name="a00")
            dep.why = why

            if len(x) == 2:
                a, b = x
                if a == b:
                    continue

                dep.name = f"inci {a.name} {b.name}"
                added["inci"].append((x, dep))

            if len(x) == 4:
                a, b, c, d = x
                if not (a != b and c != d and (a != c or b != d)):
                    continue
                added["cong"].append((a, b, c, d, dep))

            if len(x) == 6:
                a, b, c, d, num, den = x
                if not (a != b and c != d and (a != c or b != d)):
                    continue
                added["rconst"].append((a, b, c, d, num, den, dep))

        return added

    @classmethod
    def build_problem(
        cls,
        pr: problem.Problem,
        definitions: dict[str, problem.Definition],
        verbose: bool = True,
        init_copy: bool = True,
    ) -> tuple[Graph, list[Dependency]]:
        """Build a problem into a gr.Graph object."""
        check = False
        g = None
        added = None
        if verbose:
            logging.info(pr.url)
            logging.info(pr.txt())
        while not check:
            try:
                g = Graph()
                added = []
                plevel = 0
                for clause in pr.clauses:
                    adds, plevel = g.add_clause(
                        clause, plevel, definitions, verbose=verbose
                    )
                    added += adds
                g.plevel = plevel

            except (nm.InvalidLineIntersectError, nm.InvalidQuadSolveError):
                continue
            except DepCheckFailError:
                continue
            except (PointTooCloseError, PointTooFarError):
                continue

            if not pr.goal:
                break

            args = list(map(lambda x: g.get(x, lambda: int(x)), pr.goal.args))
            check = nm.check(pr.goal.name, args)

        g.url = pr.url
        g.build_def = (pr, definitions)
        for add in added:
            g.add_algebra(add, level=0)

        return g, added

    def all_points(self) -> list[Point]:
        """Return all nodes of type Point."""
        return list(self.type2nodes[Point])

    def all_nodes(self) -> list[gm.Node]:
        """Return all nodes."""
        return list(self._name2node.values())

    def add_points(self, pnames: list[str]) -> list[Point]:
        """Add new points with given names in list pnames."""
        result = [self.new_node(Point, name) for name in pnames]
        self._name2point.update(zip(pnames, result))
        return result

    def names2nodes(self, pnames: list[str]) -> list[gm.Node]:
        return [self._name2node[name] for name in pnames]

    def names2points(
        self, pnames: list[str], create_new_point: bool = False
    ) -> list[Point]:
        """Return Point objects given names."""
        result = []
        for name in pnames:
            if name not in self._name2node and not create_new_point:
                raise ValueError(f"Cannot find point {name} in graph")
            elif name in self._name2node:
                obj = self._name2node[name]
            else:
                obj = self.new_node(Point, name)
            result.append(obj)

        return result

    def names2points_or_int(self, pnames: list[str]) -> list[Point]:
        """Return Point objects given names."""
        result = []
        for name in pnames:
            if name.isdigit():
                result += [int(name)]
            elif "pi/" in name:
                n, d = name.split("pi/")
                ang, _ = self.get_or_create_const_ang(int(n), int(d))
                result += [ang]
            elif "/" in name:
                n, d = name.split("/")
                rat, _ = self.get_or_create_const_rat(int(n), int(d))
                result += [rat]
            else:
                result += [self._name2point[name]]

        return result

    def get(self, pointname: str, default_fn: Callable[[str], Point]) -> Point:
        if pointname in self._name2point:
            return self._name2point[pointname]
        if pointname in self._name2node:
            return self._name2node[pointname]
        return default_fn()

    def new_node(self, oftype: Type[gm.Node], name: str = "") -> gm.Node:
        node = oftype(name, self)

        self.type2nodes[oftype].append(node)
        self._name2node[name] = node

        if isinstance(node, Point):
            self._name2point[name] = node

        return node

    def merge(self, nodes: list[gm.Node], deps: Dependency) -> gm.Node:
        """Merge all nodes."""
        if len(nodes) < 2:
            return

        node0, *nodes1 = nodes
        all_nodes = self.type2nodes[type(node0)]

        # find node0 that exists in all_nodes to be the rep
        # and merge all other nodes into node0
        for node in nodes:
            if node in all_nodes:
                node0 = node
                nodes1 = [n for n in nodes if n != node0]
                break
        return self.merge_into(node0, nodes1, deps)

    def merge_into(
        self, node0: gm.Node, nodes1: list[gm.Node], deps: Dependency
    ) -> gm.Node:
        """Merge nodes1 into a single node0."""
        node0.merge(nodes1, deps)
        for n in nodes1:
            if n.rep() != n:
                self.remove([n])

        nodes = [node0] + nodes1
        if any([node._val for node in nodes]):
            for node in nodes:
                self.connect_val(node, deps=None)

            vals1 = [n._val for n in nodes1]
            node0._val.merge(vals1, deps)

            for v in vals1:
                if v.rep() != v:
                    self.remove([v])

        return node0

    def remove(self, nodes: list[gm.Node]) -> None:
        """Remove nodes out of self because they are merged."""
        if not nodes:
            return

        for node in nodes:
            all_nodes = self.type2nodes[type(nodes[0])]

            if node in all_nodes:
                all_nodes.remove(node)

            if node.name in self._name2node.values():
                self._name2node.pop(node.name)

    def connect(self, a: gm.Node, b: gm.Node, deps: Dependency) -> None:
        a.connect_to(b, deps)
        b.connect_to(a, deps)

    def connect_val(self, node: gm.Node, deps: Dependency) -> gm.Node:
        """Connect a node into its value (equality) node."""
        if node._val:
            return node._val
        name = None
        if isinstance(node, Line):
            name = "d(" + node.name + ")"
        if isinstance(node, Angle):
            name = "m(" + node.name + ")"
        if isinstance(node, Segment):
            name = "l(" + node.name + ")"
        if isinstance(node, Ratio):
            name = "r(" + node.name + ")"
        v = self.new_node(gm.val_type(node), name)
        self.connect(node, v, deps=deps)
        return v

    def is_equal(self, x: gm.Node, y: gm.Node, level: int = None) -> bool:
        return gm.is_equal(x, y, level)

    def add_piece(
        self, name: str, args: list[Point], deps: EmptyDependency
    ) -> list[Dependency]:
        """Add a new predicate."""
        if name in ["coll", "collx"]:
            return self.add_coll(args, deps)
        elif name == "para":
            return self.add_para(args, deps)
        elif name == "perp":
            return self.add_perp(args, deps)
        elif name == "midp":
            return self.add_midp(args, deps)
        elif name == "cong":
            return self.add_cong(args, deps)
        elif name == "circle":
            return self.add_circle(args, deps)
        elif name == "cyclic":
            return self.add_cyclic(args, deps)
        elif name in ["eqangle", "eqangle6"]:
            return self.add_eqangle(args, deps)
        elif name in ["eqratio", "eqratio6"]:
            return self.add_eqratio(args, deps)
        # numerical!
        elif name == "s_angle":
            return self.add_s_angle(args, deps)
        elif name == "aconst":
            a, b, c, d, ang = args

            if isinstance(ang, str):
                name = ang
            else:
                name = ang.name

            num, den = name.split("pi/")
            num, den = int(num), int(den)
            return self.add_aconst([a, b, c, d, num, den], deps)
        elif name == "s_angle":
            b, x, a, b, ang = args

            if isinstance(ang, str):
                name = ang
            else:
                name = ang.name

            n, d = name.split("pi/")
            ang = int(n) * 180 / int(d)
            return self.add_s_angle([a, b, x, ang], deps)
        elif name == "rconst":
            a, b, c, d, rat = args

            if isinstance(rat, str):
                name = rat
            else:
                name = rat.name

            num, den = name.split("/")
            num, den = int(num), int(den)
            return self.add_eqrat_const([a, b, c, d, num, den], deps)

        # composite pieces:
        elif name == "cong2":
            return self.add_cong2(args, deps)
        elif name == "eqratio3":
            return self.add_eqratio3(args, deps)
        elif name == "eqratio4":
            return self.add_eqratio4(args, deps)
        elif name == "simtri":
            return self.add_simtri(args, deps)
        elif name == "contri":
            return self.add_contri(args, deps)
        elif name == "simtri2":
            return self.add_simtri2(args, deps)
        elif name == "contri2":
            return self.add_contri2(args, deps)
        elif name == "simtri*":
            return self.add_simtri_check(args, deps)
        elif name == "contri*":
            return self.add_contri_check(args, deps)
        elif name in ["acompute", "rcompute"]:
            dep = deps.populate(name, args)
            self.cache_dep(name, args, dep)
            return [dep]
        elif name in ["fixl", "fixc", "fixb", "fixt", "fixp"]:
            dep = deps.populate(name, args)
            self.cache_dep(name, args, dep)
            return [dep]
        elif name in ["ind"]:
            return []
        raise ValueError(f"Not recognize {name}")

    def check(self, name: str, args: list[Point]) -> bool:
        """Symbolically check if a predicate is True."""
        if name == "ncoll":
            return self.check_ncoll(args)
        if name == "npara":
            return self.check_npara(args)
        if name == "nperp":
            return self.check_nperp(args)
        if name == "midp":
            return self.check_midp(args)
        if name == "cong":
            return self.check_cong(args)
        if name == "perp":
            return self.check_perp(args)
        if name == "para":
            return self.check_para(args)
        if name == "coll":
            return self.check_coll(args)
        if name == "cyclic":
            return self.check_cyclic(args)
        if name == "circle":
            return self.check_circle(args)
        if name == "aconst":
            return self.check_aconst(args)
        if name == "rconst":
            return self.check_rconst(args)
        if name == "acompute":
            return self.check_acompute(args)
        if name == "rcompute":
            return self.check_rcompute(args)
        if name in ["eqangle", "eqangle6"]:
            if len(args) == 5:
                return self.check_aconst(args)
            return self.check_eqangle(args)
        if name in ["eqratio", "eqratio6"]:
            if len(args) == 5:
                return self.check_rconst(args)
            return self.check_eqratio(args)
        if name in ["simtri", "simtri2", "simtri*"]:
            return self.check_simtri(args)
        if name in ["contri", "contri2", "contri*"]:
            return self.check_contri(args)
        if name == "sameside":
            return self.check_sameside(args)
        if name in "diff":
            a, b = args
            return not a.num.close(b.num)
        if name in ["fixl", "fixc", "fixb", "fixt", "fixp"]:
            return self.in_cache(name, args)
        if name in ["ind"]:
            return True
        raise ValueError(f"Not recognize {name}")

    def get_lines_thru_all(self, *points: list[gm.Point]) -> list[Line]:
        line2count = defaultdict(lambda: 0)
        points = set(points)
        for p in points:
            for line_neighbor in p.neighbors(Line):
                line2count[line_neighbor] += 1
        return [line for line, count in line2count.items() if count == len(points)]

    def _get_line(self, a: Point, b: Point) -> Optional[Line]:
        linesa = a.neighbors(Line)
        for line in b.neighbors(Line):
            if line in linesa:
                return line
        return None

    def _get_line_all(self, a: Point, b: Point) -> Generator[Line, None, None]:
        linesa = a.neighbors(Line, do_rep=False)
        linesb = b.neighbors(Line, do_rep=False)
        for lineb in linesb:
            if lineb in linesa:
                yield lineb

    def _get_lines(self, *points: list[Point]) -> list[Line]:
        """Return all lines that connect to >= 2 points."""
        line2count = defaultdict(lambda: 0)
        for p in points:
            for line_neighbor in p.neighbors(Line):
                line2count[line_neighbor] += 1
        return [line for line, count in line2count.items() if count >= 2]

    def get_circle_thru_triplet(self, p1: Point, p2: Point, p3: Point) -> Circle:
        p1, p2, p3 = sorted([p1, p2, p3], key=lambda x: x.name)
        if (p1, p2, p3) in self._triplet2circle:
            return self._triplet2circle[(p1, p2, p3)]
        return self.get_new_circle_thru_triplet(p1, p2, p3)

    def get_new_circle_thru_triplet(self, p1: Point, p2: Point, p3: Point) -> Circle:
        """Get a new Circle that goes thru three given Points."""
        p1, p2, p3 = sorted([p1, p2, p3], key=lambda x: x.name)
        name = p1.name.lower() + p2.name.lower() + p3.name.lower()
        circle = self.new_node(Circle, f"({name})")
        circle.num = nm.Circle(p1=p1.num, p2=p2.num, p3=p3.num)
        circle.points = p1, p2, p3

        self.connect(p1, circle, deps=None)
        self.connect(p2, circle, deps=None)
        self.connect(p3, circle, deps=None)
        self._triplet2circle[(p1, p2, p3)] = circle
        return circle

    def get_line_thru_pair(self, p1: Point, p2: Point) -> Line:
        if (p1, p2) in self._pair2line:
            return self._pair2line[(p1, p2)]
        if (p2, p1) in self._pair2line:
            return self._pair2line[(p2, p1)]
        return self.get_new_line_thru_pair(p1, p2)

    def get_new_line_thru_pair(self, p1: Point, p2: Point) -> Line:
        if p1.name.lower() > p2.name.lower():
            p1, p2 = p2, p1
        name = p1.name.lower() + p2.name.lower()
        line = self.new_node(Line, name)
        line.num = nm.Line(p1.num, p2.num)
        line.points = p1, p2

        self.connect(p1, line, deps=None)
        self.connect(p2, line, deps=None)
        self._pair2line[(p1, p2)] = line
        return line

    def get_line_thru_pair_why(
        self, p1: Point, p2: Point
    ) -> tuple[Line, list[Dependency]]:
        """Get one line thru two given points and the corresponding dependency list."""
        if p1.name.lower() > p2.name.lower():
            p1, p2 = p2, p1
        if (p1, p2) in self._pair2line:
            return self._pair2line[(p1, p2)].rep_and_why()

        line, why = gm.line_of_and_why([p1, p2])
        if line is None:
            line = self.get_new_line_thru_pair(p1, p2)
            why = []
        return line, why

    def coll_dep(self, points: list[Point], p: Point) -> list[Dependency]:
        """Return the dep(.why) explaining why p is coll with points."""
        for p1, p2 in utils.comb2(points):
            if self.check_coll([p1, p2, p]):
                dep = Dependency("coll", [p1, p2, p], None, None)
                return dep.why_me_or_cache(self, None)

    def add_coll(self, points: list[Point], deps: EmptyDependency) -> list[Dependency]:
        """Add a predicate that `points` are collinear."""
        points = list(set(points))
        og_points = list(points)

        all_lines = []
        for p1, p2 in utils.comb2(points):
            all_lines.append(self.get_line_thru_pair(p1, p2))
        points = sum([line.neighbors(Point) for line in all_lines], [])
        points = list(set(points))

        existed = set()
        new = set()
        for p1, p2 in utils.comb2(points):
            if p1.name > p2.name:
                p1, p2 = p2, p1
            if (p1, p2) in self._pair2line:
                line = self._pair2line[(p1, p2)]
                existed.add(line)
            else:
                line = self.get_new_line_thru_pair(p1, p2)
                new.add(line)

        existed = sorted(existed, key=lambda node: node.name)
        new = sorted(new, key=lambda node: node.name)

        existed, new = list(existed), list(new)
        if not existed:
            line0, *lines = new
        else:
            line0, lines = existed[0], existed[1:] + new

        add = []
        line0, why0 = line0.rep_and_why()
        a, b = line0.points
        for line in lines:
            c, d = line.points
            args = list({a, b, c, d})
            if len(args) < 3:
                continue

            whys = []
            for x in args:
                if x not in og_points:
                    whys.append(self.coll_dep(og_points, x))

            abcd_deps = deps
            if whys + why0:
                dep0 = deps.populate("coll", og_points)
                abcd_deps = EmptyDependency(level=deps.level, rule_name=None)
                abcd_deps.why = [dep0] + whys

            is_coll = self.check_coll(args)
            dep = abcd_deps.populate("coll", args)
            self.cache_dep("coll", args, dep)
            self.merge_into(line0, [line], dep)

            if not is_coll:
                add += [dep]

        return add

    def check_coll(self, points: list[Point]) -> bool:
        points = list(set(points))
        if len(points) < 3:
            return True
        line2count = defaultdict(lambda: 0)
        for p in points:
            for line in p.neighbors(Line):
                line2count[line] += 1
        return any([count == len(points) for _, count in line2count.items()])

    def why_coll(self, args: tuple[Line, list[Point]]) -> list[Dependency]:
        line, points = args
        return line.why_coll(points)

    def check_ncoll(self, points: list[Point]) -> bool:
        if self.check_coll(points):
            return False
        return not nm.check_coll([p.num for p in points])

    def check_sameside(self, points: list[Point]) -> bool:
        return nm.check_sameside([p.num for p in points])

    def make_equal(self, x: gm.Node, y: gm.Node, deps: Dependency) -> None:
        """Make that two nodes x and y are equal, i.e. merge their value node."""
        if x.val is None:
            x, y = y, x

        self.connect_val(x, deps=None)
        self.connect_val(y, deps=None)
        vx = x._val
        vy = y._val

        if vx == vy:
            return

        merges = [vx, vy]

        if (
            isinstance(x, Angle)
            and x not in self.aconst.values()
            and y not in self.aconst.values()
            and x.directions == y.directions[::-1]
            and x.directions[0] != x.directions[1]
        ):
            merges = [self.vhalfpi, vx, vy]

        self.merge(merges, deps)

    def merge_vals(self, vx: gm.Node, vy: gm.Node, deps: Dependency) -> None:
        if vx == vy:
            return
        merges = [vx, vy]
        self.merge(merges, deps)

    def why_equal(self, x: gm.Node, y: gm.Node, level: int) -> list[Dependency]:
        return gm.why_equal(x, y, level)

    def _why_coll4(
        self,
        a: Point,
        b: Point,
        ab: Line,
        c: Point,
        d: Point,
        cd: Line,
        level: int,
    ) -> list[Dependency]:
        return self._why_coll2(a, b, ab, level) + self._why_coll2(c, d, cd, level)

    def _why_coll8(
        self,
        a: Point,
        b: Point,
        ab: Line,
        c: Point,
        d: Point,
        cd: Line,
        m: Point,
        n: Point,
        mn: Line,
        p: Point,
        q: Point,
        pq: Line,
        level: int,
    ) -> list[Dependency]:
        """Dependency list of why 8 points are collinear."""
        why8 = self._why_coll4(a, b, ab, c, d, cd, level)
        why8 += self._why_coll4(m, n, mn, p, q, pq, level)
        return why8

    def add_para(self, points: list[Point], deps: EmptyDependency) -> list[Dependency]:
        """Add a new predicate that 4 points (2 lines) are parallel."""
        a, b, c, d = points
        ab, why1 = self.get_line_thru_pair_why(a, b)
        cd, why2 = self.get_line_thru_pair_why(c, d)

        is_equal = self.is_equal(ab, cd)

        (a, b), (c, d) = ab.points, cd.points

        dep0 = deps.populate("para", points)
        deps = EmptyDependency(level=deps.level, rule_name=None)

        deps = deps.populate("para", [a, b, c, d])
        deps.why = [dep0] + why1 + why2

        self.make_equal(ab, cd, deps)
        deps.algebra = ab._val, cd._val

        self.cache_dep("para", [a, b, c, d], deps)
        if not is_equal:
            return [deps]
        return []

    def why_para(self, args: list[Point]) -> list[Dependency]:
        ab, cd, lvl = args
        return self.why_equal(ab, cd, lvl)

    def check_para_or_coll(self, points: list[Point]) -> bool:
        return self.check_para(points) or self.check_coll(points)

    def check_para(self, points: list[Point]) -> bool:
        a, b, c, d = points
        if (a == b) or (c == d):
            return False
        ab = self._get_line(a, b)
        cd = self._get_line(c, d)
        if not ab or not cd:
            return False

        return self.is_equal(ab, cd)

    def check_npara(self, points: list[Point]) -> bool:
        if self.check_para(points):
            return False
        return not nm.check_para([p.num for p in points])

    def _get_angle(self, d1: Direction, d2: Direction) -> tuple[Angle, Optional[Angle]]:
        for a in self.type2nodes[Angle]:
            if a.directions == (d1, d2):
                return a, a.opposite
        return None, None

    def get_first_angle(self, l1: Line, l2: Line) -> tuple[Angle, list[Dependency]]:
        """Get a first angle between line l1 and line l2."""
        d1, d2 = l1._val, l2._val

        d1s = d1.all_reps()
        d2s = d2.all_reps()

        found = d1.first_angle(d2s)
        if found is None:
            found = d2.first_angle(d1s)
            if found is None:
                return None, []
            ang, x2, x1 = found
            found = ang.opposite, x1, x2

        ang, x1, x2 = found
        return ang, d1.deps_upto(x1) + d2.deps_upto(x2)

    def _get_or_create_angle(
        self, l1: Line, l2: Line, deps: Dependency
    ) -> tuple[Angle, Angle, list[Dependency]]:
        return self.get_or_create_angle_d(l1._val, l2._val, deps)

    def get_or_create_angle_d(
        self, d1: Direction, d2: Direction, deps: Dependency
    ) -> tuple[Angle, Angle, list[Dependency]]:
        """Get or create an angle between two Direction d1 and d2."""
        for a in self.type2nodes[Angle]:
            if a.directions == (d1.rep(), d2.rep()):  # directions = _d.rep()
                d1_, d2_ = a._d
                why1 = d1.why_equal([d1_], None) + d1_.why_rep()
                why2 = d2.why_equal([d2_], None) + d2_.why_rep()
                return a, a.opposite, why1 + why2

        d1, why1 = d1.rep_and_why()
        d2, why2 = d2.rep_and_why()
        a12 = self.new_node(Angle, f"{d1.name}-{d2.name}")
        a21 = self.new_node(Angle, f"{d2.name}-{d1.name}")
        self.connect(d1, a12, deps)
        self.connect(d2, a21, deps)
        self.connect(a12, a21, deps)
        a12.set_directions(d1, d2)
        a21.set_directions(d2, d1)
        a12.opposite = a21
        a21.opposite = a12
        return a12, a21, why1 + why2

    def _add_para_or_coll(
        self,
        a: Point,
        b: Point,
        c: Point,
        d: Point,
        x: Point,
        y: Point,
        m: Point,
        n: Point,
        deps: EmptyDependency,
    ) -> list[Dependency]:
        """Add a new parallel or collinear predicate."""
        extends = [("perp", [x, y, m, n])]
        if {a, b} == {x, y}:
            pass
        elif self.check_para([a, b, x, y]):
            extends.append(("para", [a, b, x, y]))
        elif self.check_coll([a, b, x, y]):
            extends.append(("coll", set(list([a, b, x, y]))))
        else:
            return None

        if m in [c, d] or n in [c, d] or c in [m, n] or d in [m, n]:
            pass
        elif self.check_coll([c, d, m]):
            extends.append(("coll", [c, d, m]))
        elif self.check_coll([c, d, n]):
            extends.append(("coll", [c, d, n]))
        elif self.check_coll([c, m, n]):
            extends.append(("coll", [c, m, n]))
        elif self.check_coll([d, m, n]):
            extends.append(("coll", [d, m, n]))
        else:
            deps = deps.extend_many(self, "perp", [a, b, c, d], extends)
            return self.add_para([c, d, m, n], deps)

        deps = deps.extend_many(self, "perp", [a, b, c, d], extends)
        return self.add_coll(list(set([c, d, m, n])), deps)

    def maybe_make_para_from_perp(
        self, points: list[Point], deps: EmptyDependency
    ) -> Optional[list[Dependency]]:
        """Maybe add a new parallel predicate from perp predicate."""
        a, b, c, d = points
        halfpi = self.aconst[(1, 2)]
        for ang in halfpi.val.neighbors(Angle):
            if ang == halfpi:
                continue
            d1, d2 = ang.directions
            x, y = d1._obj.points
            m, n = d2._obj.points

            for args in [
                (a, b, c, d, x, y, m, n),
                (a, b, c, d, m, n, x, y),
                (c, d, a, b, x, y, m, n),
                (c, d, a, b, m, n, x, y),
            ]:
                args = args + (deps,)
                add = self._add_para_or_coll(*args)
                if add:
                    return add

        return None

    def add_perp(self, points: list[Point], deps: EmptyDependency) -> list[Dependency]:
        """Add a new perpendicular predicate from 4 points (2 lines)."""
        add = self.maybe_make_para_from_perp(points, deps)
        if add is not None:
            return add

        a, b, c, d = points
        ab, why1 = self.get_line_thru_pair_why(a, b)
        cd, why2 = self.get_line_thru_pair_why(c, d)

        (a, b), (c, d) = ab.points, cd.points

        if why1 + why2:
            dep0 = deps.populate("perp", points)
            deps = EmptyDependency(level=deps.level, rule_name=None)
            deps.why = [dep0] + why1 + why2

        self.connect_val(ab, deps=None)
        self.connect_val(cd, deps=None)

        if ab.val == cd.val:
            raise ValueError(f"{ab.name} and {cd.name} Cannot be perp.")

        args = [a, b, c, d]
        i = 0
        for x, y, xy in [(a, b, ab), (c, d, cd)]:
            i += 1
            x_, y_ = xy._val._obj.points
            if {x, y} == {x_, y_}:
                continue
            if deps:
                deps = deps.extend(self, "perp", list(args), "para", [x, y, x_, y_])
            args[2 * i - 2] = x_
            args[2 * i - 1] = y_

        a12, a21, why = self._get_or_create_angle(ab, cd, deps=None)

        if why:
            dep0 = deps.populate("perp", [a, b, c, d])
            deps = EmptyDependency(level=deps.level, rule_name=None)
            deps.why = [dep0] + why

        dab, dcd = a12._d
        a, b = dab._obj.points
        c, d = dcd._obj.points

        is_equal = self.is_equal(a12, a21)
        deps = deps.populate("perp", [a, b, c, d])
        deps.algebra = [dab, dcd]
        self.make_equal(a12, a21, deps=deps)

        self.cache_dep("perp", [a, b, c, d], deps)
        self.cache_dep("eqangle", [a, b, c, d, c, d, a, b], deps)

        if not is_equal:
            return [deps]
        return []

    def why_perp(self, args: list[Union[Point, list[Dependency]]]) -> list[Dependency]:
        a, b, deps = args
        return deps + self.why_equal(a, b, None)

    def check_perpl(self, ab: Line, cd: Line) -> bool:
        if ab.val is None or cd.val is None:
            return False
        if ab.val == cd.val:
            return False
        a12, a21 = self._get_angle(ab.val, cd.val)
        if a12 is None or a21 is None:
            return False
        return self.is_equal(a12, a21)

    def check_perp(self, points: list[Point]) -> bool:
        a, b, c, d = points
        ab = self._get_line(a, b)
        cd = self._get_line(c, d)
        if not ab or not cd:
            return False
        return self.check_perpl(ab, cd)

    def check_nperp(self, points: list[Point]) -> bool:
        if self.check_perp(points):
            return False
        return not nm.check_perp([p.num for p in points])

    def _get_segment(self, p1: Point, p2: Point) -> Optional[Segment]:
        for s in self.type2nodes[Segment]:
            if s.points == {p1, p2}:
                return s
        return None

    def _get_or_create_segment(self, p1: Point, p2: Point, deps: Dependency) -> Segment:
        """Get or create a Segment object between two Points p1 and p2."""
        if p1 == p2:
            raise ValueError(f"Creating same 0-length segment {p1.name}")

        for s in self.type2nodes[Segment]:
            if s.points == {p1, p2}:
                return s

        if p1.name > p2.name:
            p1, p2 = p2, p1
        s = self.new_node(Segment, name=f"{p1.name.upper()}{p2.name.upper()}")
        self.connect(p1, s, deps=deps)
        self.connect(p2, s, deps=deps)
        s.points = {p1, p2}
        return s

    def add_cong(self, points: list[Point], deps: EmptyDependency) -> list[Dependency]:
        """Add that two segments (4 points) are congruent."""
        a, b, c, d = points
        ab = self._get_or_create_segment(a, b, deps=None)
        cd = self._get_or_create_segment(c, d, deps=None)

        is_equal = self.is_equal(ab, cd)

        dep = deps.populate("cong", [a, b, c, d])
        self.make_equal(ab, cd, deps=dep)
        dep.algebra = ab._val, cd._val

        self.cache_dep("cong", [a, b, c, d], dep)

        result = []

        if not is_equal:
            result += [dep]

        if a not in [c, d] and b not in [c, d]:
            return result

        if b in [c, d]:
            a, b = b, a
        if a == d:
            c, d = d, c

        result += self._maybe_add_cyclic_from_cong(a, b, d, dep)
        return result

    def _maybe_add_cyclic_from_cong(
        self, a: Point, b: Point, c: Point, cong_ab_ac: Dependency
    ) -> list[Dependency]:
        """Maybe add a new cyclic predicate from given congruent segments."""
        ab = self._get_or_create_segment(a, b, deps=None)

        # all eq segs with one end being a.
        segs = [s for s in ab.val.neighbors(Segment) if a in s.points]

        # all points on circle (a, b)
        points = []
        for s in segs:
            x, y = list(s.points)
            points.append(x if y == a else y)

        # for sure both b and c are in points
        points = [p for p in points if p not in [b, c]]

        if len(points) < 2:
            return []

        x, y = points[:2]

        if self.check_cyclic([b, c, x, y]):
            return []

        ax = self._get_or_create_segment(a, x, deps=None)
        ay = self._get_or_create_segment(a, y, deps=None)
        why = ab._val.why_equal([ax._val, ay._val], level=None)
        why += [cong_ab_ac]

        deps = EmptyDependency(cong_ab_ac.level, "")
        deps.why = why

        return self.add_cyclic([b, c, x, y], deps)

    def check_cong(self, points: list[Point]) -> bool:
        a, b, c, d = points
        if {a, b} == {c, d}:
            return True

        ab = self._get_segment(a, b)
        cd = self._get_segment(c, d)
        if ab is None or cd is None:
            return False
        return self.is_equal(ab, cd)

    def why_cong(self, args: tuple[Segment, Segment]) -> list[Dependency]:
        ab, cd = args
        return self.why_equal(ab, cd, None)

    def add_midp(self, points: list[Point], deps: EmptyDependency) -> list[Dependency]:
        m, a, b = points
        add = self.add_coll(points, deps=deps)
        add += self.add_cong([m, a, m, b], deps)
        return add

    def why_midp(
        self, args: tuple[Line, list[Point], Segment, Segment]
    ) -> list[Dependency]:
        line, points, ma, mb = args
        return self.why_coll([line, points]) + self.why_cong([ma, mb])

    def check_midp(self, points: list[Point]) -> bool:
        if not self.check_coll(points):
            return False
        m, a, b = points
        return self.check_cong([m, a, m, b])

    def add_circle(
        self, points: list[Point], deps: EmptyDependency
    ) -> list[Dependency]:
        o, a, b, c = points
        add = self.add_cong([o, a, o, b], deps=deps)
        add += self.add_cong([o, a, o, c], deps=deps)
        return add

    def why_circle(self, args: tuple[Segment, Segment, Segment]) -> list[Dependency]:
        oa, ob, oc = args
        return self.why_equal(oa, ob, None) and self.why_equal(oa, oc, None)

    def check_circle(self, points: list[Point]) -> bool:
        o, a, b, c = points
        return self.check_cong([o, a, o, b]) and self.check_cong([o, a, o, c])

    def get_circles_thru_all(self, *points: list[Point]) -> list[Circle]:
        circle2count = defaultdict(lambda: 0)
        points = set(points)
        for p in points:
            for c in p.neighbors(Circle):
                circle2count[c] += 1
        return [c for c, count in circle2count.items() if count == len(points)]

    def _get_circles(self, *points: list[Point]) -> list[Circle]:
        circle2count = defaultdict(lambda: 0)
        for p in points:
            for c in p.neighbors(Circle):
                circle2count[c] += 1
        return [c for c, count in circle2count.items() if count >= 3]

    def cyclic_dep(self, points: list[Point], p: Point) -> list[Dependency]:
        for p1, p2, p3 in utils.comb3(points):
            if self.check_cyclic([p1, p2, p3, p]):
                dep = Dependency("cyclic", [p1, p2, p3, p], None, None)
                return dep.why_me_or_cache(self, None)

    def add_cyclic(
        self, points: list[Point], deps: EmptyDependency
    ) -> list[Dependency]:
        """Add a new cyclic predicate that 4 points are concyclic."""
        points = list(set(points))
        og_points = list(points)

        all_circles = []
        for p1, p2, p3 in utils.comb3(points):
            all_circles.append(self.get_circle_thru_triplet(p1, p2, p3))
        points = sum([c.neighbors(Point) for c in all_circles], [])
        points = list(set(points))

        existed = set()
        new = set()
        for p1, p2, p3 in utils.comb3(points):
            p1, p2, p3 = sorted([p1, p2, p3], key=lambda x: x.name)

            if (p1, p2, p3) in self._triplet2circle:
                circle = self._triplet2circle[(p1, p2, p3)]
                existed.add(circle)
            else:
                circle = self.get_new_circle_thru_triplet(p1, p2, p3)
                new.add(circle)

        existed = sorted(existed, key=lambda node: node.name)
        new = sorted(new, key=lambda node: node.name)

        existed, new = list(existed), list(new)
        if not existed:
            circle0, *circles = new
        else:
            circle0, circles = existed[0], existed[1:] + new

        add = []
        circle0, why0 = circle0.rep_and_why()
        a, b, c = circle0.points
        for circle in circles:
            d, e, f = circle.points
            args = list({a, b, c, d, e, f})
            if len(args) < 4:
                continue
            whys = []
            for x in [a, b, c, d, e, f]:
                if x not in og_points:
                    whys.append(self.cyclic_dep(og_points, x))
            abcdef_deps = deps
            if whys + why0:
                dep0 = deps.populate("cyclic", og_points)
                abcdef_deps = EmptyDependency(level=deps.level, rule_name=None)
                abcdef_deps.why = [dep0] + whys

            is_cyclic = self.check_cyclic(args)

            dep = abcdef_deps.populate("cyclic", args)
            self.cache_dep("cyclic", args, dep)
            self.merge_into(circle0, [circle], dep)
            if not is_cyclic:
                add += [dep]

        return add

    def check_cyclic(self, points: list[Point]) -> bool:
        points = list(set(points))
        if len(points) < 4:
            return True
        circle2count = defaultdict(lambda: 0)
        for p in points:
            for c in p.neighbors(Circle):
                circle2count[c] += 1
        return any([count == len(points) for _, count in circle2count.items()])

    def make_equal_pairs(
        self,
        a: Point,
        b: Point,
        c: Point,
        d: Point,
        m: Point,
        n: Point,
        p: Point,
        q: Point,
        ab: Line,
        cd: Line,
        mn: Line,
        pq: Line,
        deps: EmptyDependency,
    ) -> list[Dependency]:
        """Add ab/cd = mn/pq in case either two of (ab,cd,mn,pq) are equal."""
        depname = "eqratio" if isinstance(ab, Segment) else "eqangle"
        eqname = "cong" if isinstance(ab, Segment) else "para"

        is_equal = self.is_equal(mn, pq)

        if ab != cd:
            dep0 = deps.populate(depname, [a, b, c, d, m, n, p, q])
            deps = EmptyDependency(level=deps.level, rule_name=None)

            dep = Dependency(eqname, [a, b, c, d], None, deps.level)
            deps.why = [dep0, dep.why_me_or_cache(self, None)]

        elif eqname == "para":  # ab == cd.
            colls = [a, b, c, d]
            if len(set(colls)) > 2:
                dep0 = deps.populate(depname, [a, b, c, d, m, n, p, q])
                deps = EmptyDependency(level=deps.level, rule_name=None)

                dep = Dependency("collx", colls, None, deps.level)
                deps.why = [dep0, dep.why_me_or_cache(self, None)]

        deps = deps.populate(eqname, [m, n, p, q])
        self.make_equal(mn, pq, deps=deps)

        deps.algebra = mn._val, pq._val
        self.cache_dep(eqname, [m, n, p, q], deps)

        if is_equal:
            return []
        return [deps]

    def maybe_make_equal_pairs(
        self,
        a: Point,
        b: Point,
        c: Point,
        d: Point,
        m: Point,
        n: Point,
        p: Point,
        q: Point,
        ab: Line,
        cd: Line,
        mn: Line,
        pq: Line,
        deps: EmptyDependency,
    ) -> Optional[list[Dependency]]:
        """Add ab/cd = mn/pq in case maybe either two of (ab,cd,mn,pq) are equal."""
        level = deps.level
        if self.is_equal(ab, cd, level):
            return self.make_equal_pairs(a, b, c, d, m, n, p, q, ab, cd, mn, pq, deps)
        elif self.is_equal(mn, pq, level):
            return self.make_equal_pairs(
                m,
                n,
                p,
                q,
                a,
                b,
                c,
                d,
                mn,
                pq,
                ab,
                cd,
                deps,
            )
        elif self.is_equal(ab, mn, level):
            return self.make_equal_pairs(
                a,
                b,
                m,
                n,
                c,
                d,
                p,
                q,
                ab,
                mn,
                cd,
                pq,
                deps,
            )
        elif self.is_equal(cd, pq, level):
            return self.make_equal_pairs(
                c,
                d,
                p,
                q,
                a,
                b,
                m,
                n,
                cd,
                pq,
                ab,
                mn,
                deps,
            )
        else:
            return None

    def _add_eqangle(
        self,
        a: Point,
        b: Point,
        c: Point,
        d: Point,
        m: Point,
        n: Point,
        p: Point,
        q: Point,
        ab: Line,
        cd: Line,
        mn: Line,
        pq: Line,
        deps: EmptyDependency,
    ) -> list[Dependency]:
        """Add eqangle core."""
        if deps:
            deps = deps.copy()

        args = [a, b, c, d, m, n, p, q]
        i = 0
        for x, y, xy in [(a, b, ab), (c, d, cd), (m, n, mn), (p, q, pq)]:
            i += 1
            x_, y_ = xy._val._obj.points
            if {x, y} == {x_, y_}:
                continue
            if deps:
                deps = deps.extend(self, "eqangle", list(args), "para", [x, y, x_, y_])

                args[2 * i - 2] = x_
                args[2 * i - 1] = y_

        add = []
        ab_cd, cd_ab, why1 = self._get_or_create_angle(ab, cd, deps=None)
        mn_pq, pq_mn, why2 = self._get_or_create_angle(mn, pq, deps=None)

        why = why1 + why2
        if why:
            dep0 = deps.populate("eqangle", args)
            deps = EmptyDependency(level=deps.level, rule_name=None)
            deps.why = [dep0] + why

        dab, dcd = ab_cd._d
        dmn, dpq = mn_pq._d

        a, b = dab._obj.points
        c, d = dcd._obj.points
        m, n = dmn._obj.points
        p, q = dpq._obj.points

        is_eq1 = self.is_equal(ab_cd, mn_pq)
        deps1 = None
        if deps:
            deps1 = deps.populate("eqangle", [a, b, c, d, m, n, p, q])
            deps1.algebra = [dab, dcd, dmn, dpq]
        if not is_eq1:
            add += [deps1]
        self.cache_dep("eqangle", [a, b, c, d, m, n, p, q], deps1)
        self.make_equal(ab_cd, mn_pq, deps=deps1)

        is_eq2 = self.is_equal(cd_ab, pq_mn)
        deps2 = None
        if deps:
            deps2 = deps.populate("eqangle", [c, d, a, b, p, q, m, n])
            deps2.algebra = [dcd, dab, dpq, dmn]
        if not is_eq2:
            add += [deps2]
        self.cache_dep("eqangle", [c, d, a, b, p, q, m, n], deps2)
        self.make_equal(cd_ab, pq_mn, deps=deps2)

        return add

    def add_eqangle(
        self, points: list[Point], deps: EmptyDependency
    ) -> list[Dependency]:
        """Add eqangle made by 8 points in `points`."""
        if deps:
            deps = deps.copy()
        a, b, c, d, m, n, p, q = points
        ab, why1 = self.get_line_thru_pair_why(a, b)
        cd, why2 = self.get_line_thru_pair_why(c, d)
        mn, why3 = self.get_line_thru_pair_why(m, n)
        pq, why4 = self.get_line_thru_pair_why(p, q)

        a, b = ab.points
        c, d = cd.points
        m, n = mn.points
        p, q = pq.points

        if deps and why1 + why2 + why3 + why4:
            dep0 = deps.populate("eqangle", points)
            deps = EmptyDependency(level=deps.level, rule_name=None)
            deps.why = [dep0] + why1 + why2 + why3 + why4

        add = self.maybe_make_equal_pairs(a, b, c, d, m, n, p, q, ab, cd, mn, pq, deps)

        if add is not None:
            return add

        self.connect_val(ab, deps=None)
        self.connect_val(cd, deps=None)
        self.connect_val(mn, deps=None)
        self.connect_val(pq, deps=None)

        add = []
        if (
            ab.val != cd.val
            and mn.val != pq.val
            and (ab.val != mn.val or cd.val != pq.val)
        ):
            add += self._add_eqangle(a, b, c, d, m, n, p, q, ab, cd, mn, pq, deps)

        if (
            ab.val != mn.val
            and cd.val != pq.val
            and (ab.val != cd.val or mn.val != pq.val)
        ):
            add += self._add_eqangle(
                a,
                b,
                m,
                n,
                c,
                d,
                p,
                q,
                ab,
                mn,
                cd,
                pq,
                deps,
            )

        return add

    def add_aconst(
        self, points: list[Point], deps: EmptyDependency
    ) -> list[Dependency]:
        """Add that an angle is equal to some constant."""
        a, b, c, d, num, den = points
        nd, dn = self.get_or_create_const_ang(num, den)

        if nd == self.halfpi:
            return self.add_perp([a, b, c, d], deps)

        ab, why1 = self.get_line_thru_pair_why(a, b)
        cd, why2 = self.get_line_thru_pair_why(c, d)

        (a, b), (c, d) = ab.points, cd.points
        if why1 + why2:
            args = points[:-2] + [nd]
            dep0 = deps.populate("aconst", args)
            deps = EmptyDependency(level=deps.level, rule_name=None)
            deps.why = [dep0] + why1 + why2

        self.connect_val(ab, deps=None)
        self.connect_val(cd, deps=None)

        if ab.val == cd.val:
            raise ValueError(f"{ab.name} - {cd.name} cannot be {nd.name}")

        args = [a, b, c, d, nd]
        i = 0
        for x, y, xy in [(a, b, ab), (c, d, cd)]:
            i += 1
            x_, y_ = xy._val._obj.points
            if {x, y} == {x_, y_}:
                continue
            if deps:
                deps = deps.extend(self, "aconst", list(args), "para", [x, y, x_, y_])
            args[2 * i - 2] = x_
            args[2 * i - 1] = y_

        ab_cd, cd_ab, why = self._get_or_create_angle(ab, cd, deps=None)
        if why:
            dep0 = deps.populate("aconst", [a, b, c, d, nd])
            deps = EmptyDependency(level=deps.level, rule_name=None)
            deps.why = [dep0] + why

        dab, dcd = ab_cd._d
        a, b = dab._obj.points
        c, d = dcd._obj.points

        ang = int(num) * 180 / int(den)
        add = []
        if not self.is_equal(ab_cd, nd):
            deps1 = deps.populate("aconst", [a, b, c, d, nd])
            deps1.algebra = dab, dcd, ang % 180
            self.make_equal(ab_cd, nd, deps=deps1)
            self.cache_dep("aconst", [a, b, c, d, nd], deps1)
            add += [deps1]

        if not self.is_equal(cd_ab, dn):
            deps2 = deps.populate("aconst", [c, d, a, b, dn])
            deps2.algebra = dcd, dab, 180 - ang % 180
            self.make_equal(cd_ab, dn, deps=deps2)
            self.cache_dep("aconst", [c, d, a, b, dn], deps2)
            add += [deps2]
        return add

    def add_s_angle(
        self, points: list[Point], deps: EmptyDependency
    ) -> list[Dependency]:
        """Add that an angle abx is equal to constant y."""
        a, b, x, y = points

        n, d = ar.simplify(y % 180, 180)
        nd, dn = self.get_or_create_const_ang(n, d)

        if nd == self.halfpi:
            return self.add_perp([a, b, b, x], deps)

        ab, why1 = self.get_line_thru_pair_why(a, b)
        bx, why2 = self.get_line_thru_pair_why(b, x)

        self.connect_val(ab, deps=None)
        self.connect_val(bx, deps=None)
        add = []

        if ab.val == bx.val:
            return add

        deps.why += why1 + why2

        for p, q, pq in [(a, b, ab), (b, x, bx)]:
            p_, q_ = pq.val._obj.points
            if {p, q} == {p_, q_}:
                continue
            dep = Dependency("para", [p, q, p_, q_], None, deps.level)
            deps.why += [dep.why_me_or_cache(self, None)]

        xba, abx, why = self._get_or_create_angle(bx, ab, deps=None)
        if why:
            dep0 = deps.populate("aconst", [b, x, a, b, nd])
            deps = EmptyDependency(level=deps.level, rule_name=None)
            deps.why = [dep0] + why

        dab, dbx = abx._d
        a, b = dab._obj.points
        c, x = dbx._obj.points

        if not self.is_equal(xba, nd):
            deps1 = deps.populate("aconst", [c, x, a, b, nd])
            deps1.algebra = dbx, dab, y % 180

            self.make_equal(xba, nd, deps=deps1)
            self.cache_dep("aconst", [c, x, a, b, nd], deps1)
            add += [deps1]

        if not self.is_equal(abx, dn):
            deps2 = deps.populate("aconst", [a, b, c, x, dn])
            deps2.algebra = dab, dbx, 180 - (y % 180)

            self.make_equal(abx, dn, deps=deps2)
            self.cache_dep("s_angle", [a, b, c, x, dn], deps2)
            add += [deps2]
        return add

    def check_aconst(self, points: list[Point], verbose: bool = False) -> bool:
        """Check if the angle is equal to a certain constant."""
        a, b, c, d, nd = points
        _ = verbose
        if isinstance(nd, str):
            name = nd
        else:
            name = nd.name
        num, den = name.split("pi/")
        ang, _ = self.get_or_create_const_ang(int(num), int(den))

        ab = self._get_line(a, b)
        cd = self._get_line(c, d)
        if not ab or not cd:
            return False

        if not (ab.val and cd.val):
            return False

        for ang1, _, _ in gm.all_angles(ab._val, cd._val):
            if self.is_equal(ang1, ang):
                return True
        return False

    def check_acompute(self, points: list[Point]) -> bool:
        """Check if an angle has a constant value."""
        a, b, c, d = points
        ab = self._get_line(a, b)
        cd = self._get_line(c, d)
        if not ab or not cd:
            return False

        if not (ab.val and cd.val):
            return False

        for ang0 in self.aconst.values():
            for ang in ang0.val.neighbors(Angle):
                d1, d2 = ang.directions
                if ab.val == d1 and cd.val == d2:
                    return True
        return False

    def check_eqangle(self, points: list[Point]) -> bool:
        """Check if two angles are equal."""
        a, b, c, d, m, n, p, q = points

        if {a, b} == {c, d} and {m, n} == {p, q}:
            return True
        if {a, b} == {m, n} and {c, d} == {p, q}:
            return True

        if (a == b) or (c == d) or (m == n) or (p == q):
            return False
        ab = self._get_line(a, b)
        cd = self._get_line(c, d)
        mn = self._get_line(m, n)
        pq = self._get_line(p, q)

        if {a, b} == {c, d} and mn and pq and self.is_equal(mn, pq):
            return True
        if {a, b} == {m, n} and cd and pq and self.is_equal(cd, pq):
            return True
        if {p, q} == {m, n} and ab and cd and self.is_equal(ab, cd):
            return True
        if {p, q} == {c, d} and ab and mn and self.is_equal(ab, mn):
            return True

        if not ab or not cd or not mn or not pq:
            return False

        if self.is_equal(ab, cd) and self.is_equal(mn, pq):
            return True
        if self.is_equal(ab, mn) and self.is_equal(cd, pq):
            return True

        if not (ab.val and cd.val and mn.val and pq.val):
            return False

        if (ab.val, cd.val) == (mn.val, pq.val) or (ab.val, mn.val) == (
            cd.val,
            pq.val,
        ):
            return True

        for ang1, _, _ in gm.all_angles(ab._val, cd._val):
            for ang2, _, _ in gm.all_angles(mn._val, pq._val):
                if self.is_equal(ang1, ang2):
                    return True

        if self.check_perp([a, b, m, n]) and self.check_perp([c, d, p, q]):
            return True
        if self.check_perp([a, b, p, q]) and self.check_perp([c, d, m, n]):
            return True

        return False

    def _get_ratio(self, l1: Length, l2: Length) -> tuple[Ratio, Ratio]:
        for r in self.type2nodes[Ratio]:
            if r.lengths == (l1, l2):
                return r, r.opposite
        return None, None

    def _get_or_create_ratio(
        self, s1: Segment, s2: Segment, deps: Dependency
    ) -> tuple[Ratio, Ratio, list[Dependency]]:
        return self._get_or_create_ratio_l(s1._val, s2._val, deps)

    def _get_or_create_ratio_l(
        self, l1: Length, l2: Length, deps: Dependency
    ) -> tuple[Ratio, Ratio, list[Dependency]]:
        """Get or create a new Ratio from two Lenghts l1 and l2."""
        for r in self.type2nodes[Ratio]:
            if r.lengths == (l1.rep(), l2.rep()):
                l1_, l2_ = r._l
                why1 = l1.why_equal([l1_], None) + l1_.why_rep()
                why2 = l2.why_equal([l2_], None) + l2_.why_rep()
                return r, r.opposite, why1 + why2

        l1, why1 = l1.rep_and_why()
        l2, why2 = l2.rep_and_why()
        r12 = self.new_node(Ratio, f"{l1.name}/{l2.name}")
        r21 = self.new_node(Ratio, f"{l2.name}/{l1.name}")
        self.connect(l1, r12, deps)
        self.connect(l2, r21, deps)
        self.connect(r12, r21, deps)
        r12.set_lengths(l1, l2)
        r21.set_lengths(l2, l1)
        r12.opposite = r21
        r21.opposite = r12
        return r12, r21, why1 + why2

    def add_cong2(self, points: list[Point], deps: EmptyDependency) -> list[Dependency]:
        m, n, a, b = points
        add = []
        add += self.add_cong([m, a, n, a], deps)
        add += self.add_cong([m, b, n, b], deps)
        return add

    def add_eqratio3(
        self, points: list[Point], deps: EmptyDependency
    ) -> list[Dependency]:
        """Add three eqratios through a list of 6 points (due to parallel lines)."""
        a, b, c, d, m, n = points
        #   a -- b
        #  m  --  n
        # c   --   d
        add = []
        add += self.add_eqratio([m, a, m, c, n, b, n, d], deps)
        add += self.add_eqratio([a, m, a, c, b, n, b, d], deps)
        add += self.add_eqratio([c, m, c, a, d, n, d, b], deps)
        if m == n:
            add += self.add_eqratio([m, a, m, c, a, b, c, d], deps)
        return add

    def add_eqratio4(
        self, points: list[Point], deps: EmptyDependency
    ) -> list[Dependency]:
        o, a, b, c, d = points
        #   o
        #  a b
        # c   d
        add = self.add_eqratio3([a, b, c, d, o, o], deps)
        add += self.add_eqratio([o, a, o, c, a, b, c, d], deps)
        return add

    def _add_eqratio(
        self,
        a: Point,
        b: Point,
        c: Point,
        d: Point,
        m: Point,
        n: Point,
        p: Point,
        q: Point,
        ab: Segment,
        cd: Segment,
        mn: Segment,
        pq: Segment,
        deps: EmptyDependency,
    ) -> list[Dependency]:
        """Add a new eqratio from 8 points (core)."""
        if deps:
            deps = deps.copy()

        args = [a, b, c, d, m, n, p, q]
        i = 0
        for x, y, xy in [(a, b, ab), (c, d, cd), (m, n, mn), (p, q, pq)]:
            if {x, y} == set(xy.points):
                continue
            x_, y_ = list(xy.points)
            if deps:
                deps = deps.extend(self, "eqratio", list(args), "cong", [x, y, x_, y_])
            args[2 * i - 2] = x_
            args[2 * i - 1] = y_

        add = []
        ab_cd, cd_ab, why1 = self._get_or_create_ratio(ab, cd, deps=None)
        mn_pq, pq_mn, why2 = self._get_or_create_ratio(mn, pq, deps=None)

        why = why1 + why2
        if why:
            dep0 = deps.populate("eqratio", args)
            deps = EmptyDependency(level=deps.level, rule_name=None)
            deps.why = [dep0] + why

        lab, lcd = ab_cd._l
        lmn, lpq = mn_pq._l

        a, b = lab._obj.points
        c, d = lcd._obj.points
        m, n = lmn._obj.points
        p, q = lpq._obj.points

        is_eq1 = self.is_equal(ab_cd, mn_pq)
        deps1 = None
        if deps:
            deps1 = deps.populate("eqratio", [a, b, c, d, m, n, p, q])
            deps1.algebra = [ab._val, cd._val, mn._val, pq._val]
        if not is_eq1:
            add += [deps1]
        self.cache_dep("eqratio", [a, b, c, d, m, n, p, q], deps1)
        self.make_equal(ab_cd, mn_pq, deps=deps1)

        is_eq2 = self.is_equal(cd_ab, pq_mn)
        deps2 = None
        if deps:
            deps2 = deps.populate("eqratio", [c, d, a, b, p, q, m, n])
            deps2.algebra = [cd._val, ab._val, pq._val, mn._val]
        if not is_eq2:
            add += [deps2]
        self.cache_dep("eqratio", [c, d, a, b, p, q, m, n], deps2)
        self.make_equal(cd_ab, pq_mn, deps=deps2)
        return add

    def add_eqratio(
        self, points: list[Point], deps: EmptyDependency
    ) -> list[Dependency]:
        """Add a new eqratio from 8 points."""
        if deps:
            deps = deps.copy()
        a, b, c, d, m, n, p, q = points
        ab = self._get_or_create_segment(a, b, deps=None)
        cd = self._get_or_create_segment(c, d, deps=None)
        mn = self._get_or_create_segment(m, n, deps=None)
        pq = self._get_or_create_segment(p, q, deps=None)

        add = self.maybe_make_equal_pairs(a, b, c, d, m, n, p, q, ab, cd, mn, pq, deps)

        if add is not None:
            return add

        self.connect_val(ab, deps=None)
        self.connect_val(cd, deps=None)
        self.connect_val(mn, deps=None)
        self.connect_val(pq, deps=None)

        add = []
        if (
            ab.val != cd.val
            and mn.val != pq.val
            and (ab.val != mn.val or cd.val != pq.val)
        ):
            add += self._add_eqratio(a, b, c, d, m, n, p, q, ab, cd, mn, pq, deps)

        if (
            ab.val != mn.val
            and cd.val != pq.val
            and (ab.val != cd.val or mn.val != pq.val)
        ):
            add += self._add_eqratio(
                a,
                b,
                m,
                n,
                c,
                d,
                p,
                q,
                ab,
                mn,
                cd,
                pq,
                deps,
            )
        return add

    def check_rconst(self, points: list[Point], verbose: bool = False) -> bool:
        """Check whether a ratio is equal to some given constant."""
        _ = verbose
        a, b, c, d, nd = points
        if isinstance(nd, str):
            name = nd
        else:
            name = nd.name
        num, den = name.split("/")
        rat, _ = self.get_or_create_const_rat(int(num), int(den))

        ab = self._get_segment(a, b)
        cd = self._get_segment(c, d)

        if not ab or not cd:
            return False

        if not (ab.val and cd.val):
            return False

        for rat1, _, _ in gm.all_ratios(ab._val, cd._val):
            if self.is_equal(rat1, rat):
                return True
        return False

    def check_rcompute(self, points: list[Point]) -> bool:
        """Check whether a ratio is equal to some constant."""
        a, b, c, d = points
        ab = self._get_segment(a, b)
        cd = self._get_segment(c, d)

        if not ab or not cd:
            return False

        if not (ab.val and cd.val):
            return False

        for rat0 in self.rconst.values():
            for rat in rat0.val.neighbors(Ratio):
                l1, l2 = rat.lengths
                if ab.val == l1 and cd.val == l2:
                    return True
        return False

    def check_eqratio(self, points: list[Point]) -> bool:
        """Check if 8 points make an eqratio predicate."""
        a, b, c, d, m, n, p, q = points

        if {a, b} == {c, d} and {m, n} == {p, q}:
            return True
        if {a, b} == {m, n} and {c, d} == {p, q}:
            return True

        ab = self._get_segment(a, b)
        cd = self._get_segment(c, d)
        mn = self._get_segment(m, n)
        pq = self._get_segment(p, q)

        if {a, b} == {c, d} and mn and pq and self.is_equal(mn, pq):
            return True
        if {a, b} == {m, n} and cd and pq and self.is_equal(cd, pq):
            return True
        if {p, q} == {m, n} and ab and cd and self.is_equal(ab, cd):
            return True
        if {p, q} == {c, d} and ab and mn and self.is_equal(ab, mn):
            return True

        if not ab or not cd or not mn or not pq:
            return False

        if self.is_equal(ab, cd) and self.is_equal(mn, pq):
            return True
        if self.is_equal(ab, mn) and self.is_equal(cd, pq):
            return True

        if not (ab.val and cd.val and mn.val and pq.val):
            return False

        if (ab.val, cd.val) == (mn.val, pq.val) or (ab.val, mn.val) == (
            cd.val,
            pq.val,
        ):
            return True

        for rat1, _, _ in gm.all_ratios(ab._val, cd._val):
            for rat2, _, _ in gm.all_ratios(mn._val, pq._val):
                if self.is_equal(rat1, rat2):
                    return True
        return False

    def add_simtri_check(
        self, points: list[Point], deps: EmptyDependency
    ) -> list[Dependency]:
        if nm.same_clock(*[p.num for p in points]):
            return self.add_simtri(points, deps)
        return self.add_simtri2(points, deps)

    def add_contri_check(
        self, points: list[Point], deps: EmptyDependency
    ) -> list[Dependency]:
        if nm.same_clock(*[p.num for p in points]):
            return self.add_contri(points, deps)
        return self.add_contri2(points, deps)

    def enum_sides(self, points: list[Point]) -> Generator[list[Point], None, None]:
        a, b, c, x, y, z = points
        yield [a, b, x, y]
        yield [b, c, y, z]
        yield [c, a, z, x]

    def enum_triangle(self, points: list[Point]) -> Generator[list[Point], None, None]:
        a, b, c, x, y, z = points
        yield [a, b, a, c, x, y, x, z]
        yield [b, a, b, c, y, x, y, z]
        yield [c, a, c, b, z, x, z, y]

    def enum_triangle2(self, points: list[Point]) -> Generator[list[Point], None, None]:
        a, b, c, x, y, z = points
        yield [a, b, a, c, x, z, x, y]
        yield [b, a, b, c, y, z, y, x]
        yield [c, a, c, b, z, y, z, x]

    def add_simtri(
        self, points: list[Point], deps: EmptyDependency
    ) -> list[Dependency]:
        """Add two similar triangles."""
        add = []
        hashs = [d.hashed() for d in deps.why]

        for args in self.enum_triangle(points):
            if problem.hashed("eqangle6", args) in hashs:
                continue
            add += self.add_eqangle(args, deps=deps)

        for args in self.enum_triangle(points):
            if problem.hashed("eqratio6", args) in hashs:
                continue
            add += self.add_eqratio(args, deps=deps)

        return add

    def check_simtri(self, points: list[Point]) -> bool:
        a, b, c, x, y, z = points
        return self.check_eqangle([a, b, a, c, x, y, x, z]) and self.check_eqangle(
            [b, a, b, c, y, x, y, z]
        )

    def add_simtri2(
        self, points: list[Point], deps: EmptyDependency
    ) -> list[Dependency]:
        """Add two similar reflected triangles."""
        add = []
        hashs = [d.hashed() for d in deps.why]
        for args in self.enum_triangle2(points):
            if problem.hashed("eqangle6", args) in hashs:
                continue
            add += self.add_eqangle(args, deps=deps)

        for args in self.enum_triangle(points):
            if problem.hashed("eqratio6", args) in hashs:
                continue
            add += self.add_eqratio(args, deps=deps)

        return add

    def add_contri(
        self, points: list[Point], deps: EmptyDependency
    ) -> list[Dependency]:
        """Add two congruent triangles."""
        add = []
        hashs = [d.hashed() for d in deps.why]
        for args in self.enum_triangle(points):
            if problem.hashed("eqangle6", args) in hashs:
                continue
            add += self.add_eqangle(args, deps=deps)

        for args in self.enum_sides(points):
            if problem.hashed("cong", args) in hashs:
                continue
            add += self.add_cong(args, deps=deps)
        return add

    def check_contri(self, points: list[Point]) -> bool:
        a, b, c, x, y, z = points
        return (
            self.check_cong([a, b, x, y])
            and self.check_cong([b, c, y, z])
            and self.check_cong([c, a, z, x])
        )

    def add_contri2(
        self, points: list[Point], deps: EmptyDependency
    ) -> list[Dependency]:
        """Add two congruent reflected triangles."""
        add = []
        hashs = [d.hashed() for d in deps.why]
        for args in self.enum_triangle2(points):
            if problem.hashed("eqangle6", args) in hashs:
                continue
            add += self.add_eqangle(args, deps=deps)

        for args in self.enum_sides(points):
            if problem.hashed("cong", args) in hashs:
                continue
            add += self.add_cong(args, deps=deps)

        return add

    def in_cache(self, name: str, args: list[Point]) -> bool:
        return problem.hashed(name, args) in self.cache

    def cache_dep(
        self, name: str, args: list[Point], premises: list[Dependency]
    ) -> None:
        hashed = problem.hashed(name, args)
        if hashed in self.cache:
            return
        self.cache[hashed] = premises

    def all_same_line(
        self, a: Point, b: Point
    ) -> Generator[tuple[Point, Point], None, None]:
        ab = self._get_line(a, b)
        if ab is None:
            return
        for p1, p2 in utils.comb2(ab.neighbors(Point)):
            if {p1, p2} != {a, b}:
                yield p1, p2

    def all_same_angle(
        self, a: Point, b: Point, c: Point, d: Point
    ) -> Generator[tuple[Point, Point, Point, Point], None, None]:
        for x, y in self.all_same_line(a, b):
            for m, n in self.all_same_line(c, d):
                yield x, y, m, n

    def additionally_draw(self, name: str, args: list[Point]) -> None:
        """Draw some extra line/circles for illustration purpose."""

        if name in ["circle"]:
            center, point = args[:2]
            circle = self.new_node(Circle, f"({center.name},{point.name})")
            circle.num = nm.Circle(center.num, p1=point.num)
            circle.points = center, point

        if name in ["on_circle", "tangent"]:
            center, point = args[-2:]
            circle = self.new_node(Circle, f"({center.name},{point.name})")
            circle.num = nm.Circle(center.num, p1=point.num)
            circle.points = center, point

        if name in ["incenter", "excenter", "incenter2", "excenter2"]:
            d, a, b, c = [x for x in args[-4:]]
            a, b, c = sorted([a, b, c], key=lambda x: x.name.lower())
            circle = self.new_node(Circle, f"({d.name},h.{a.name}{b.name})")
            p = d.num.foot(nm.Line(a.num, b.num))
            circle.num = nm.Circle(d.num, p1=p)
            circle.points = d, a, b, c

        if name in ["cc_tangent"]:
            o, a, w, b = args[-4:]
            c1 = self.new_node(Circle, f"({o.name},{a.name})")
            c1.num = nm.Circle(o.num, p1=a.num)
            c1.points = o, a

            c2 = self.new_node(Circle, f"({w.name},{b.name})")
            c2.num = nm.Circle(w.num, p1=b.num)
            c2.points = w, b

        if name in ["ninepoints"]:
            a, b, c = args[-3:]
            a, b, c = sorted([a, b, c], key=lambda x: x.name.lower())
            circle = self.new_node(Circle, f"(,m.{a.name}{b.name}{c.name})")
            p1 = (b.num + c.num) * 0.5
            p2 = (c.num + a.num) * 0.5
            p3 = (a.num + b.num) * 0.5
            circle.num = nm.Circle(p1=p1, p2=p2, p3=p3)
            circle.points = (None, None, a, b, c)

        if name in ["2l1c"]:
            a, b, c, o = args[:4]
            a, b, c = sorted([a, b, c], key=lambda x: x.name.lower())
            circle = self.new_node(Circle, f"({o.name},{a.name}{b.name}{c.name})")
            circle.num = nm.Circle(p1=a.num, p2=b.num, p3=c.num)
            circle.points = (a, b, c)

    def add_clause(
        self,
        clause: problem.Clause,
        plevel: int,
        definitions: dict[str, problem.Definition],
        verbose: int = False,
    ) -> tuple[list[Dependency], int]:
        """Add a new clause of construction, e.g. a new excenter."""
        existing_points = self.all_points()
        new_points = [Point(name) for name in clause.points]

        new_points_dep_points = set()
        new_points_dep = []

        # Step 1: check for all deps.
        for c in clause.constructions:
            cdef = definitions[c.name]

            if len(cdef.construction.args) != len(c.args):
                if len(cdef.construction.args) - len(c.args) == len(clause.points):
                    c.args = clause.points + c.args
                else:
                    correct_form = " ".join(cdef.points + ["=", c.name] + cdef.args)
                    raise ValueError("Argument mismatch. " + correct_form)

            mapping = dict(zip(cdef.construction.args, c.args))
            c_name = "midp" if c.name == "midpoint" else c.name
            deps = EmptyDependency(level=0, rule_name=problem.CONSTRUCTION_RULE)
            deps.construction = Dependency(c_name, c.args, rule_name=None, level=0)

            for d in cdef.deps.constructions:
                args = self.names2points([mapping[a] for a in d.args])
                new_points_dep_points.update(args)
                if not self.check(d.name, args):
                    raise DepCheckFailError(
                        d.name + " " + " ".join([x.name for x in args])
                    )
                deps.why += [
                    Dependency(
                        d.name, args, rule_name=problem.CONSTRUCTION_RULE, level=0
                    )
                ]

            new_points_dep += [deps]

        # Step 2: draw.
        def range_fn() -> (
            list[Union[nm.Point, nm.Line, nm.Circle, nm.HalfLine, nm.HoleCircle]]
        ):
            to_be_intersected = []
            for c in clause.constructions:
                cdef = definitions[c.name]
                mapping = dict(zip(cdef.construction.args, c.args))
                for n in cdef.numerics:
                    args = [mapping[a] for a in n.args]
                    args = list(map(lambda x: self.get(x, lambda: int(x)), args))
                    to_be_intersected += nm.sketch(n.name, args)

            return to_be_intersected

        is_total_free = (
            len(clause.constructions) == 1 and clause.constructions[0].name in FREE
        )
        is_semi_free = (
            len(clause.constructions) == 1 and clause.constructions[0].name in INTERSECT
        )

        existing_points = [p.num for p in existing_points]

        def draw_fn() -> list[nm.Point]:
            to_be_intersected = range_fn()
            return nm.reduce(to_be_intersected, existing_points)

        rely_on = set()
        for c in clause.constructions:
            cdef = definitions[c.name]
            mapping = dict(zip(cdef.construction.args, c.args))
            for n in cdef.numerics:
                args = [mapping[a] for a in n.args]
                args = list(map(lambda x: self.get(x, lambda: int(x)), args))
                rely_on.update([a for a in args if isinstance(a, Point)])

        for p in rely_on:
            p.change.update(new_points)

        nums = draw_fn()
        for p, num, num0 in zip(new_points, nums, clause.nums):
            p.co_change = new_points
            if isinstance(num0, nm.Point):
                num = num0
            elif isinstance(num0, (tuple, list)):
                x, y = num0
                num = nm.Point(x, y)

            p.num = num

        # check two things.
        if nm.check_too_close(nums, existing_points):
            raise PointTooCloseError()
        if nm.check_too_far(nums, existing_points):
            raise PointTooFarError()

        # Commit: now that all conditions are passed.
        # add these points to current graph.
        for p in new_points:
            self._name2point[p.name] = p
            self._name2node[p.name] = p
            self.type2nodes[Point].append(p)

        for p in new_points:
            p.why = sum([d.why for d in new_points_dep], [])  # to generate txt logs.
            p.group = new_points
            p.dep_points = new_points_dep_points
            p.dep_points.update(new_points)
            p.plevel = plevel

        # movement dependency:
        rely_dict_0 = defaultdict(lambda: [])

        for c in clause.constructions:
            cdef = definitions[c.name]
            mapping = dict(zip(cdef.construction.args, c.args))
            for p, ps in cdef.rely.items():
                p = mapping[p]
                ps = [mapping[x] for x in ps]
                rely_dict_0[p].append(ps)

        rely_dict = {}
        for p, pss in rely_dict_0.items():
            ps = sum(pss, [])
            if len(pss) > 1:
                ps = [x for x in ps if x != p]

            p = self._name2point[p]
            ps = self.names2nodes(ps)
            rely_dict[p] = ps

        for p in new_points:
            p.rely_on = set(rely_dict.get(p, []))
            for x in p.rely_on:
                if not hasattr(x, "base_rely_on"):
                    x.base_rely_on = set()
            p.base_rely_on = set.union(*[x.base_rely_on for x in p.rely_on] + [set()])
            if is_total_free or is_semi_free:
                p.rely_on.add(p)
                p.base_rely_on.add(p)

        plevel_done = set()
        added = []
        basics = []
        # Step 3: build the basics.
        for c, deps in zip(clause.constructions, new_points_dep):
            cdef = definitions[c.name]
            mapping = dict(zip(cdef.construction.args, c.args))

            # not necessary for proofing, but for visualization.
            c_args = list(map(lambda x: self.get(x, lambda: int(x)), c.args))
            self.additionally_draw(c.name, c_args)

            for points, bs in cdef.basics:
                if points:
                    points = self.names2nodes([mapping[p] for p in points])
                    points = [p for p in points if p not in plevel_done]
                    for p in points:
                        p.plevel = plevel
                    plevel_done.update(points)
                    plevel += 1
                else:
                    continue

                for b in bs:
                    if b.name != "rconst":
                        args = [mapping[a] for a in b.args]
                    else:
                        num, den = map(int, b.args[-2:])
                        rat, _ = self.get_or_create_const_rat(num, den)
                        args = [mapping[a] for a in b.args[:-2]] + [rat.name]

                    args = list(map(lambda x: self.get(x, lambda: int(x)), args))

                    adds = self.add_piece(name=b.name, args=args, deps=deps)
                    basics.append((b.name, args, deps))
                    if adds:
                        added += adds
                        for add in adds:
                            self.cache_dep(add.name, add.args, add)

        assert len(plevel_done) == len(new_points)
        for p in new_points:
            p.basics = basics

        return added, plevel

    def all_eqangle_same_lines(self) -> Generator[tuple[Point, ...], None, None]:
        for l1, l2 in utils.perm2(self.type2nodes[Line]):
            for a, b, c, d, e, f, g, h in utils.all_8points(l1, l2, l1, l2):
                if (a, b, c, d) != (e, f, g, h):
                    yield a, b, c, d, e, f, g, h

    def all_eqangles_distinct_linepairss(
        self,
    ) -> Generator[tuple[Line, ...], None, None]:
        """No eqangles betcause para-para, or para-corresponding, or same."""

        for measure in self.type2nodes[Measure]:
            angs = measure.neighbors(Angle)
            line_pairss = []
            for ang in angs:
                d1, d2 = ang.directions
                if d1 is None or d2 is None:
                    continue
                l1s = d1.neighbors(Line)
                l2s = d2.neighbors(Line)
                # Any pair in this is para-para.
                para_para = list(utils.cross(l1s, l2s))
                line_pairss.append(para_para)

            for pairs1, pairs2 in utils.comb2(line_pairss):
                for pair1, pair2 in utils.cross(pairs1, pairs2):
                    (l1, l2), (l3, l4) = pair1, pair2
                    yield l1, l2, l3, l4

    def all_eqangles_8points(self) -> Generator[tuple[Point, ...], None, None]:
        """List all sets of 8 points that make two equal angles."""
        # Case 1: (l1-l2) = (l3-l4), including because l1//l3, l2//l4 (para-para)
        angss = []
        for measure in self.type2nodes[Measure]:
            angs = measure.neighbors(Angle)
            angss.append(angs)

        # include the angs that do not have any measure.
        angss.extend([[ang] for ang in self.type2nodes[Angle] if ang.val is None])

        line_pairss = []
        for angs in angss:
            line_pairs = set()
            for ang in angs:
                d1, d2 = ang.directions
                if d1 is None or d2 is None:
                    continue
                l1s = d1.neighbors(Line)
                l2s = d2.neighbors(Line)
                line_pairs.update(set(utils.cross(l1s, l2s)))
            line_pairss.append(line_pairs)

        # include (d1, d2) in which d1 does not have any angles.
        noang_ds = [d for d in self.type2nodes[Direction] if not d.neighbors(Angle)]

        for d1 in noang_ds:
            for d2 in self.type2nodes[Direction]:
                if d1 == d2:
                    continue
                l1s = d1.neighbors(Line)
                l2s = d2.neighbors(Line)
                if len(l1s) < 2 and len(l2s) < 2:
                    continue
                line_pairss.append(set(utils.cross(l1s, l2s)))
                line_pairss.append(set(utils.cross(l2s, l1s)))

        # Case 2: d1 // d2 => (d1-d3) = (d2-d3)
        # include lines that does not have any direction.
        nodir_ls = [line for line in self.type2nodes[Line] if line.val is None]

        for line in nodir_ls:
            for d in self.type2nodes[Direction]:
                l1s = d.neighbors(Line)
                if len(l1s) < 2:
                    continue
                l2s = [line]
                line_pairss.append(set(utils.cross(l1s, l2s)))
                line_pairss.append(set(utils.cross(l2s, l1s)))

        record = set()
        for line_pairs in line_pairss:
            for pair1, pair2 in utils.perm2(list(line_pairs)):
                (l1, l2), (l3, l4) = pair1, pair2
                if l1 == l2 or l3 == l4:
                    continue
                if (l1, l2) == (l3, l4):
                    continue
                if (l1, l2, l3, l4) in record:
                    continue
                record.add((l1, l2, l3, l4))
                for a, b, c, d, e, f, g, h in utils.all_8points(l1, l2, l3, l4):
                    yield (a, b, c, d, e, f, g, h)

        for a, b, c, d, e, f, g, h in self.all_eqangle_same_lines():
            yield a, b, c, d, e, f, g, h

    def all_eqangles_6points(self) -> Generator[tuple[Point, ...], None, None]:
        """List all sets of 6 points that make two equal angles."""
        record = set()
        for a, b, c, d, e, f, g, h in self.all_eqangles_8points():
            if (
                a not in (c, d)
                and b not in (c, d)
                or e not in (g, h)
                and f not in (g, h)
            ):
                continue

            if b in (c, d):
                a, b = b, a  # now a in c, d
            if f in (g, h):
                e, f = f, e  # now e in g, h
            if a == d:
                c, d = d, c  # now a == c
            if e == h:
                g, h = h, g  # now e == g
            if (a, b, c, d, e, f, g, h) in record:
                continue
            record.add((a, b, c, d, e, f, g, h))
            yield a, b, c, d, e, f, g, h  # where a==c, e==g

    def all_paras(self) -> Generator[tuple[Point, ...], None, None]:
        for d in self.type2nodes[Direction]:
            for l1, l2 in utils.perm2(d.neighbors(Line)):
                for a, b, c, d in utils.all_4points(l1, l2):
                    yield a, b, c, d

    def all_perps(self) -> Generator[tuple[Point, ...], None, None]:
        for ang in self.vhalfpi.neighbors(Angle):
            d1, d2 = ang.directions
            if d1 is None or d2 is None:
                continue
            if d1 == d2:
                continue
            for l1, l2 in utils.cross(d1.neighbors(Line), d2.neighbors(Line)):
                for a, b, c, d in utils.all_4points(l1, l2):
                    yield a, b, c, d

    def all_congs(self) -> Generator[tuple[Point, ...], None, None]:
        for lenght in self.type2nodes[Length]:
            for s1, s2 in utils.perm2(lenght.neighbors(Segment)):
                (a, b), (c, d) = s1.points, s2.points
                for x, y in [(a, b), (b, a)]:
                    for m, n in [(c, d), (d, c)]:
                        yield x, y, m, n

    def all_eqratios_8points(self) -> Generator[tuple[Point, ...], None, None]:
        """List all sets of 8 points that make two equal ratios."""
        ratss = []
        for value in self.type2nodes[Value]:
            rats = value.neighbors(Ratio)
            ratss.append(rats)

        # include the rats that do not have any val.
        ratss.extend([[rat] for rat in self.type2nodes[Ratio] if rat.val is None])

        seg_pairss = []
        for rats in ratss:
            seg_pairs = set()
            for rat in rats:
                l1, l2 = rat.lengths
                if l1 is None or l2 is None:
                    continue
                s1s = l1.neighbors(Segment)
                s2s = l2.neighbors(Segment)
                seg_pairs.update(utils.cross(s1s, s2s))
            seg_pairss.append(seg_pairs)

        # include (l1, l2) in which l1 does not have any ratio.
        norat_ls = [
            lenght for lenght in self.type2nodes[Length] if not lenght.neighbors(Ratio)
        ]

        for l1 in norat_ls:
            for l2 in self.type2nodes[Length]:
                if l1 == l2:
                    continue
                s1s = l1.neighbors(Segment)
                s2s = l2.neighbors(Segment)
                if len(s1s) < 2 and len(s2s) < 2:
                    continue
                seg_pairss.append(set(utils.cross(s1s, s2s)))
                seg_pairss.append(set(utils.cross(s2s, s1s)))

        # include Seg that does not have any Length.
        nolen_ss = [s for s in self.type2nodes[Segment] if s.val is None]

        for seg in nolen_ss:
            for lenght in self.type2nodes[Length]:
                s1s = lenght.neighbors(Segment)
                if len(s1s) == 1:
                    continue
                s2s = [seg]
                seg_pairss.append(set(utils.cross(s1s, s2s)))
                seg_pairss.append(set(utils.cross(s2s, s1s)))

        record = set()
        for seg_pairs in seg_pairss:
            for pair1, pair2 in utils.perm2(list(seg_pairs)):
                (s1, s2), (s3, s4) = pair1, pair2
                if s1 == s2 or s3 == s4:
                    continue
                if (s1, s2) == (s3, s4):
                    continue
                if (s1, s2, s3, s4) in record:
                    continue
                record.add((s1, s2, s3, s4))
                a, b = s1.points
                c, d = s2.points
                e, f = s3.points
                g, h = s4.points

                for x, y in [(a, b), (b, a)]:
                    for z, t in [(c, d), (d, c)]:
                        for m, n in [(e, f), (f, e)]:
                            for p, q in [(g, h), (h, g)]:
                                yield (x, y, z, t, m, n, p, q)

        segss = []
        # finally the list of ratios that is equal to 1.0
        for length in self.type2nodes[Length]:
            segs = length.neighbors(Segment)
            segss.append(segs)

        segs_pair = list(utils.perm2(list(segss)))
        segs_pair += list(zip(segss, segss))
        for segs1, segs2 in segs_pair:
            for s1, s2 in utils.perm2(list(segs1)):
                for s3, s4 in utils.perm2(list(segs2)):
                    if (s1, s2) == (s3, s4) or (s1, s3) == (s2, s4):
                        continue
                    if (s1, s2, s3, s4) in record:
                        continue
                    record.add((s1, s2, s3, s4))
                    a, b = s1.points
                    c, d = s2.points
                    e, f = s3.points
                    g, h = s4.points

                    for x, y in [(a, b), (b, a)]:
                        for z, t in [(c, d), (d, c)]:
                            for m, n in [(e, f), (f, e)]:
                                for p, q in [(g, h), (h, g)]:
                                    yield (x, y, z, t, m, n, p, q)

    def all_eqratios_6points(self) -> Generator[tuple[Point, ...], None, None]:
        """List all sets of 6 points that make two equal angles."""
        record = set()
        for a, b, c, d, e, f, g, h in self.all_eqratios_8points():
            if (
                a not in (c, d)
                and b not in (c, d)
                or e not in (g, h)
                and f not in (g, h)
            ):
                continue
            if b in (c, d):
                a, b = b, a
            if f in (g, h):
                e, f = f, e
            if a == d:
                c, d = d, c
            if e == h:
                g, h = h, g
            if (a, b, c, d, e, f, g, h) in record:
                continue
            record.add((a, b, c, d, e, f, g, h))
            yield a, b, c, d, e, f, g, h  # now a==c, e==g

    def all_cyclics(self) -> Generator[tuple[Point, ...], None, None]:
        for c in self.type2nodes[Circle]:
            for x, y, z, t in utils.perm4(c.neighbors(Point)):
                yield x, y, z, t

    def all_colls(self) -> Generator[tuple[Point, ...], None, None]:
        for line in self.type2nodes[Line]:
            for x, y, z in utils.perm3(line.neighbors(Point)):
                yield x, y, z

    def all_midps(self) -> Generator[tuple[Point, ...], None, None]:
        for line in self.type2nodes[Line]:
            for a, b, c in utils.perm3(line.neighbors(Point)):
                if self.check_cong([a, b, a, c]):
                    yield a, b, c

    def all_circles(self) -> Generator[tuple[Point, ...], None, None]:
        for lenght in self.type2nodes[Length]:
            p2p = defaultdict(list)
            for s in lenght.neighbors(Segment):
                a, b = s.points
                p2p[a].append(b)
                p2p[b].append(a)
            for p, ps in p2p.items():
                if len(ps) >= 3:
                    for a, b, c in utils.perm3(ps):
                        yield p, a, b, c

    def two_points_on_direction(self, d: Direction) -> tuple[Point, Point]:
        line_neighbor = d.neighbors(Line)[0]
        p1, p2 = line_neighbor.neighbors(Point)[:2]
        return p1, p2

    def two_points_of_length(self, lenght: Length) -> tuple[Point, Point]:
        s = lenght.neighbors(Segment)[0]
        p1, p2 = s.points
        return p1, p2


def create_consts_str(g: Graph, s: str) -> Union[Ratio, Angle]:
    if "pi/" in s:
        n, d = s.split("pi/")
        n, d = int(n), int(d)
        p0, _ = g.get_or_create_const_ang(n, d)
    else:
        n, d = s.split("/")
        n, d = int(n), int(d)
        p0, _ = g.get_or_create_const_rat(n, d)
    return p0


def create_consts(g: Graph, p: gm.Node) -> Union[Ratio, Angle]:
    if isinstance(p, Angle):
        n, d = p.name.split("pi/")
        n, d = int(n), int(d)
        p0, _ = g.get_or_create_const_ang(n, d)
    if isinstance(p, Ratio):
        n, d = p.name.split("/")
        n, d = int(n), int(d)
        p0, _ = g.get_or_create_const_rat(n, d)
    return p0
