import math

from svg.path import parse_path

DEFAULT_MOVEMENT = 1600  # If no other movement is specified, go 1600px to the right.
POSITION_ATTRIBS = [
    "data-x",
    "data-y",
    "data-z",
    "data-rotate-x",
    "data-rotate-y",
    "data-rotate-z",
    "data-scale",
]


def gather_positions(tree):
    """Makes a list of positions and position commands from the tree"""
    pos = {
        "data-x": "r0",
        "data-y": "r0",
        "data-z": "r0",
        "data-rotate-x": "r0",
        "data-rotate-y": "r0",
        "data-rotate-z": "r0",
        "data-scale": "r0",
        "is_path": False,
    }

    steps = 0
    default_movement = True

    for step in tree.findall("step"):
        steps += 1

        for key in POSITION_ATTRIBS:
            value = step.get(key)

            if value is not None:
                # We have a new value
                default_movement = False  # No longer use the default movement
                pos[key] = value
            elif pos[key] and not pos[key].startswith("r"):
                # The old value was absolute and no new value, so stop
                pos[key] = "r0"
            # We had no new value, and the old value was a relative
            # movement, so we just keep moving.

        if steps == 1 and pos["data-scale"] == "r0":
            # No scale given for first slide, it needs to start at 1
            pos["data-scale"] = "1"

        if default_movement and steps != 1:
            # No positioning has been given, use default:
            pos["data-x"] = "r%s" % DEFAULT_MOVEMENT

        if "data-rotate" in step.attrib:
            # data-rotate is an alias for data-rotate-z
            pos["data-rotate-z"] = step.get("data-rotate")
            del step.attrib["data-rotate"]

        if "hovercraft-path" in step.attrib:
            # Path given x and y will be calculated from the path
            default_movement = False  # No longer use the default movement
            pos["is_path"] = True
            # Add the path spec
            pos["path"] = step.attrib["hovercraft-path"]
            yield pos.copy()
            # And get rid of it for the next step
            del pos["path"]
        else:
            if "data-x" in step.attrib or "data-y" in step.attrib:
                # No longer using a path
                pos["is_path"] = False
            yield pos.copy()


def _coord_to_pos(coord):
    return {"data-x": int(coord.real), "data-y": int(coord.imag)}


def _pos_to_cord(coord):
    return coord["data-x"] + coord["data-y"] * 1j


def _path_angle(path, point):
    start = point - 0.01
    end = point + 0.01
    if start < 0:
        start = 0
        end += 0.01
    elif end > 1:
        end = 1
        start -= 0.01

    distance = path.point(end) - path.point(start)
    hyp = math.hypot(distance.real, distance.imag)
    result = math.degrees(math.asin(distance.imag / hyp))

    if distance.real < 0:
        result = -180 - result

    if abs(result) < 0.1:
        result = 0

    return result


def num(s):
    try:
        return int(s)
    except ValueError:
        return float(s)


def _update_position(pos1, pos2):

    for key in POSITION_ATTRIBS:
        val = pos2.get(key)
        if val is not None:
            plus = val.find("+")
            minus = val.find("-")
            if plus > -1:
                newval = num(val[plus + 1 :])
                pos1[key + "-rel"] = val[0:plus]
            elif minus > -1 and not val.startswith("r-"):
                newval = num(val[minus:])
                pos1[key + "-rel"] = val[0:minus]
            else:
                if val[0] == "r":
                    # Relative movement
                    newval = pos1[key] + num(val[1:])
                else:
                    newval = num(val)
                    pos1.pop(key + "-rel", None)
            pos1[key] = newval


def calculate_positions(positions):
    """Calculates position information"""
    current_position = {
        "data-x": 0,
        "data-y": 0,
        "data-z": 0,
        "data-rotate-x": 0,
        "data-rotate-y": 0,
        "data-rotate-z": 0,
        "data-scale": 1,
    }

    positer = iter(positions)
    position = next(positer)
    _update_position(current_position, position)

    while True:

        if "path" in position:
            # Start of a new path!
            path = position["path"]
            # Follow the path specification
            first_point = _pos_to_cord(current_position)

            # Paths that end in Z or z are closed.
            closed_path = path.strip()[-1].upper() == "Z"
            path = parse_path(path)

            # Find out how many positions should be calculated:
            count = 1
            last = False
            deferred_positions = []
            while True:
                try:
                    position = next(positer)
                    deferred_positions.append(position)
                except StopIteration:
                    last = True  # This path goes to the end
                    break
                if not position.get("is_path") or "path" in position:
                    # The end of the path, or the start of a new one
                    break
                count += 1

            if count < 2:
                raise AssertionError(
                    "The path specification is only used for "
                    "one slide, which makes it pointless."
                )

            if closed_path:
                # This path closes in on itself. Skip the last part, so that
                # the first and last step doesn't overlap.
                endcount = count + 1
            else:
                endcount = count

            multiplier = (endcount * DEFAULT_MOVEMENT) / path.length()
            offset = path.point(0)

            path_iter = iter(deferred_positions)
            for x in range(count):

                point = path.point(x / (endcount - 1))
                point = ((point - offset) * multiplier) + first_point

                current_position.update(_coord_to_pos(point))

                rotation = _path_angle(path, x / (endcount - 1))
                current_position["data-rotate-z"] = rotation
                yield current_position.copy()
                try:
                    position = next(path_iter)
                except StopIteration:
                    last = True
                    break
                _update_position(current_position, position)

            if last:
                break

            continue

        yield current_position.copy()
        try:
            position = next(positer)
        except StopIteration:
            break
        _update_position(current_position, position)


def update_positions(tree, positions):
    """Updates the tree with new positions"""

    for step, pos in zip(tree.findall("step"), positions):
        for key in sorted(pos):
            value = pos.get(key)
            if key.endswith("-rel"):
                abs_key = key[: key.index("-rel")]
                if value is not None:
                    els = tree.findall(".//*[@id='" + value + "']")
                    for el in els:
                        pos[abs_key] = num(el.get(abs_key)) + pos.get(abs_key)
                        step.attrib[abs_key] = str(pos.get(abs_key))
            else:
                step.attrib[key] = str(pos[key])

        if "hovercraft-path" in step.attrib:
            del step.attrib["hovercraft-path"]


def position_slides(tree):
    """Position the slides in the tree"""

    positions = gather_positions(tree)
    positions = calculate_positions(positions)
    update_positions(tree, positions)
