from fontTools.pens.recordingPen import (
    RecordingPen,
    DecomposingRecordingPen,
    RecordingPointPen,
)
from fontTools.pens.boundsPen import ControlBoundsPen
from fontTools.pens.cairoPen import CairoPen
from fontTools.pens.pointPen import (
    SegmentToPointPen,
    PointToSegmentPen,
    ReverseContourPointPen,
)
from fontTools.varLib.interpolatable import (
    PerContourOrComponentPen,
    SimpleRecordingPointPen,
)
from itertools import cycle
from functools import wraps
from io import BytesIO
import cairo
import math
import logging

log = logging.getLogger("fontTools.varLib.interpolatable")


class LerpGlyphSet:
    def __init__(self, glyphset1, glyphset2, factor=0.5):
        self.glyphset1 = glyphset1
        self.glyphset2 = glyphset2
        self.factor = factor

    def __getitem__(self, glyphname):
        return LerpGlyph(glyphname, self)


class LerpGlyph:
    def __init__(self, glyphname, glyphset):
        self.glyphset = glyphset
        self.glyphname = glyphname

    def draw(self, pen):
        recording1 = DecomposingRecordingPen(self.glyphset.glyphset1)
        self.glyphset.glyphset1[self.glyphname].draw(recording1)
        recording2 = DecomposingRecordingPen(self.glyphset.glyphset2)
        self.glyphset.glyphset2[self.glyphname].draw(recording2)

        factor = self.glyphset.factor
        for (op1, args1), (op2, args2) in zip(recording1.value, recording2.value):
            if op1 != op2:
                raise ValueError("Mismatching operations: %s, %s" % (op1, op2))
            mid_args = [
                (x1 + (x2 - x1) * factor, y1 + (y2 - y1) * factor)
                for (x1, y1), (x2, y2) in zip(args1, args2)
            ]
            getattr(pen, op1)(*mid_args)


class OverridingDict(dict):
    def __init__(self, parent_dict):
        self.parent_dict = parent_dict

    def __missing__(self, key):
        return self.parent_dict[key]


