"""This submodule contains functions related to smoothing paths of Bezier
curves."""

# External Dependencies
from __future__ import division, absolute_import, print_function

# Internal Dependencies
from .path import Path, CubicBezier, Line
from .misctools import isclose
from .paths2svg import disvg


def is_differentiable(path, tol=1e-8):
    for idx in range(len(path)):
        u = path[(idx - 1) % len(path)].unit_tangent(1)
        v = path[idx].unit_tangent(0)
        u_dot_v = u.real * v.real + u.imag * v.imag
        if abs(u_dot_v - 1) > tol:
            return False
    return True


def kinks(path, tol=1e-8):
    """returns indices of segments that start on a non-differentiable joint."""
    kink_list = []
    for idx in range(len(path)):
        if idx == 0 and not path.isclosed():
            continue
        try:
            u = path[(idx - 1) % len(path)].unit_tangent(1)
            v = path[idx].unit_tangent(0)
            u_dot_v = u.real * v.real + u.imag * v.imag
            flag = False
        except ValueError:
            flag = True

        if flag or abs(u_dot_v - 1) > tol:
            kink_list.append(idx)
    return kink_list


def _report_unfixable_kinks(_path, _kink_list):
    mes = (
        "\n%s kinks have been detected at that cannot be smoothed.\n"
        "To ignore these kinks and fix all others, run this function "
        "again with the second argument 'ignore_unfixable_kinks=True' "
        "The locations of the unfixable kinks are at the beginnings of "
        "segments: %s" % (len(_kink_list), _kink_list)
    )
    disvg(_path, nodes=[_path[idx].start for idx in _kink_list])
    raise Exception(mes)


def smoothed_joint(seg0, seg1, maxjointsize=3, tightness=1.99):
    """See Andy's notes on
    Smoothing Bezier Paths for an explanation of the method.
    Input: two segments seg0, seg1 such that seg0.end==seg1.start, and
    jointsize, a positive number

    Output: seg0_trimmed, elbow, seg1_trimmed, where elbow is a cubic bezier
        object that smoothly connects seg0_trimmed and seg1_trimmed.

    """
    assert seg0.end == seg1.start
    assert 0 < maxjointsize
    assert 0 < tightness < 2
    #    sgn = lambda x:x/abs(x)
    q = seg0.end

    try:
        v = seg0.unit_tangent(1)
    except:
        v = seg0.unit_tangent(1 - 1e-4)
    try:
        w = seg1.unit_tangent(0)
    except:
        w = seg1.unit_tangent(1e-4)

    max_a = maxjointsize / 2
    a = min(max_a, min(seg1.length(), seg0.length()) / 20)
    if isinstance(seg0, Line) and isinstance(seg1, Line):
        """
        Note: Letting
            c(t) = elbow.point(t), v= the unit tangent of seg0 at 1, w = the
            unit tangent vector of seg1 at 0,
            Q = seg0.point(1) = seg1.point(0), and a,b>0 some constants.
            The elbow will be the unique CubicBezier, c, such that
            c(0)= Q-av, c(1)=Q+aw, c'(0) = bv, and c'(1) = bw
            where a and b are derived above/below from tightness and
            maxjointsize.
        """
        #        det = v.imag*w.real-v.real*w.imag
        # Note:
        # If det is negative, the curvature of elbow is negative for all
        # real t if and only if b/a > 6
        # If det is positive, the curvature of elbow is negative for all
        # real t if and only if b/a < 2

        #        if det < 0:
        #            b = (6+tightness)*a
        #        elif det > 0:
        #            b = (2-tightness)*a
        #        else:
        #            raise Exception("seg0 and seg1 are parallel lines.")
        b = (2 - tightness) * a
        elbow = CubicBezier(
            q - a * v, q - (a - b / 3) * v, q + (a - b / 3) * w, q + a * w
        )
        seg0_trimmed = Line(seg0.start, elbow.start)
        seg1_trimmed = Line(elbow.end, seg1.end)
        return seg0_trimmed, [elbow], seg1_trimmed
    elif isinstance(seg0, Line):
        """
        Note: Letting
            c(t) = elbow.point(t), v= the unit tangent of seg0 at 1,
            w = the unit tangent vector of seg1 at 0,
            Q = seg0.point(1) = seg1.point(0), and a,b>0 some constants.
            The elbow will be the unique CubicBezier, c, such that
            c(0)= Q-av, c(1)=Q, c'(0) = bv, and c'(1) = bw
            where a and b are derived above/below from tightness and
            maxjointsize.
        """
        #        det = v.imag*w.real-v.real*w.imag
        # Note: If g has the same sign as det, then the curvature of elbow is
        # negative for all real t if and only if b/a < 4
        b = (4 - tightness) * a
        #        g = sgn(det)*b
        elbow = CubicBezier(q - a * v, q + (b / 3 - a) * v, q - b / 3 * w, q)
        seg0_trimmed = Line(seg0.start, elbow.start)
        return seg0_trimmed, [elbow], seg1
    elif isinstance(seg1, Line):
        args = (seg1.reversed(), seg0.reversed(), maxjointsize, tightness)
        rseg1_trimmed, relbow, rseg0 = smoothed_joint(*args)
        elbow = relbow[0].reversed()
        return seg0, [elbow], rseg1_trimmed.reversed()
    else:
        # find a point on each seg that is about a/2 away from joint.  Make
        # line between them.
        t0 = seg0.ilength(seg0.length() - a / 2)
        t1 = seg1.ilength(a / 2)
        seg0_trimmed = seg0.cropped(0, t0)
        seg1_trimmed = seg1.cropped(t1, 1)
        seg0_line = Line(seg0_trimmed.end, q)
        seg1_line = Line(q, seg1_trimmed.start)

        args = (seg0_trimmed, seg0_line, maxjointsize, tightness)
        dummy, elbow0, seg0_line_trimmed = smoothed_joint(*args)

        args = (seg1_line, seg1_trimmed, maxjointsize, tightness)
        seg1_line_trimmed, elbow1, dummy = smoothed_joint(*args)

        args = (seg0_line_trimmed, seg1_line_trimmed, maxjointsize, tightness)
        seg0_line_trimmed, elbowq, seg1_line_trimmed = smoothed_joint(*args)

        elbow = elbow0 + [seg0_line_trimmed] + elbowq + [seg1_line_trimmed] + elbow1
        return seg0_trimmed, elbow, seg1_trimmed


