import pygame
import time
import random
from datetime import datetime

DF = "default"

# Initialize pygame
pygame.init()

class Camera:
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y
        self.zoom = 1

    def move(self, dx, dy):
        self.x += dx
        self.y += dy

    def follow(self, obj, speed, game, ox = 0, oy = 0):
        self.x -= speed * ((obj.x + ox) + (self.x - game.width / 2))
        self.y -= speed * ((obj.y + oy) + (self.y - game.height / 2))

    def center(self, game):
        self.x = game.width / 2
        self.y = game.height / 2

    def default(self):
        self.x = 0
        self.y = 0

class Mouse:
    def __init__(self, x=0, dx=0, y=0, dy=0, pressed: bool = False, button = 1):
        self.x = x
        self.y = y
        self.dx = dx
        self.dy = dy
        self.down = pressed
        self.button = button

    def update(self, x=0, y=0, dx=0, dy=0):
        self.x = x
        self.y = y
        self.dx = dx
        self.dy = dy

class Keyboard:
    def __init__(self, keys: list = [], pressed: bool = False):
        self.keys = keys
        self.pressed = pressed
        self.layers = 0
        self.dx = 0
        self.dy = 0

    def update(self, key, pressed: bool, add: bool):
        if add:
            if key not in self.keys:
                self.keys.append(key)
        else:
            if key in self.keys:
                self.keys.remove(key)
        self.layers += int(add) * 2 - 1
        self.pressed = (self.layers > 0)
        #print(self.layers)
        self.dx = int(pygame.K_RIGHT in self.keys) - int(pygame.K_LEFT in self.keys)
        self.dy = int(pygame.K_DOWN in self.keys) - int(pygame.K_UP in self.keys) 

class Hitbox:
    def __init__(self, ox=0, oy=0, radius=10, x=0, y=0, object = None, type=0):
        if object == None:
            self.x = x + ox
            self.y = x + oy
        else:
            self.x = object.x + ox
            self.y = object.y + oy

        self.ox = ox
        self.oy = oy
        self.object = object
        self.radius = radius

        # Type 0: Circle
        # Type 1: Square
        self.type = type

    def update(self, obj):
        # Update position based on velocity
        self.x = obj.x + self.ox
        self.y = obj.y + self.oy
    
    def collide(self,other):
        if (self.type == 0) & (other.type == 1):
            cx = self.clamp(self.x, other.x-other.radius, other.x+other.radius)
            cy = self.clamp(self.y, other.y-other.radius, other.y+other.radius)
            dx = self.x - cx
            dy = self.y - cy
            distance = (dx**2 + dy**2)
            return (distance < (self.radius)**2)
        
        if (self.type == 1) & (other.type == 0):
            cx = self.clamp(other.x, self.x-self.radius, self.x+self.radius)
            cy = self.clamp(other.y, self.y-self.radius, self.y+self.radius)
            dx = other.x - cx
            dy = other.y - cy
            distance = (dx**2 + dy**2)
            return (distance < (other.radius)**2)

        if (self.type == 0) & (other.type == 0):
            dx = self.x - other.x
            dy = self.y - other.y
            distance = (dx**2 + dy**2)
            return (distance < (self.radius + other.radius)**2)
        
        if (self.type == 1) & (other.type == 1):
            dx = abs(self.x - other.x)
            dy = abs(self.y - other.y)
            return (dx < (self.radius + other.radius)) & (dy < (self.radius + other.radius))
        
    def clamp(self, value, min_val, max_val):
        return max(min(value, max_val), min_val)
        