class InterpolatablePlot:
    width = 640
    height = 480
    pad = 16
    line_height = 36
    head_color = (0.3, 0.3, 0.3)
    label_color = (0.2, 0.2, 0.2)
    border_color = (0.9, 0.9, 0.9)
    border_width = 1
    fill_color = (0.8, 0.8, 0.8)
    stroke_color = (0.1, 0.1, 0.1)
    stroke_width = 2
    oncurve_node_color = (0, 0.8, 0)
    oncurve_node_diameter = 10
    offcurve_node_color = (0, 0.5, 0)
    offcurve_node_diameter = 8
    handle_color = (0.2, 1, 0.2)
    handle_width = 1
    other_start_point_color = (0, 0, 1)
    reversed_start_point_color = (0, 1, 0)
    start_point_color = (1, 0, 0)
    start_point_width = 15
    start_handle_width = 5
    start_handle_length = 100
    start_handle_arrow_length = 5
    contour_colors = ((1, 0, 0), (0, 0, 1), (0, 1, 0), (1, 1, 0), (1, 0, 1), (0, 1, 1))
    contour_alpha = 0.5
    cupcake_color = (0.3, 0, 0.3)
    cupcake = r"""
                          ,@.
                        ,@.@@,.
                  ,@@,.@@@.  @.@@@,.
                ,@@. @@@.     @@. @@,.
        ,@@@.@,.@.              @.  @@@@,.@.@@,.
   ,@@.@.     @@.@@.            @,.    .@’ @’  @@,
 ,@@. @.          .@@.@@@.  @@’                  @,
,@.  @@.                                          @,
@.     @,@@,.     ,                             .@@,
@,.       .@,@@,.         .@@,.  ,       .@@,  @, @,
@.                             .@. @ @@,.    ,      @
 @,.@@.     @,.      @@,.      @.           @,.    @’
  @@||@,.  @’@,.       @@,.  @@ @,.        @’@@,  @’
     \\@@@@’  @,.      @’@@@@’   @@,.   @@@’ //@@@’
      |||||||| @@,.  @@’ |||||||  |@@@|@||  ||
       \\\\\\\  ||@@@||  |||||||  |||||||  //
        |||||||  ||||||  ||||||   ||||||  ||
         \\\\\\  ||||||  ||||||  ||||||  //
          ||||||  |||||  |||||   |||||  ||
           \\\\\  |||||  |||||  |||||  //
            |||||  ||||  |||||  ||||  ||
             \\\\  ||||  ||||  ||||  //
              ||||||||||||||||||||||||
"""
    shrug_color = (0, 0.3, 0.3)
    shrug = r"""\_(")_/"""

    def __init__(self, out, glyphsets, names=None, **kwargs):
        self.out = out
        self.glyphsets = glyphsets
        self.names = names or [repr(g) for g in glyphsets]

        for k, v in kwargs.items():
            if not hasattr(self, k):
                raise TypeError("Unknown keyword argument: %s" % k)
            setattr(self, k, v)

    def __enter__(self):
        return self

    def __exit__(self, type, value, traceback):
        pass

    def set_size(self, width, height):
        raise NotImplementedError

    def show_page(self):
        raise NotImplementedError

    def add_problems(self, problems):
        for glyph, glyph_problems in problems.items():
            last_masters = None
            current_glyph_problems = []
            for p in glyph_problems:
                masters = (
                    p["master"] if "master" in p else (p["master_1"], p["master_2"])
                )
                if masters == last_masters:
                    current_glyph_problems.append(p)
                    continue
                # Flush
                if current_glyph_problems:
                    self.add_problem(glyph, current_glyph_problems)
                    self.show_page()
                    current_glyph_problems = []
                last_masters = masters
                current_glyph_problems.append(p)
            if current_glyph_problems:
                self.add_problem(glyph, current_glyph_problems)
                self.show_page()

    def add_problem(self, glyphname, problems):
        if type(problems) not in (list, tuple):
            problems = [problems]

        problem_type = problems[0]["type"]
        problem_types = set(problem["type"] for problem in problems)
        if not all(pt == problem_type for pt in problem_types):
            problem_type = ", ".join(sorted({problem["type"] for problem in problems}))

        log.info("Drawing %s: %s", glyphname, problem_type)

        master_keys = (
            ("master",) if "master" in problems[0] else ("master_1", "master_2")
        )
        master_names = [problems[0][k] for k in master_keys]
        master_indices = [self.names.index(n) for n in master_names]

        if problem_type == "missing":
            sample_glyph = next(
                i for i, m in enumerate(self.glyphsets) if m[glyphname] is not None
            )
            master_indices.insert(0, sample_glyph)

        total_width = self.width * 2 + 3 * self.pad
        total_height = (
            self.pad
            + self.line_height
            + self.pad
            + len(master_indices) * (self.height + self.pad * 2 + self.line_height)
            + self.pad
        )

        self.set_size(total_width, total_height)

        x = self.pad
        y = self.pad

        self.draw_label(glyphname, x=x, y=y, color=self.head_color, align=0, bold=True)
        self.draw_label(
            problem_type,
            x=x + self.width + self.pad,
            y=y,
            color=self.head_color,
            align=1,
            bold=True,
        )
        y += self.line_height + self.pad

        for which, master_idx in enumerate(master_indices):
            glyphset = self.glyphsets[master_idx]
            name = self.names[master_idx]

            self.draw_label(name, x=x, y=y, color=self.label_color, align=0.5)
            y += self.line_height + self.pad

            if glyphset[glyphname] is not None:
                self.draw_glyph(glyphset, glyphname, problems, which, x=x, y=y)
            else:
                self.draw_shrug(x=x, y=y)
            y += self.height + self.pad

        if any(pt in ("wrong_start_point", "contour_order") for pt in problem_types):
            x = self.pad + self.width + self.pad
            y = self.pad
            y += self.line_height + self.pad

            glyphset1 = self.glyphsets[master_indices[0]]
            glyphset2 = self.glyphsets[master_indices[1]]

            # Draw the mid-way of the two masters

            self.draw_label(
                "midway interpolation", x=x, y=y, color=self.head_color, align=0.5
            )
            y += self.line_height + self.pad

            midway_glyphset = LerpGlyphSet(glyphset1, glyphset2)
            self.draw_glyph(
                midway_glyphset, glyphname, {"type": "midway"}, None, x=x, y=y
            )
            y += self.height + self.pad

            # Draw the fixed mid-way of the two masters

            self.draw_label("proposed fix", x=x, y=y, color=self.head_color, align=0.5)
            y += self.line_height + self.pad

            overriding1 = OverridingDict(glyphset1)
            overriding2 = OverridingDict(glyphset2)
            perContourPen1 = PerContourOrComponentPen(
                RecordingPen, glyphset=overriding1
            )
            perContourPen2 = PerContourOrComponentPen(
                RecordingPen, glyphset=overriding2
            )
            glyphset1[glyphname].draw(perContourPen1)
            glyphset2[glyphname].draw(perContourPen2)

            for problem in problems:
                if problem["type"] == "contour_order":
                    fixed_contours = [
                        perContourPen2.value[i] for i in problems[0]["value_2"]
                    ]
                    perContourPen2.value = fixed_contours

            for problem in problems:
                if problem["type"] == "wrong_start_point":
                    # Save the wrong contours
                    wrongContour1 = perContourPen1.value[problem["contour"]]
                    wrongContour2 = perContourPen2.value[problem["contour"]]

                    # Convert the wrong contours to point pens
                    points1 = RecordingPointPen()
                    converter = SegmentToPointPen(points1, False)
                    wrongContour1.replay(converter)
                    points2 = RecordingPointPen()
                    converter = SegmentToPointPen(points2, False)
                    wrongContour2.replay(converter)

                    proposed_start = problem["value_2"]

                    # See if we need reversing; fragile but worth a try
                    if problem["reversed"]:
                        new_points2 = RecordingPointPen()
                        reversedPen = ReverseContourPointPen(new_points2)
                        points2.replay(reversedPen)
                        points2 = new_points2
                        proposed_start = len(points2.value) - 2 - proposed_start

                    # Rotate points2 so that the first point is the same as in points1
                    beginPath = points2.value[:1]
                    endPath = points2.value[-1:]
                    pts = points2.value[1:-1]
                    pts = pts[proposed_start:] + pts[:proposed_start]
                    points2.value = beginPath + pts + endPath

                    # Convert the point pens back to segment pens
                    segment1 = RecordingPen()
                    converter = PointToSegmentPen(segment1, True)
                    points1.replay(converter)
                    segment2 = RecordingPen()
                    converter = PointToSegmentPen(segment2, True)
                    points2.replay(converter)

                    # Replace the wrong contours
                    wrongContour1.value = segment1.value
                    wrongContour2.value = segment2.value

            # Assemble
            fixed1 = RecordingPen()
            fixed2 = RecordingPen()
            for contour in perContourPen1.value:
                fixed1.value.extend(contour.value)
            for contour in perContourPen2.value:
                fixed2.value.extend(contour.value)
            fixed1.draw = fixed1.replay
            fixed2.draw = fixed2.replay

            overriding1[glyphname] = fixed1
            overriding2[glyphname] = fixed2

            try:
                midway_glyphset = LerpGlyphSet(overriding1, overriding2)
                self.draw_glyph(
                    midway_glyphset, glyphname, {"type": "fixed"}, None, x=x, y=y
                )
            except ValueError:
                self.draw_shrug(x=x, y=y)
            y += self.height + self.pad

    def draw_label(self, label, *, x, y, color=(0, 0, 0), align=0, bold=False):
        cr = cairo.Context(self.surface)
        cr.select_font_face(
            "@cairo:",
            cairo.FONT_SLANT_NORMAL,
            cairo.FONT_WEIGHT_BOLD if bold else cairo.FONT_WEIGHT_NORMAL,
        )
        cr.set_font_size(self.line_height)
        font_extents = cr.font_extents()
        font_size = self.line_height * self.line_height / font_extents[2]
        cr.set_font_size(font_size)
        font_extents = cr.font_extents()

        cr.set_source_rgb(*color)

        extents = cr.text_extents(label)
        if extents.width > self.width:
            # Shrink
            font_size *= self.width / extents.width
            cr.set_font_size(font_size)
            font_extents = cr.font_extents()
            extents = cr.text_extents(label)

        # Center
        label_x = x + (self.width - extents.width) * align
        label_y = y + font_extents[0]
        cr.move_to(label_x, label_y)
        cr.show_text(label)

    def draw_glyph(self, glyphset, glyphname, problems, which, *, x=0, y=0):
        if type(problems) not in (list, tuple):
            problems = [problems]

        problem_type = problems[0]["type"]
        problem_types = set(problem["type"] for problem in problems)
        if not all(pt == problem_type for pt in problem_types):
            problem_type = "mixed"
        glyph = glyphset[glyphname]

        recording = RecordingPen()
        glyph.draw(recording)

        boundsPen = ControlBoundsPen(glyphset)
        recording.replay(boundsPen)

        glyph_width = boundsPen.bounds[2] - boundsPen.bounds[0]
        glyph_height = boundsPen.bounds[3] - boundsPen.bounds[1]

        scale = min(self.width / glyph_width, self.height / glyph_height)

        cr = cairo.Context(self.surface)
        cr.translate(x, y)
        # Center
        cr.translate(
            (self.width - glyph_width * scale) / 2,
            (self.height - glyph_height * scale) / 2,
        )
        cr.scale(scale, -scale)
        cr.translate(-boundsPen.bounds[0], -boundsPen.bounds[3])

        if self.border_color:
            cr.set_source_rgb(*self.border_color)
            cr.rectangle(
                boundsPen.bounds[0], boundsPen.bounds[1], glyph_width, glyph_height
            )
            cr.set_line_width(self.border_width / scale)
            cr.stroke()

        if self.fill_color and problem_type != "open_path":
            pen = CairoPen(glyphset, cr)
            recording.replay(pen)
            cr.set_source_rgb(*self.fill_color)
            cr.fill()

        if self.stroke_color:
            pen = CairoPen(glyphset, cr)
            recording.replay(pen)
            cr.set_source_rgb(*self.stroke_color)
            cr.set_line_width(self.stroke_width / scale)
            cr.stroke()

        if problem_type in ("node_count", "node_incompatibility"):
            cr.set_line_cap(cairo.LINE_CAP_ROUND)

            # Oncurve nodes
            for segment, args in recording.value:
                if not args:
                    continue
                x, y = args[-1]
                cr.move_to(x, y)
                cr.line_to(x, y)
            cr.set_source_rgb(*self.oncurve_node_color)
            cr.set_line_width(self.oncurve_node_diameter / scale)
            cr.stroke()

            # Offcurve nodes
            for segment, args in recording.value:
                for x, y in args[:-1]:
                    cr.move_to(x, y)
                    cr.line_to(x, y)
            cr.set_source_rgb(*self.offcurve_node_color)
            cr.set_line_width(self.offcurve_node_diameter / scale)
            cr.stroke()

            # Handles
            for segment, args in recording.value:
                if not args:
                    pass
                elif segment in ("moveTo", "lineTo"):
                    cr.move_to(*args[0])
                elif segment == "qCurveTo":
                    for x, y in args:
                        cr.line_to(x, y)
                    cr.new_sub_path()
                    cr.move_to(*args[-1])
                elif segment == "curveTo":
                    cr.line_to(*args[0])
                    cr.new_sub_path()
                    cr.move_to(*args[1])
                    cr.line_to(*args[2])
                    cr.new_sub_path()
                    cr.move_to(*args[-1])
                else:
                    assert False

            cr.set_source_rgb(*self.handle_color)
            cr.set_line_width(self.handle_width / scale)
            cr.stroke()

        for problem in problems:
            if problem["type"] == "contour_order":
                matching = problem["value_2"]
                colors = cycle(self.contour_colors)
                perContourPen = PerContourOrComponentPen(
                    RecordingPen, glyphset=glyphset
                )
                recording.replay(perContourPen)
                for i, contour in enumerate(perContourPen.value):
                    if matching[i] == i:
                        continue
                    color = next(colors)
                    contour.replay(CairoPen(glyphset, cr))
                    cr.set_source_rgba(*color, self.contour_alpha)
                    cr.fill()

        for problem in problems:
            if problem["type"] == "wrong_start_point":
                idx = problem["contour"]

                # Draw suggested point
                if which == 1:
                    perContourPen = PerContourOrComponentPen(
                        RecordingPen, glyphset=glyphset
                    )
                    recording.replay(perContourPen)
                    points = SimpleRecordingPointPen()
                    converter = SegmentToPointPen(points, False)
                    perContourPen.value[idx].replay(converter)
                    targetPoint = points.value[problem["value_2"]][0]
                    cr.move_to(*targetPoint)
                    cr.line_to(*targetPoint)
                    cr.set_line_cap(cairo.LINE_CAP_ROUND)
                    cr.set_source_rgb(*self.other_start_point_color)
                    cr.set_line_width(self.start_point_width / scale)
                    cr.stroke()

                # Draw start point
                cr.set_line_cap(cairo.LINE_CAP_ROUND)
                i = 0
                for segment, args in recording.value:
                    if segment == "moveTo":
                        if i == idx:
                            cr.move_to(*args[0])
                            cr.line_to(*args[0])
                        i += 1

                if which == 0 or not problem["reversed"]:
                    cr.set_source_rgb(*self.start_point_color)
                else:
                    cr.set_source_rgb(*self.reversed_start_point_color)
                cr.set_line_width(self.start_point_width / scale)
                cr.stroke()

                # Draw arrow
                cr.set_line_cap(cairo.LINE_CAP_SQUARE)
                first_pt = None
                i = 0
                for segment, args in recording.value:
                    if segment == "moveTo":
                        first_pt = args[0]
                        continue
                    if first_pt is None:
                        continue
                    second_pt = args[0]

                    if i == idx:
                        first_pt = complex(*first_pt)
                        second_pt = complex(*second_pt)
                        length = abs(second_pt - first_pt)
                        if length:
                            # Draw handle
                            length *= scale
                            second_pt = (
                                first_pt
                                + (second_pt - first_pt)
                                / length
                                * self.start_handle_length
                            )
                            cr.move_to(first_pt.real, first_pt.imag)
                            cr.line_to(second_pt.real, second_pt.imag)
                            # Draw arrowhead
                            cr.save()
                            cr.translate(second_pt.real, second_pt.imag)
                            cr.rotate(
                                math.atan2(
                                    second_pt.imag - first_pt.imag,
                                    second_pt.real - first_pt.real,
                                )
                            )
                            cr.scale(1 / scale, 1 / scale)
                            cr.translate(self.start_handle_width, 0)
                            cr.move_to(0, 0)
                            cr.line_to(
                                -self.start_handle_arrow_length,
                                -self.start_handle_arrow_length,
                            )
                            cr.line_to(
                                -self.start_handle_arrow_length,
                                self.start_handle_arrow_length,
                            )
                            cr.close_path()
                            cr.restore()

                    first_pt = None
                    i += 1

                cr.set_line_width(self.start_handle_width / scale)
                cr.stroke()

    def draw_cupcake(self):
        self.set_size(self.width, self.height)
        cupcake = self.cupcake.splitlines()
        cr = cairo.Context(self.surface)
        cr.set_source_rgb(*self.cupcake_color)
        cr.set_font_size(self.line_height)
        cr.select_font_face(
            "@cairo:monospace", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL
        )
        width = 0
        height = 0
        for line in cupcake:
            extents = cr.text_extents(line)
            width = max(width, extents.width)
            height += extents.height
        if not width:
            return
        cr.scale(self.width / width, self.height / height)
        for line in cupcake:
            cr.translate(0, cr.text_extents(line).height)
            cr.move_to(0, 0)
            cr.show_text(line)

    def draw_shrug(self, x=0, y=0):
        cr = cairo.Context(self.surface)
        cr.translate(x, y)
        cr.set_source_rgb(*self.shrug_color)
        cr.set_font_size(self.line_height)
        cr.select_font_face(
            "@cairo:monospace", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL
        )
        extents = cr.text_extents(self.shrug)
        if not extents.width:
            return
        cr.translate(0, self.height * 0.6)
        scale = self.width / extents.width
        cr.scale(scale, scale)
        cr.move_to(-extents.x_bearing, 0)
        cr.show_text(self.shrug)