def smoothed_path(path, maxjointsize=3, tightness=1.99, ignore_unfixable_kinks=False):
    """returns a path with no non-differentiable joints."""
    if len(path) == 1:
        return path

    assert path.iscontinuous()

    sharp_kinks = []
    new_path = [path[0]]
    for idx in range(len(path)):
        if idx == len(path) - 1:
            if not path.isclosed():
                continue
            else:
                seg1 = new_path[0]
        else:
            seg1 = path[idx + 1]
        seg0 = new_path[-1]

        try:
            unit_tangent0 = seg0.unit_tangent(1)
            unit_tangent1 = seg1.unit_tangent(0)
            flag = False
        except ValueError:
            flag = True  # unit tangent not well-defined

        if not flag and isclose(
            unit_tangent0, unit_tangent1
        ):  # joint is already smooth
            if idx != len(path) - 1:
                new_path.append(seg1)
            continue
        else:
            kink_idx = (idx + 1) % len(path)  # kink at start of this seg
            if not flag and isclose(-unit_tangent0, unit_tangent1):
                # joint is sharp 180 deg (must be fixed manually)
                new_path.append(seg1)
                sharp_kinks.append(kink_idx)
            else:  # joint is not smooth, let's  smooth it.
                args = (seg0, seg1, maxjointsize, tightness)
                new_seg0, elbow_segs, new_seg1 = smoothed_joint(*args)
                new_path[-1] = new_seg0
                new_path += elbow_segs
                if idx == len(path) - 1:
                    new_path[0] = new_seg1
                else:
                    new_path.append(new_seg1)

    # If unfixable kinks were found, let the user know
    if sharp_kinks and not ignore_unfixable_kinks:
        _report_unfixable_kinks(path, sharp_kinks)

    return Path(*new_path)
