import turtle
import random

turtle.colormode(255)

def _create_pen(pos, thickness, start_angle, speed):
    pen = turtle.Turtle()
    pen.hideturtle()
    pen.speed(speed)
    pen.teleport(*pos)
    pen.left(start_angle)
    pen.pensize(thickness)

    return pen

def draw_forward(distance):
    return f"df{distance}"

def draw_backwards(distance):
    return f"db{distance}"

def move_forward(distance):
    return f"mf{distance}"

def move_backwards(distance):
    return f"mb{distance}"

def random_color():
    return (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))

DRAW_FORWARD = draw_forward(1)
DRAW_BACKWARDS = draw_backwards(1)
MOVE_FORWARD = move_forward(1)
MOVE_BACKWARDS = move_backwards(1)
TURN_LEFT = "tl"
TURN_RIGHT = "tr"
SAVE_POS_AND_ANGLE = "s"
GO_TO_POS_ANGLE_AND_RESET_THEM = "r"
CONTROL_VARIABLE = "c"

class LSystem:
    def __init__(self, iterations, start, rules, variable_rules, constant_rules, angle, scale, thickness, start_angle):
        def replace(base_string, replaces):
            for find, replace_with in replaces:
                base_string = base_string.replace(find, replace_with)

            return base_string

        var_const = variable_rules + constant_rules

        for i in range(iterations):
            start = replace(start, rules)

        for var, r in var_const:
            if r == CONTROL_VARIABLE:
                start = start.replace(var, "")

        self.string = start
        self.iterations = iterations
        self.start = start
        self.rules = rules
        self.variable_rules = variable_rules
        self.constant_rules = constant_rules
        self.angle = angle
        self.scale = scale
        self.var_const_rules = var_const
        self.thickness = thickness
        self.start_angle = start_angle

    def draw_turtle(self, pos, speed = 0, end_turtle = True, instant = False, make_colorful = False, color_density = 20, color_func = random_color):
        if instant:
            turtle.tracer(0)

        fractals = [(_create_pen(pos, self.thickness, self.start_angle, speed), self.string, [])]

        for fractal in fractals:
            for i, move in enumerate(fractal[1]):
                for rule in self.var_const_rules:
                    if move == rule[0]:
                        if rule[1] == DRAW_FORWARD:
                            if i % color_density == 0 and make_colorful:
                                fractal[0].color(*color_func())

                            fractal[0].forward(self.scale * int(rule[1][2:]))
                        elif rule[1] == TURN_LEFT:
                            fractal[0].left(self.angle)
                        elif rule[1] == TURN_RIGHT:
                            fractal[0].right(self.angle)
                        elif rule[1] == SAVE_POS_AND_ANGLE:
                            pos = fractal[0].position()
                            heading = fractal[0].heading()
                            fractal[2].append((pos, heading))
                        elif rule[1] == GO_TO_POS_ANGLE_AND_RESET_THEM:
                            pos, heading = fractal[2].pop()
                            fractal[0].penup()
                            fractal[0].goto(pos)
                            fractal[0].setheading(heading)
                            fractal[0].pendown()
                        elif rule[1] == DRAW_BACKWARDS:
                            fractal[0].back(self.scale * int(rule[1][2:]))
                        elif rule[1] == MOVE_FORWARD:
                            fractal[0].penup()
                            fractal[0].forward(self.scale * int(rule[1][2:]))
                            fractal[0].pendown()
                        elif rule[1] == MOVE_BACKWARDS:
                            fractal[0].penup()
                            fractal[0].back(self.scale * int(rule[1][2:]))
                            fractal[0].pendown()

        if end_turtle:
            turtle.done()

class FractalPlant(LSystem):
    def __init__(self, iterations, start = "-X", scale = 5, thickness = 2, start_angle = 90):
        rules = [("F", "FF"), ("X", "F+[[X]-X]-F[-FX]+X")]
        variable_rules = [("F", DRAW_FORWARD), ("X", CONTROL_VARIABLE)]
        constant_rules = [("-", TURN_RIGHT), ("+", TURN_LEFT), ("[", SAVE_POS_AND_ANGLE), ("]", GO_TO_POS_ANGLE_AND_RESET_THEM)]
        super().__init__(iterations, start, rules, variable_rules, constant_rules, 25, scale, thickness, start_angle)

class KochCurve(LSystem):
    def __init__(self, iterations, start = "F", scale = 5, thickness = 2, start_angle = 0):
        rules = [("F", "F+F-F-F+F")]
        variable_rules = [("F", DRAW_FORWARD)]
        constant_rules = [("-", TURN_RIGHT), ("+", TURN_LEFT)]
        super().__init__(iterations, start, rules, variable_rules, constant_rules, 90, scale, thickness, start_angle)

class SierpinskiTriangle(LSystem):
    def __init__(self, iterations, start = "F-G-G", scale = 5, thickness = 2, start_angle = 90):
        rules = [("F", "F-G+F+G-F"), ("G", "GG")]
        variable_rules = [("F", DRAW_FORWARD), ("G", DRAW_FORWARD)]
        constant_rules = [("-", TURN_RIGHT), ("+", TURN_LEFT)]
        super().__init__(iterations, start, rules, variable_rules, constant_rules, 120, scale, thickness, start_angle)

class SierpinskiArrowheadCurve(LSystem):
    def __init__(self, iterations, start = "A", scale = 5, thickness = 2, start_angle = 90):
        rules = [("A", "B-A-B"), ("B", "A+B+A")]
        variable_rules = [("A", DRAW_FORWARD), ("B", DRAW_FORWARD)]
        constant_rules = [("-", TURN_RIGHT), ("+", TURN_LEFT)]
        super().__init__(iterations, start, rules, variable_rules, constant_rules, 60, scale, thickness, start_angle)

class DragonCurve(LSystem):
    def __init__(self, iterations, start = "F", scale = 5, thickness = 2, start_angle = 0):
        rules = [("F", "F+G"), ("G", "F-G")]
        variable_rules = [("F", DRAW_FORWARD), ("G", DRAW_FORWARD)]
        constant_rules = [("-", TURN_RIGHT), ("+", TURN_LEFT)]
        super().__init__(iterations, start, rules, variable_rules, constant_rules, 90, scale, thickness, start_angle)
