"""Recolour a palette by decomposing each pixel into a blend of two anchors.

The upstream assets antialias by blending between two theme colours rather than
by alpha alone -- a checkmark, for instance, fades from the accent fill to
white across two pixels. Mapping colours one at a time would leave those
gradients stranded between the old palette and the new.

So for each input colour we find the anchor pair (a, b) and the blend factor t
that best explain it, then emit the same blend of the mapped anchors (a', b').
Exact anchors fall out as the t=0 and t=1 cases.
"""


def _sub(p, q):
    return (p[0] - q[0], p[1] - q[1], p[2] - q[2])


def _dot(p, q):
    return p[0] * q[0] + p[1] * q[1] + p[2] * q[2]


class Remapper:
    def __init__(self, anchors):
        """anchors: list of (source_rgb, target_rgb) tuples."""
        self.anchors = list(anchors)
        self._cache = {}

    def __call__(self, color):
        hit = self._cache.get(color)
        if hit is None:
            hit = self._cache[color] = self._solve(color)
        return hit

    def _solve(self, c):
        best_err = None
        best = None
        for i, (a_src, a_dst) in enumerate(self.anchors):
            for b_src, b_dst in self.anchors[i:]:
                v = _sub(b_src, a_src)
                vv = _dot(v, v)
                if vv == 0:
                    t = 0.0
                else:
                    t = _dot(_sub(c, a_src), v) / vv
                    t = 0.0 if t < 0 else (1.0 if t > 1 else t)
                proj = (a_src[0] + t * v[0], a_src[1] + t * v[1], a_src[2] + t * v[2])
                d = _sub(c, proj)
                err = _dot(d, d)
                if best_err is None or err < best_err:
                    best_err = err
                    best = tuple(
                        a_dst[k] + t * (b_dst[k] - a_dst[k]) for k in range(3)
                    )
        return tuple(
            0 if v < 0 else (255 if v > 255 else v)
            for v in (int(round(x)) for x in best)
        )