class InterpolatablePostscriptLike(InterpolatablePlot):
    @wraps(InterpolatablePlot.__init__)
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def __exit__(self, type, value, traceback):
        self.surface.finish()

    def set_size(self, width, height):
        self.surface.set_size(width, height)

    def show_page(self):
        self.surface.show_page()

    def __enter__(self):
        self.surface = cairo.PSSurface(self.out, self.width, self.height)
        return self


class InterpolatablePS(InterpolatablePostscriptLike):
    def __enter__(self):
        self.surface = cairo.PSSurface(self.out, self.width, self.height)
        return self


class InterpolatablePDF(InterpolatablePostscriptLike):
    def __enter__(self):
        self.surface = cairo.PDFSurface(self.out, self.width, self.height)
        self.surface.set_metadata(
            cairo.PDF_METADATA_CREATOR, "fonttools varLib.interpolatable"
        )
        self.surface.set_metadata(cairo.PDF_METADATA_CREATE_DATE, "")
        return self


class InterpolatableSVG(InterpolatablePlot):
    @wraps(InterpolatablePlot.__init__)
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def __enter__(self):
        self.surface = None
        return self

    def __exit__(self, type, value, traceback):
        if self.surface is not None:
            self.show_page()

    def set_size(self, width, height):
        self.sink = BytesIO()
        self.surface = cairo.SVGSurface(self.sink, width, height)

    def show_page(self):
        self.surface.finish()
        self.out.append(self.sink.getvalue())
        self.surface = None