class Object:
    def __init__(self, x, y, hitboxes = [], color=(0,0,0), vel_x=0, vel_y=0, id=0, UI=False, visible = True, group = 0):
        self.x = x
        self.y = y
        self.hitboxes = hitboxes
        self.color = color
        self.vel_x = vel_x
        self.vel_y = vel_y
        self.id = id
        self.ui = UI
        self.edge = False
        self.visible = visible
        self.group = group
        self.hit = []
        self.bounce = 0
        self.friction = 0
        self.resistance = 0
        self.z_layer = 0
        self.image = None

    def update(self, game):
        # Update position based on velocity
        for hbx in self.hitboxes:
            hbx.update(self)
        # if abs(self.vel_x) + abs(self.vel_y) > 0:
        #     hit = self.processMovement(self.vel_x, self.vel_y, game, self.bounce, 25*int(abs(self.vel_x) + abs(self.vel_y)), self.friction)
        #     if hit == False:
        #         self.vel_x *= self.resistance

        #print(self.isTouching(game))
        
        self.edge = False
        for hitbox in self.hitboxes:
            if ((self.x - hitbox.radius <= 0) or (self.x + hitbox.radius >= game.width) or (self.y - hitbox.radius <= 0) or (self.y + hitbox.radius >= game.height)):
                self.edge = True
                return
    
    def processMovement(self, vx, vy, game, bounce, precision, friction):
        for vx, vy in zip([vx/precision for i in range(precision)], [vy/precision for i in range(precision)]):
            self.x += vx
            self.y += vy
            if self.isTouching(game):
                self.x -= vx
                if self.isTouching(game):
                    self.x += vx
                    self.y -= vy
                    if self.isTouching(game):
                        self.x -= vx
                        self.vel_x *= -bounce
                        self.vel_y *= -bounce
                        #while self.isTouching(game):
                        self.x += vx
                        self.y += vy
                    else:
                        self.vel_x *= friction
                        self.vel_y *= -bounce
                        #while self.isTouching(game):
                        self.y -= vy
                else:
                    self.vel_x *= -bounce
                    #while self.isTouching(game):
                    self.x += vx

                return True

        return False
    
    def isTouching(self, game):
        return (len(self.getcolliders([obj for obj in game.objects if (obj.group != game.noCollisionGroup)])) > 0)
                

    def getcolliders(self, pool):
        colliders = []
        for obj in pool:
            if self.collide(obj) & (obj != self):
                colliders.append(obj)
        return colliders


    def collide(self, obj):
        c=False
        for sh in self.hitboxes:
            for oh in obj.hitboxes:
                c = sh.collide(oh)
        return c
    
    def getcollider(self, obj):

        for sh in self.hitboxes:
            for oh in obj.hitboxes:
                if sh.collide(oh):
                    return [sh, oh]
                
    def loadImage(self, path):
        self.image = pygame.image.load(path)

    def resizeImage(self, x, y):
        self.image = pygame.transform.scale(self.image, (x, y))

    def edit(self, dx = 0, dy = 0, x = None, y = None):
        if x != None:
            self.x = x + dx
        else:
            self.x += dx
        if y != None:
            self.y = y + dy
        else:
            self.y += dy

    def addcollision(self, obj):
        self.hit.append(obj)

    def render(self, game):
        zoom = game.camera.zoom
        if self.visible:
            if self.ui:
                if self.image == None:
                    for hitbox in self.hitboxes:
                        if hitbox.type == 0:
                            pygame.draw.circle(game.screen, self.color, (hitbox.x * zoom, hitbox.y * zoom), hitbox.radius  * zoom)
                        else:
                            pygame.draw.rect(game.screen, self.color, [(hitbox.x - hitbox.radius) * zoom, (hitbox.y - hitbox.radius)  * zoom, 2 * hitbox.radius  * zoom, 2 * hitbox.radius  * zoom])
                else:
                    game.screen.blit(self.image, (self.x * zoom, self.y * zoom))
            else:
                if self.image == None:
                    for hitbox in self.hitboxes:
                        if hitbox.type == 0:
                            pygame.draw.circle(game.screen, self.color, ((hitbox.x * zoom + game.camera.x), (hitbox.y * zoom + game.camera.y)), hitbox.radius * zoom)
                        else:
                            pygame.draw.rect(game.screen, self.color, [((hitbox.x - hitbox.radius) * zoom + game.camera.x), ((hitbox.y - hitbox.radius) * zoom + game.camera.y), 2 * hitbox.radius * zoom, 2 * hitbox.radius * zoom])
                else:
                    game.screen.blit(self.image, ((self.x * zoom + game.camera.x), (self.y * zoom + game.camera.y)))

    def set_position(self, x, y):
        self.x = x
        self.y = y

    def set_velocity(self, vel_x, vel_y):
        self.vel_x = vel_x
        self.vel_y = vel_y

    def delete(self, game):
        try:
            game.objects.remove(self)
        except:
            print(f"\033[31mUnable to delete object from group {self.group}\033[0m")
    
    def keycontrol(self, game, speed):
        self.vel_x += speed * game.keyboard.dx
        self.vel_y += speed * game.keyboard.dy
    
    def accelerate(self, dx = None, dy = None):
        if dx != None:
            self.vel_x += dx
        if dy != None:
            self.vel_y += dy

    def newhbx(self, ox = DF, oy = DF, radius = DF, type = DF):
        """Type 0: Circle, Type 1: Square"""
        if (ox == DF or oy == DF or radius == DF or type == DF) & (len(self.hitboxes) > 0):

            ox=[ox if ox != DF else self.hitboxes[len(self.hitboxes) - 1].ox][0]
            oy=[oy if oy != DF else self.hitboxes[len(self.hitboxes) - 1].oy][0]
            radius=[radius if radius != DF else self.hitboxes[len(self.hitboxes) - 1].radius][0]
            type=[type if type != DF else self.hitboxes[len(self.hitboxes) - 1].type][0]
            hbx = Hitbox(x=self.x, y=self.y, ox=ox, oy=oy, radius=radius, type=type)
            self.hitboxes.append(hbx)
            return hbx
        else:
            hbx = Hitbox(x=self.x, y=self.y, ox=ox, oy=oy, radius=radius, type=type)
            self.hitboxes.append(hbx)
            return hbx
        

    def mousepress(self, game):
        for hitbox in self.hitboxes:
            if hitbox.type == 0:
                return (((hitbox.x - game.camera.x) - game.mouse.x)**2 + ((hitbox.y - game.camera.y) - game.mouse.y)**2 <= hitbox.radius**2)
            else: 
                return ((hitbox.x - game.camera.x - 2 * hitbox.radius <= game.mouse.x) & (hitbox.x - game.camera.x + 2 * hitbox.radius >= game.mouse.x)) & ((hitbox.y - game.camera.y - 2 * hitbox.radius <= game.mouse.y) & (hitbox.y - game.camera.y + 2 * hitbox.radius >= game.mouse.y))

class Text:
    def __init__(self, text = "", font = pygame.font.SysFont(None, 30), x = 0, y = 0, color = (0,0,0), id=0, ui=False):
        self.text = text
        self.font = font
        self.x = x
        self.y = y
        self.color = color
        self.id = id
        self.ui = ui
    
    def render(self, game):
        frame_text = self.font.render(self.text, True, self.color)
        if self.ui:
            game.screen.blit(frame_text,  frame_text.get_rect(topleft=(self.x, self.y)))
        else:
            game.screen.blit(frame_text,  frame_text.get_rect(topleft=(self.x + game.camera.x, self.y + game.camera.y)))
        
    
    def edit(self, text = None, font = None, x = None, y = None, color = None):
        if text != None:
            self.text = text
        if font != None:
            self.font = font
        if x != None:
            self.x = x
        if y != None:
            self.y = y
        if color != None:
            self.color = color


class Game:
    def __init__(self, width = 800, height = 600, fps = 60, run = True):
        self.run = run
        self.width = width
        self.height = height
        self.FPS = fps
        self.objects = []
        self.texts = []
        self.frame_count = 0
        self.clock = pygame.time.Clock()
        self.camera = Camera()
        self.mouse = Mouse()
        self.keyboard = Keyboard()
        self.events = []
        self.listeners = []
        self.repeats = []
        self.screen = pygame.display.set_mode((width, height))
        self.noCollisionGroup = 0
        self.playerCollisionGroup = -1
        self.antiAutoCollisionGroup = -2
        self.values = {}
        self.background = (255,255,255)
        print(f"{datetime.now().strftime("%Y-%m-%d %H:%M:%S"):_^100}")
        open(".\\log.txt", "a").write("\n\n\n" + f"{datetime.now().strftime("%Y-%m-%d %H:%M:%S"):_^100}" + "\n")
        #pygame.event.set_grab(True)

    def newobj(self, x, y, hitboxes = [], color = (0,0,0), vel_x=0, vel_y=0, ui=False, visible = True, radius = 10, type = 0, group = 0):
        
        if hitboxes == DF:
            obj = Object(x, y, [], color, vel_x, vel_y, len(self.objects), ui, visible, group = group)
            obj.hitboxes.append(Hitbox(0, 0, radius, x, y, object = obj, type = type))

        else:
            obj = Object(x, y, [hitboxes], color, vel_x, vel_y, len(self.objects), ui, visible, group = group)

        self.objects.append(obj)
        #print(f"Created \033[38;2;220;170;50m{obj}\033[0m")
        data = {
            "x": x,
            "y": y,
            "hitboxes": hitboxes,
            "color": color,
            "vx": vel_x,
            "vy": vel_y,
            "ui": ui,
            "id" : len(self.objects),
            "visible": visible
        }
        #print(f"\033[35m{data}\033[0m")
        #print()

        return obj
    
    def newtxt(self, str = "", x = 10, y = 10, color = (0,0,0), ui=False):
        txt = Text(text=str, x=x, y=y, color=color, id=len(self.texts), ui=ui)
        self.texts.append(txt)
        return txt
    
    def editValue(self, name, value):
        self.values[name] = value

    def quit(self):
        self.run = False
        pygame.quit()

    def random(self):
        return random.random()

    def update(self):
        for obj in self.objects:
            obj.update(self)
        
        mousex, mousey = pygame.mouse.get_pos()

        for event in pygame.event.get():

            if event.type == pygame.MOUSEBUTTONDOWN:
                self.mouse.button = event.button
                self.mouse.down = True
                self.events.append({
                    "type" : "mousedown",
                    "button" : event.button,
                    "x" : mousex,
                    "y" : mousey
                })
                
                for obj in self.objects:
                    if obj.mousepress(self):
                        self.events.append({
                            "type" : "buttondown",
                            "object" : obj,
                            "button" : event.button,
                            "x" : mousex,
                            "y" : mousey,
                            "id" : obj.id,
                            "group": obj.group
                        })

                
            
            if event.type == pygame.MOUSEBUTTONUP:
                self.mouse.down = False
                self.events.append({
                    "type" : "mouseup",
                    "button" : event.button,
                    "x" : mousex,
                    "y" : mousey
                })
                
                for obj in self.objects:

                    if obj.mousepress(self):
                        self.events.append({
                            "type" : "buttonup",
                            "object" : obj,
                            "button" : event.button,
                            "x" : mousex,
                            "y" : mousey,
                            "id" : obj.id,
                            "group": obj.group
                        })

            if event.type == pygame.QUIT:
                self.events.append({
                        "type" : "quit",
                        "time" : time.time()
                    })
                
            if event.type == pygame.KEYDOWN:
                self.events.append({
                    "type" : "keydown",
                    "key" : event.key,
                })
                self.keyboard.update(event.key, True, True)
            
            if event.type == pygame.KEYUP:
                self.events.append({
                    "type" : "keyup",
                    "key" : event.key,
                })
                self.keyboard.update(event.key, False, False)
            
        self.mouse.update(mousex, mousey, mousex - self.mouse.x, mousey - self.mouse.y)

        for i in range(len(self.objects)):
            if (self.objects[i].group != self.noCollisionGroup) and (self.objects[i].group != self.playerCollisionGroup):
                for j in range(i + 1, len(self.objects)):
                    if (self.objects[j].group != self.noCollisionGroup) and (( (self.objects[i].group == self.antiAutoCollisionGroup) and (self.objects[j].group == self.antiAutoCollisionGroup) ) != True):
                        if self.objects[i].collide(self.objects[j]):
                                
                                self.events.append({
                                    "type" : "collision",
                                    "id" : [
                                        i,
                                        j
                                    ],
                                    "group" : [
                                        self.objects[i].group,
                                        self.objects[j].group
                                    ],
                                    "object" : [
                                        self.objects[i],
                                        self.objects[j]
                                    ],
                                    "pos" : [
                                        {"x": self.objects[i].x, "y": self.objects[i].y},
                                        {"x": self.objects[j].x, "y": self.objects[j].y}
                                    ],
                                    "hitbox" : self.objects[i].getcollider(self.objects[j])
                                })

        self.printEventListeners()
        self.checkEventListeners()
        self.checkRepeatingFunctions()

    def printout(self, text, list):
        print(text)
        return list.append(text)

    def printEventListeners(self):

        colored_text = []

        if len(self.events) > 0:
            print(f"New  Frame Starting:")
            print("")

        for event in self.events:
            empty = 0
            print()
            if len(self.events) > 1:
                print(f"\033[38;2;68;168;158m{len(self.events)} Events in list\033[0m")
            else:
                print(f"\033[38;2;68;168;158m{len(self.events)} Event in list\033[0m")
            
            print(f"Event of type: \033[32m{event["type"]:^}\033[0m")
            colored_text.append(f"Event of type: {event["type"]:^}")
            print(f"\033[35m{event}\033[0m")
            colored_text.append(f"{event}")
            functions = []

            for listener in self.listeners:
                colored_text.append(f"{listener}")
                passSpecifiers = True

                for name, val in zip(listener["name"], listener["val"]):
                    if name in event:
                        if event[name] != val:
                            #print(f"Failed Specifiers, Name: {name}, Val: {val}, Part of Name: {event[name]}, Name in Event: {name in event}")
                            #colored_text.append(f"Failed Specifiers, Name: {name}, Val: {val}")
                            passSpecifiers = False
                    else:
                        passSpecifiers = False
                
                if (event["type"] == listener["type"]) & passSpecifiers:
                    empty += 1
                    
                    functions.append(f"{listener["func"].__name__}")

            if empty == 0:
                print(f"\033[31mNo Listeners Found\033[0m")
                colored_text.append(f"No Listeners Found")
            elif empty > 1:
                print(f"\033[34m{empty} Listeners Found:\033[0m")
                colored_text.append(f"{empty} Listeners Found:")
            elif empty == 1:
                print(f"\033[34m{empty} Listener Found:\033[0m")
                colored_text.append(f"{empty} Listener Found:")
            
            for function in functions:
                print(f"{"":4} Function {"":1}\033[38;2;200;200;40m{function}\033[0m")
                colored_text.append(f"{"":4} Function {"":1}{function}")

            print("")

    def checkEventListeners(self):

        counter = 0

        while len(self.events) > 0:
        
            event = self.events[0]

            functions = []
            
            for listener in self.listeners:

                passSpecifiers = True

                for name, val in zip(listener["name"], listener["val"]):
                    if name in event:
                        if event[name] != val:
                            #print(f"Failed Specifiers, Name: {name}, Val: {val}, event[name]: {event[name]}")
                            passSpecifiers = False
                    else:
                        if (event["type"] == listener["type"]):
                            print(f"Name not in event, Name: {name}, Val: {val}")
                        passSpecifiers = False

                    if (event["type"] == listener["type"]) & (passSpecifiers):
                        functions.append(listener)

            for func in functions:
                    
                # Append event to args (so it's passed to the function)
                if func["input"]:
                    args = func["args"] + (event,)
                else:
                    args = func["args"]
                kwargs = func["kwargs"]

                if callable(func["func"]):
                    func["func"](*args, **kwargs)  # Call the function directly
                else:
                    print(f"Function \033[38;2;200;200;40m'{listener["func"]}'\033[0m is not callable.")

            counter +=1
            if len(self.events) > 0:
                self.events.pop(0)

    def addEventListener(self, type, func, name=["type"], val=None, input = True, *args, **kwargs):
        if callable(func):
            for listener in self.listeners:
                if listener["type"] == type:
                    listener["pop"] = False
            if val == None:
                val = [type]
            self.listeners.append({
                "type":str(type),
                "func":func,
                "name":name,
                "val":val,
                "args":args,
                "kwargs":kwargs,
                "pop":True,
                "input":input
            })
            data = {
                "type":str(type),
                "func":func.__name__,
                "name":name,
                "val":val,
                "args":args,
                "kwargs":kwargs,
                "pop":True,
                "input":input
            }
            print(f"Listener \033[38;2;220;170;50m{func}\033[0m")
            print(f"\033[35m{data}\033[0m")
            print()
        else:
            raise ValueError(f"Function '{func}' is not callable.")
    
    def name(self, name):
        pygame.display.set_caption(name)

    def checkRepeatingFunctions(self):
        for repeat in self.repeats:
            func = repeat["func"]
            interval = repeat["int"]
            if self.frame_count % interval == interval - 1:
                func()

    def addRepeatingFunction(self, func, interval):
        if callable(func):

            self.repeats.append({
                "func": func,
                "int": interval
            })
            data = {
                "func": func,
                "int": interval
            }
            print(f"Repeating Function \033[38;2;220;170;50m{func}\033[0m")
            print(f"\033[35m{data}\033[0m")
            print()
        else:
            raise ValueError(f"Function '{func}' is not callable.")
    
    def name(self, name):
        pygame.display.set_caption(name)


    def render(self):

        if self.run != True:
            return

        self.screen.fill(self.background)
        
        # Update frame count
        self.frame_count += 1
        
        # Update object positions and check collisions
        self.objects = sorted(self.objects, key=lambda object: object.z_layer)
        for obj in self.objects:
            obj.render(self)
        
        for txt in self.texts:
            txt.render(self)

        pygame.display.flip()
        
        # Frame rate control
        self.clock.tick(self.FPS)