perbaiki script berikut ini, saya ingin saat script di jalankan langsung saja game di mulai dengan file mid yang di tentukan dengan waktu tunggu 3 detik, hilangkan main menu dan sebagai nya yang tidak diperlukan, buatkan dengan lengkap dan tepat : 
import pygame
import cv2
import numpy as np
import random
import moderngl
import mido
import subprocess
from os import getcwd, listdir
from os.path import join, isfile
from io import BytesIO
from enum import Enum
from math import sin, pi
from pygame import Color
from array import array
from random import randint
from zipfile import ZipFile
from time import time as get_current_time
from json import load, dump, loads
from platform import system as get_os
from typing import Union, Optional, Any
from time import time as get_current_time
from sys import platform
from sys import setrecursionlimit
setrecursionlimit(10000)  # increase if more notes

pygame.init()


class Config:
    # constants
    rSCREEN_WIDTH = pygame.display.Info().current_w if pygame.display.Info().current_w else 1920
    rSCREEN_HEIGHT = pygame.display.Info().current_h if pygame.display.Info().current_h else 1080
    # SCREEN_WIDTH = pygame.display.Info().current_w if pygame.display.Info().current_w else 1920
    # SCREEN_HEIGHT = pygame.display.Info().current_h if pygame.display.Info().current_h else 1080
    CAMERA_SPEED = 500
    SQUARE_SIZE = 50
    PARTICLE_SPEED = 10

    # colors
    #
    # each color theme requires a hallway color, a background color, and at least one square color
    # optionally, the color theme provides an hp_bar_border color (default 10, 9, 8),
    # an hp_bar_background color (default 34, 51, 59), and a list
    # of hp_bar_fill colors (default (156, 198, 155), (189, 228, 168), (215, 242, 186))
    #
    color_themes = {
        "rainbow": {
            "hallway": pygame.Color((214, 209, 205)),
            "background": pygame.Color((60, 63, 65)),
            "square": [
                pygame.Color(0, 0, 0)
            ]
        }
    }

    # intended configurable settings
    SCREEN_WIDTH = pygame.display.Info().current_w if pygame.display.Info().current_w else 1920
    SCREEN_HEIGHT = pygame.display.Info().current_h if pygame.display.Info().current_h else 1080
    theme: Optional[str] = "dark"
    seed: Optional[int] = None
    camera_mode: Optional[int] = 2
    start_playing_delay = 3000
    max_notes: Optional[int] = None
    bounce_min_spacing: Optional[float] = 30
    square_speed: Optional[int] = 600
    volume: Optional[int] = 70
    music_offset: Optional[int] = 0
    direction_change_chance: Optional[int] = 30
    hp_drain_rate = 10
    theatre_mode = True
    particle_trail = True
    shader_file_name = "none.glsl"
    do_color_bounce_pegs = False
    do_particles_on_bounce = True

    # settings that are not configurable (yet)
    backtrack_chance: Optional[float] = 0.02
    backtrack_amount: Optional[int] = 40
    rainbow_speed: Optional[int] = 30
    square_swipe_anim_speed: Optional[int] = 4
    particle_amount = 10
    language = "english"

    # other random stuff
    current_song = None
    ctx: moderngl.Context = None
    glsl_program: moderngl.Program = None
    render_object: moderngl.VertexArray = None
    screen: pygame.Surface = None
    dt = 0.01

    # ascii shader
    ascii_tex: moderngl.Texture = None

    # keys to save and load
    save_attrs = ["theme", "seed", "camera_mode", "start_playing_delay", "max_notes", "bounce_min_spacing",
                  "square_speed", "volume", "music_offset", "direction_change_chance", "hp_drain_rate", "theatre_mode",
                  "particle_trail", "shader_file_name", "do_color_bounce_pegs", 
                  "do_particles_on_bounce", "language",
                  "SCREEN_WIDTH", "SCREEN_HEIGHT"]

    # glow effect, for dark_modern only for now
    square_glow = True
    square_glow_duration = 0.8
    glow_intensity = 15  # 1-40
    square_min_glow = 7
    border_color = pygame.Color(255, 255, 255)
    glow_color = pygame.Color(255, 255, 255)


def get_colors():
    return Config.color_themes.get(Config.theme, "dark")


def save_to_file(dat: Optional[dict[str, Any]] = None):
    if dat is None:
        dat = {k: getattr(Config, k) for k in Config.save_attrs}
    with open("./assets/settings.json", "w") as f:
        dump(dat, f, indent=4)


def load_from_file():
    try:
        if isfile("./assets/settings.json"):
            with open("./assets/settings.json", "r") as f:
                data = load(f)
                for setting in data:
                    setattr(Config, setting, data[setting])
        else:
            with open("./assets/settings.json", "w") as f:
                f.write('{}')
    except Exception as e:
        print(f"Error: {e}")


if __name__ == "config":
    load_from_file()



class InvalidSongError(Exception):
    """The song is invalid somehow"""
    pass


class UserCancelsLoadingError(Exception):
    """User cancels the loading screen"""
    pass


class MapLoadingFailureError(Exception):
    """The map fails to load (recurs function fails)"""
    pass



# vsync framerate if on windows, else 60
if platform == "win32":
    try:
        # noinspection PyPackageRequirements
        import win32api
        FRAMERATE = win32api.EnumDisplaySettings(win32api.EnumDisplayDevices().DeviceName, -1).DisplayFrequency
    except ModuleNotFoundError:
        print("The win32api python module is not found! Install it with \"pip install pywin32\"")
        FRAMERATE = 60
elif "linux" in platform:
    # noinspection PyPackageRequirements
    from Xlib import display
    # noinspection PyPackageRequirements
    from Xlib.ext import randr
    d = display.Display()
    default_screen = d.get_default_screen()
    info = d.screen(default_screen)

    resources = randr.get_screen_resources(info.root)
    active_modes = set()
    for crtc in resources.crtcs:
        crtc_info = randr.get_crtc_info(info.root, crtc, resources.config_timestamp)
        if crtc_info.mode:
            active_modes.add(crtc_info.mode)

    for mode in resources.modes:
        if mode.id in active_modes:
            FRAMERATE = round(mode.dot_clock / (mode.h_total * mode.v_total))
            break
else:
    FRAMERATE = 60


# camera follow modes
class CameraFollow(Enum):
    Center = 0  # center the square
    Lazy = 1  # lazy camera, used by Crazy Nutter 101
    Smoothed = 2  # smoothed camera, interpolates camera a little bit between current and previous every frame
    Predictive = 3  # smoothed camera, but you can see where the square will bounce better


def read_osu_file(filedata: bytes):
    filedata = filedata.decode("utf-8")
    filelines = filedata.splitlines(False)
    started_counting = False
    timestamps = []
    for line in filelines:
        if not started_counting:
            if "HitObjects" in line:
                started_counting = True
            continue
        if len(line) < 3:
            continue
        args = line.split(",")
        if (int(args[3]) & 3) == 0:
            continue
        timestamps.append(int(args[2])/1000)
    return timestamps


def surf_to_texture(in_surface: pygame.Surface) -> moderngl.Texture:
    tex = Config.ctx.texture(in_surface.get_size(), 4)
    tex.filter = (moderngl.NEAREST, moderngl.NEAREST)
    tex.swizzle = 'BGRA'
    tex.write(in_surface.get_view('1'))
    return tex


def update_screen(screen: pygame.Surface, glsl_program: moderngl.Program, render_object: moderngl.VertexArray):
    frame_tex = surf_to_texture(screen)
    frame_tex.use(0)
    glsl_program['tex'] = 0
    if "ascii.glsl" in Config.shader_file_name:
        if Config.ascii_tex is None:
            Config.ascii_tex = surf_to_texture(pygame.image.load('./assets/shaders/ascii.png').convert_alpha())
            Config.ascii_tex.use(1)
        try:
            glsl_program['asciipng'] = 1
        except KeyError:
            pass
    render_object.render(mode=moderngl.TRIANGLE_STRIP)

    pygame.display.flip()

    frame_tex.release()


def open_file(filename):
    if platform == "win32":
        from os import startfile
        startfile(filename)
    else:
        opener = "open" if platform == "darwin" else "xdg-open"
        subprocess.call([opener, filename])


# noinspection PyUnresolvedReferences
def read_midi_file(file):
    midi_file = mido.MidiFile(file=file)

    notes = []
    current_time = 0

    for msg in midi_file:
        if msg.type == 'note_on' and msg.velocity != 0:
            timestamp = current_time + msg.time
            notes.append(round(timestamp*1000)/1000)
        current_time += msg.time
    return notes


# remove values that are too close to each other
def remove_too_close_values(lst: list[float], threshold=30) -> list[float]:
    """Assumes the list is sorted"""
    new = []
    before = None
    for _ in lst:
        if before is None:
            before = _
            new.append(_)
            continue
        if before+threshold/1000 > _:
            continue
        before = _
        new.append(_)
    return new


def fix_overlap(rects: list[pygame.Rect], callback=None):
    if callback is None:
        callback = lambda _: None
    xvs = set()
    yvs = set()
    for rect in rects:
        xvs.add(rect.right)
        xvs.add(rect.left)
        yvs.add(rect.top)
        yvs.add(rect.bottom)
    xvs = sorted(list(xvs))
    yvs = sorted(list(yvs))

    outputs = []
    for xv1 in range(len(xvs)-1):
        xv2 = xv1+1
        for yv1 in range(len(yvs)-1):
            yv2 = yv1+1
            r = pygame.Rect(xvs[xv1], yvs[yv1], xvs[xv2]-xvs[xv1], yvs[yv2]-yvs[yv1])
            if r.collidelist(rects)+1:
                outputs.append(r)

        if callback(f"Checking minirectangles ({int(100*xv1*len(yvs)/(len(xvs)*len(yvs)))}% done)"):
            raise UserCancelsLoadingError()

    callback("Merging adjacent minirectangles")

    for ai in range(len(outputs)-1):
        a = outputs[ai+1]
        b = outputs[ai]
        if a.width == 0 or a.height == 0 or b.width == 0 or b.height == 0:
            continue
        if not (a.right == b.left or a.left == b.right or a.top == b.bottom or b.bottom == a.top):
            continue
        if a.x == b.x and a.width == b.width:
            a.y = min(a.y, b.y)
            a.height = a.height+b.height
            b.height = 0
            continue

    outputs = [out for out in outputs if out.width > 0 and out.height > 0]

    outputs.sort(key=lambda _: _.y*100000+_.x)

    for ai in range(len(outputs)-1):
        a = outputs[ai+1]
        b = outputs[ai]
        if a.width == 0 or a.height == 0 or b.width == 0 or b.height == 0:
            continue
        if not (a.right == b.left or a.left == b.right or a.top == b.bottom or b.bottom == a.top):
            continue
        if a.y == b.y and a.height == b.height:
            a.x = min(a.x, b.x)
            a.width = a.width+b.width
            b.width = 0
            continue

    callback("Finished loading")

    return [out for out in outputs if out.width > 0 and out.height > 0]

_channels = []
_sound_registry: dict[str, pygame.mixer.Sound] = {}


def play_sound(snd_name: str, vol: float = 0.5):
    if len(_channels) == 0:
        pygame.mixer.set_num_channels(40)
        for _ in range(1, 20):
            _channels.append(pygame.mixer.Channel(_))

    if snd_name not in _sound_registry:
        _sound_registry[snd_name] = pygame.mixer.Sound(join("assets", snd_name))

    chan = pygame.mixer.find_channel(False)
    if not chan:
        return False
    if chan.get_busy():
        return False
    chan.set_volume(vol)
    chan.play(_sound_registry[snd_name])
    return True


def interpolate_fn(n):
    """Interpolate sigmoidally from 0-1"""
    n = min(max(n, 0), 1)

    return sin(pi * (n - 0.5)) / 2 + 0.5


class Particle:
    SPEED_VARIATION = 4
    SIZE_MIN = 7
    SIZE_MAX = 14
    AGE_RATE = 20
    SLOW_DOWN_RATE = 1.2

    def __init__(self, pos: list[float], delta: list[float], invert_color: bool = False):
        self.pos = pos.copy()
        self.size = random.randint(Particle.SIZE_MIN, Particle.SIZE_MAX)
        self.delta = delta.copy()
        self.delta[0] += random.randint(-Particle.SPEED_VARIATION, Particle.SPEED_VARIATION)/8
        self.delta[1] += random.randint(-Particle.SPEED_VARIATION, Particle.SPEED_VARIATION)/8
        # color is hallway color if invert_color is false, else it's background color
        self.color = get_colors()["hallway"] if not invert_color else get_colors()["background"]

    def age(self):
        self.size -= Particle.AGE_RATE*Config.dt
        self.x += self.delta[0] * Config.PARTICLE_SPEED
        self.y += self.delta[1] * Config.PARTICLE_SPEED
        self.delta[0] /= (Particle.SLOW_DOWN_RATE+FRAMERATE) * Config.dt
        self.delta[1] /= (Particle.SLOW_DOWN_RATE+FRAMERATE) * Config.dt
        return self.size <= 0

    @property
    def x(self):
        return self.pos[0]

    @x.setter
    def x(self, val: float):
        self.pos[0] = val

    @property
    def y(self):
        return self.pos[1]

    @y.setter
    def y(self, val: float):
        self.pos[1] = val

    @property
    def rect(self):
        return pygame.Rect(self.x-self.size/2, self.y-self.size/2, *(2*[self.size]))



"""
https://stackoverflow.com/questions/67561142/bloom-effect-in-pygame-so-that-text-glows
"""


class Colors:
    WHITE_ISH = (246, 246, 246)
    YELLOW_ISH = (214, 198, 136)
    RED_ISH = (156, 60, 60)


def create_border(image: np.ndarray, margin: int, thickness: int, color: Colors) -> np.ndarray:
    height, width = image.shape[:2]
    cv2.rectangle(image, (margin, margin), (width - margin, height - margin), color, thickness=thickness)
    return image


def apply_blooming(image: np.ndarray) -> np.ndarray:
    # Provide some blurring to image, to create some bloom.
    cv2.GaussianBlur(image, ksize=(15, 15), sigmaX=20, sigmaY=20, dst=image)
    cv2.blur(image, ksize=(12, 12), dst=image)
    return image


def glowing_border(image: np.ndarray, margin=20, thickness=20, color: Colors = Colors.WHITE_ISH):
    """

    Create a glowing border around an image.

    Args:
        image: The image, that requires a border.
        margin: The border distance from the sides of the image.
        thickness: The thickness of the border.
        color: The border color, by default a slightly yellow color.

    Modifies:
        The input image, will be modified with a blooming border.

    Returns:
        The same image, with a blooming border inserted.
    """

    # Generate yellowish colored box
    image = create_border(image, margin, thickness, color)

    # Apply the blooming.
    image = apply_blooming(image)

    # Reassert the original border, to get a clear outline.
    # Similar to the Watson-Scott test, two borders were added here.
    image = create_border(image, margin - 1, 1, color)
    image = create_border(image, margin + 1, 1, color)
    return image


def make_glowy2(size, color, intensity=0) -> pygame.Surface:
    image = np.zeros((*size[::-1], 3), dtype=np.uint8)
    border = glowing_border(image.copy(), color=color, thickness=intensity)
    return pygame.surfarray.make_surface(np.fliplr(np.rot90(border, k=-1)))

class Bounce:
    def __init__(self, sq_pos: list[float], sq_dir: list[int], time: float, bounce_dir: int):
        self.square_pos = sq_pos  # new square position
        self.square_dir = sq_dir  # new square direction
        self.bounce_dir = bounce_dir  # bounce direction for squish effect; i can't remember if 0 or 1 is horizontal
        self.time = time  # time during bounce

    def get_collision_rect(self):
        sx, sy = self.square_pos
        if self.bounce_dir == 0:
            # bounce left or right wall
            if self.square_dir[0] == -1:
                # right wall
                return pygame.Rect(
                    sx+Config.SQUARE_SIZE/2+1,
                    sy-10,
                    10,
                    20
                )
            elif self.square_dir[0] == 1:
                # left wall
                return pygame.Rect(
                    sx-10-Config.SQUARE_SIZE/2-1,
                    sy-10,
                    10,
                    20
                )
        elif self.bounce_dir == 1:
            # bounce top or bottom wall
            if self.square_dir[1] == -1:
                # bottom wall
                return pygame.Rect(
                    sx-10,
                    sy+Config.SQUARE_SIZE/2+1,
                    20,
                    10
                )
            elif self.square_dir[1] == 1:
                # top wall
                return pygame.Rect(
                    sx-10,
                    sy-10-Config.SQUARE_SIZE/2-1,
                    20,
                    10
                )

    def copy(self) -> "Bounce":
        return Bounce(self.square_pos, self.square_dir, self.time, self.bounce_dir)

    def __repr__(self):
        return f"<Bounce(sq_pos={self.square_pos}, sq_dir={self.square_dir}, time={self.time}, dir={self.bounce_dir})"



class Square:
    def __init__(self, x: float = 0, y: float = 0, dx: int = 1, dy: int = 1):
        self.pos: list[float] = [x, y]
        self.dir: list[int] = [dx, dy]
        self.last_bounce_time = -100
        self.latest_bounce_direction = 0  # 0 = horiz, 1 = vert
        self.past_colors = []
        self.died = False

        self.time_since_glow_start = 0
        self.glowy_surfaces = {}

    def register_past_color(self, col: tuple[int, int, int]):
        for _ in range(max(Config.square_swipe_anim_speed, 1)):
            self.past_colors.insert(0, col)
        while len(self.past_colors) > Config.SQUARE_SIZE * 4 / 5:
            self.past_colors.pop()

    def get_surface(self, size: tuple[int, int]):
        ss = int(Config.SQUARE_SIZE * 4 / 5)
        surf = pygame.Surface((ss, ss))
        for index, col in enumerate(self.past_colors):
            y = index if self.dir_y != 1 else ss - 1 - index
            pygame.draw.line(surf, col, (0, y), (ss, y))
        return pygame.transform.scale(surf, size)

    def copy(self) -> "Square":
        new = Square(*self.pos, *self.dir)
        new.last_bounce_time = self.last_bounce_time
        new.latest_bounce_direction = self.latest_bounce_direction
        return new

    @property
    def x(self):
        return self.pos[0]

    @property
    def y(self):
        return self.pos[1]

    def title_screen_physics(self, bounding: pygame.Rect):
        self.reg_move()
        r = self.rect
        if r.right > bounding.right:
            self.dir[0] = -1
            self.latest_bounce_direction = 0
        elif r.left < bounding.left:
            self.dir[0] = 1
            self.latest_bounce_direction = 0
        elif r.bottom > bounding.bottom:
            self.dir[1] = -1
            self.latest_bounce_direction = 1
        elif r.top < bounding.top:
            self.dir[1] = 1
            self.latest_bounce_direction = 1
        else:
            return False
        self.start_bounce()
        self.last_bounce_time = get_current_time()
        return True

    def compute_glowy_surface(self, rect, val):
        glowy_borders = make_glowy2((rect.size[0] + 40, rect.size[1] + 40), Color(Config.glow_color), val)
        surface = pygame.Surface(rect.inflate(100, 100).size, pygame.SRCALPHA)
        surface.blit(glowy_borders, (20, 20), special_flags=pygame.BLEND_RGBA_ADD)
        return surface

    def draw_glowing3(self, win, rect):
        if self.died:
            return

        if Config.square_glow:
            if pygame.time.get_ticks() - self.time_since_glow_start < Config.square_glow_duration * 1000:
                progress = 1 - (pygame.time.get_ticks() - self.time_since_glow_start) / (
                        Config.square_glow_duration * 1000)
                val = int(progress * Config.glow_intensity)
            else:
                val = 1
            val = max(val, Config.square_min_glow)
            surf = self.compute_glowy_surface(rect, val)

            win.blit(surf, rect.move(-40, -40).topleft, special_flags=pygame.BLEND_RGBA_ADD)

    def draw(self, screen: pygame.Surface, sqrect: pygame.Rect):
        if self.died:
            return
        square_color_index = round((self.dir_x + 1) / 2 + self.dir_y + 1)
        self.register_past_color(get_colors()["square"][square_color_index % len(get_colors()["square"])])

        if Config.theme == "dark_modern" and make_glowy2 is not None:
            self.draw_glowing3(screen, sqrect)
        else:
            pygame.draw.rect(screen, (0, 0, 0), sqrect)
            sq_surf = self.get_surface(
                tuple(sqrect.inflate(-int(Config.SQUARE_SIZE / 5), -int(Config.SQUARE_SIZE / 5))[2:]))
            screen.blit(sq_surf, sq_surf.get_rect(center=sqrect.center))

    @x.setter
    def x(self, val: int):
        self.pos[0] = val

    @y.setter
    def y(self, val: int):
        self.pos[1] = val

    @property
    def dir_x(self):
        return self.dir[0]

    @property
    def dir_y(self):
        return self.dir[1]

    @property
    def rect(self):
        return pygame.Rect(self.x - Config.SQUARE_SIZE / 2, self.y - Config.SQUARE_SIZE / 2,
                           *([Config.SQUARE_SIZE] * 2))

    def start_bounce(self):
        self.time_since_glow_start = pygame.time.get_ticks()

    def obey_bounce(self, bounce: Bounce):
        # planned bounces
        self.start_bounce()
        self.pos = bounce.square_pos
        self.dir = bounce.square_dir
        self.latest_bounce_direction = bounce.bounce_dir
        self.last_bounce_time = bounce.time
        return

    def reg_move(self, use_dt: bool = True):
        self.x += self.dir_x * Config.square_speed * (Config.dt if use_dt else 1 / FRAMERATE)
        self.y += self.dir_y * Config.square_speed * (Config.dt if use_dt else 1 / FRAMERATE)

class Camera:
    def __init__(self, x: int = 0, y: int = 0):
        self.x = x
        self.y = y
        # ?x, ?y are variables for misc things
        self.ax = 0
        self.ay = 0
        self.bx = 0
        self.by = 0
        self.locked_on_square = True
        self.lock_type: CameraFollow = CameraFollow(2)

    def attempt_movement(self):
        if not self.locked_on_square:
            keys = pygame.key.get_pressed()
            shift_modifier = (keys[pygame.K_LSHIFT] | keys[pygame.K_RSHIFT]) + 1
            self.x += (keys[pygame.K_d] - keys[pygame.K_a]) * Config.CAMERA_SPEED * shift_modifier / FRAMERATE
            self.y += (keys[pygame.K_s] - keys[pygame.K_w]) * Config.CAMERA_SPEED * shift_modifier / FRAMERATE

    @property
    def pos(self):
        return self.x, self.y

    @pos.setter
    def pos(self, val: Union[tuple[int, int], list[int]]):
        self.x, self.y = val

    def offset(self, pos_or_rect: Union[pygame.Rect, tuple[int, int]]) -> Union[pygame.Rect, list[int]]:
        if isinstance(pos_or_rect, pygame.Rect):
            return pos_or_rect.move(-self.x, -self.y)
        else:
            return [pos_or_rect[0]-self.x, pos_or_rect[1]-self.y]

    def follow(self, square: Square):

        # square in center
        if self.lock_type == CameraFollow.Center:
            self.pos = [square.x - Config.SCREEN_WIDTH / 2, square.y - Config.SCREEN_HEIGHT / 2]

        # camera only follows if necessary
        if self.lock_type == CameraFollow.Lazy:
            lazy_follow_distance = 250
            while square.x - Config.SCREEN_WIDTH + lazy_follow_distance > self.x:
                self.x += 1
            while square.y - Config.SCREEN_HEIGHT + lazy_follow_distance > self.y:
                self.y += 1
            while square.x - lazy_follow_distance < self.x:
                self.x -= 1
            while square.y - lazy_follow_distance < self.y:
                self.y -= 1

        # smooth camera
        if self.lock_type == CameraFollow.Smoothed:
            easing_rate = 3
            self.x = (square.x - Config.SCREEN_WIDTH / 2) * easing_rate * Config.dt + self.x - easing_rate * self.x * Config.dt
            self.y = (square.y - Config.SCREEN_HEIGHT / 2) * easing_rate * Config.dt + self.y - easing_rate * self.y * Config.dt

        # camera in front of square
        if self.lock_type == CameraFollow.Predictive:
            self.ax = (square.x - Config.SCREEN_WIDTH / 2) * 3 * Config.dt + self.ax - 3 * self.ax * Config.dt
            self.ay = (square.y - Config.SCREEN_HEIGHT / 2) * 3 * Config.dt + self.ay - 3 * self.ay * Config.dt
            damping = 1
            self.bx = square.x - damping * (self.ax - square.x) - Config.SCREEN_WIDTH / 2 - Config.SCREEN_WIDTH / 2 * damping
            self.by = square.y - damping * (self.ay - square.y) - Config.SCREEN_HEIGHT / 2 - Config.SCREEN_HEIGHT / 2 * damping
            self.x = self.x*(1-3*Config.dt)+self.bx*3*Config.dt
            self.y = self.y*(1-3*Config.dt)+self.by*3*Config.dt



class World:
    """it's a cruel world out there"""

    def __init__(self):
        self.future_bounces: list[Bounce] = []
        self.past_bounces: list[Bounce] = []
        self.start_time = 0
        self.time = 0
        self.rectangles: list[pygame.Rect] = []
        self.collision_times: list[float] = []
        self.particles: list[Particle] = []
        self.timestamps = []
        self.square = Square()
        self.colors = []           

    def update_time(self) -> None:
        self.time = get_current_time() - self.start_time

    def get_next_bounce(self) -> Bounce:
        """Also pops the bounce from the future_bounces list"""
        self.past_bounces.append(self.future_bounces.pop(0))
        return self.past_bounces[-1]

    def add_bounce_particles(self, sp: list[float], sd: list[float]):
        for _ in range(Config.particle_amount):
            new = Particle([sp[0]+random.randint(-10, 10), sp[1]+random.randint(-10, 10)], sd)
            self.particles.append(new)

    def handle_bouncing(self, square: Square):
        if len(self.future_bounces):
            if (self.time * 1000 + Config.music_offset)/1000 > self.future_bounces[0].time:
                current_bounce = self.get_next_bounce()
                before = square.dir.copy()
                square.obey_bounce(current_bounce)
                changed = square.dir.copy()
                for _ in range(2):
                    if before[_] == changed[_]:
                        changed[_] = 0
                    else:
                        changed[_] = -changed[_]
                if Config.do_particles_on_bounce:
                    self.add_bounce_particles(square.pos, changed)

                # stop square at end
                if len(self.future_bounces) == 0:
                    square.dir = [0, 0]
                    square.pos = current_bounce.square_pos

    def handle_keypress(self, time_from_start, misses):
        return self.scorekeeper.do_keypress(time_from_start, misses)

    def gen_future_bounces(self, _start_notes: list[tuple[int, int, int]], percent_update_callback):
        """Recursive solution may be necessary"""
        total_notes = len(_start_notes)
        max_percent = 0
        path = []
        safe_areas = []
        force_return = 0

        def recurs(
                square: Square,
                notes: list[float],
                bounces_so_far: list[Bounce] = None,
                prev_index_priority=None,
                t: float = 0,
                depth: int = 0
        ) -> Union[list[Bounce], bool]:
            nonlocal force_return, max_percent
            if prev_index_priority is None:
                prev_index_priority = [0, 1]
            if bounces_so_far is None:
                bounces_so_far = []
            gone_through_percent = (total_notes-len(notes)) * 100 // total_notes
            while gone_through_percent > max_percent:
                max_percent += 1
                if percent_update_callback(f"{max_percent}% done generating map"):
                    raise UserCancelsLoadingError()

            all_bounce_rects = [_bounc.get_collision_rect() for _bounc in bounces_so_far]
            if len(notes) == 0:
                return bounces_so_far
            # print(depth * 100 // total_notes)
            path_segment_start = len(path)
            start_rect = square.rect.copy()
            while True:
                t += 1/FRAMERATE
                square.reg_move(False)
                path.append(square.rect)
                if t > notes[0]:
                    # no collision (we good)
                    bounce_indexes = prev_index_priority

                    # randomly change direction every X% of the time
                    if random.random() * 100 < Config.direction_change_chance:
                        bounce_indexes = list(bounce_indexes.__reversed__())

                    # add safe area
                    safe_areas.append(start_rect.union(square.rect))

                    for direction_to_bounce in bounce_indexes:
                        square.dir[direction_to_bounce] *= -1
                        bounces_so_far.append(Bounce(square.pos, square.dir, t, direction_to_bounce))

                        toextend = recurs(
                            square=square.copy(),
                            notes=notes[1:],
                            bounces_so_far=[_b.copy() for _b in bounces_so_far],
                            t=t,
                            prev_index_priority=bounce_indexes.copy(),
                            depth=depth+1
                        )

                        if toextend:
                            return toextend
                        else:
                            bounces_so_far = bounces_so_far[:-1]
                            square.dir[direction_to_bounce] *= -1

                            # instead of trying other path from here, just exit a bit back to try another from previous
                            if force_return:
                                force_return -= 1
                                while len(path) != path_segment_start:
                                    path.pop()
                                return False

                            continue
                    while len(path) != path_segment_start:
                        path.pop()
                    return False

                othercheck = False
                if len(bounces_so_far):
                    othercheck = bounces_so_far[-1].get_collision_rect().collidelist(path[:-10])+1

                if square.rect.collidelist(all_bounce_rects) != -1 or othercheck:
                    if depth > 200:
                        if random.random() < Config.backtrack_chance:
                            max_percent -= (Config.backtrack_amount * 100 // total_notes) + 1
                            force_return = Config.backtrack_amount

                    while len(path) != path_segment_start:
                        path.pop()
                    return False

        _start_notes = _start_notes[:Config.max_notes] if Config.max_notes is not None else _start_notes

        self.scorekeeper.unhit_notes = remove_too_close_values([_sn for _sn in _start_notes], Config.bounce_min_spacing)

        self.future_bounces = recurs(
            square=self.square.copy(),
            notes=remove_too_close_values(
                [_sn for _sn in _start_notes],
                threshold=Config.bounce_min_spacing
            )
        )

        if self.future_bounces is False:
            raise MapLoadingFailureError("The map failed to generate because of the recursion function. " +
                                         "If the midi has too many notes too close, it may not generate. " +
                                         "Maybe try changing the \"bounce min spacing\", \"square speed\" or \"change dir chance\" in the config")

        if len(self.future_bounces) == 0:
            raise MapLoadingFailureError("Map safearea list empty. Please report to the github under the issues tab")

        percent_update_callback("Removing overlapping safe areas")

        # eliminate fully overlapping safe areas
        safe_areas: list[pygame.Rect]
        while True:
            new = []
            before_safe_count = len(safe_areas)
            for safe1 in safe_areas:
                for safe2 in safe_areas:
                    if safe2 == safe1:
                        continue
                    if safe2.contains(safe1):
                        break
                else:
                    new.append(safe1)
            safe_areas = new.copy()
            after_safe_count = len(safe_areas)
            if after_safe_count == before_safe_count:
                break
        safe_areas = safe_areas

        self.rectangles = [_fb.get_collision_rect() for _fb in self.future_bounces]
        self.collision_times = [_fb.time for _fb in self.future_bounces]
        
        # Setting random colors for the generated pegs
        self.colors = [random.choice([(224, 50, 50), (80, 210, 100), (230, 220, 50), (174, 170, 210), (245, 77, 247), (255, 153, 0)]) for _ in self.future_bounces]
        return safe_areas

class Game:
    def __init__(self):
        self.active = False
        self.notes = []
        self.camera = Camera()
        self.world = World()
        self.safe_areas: list[pygame.Rect] = []
        self.camera_ctrl_text = pygame.font.Font(None, 24).render("Manual Camera Control Activated", True, (0, 255, 0))
        self.music_has_played = False
        self.offset_happened = False
        self.loading_text = pygame.font.Font(None, 24).render("Loading...", True, (255, 255, 255))
        self.misses = 0
        self.mouse_down = False
        self.hitted_rectangles = []                           

    def start_song(self, screen: pygame.Surface):
        random.seed(Config.seed)

        # load song and notes
        with ZipFile(Config.current_song.fp) as zf:
            if Config.current_song.fp.lower().endswith(".osz"):
                # Config.music_offset = 0
                notes = read_osu_file(filedata=zf.read(Config.current_song.song_file_name))
            else:
                with zf.open(Config.current_song.song_file_name) as f:
                    notes = read_midi_file(file=f)
        notes = [note for note in notes]
        self.notes = notes

        # other settings

        self.world = World()
        self.music_has_played = False
        self.offset_happened = False
        self.camera.lock_type = CameraFollow(Config.camera_mode)
        screen.fill(get_colors()["background"])
        information_texts = [
            "Map loading stuck at some percentage? Try changing the \"Square speed\" in the config",
            "Also try changing the \"Change dir chance\" and the \"Bounce min spacing\"",
            "",
            "Too large maps will crash the program, so try with some smaller ones first",
            "",
            "Don't want to play the game? Turn on \"Theatre mode\" in the config",
            "",
            "Music off-sync? Change the \"Music offset\" in the settings"
        ]
        for index, info_text in enumerate(information_texts):
            rendered_text = (24).render(info_text, True, get_colors()["hallway"])
            screen.blit(rendered_text, (50, 200 + 30 * index))
        pygame.display.flip()

        def update_loading_screen(message: str):
            screen.fill(get_colors()["background"], pygame.Rect(0, 0, Config.SCREEN_WIDTH, 100))
            screen.blit((60).render(message, True, get_colors()["hallway"]), (10, 10))
            for event in pygame.event.get():
                if event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_ESCAPE:
                        return 1
            update_screen(Config.screen, Config.glsl_program, Config.render_object)

        try:
            self.safe_areas = self.world.gen_future_bounces(self.notes, update_loading_screen)
            self.safe_areas = fix_overlap(self.safe_areas, update_loading_screen)
        except UserCancelsLoadingError:
            return True
        except MapLoadingFailureError as e:
            return e.args[0] if len(e.args) else "Big error... can't load map :("
        self.world.start_time = get_current_time()
        self.world.square.dir = [0, 0]
        self.world.square.pos = self.world.future_bounces[0].square_pos

    def draw(self, screen: pygame.Surface, n_frames: int):

        if not self.active:
            return

        if not self.music_has_played:
            if not self.offset_happened:
                for bnc_change in self.world.future_bounces:
                    bnc_change.time += Config.start_playing_delay / 1000
            self.offset_happened = True
            if self.world.time-Config.current_song.music_offset/1000 > Config.start_playing_delay/1000:
                self.music_has_played = True
                song_load_before = get_current_time()
                pygame.mixer.music.play()
                for bnc_change in self.world.future_bounces:
                    bnc_change.time += (get_current_time()-song_load_before)/1000

        screen_rect = screen.get_rect()

        # set world time
        self.world.update_time()

        # move camera (only works if not locked on square)
        self.camera.attempt_movement()

        # handle square bounces
        self.world.handle_bouncing(self.world.square)

        # move square
        self.world.square.reg_move()

        # square in center of camera if locked
        if self.camera.locked_on_square:
            self.camera.follow(self.world.square)

        # bounce anim
        sqrect = self.camera.offset(self.world.square.rect)
        if (self.world.time - 0.25) + Config.music_offset / 1000 < self.world.square.last_bounce_time:
            lerp = abs((self.world.time - 0.25 + Config.music_offset / 1000) - self.world.square.last_bounce_time) * 5
            lerp = lerp ** 2  # square it for better-looking interpolation
            if self.world.square.latest_bounce_direction:
                sqrect.inflate_ip((lerp * 5, -10 * lerp))
            else:
                sqrect.inflate_ip((-10 * lerp, lerp * 5))

        # safe areas
        total_rects = 0
        for safe_area in self.safe_areas:
            offsetted = self.camera.offset(safe_area)
            if screen_rect.colliderect(offsetted):
                total_rects += 1
                pygame.draw.rect(screen, get_colors()["hallway"], offsetted)

        # draw pegs
        for i, bounce_rect in enumerate(self.world.rectangles):
            offsetted = self.camera.offset(bounce_rect)

            if offsetted.colliderect(screen_rect):
                total_rects += 1
                if Config.do_color_bounce_pegs and self.world.collision_times[i] < (self.world.time * 1000 + Config.music_offset - Config.start_playing_delay)/1000:
                    pygame.draw.rect(screen, self.world.colors[i], offsetted)
                else:
                    pygame.draw.rect(screen, get_colors()["background"], offsetted)

        # particles
        for particle in self.world.particles:
            pygame.draw.rect(screen, particle.color, self.camera.offset(particle.rect))
        for remove_particle in [particle for particle in self.world.particles if particle.age()]:
            self.world.particles.remove(remove_particle)

        # particle trail in game
        if Config.particle_trail:
            # every 2 frames add a particle
            if not self.world.square.died:
                if n_frames % 2 == 0:
                    new = Particle(self.world.square.pos, [0, 0], True)
                    new.delta = [random.randint(-10, 10)/20, random.randint(-10, 10)/20]
                    self.world.particles.append(new)
                
        # scorekeeper drawing
        time_from_start = self.world.time-Config.start_playing_delay/1000+Config.music_offset/1000
        if not Config.theatre_mode:
            self.misses = self.world.scorekeeper.draw(screen, time_from_start if len(self.world.future_bounces) else -1, self.misses)

            # hit icons
            to_remove = []
            for hiticon in self.world.scorekeeper.hit_icons:
                if hiticon.draw(screen, self.camera):
                    to_remove.append(hiticon)
            for remove in to_remove:
                self.world.scorekeeper.hit_icons.remove(remove)

            if self.world.scorekeeper.hp <= 0 and not self.world.square.died:
                self.world.square.died = True
                self.world.square.dir = [0, 0]
                pygame.mixer.music.stop()
                play_sound("death.mp3", 0.5)
                self.world.future_bounces = []
                for _ in range(100):
                    self.world.particles.append(Particle(self.world.square.pos, [random.randint(-3, 3), random.randint(-3, 3)]))
            if self.world.square.died:
                self.world.scorekeeper.hp = 0

        # draw square
        self.world.square.draw(screen, sqrect)

        if not Config.theatre_mode:
            # keystrokes
            self.keystrokes.draw(screen)

            # countdown to start
            if time_from_start < 0:
                repr_time = f"{abs(int((time_from_start+0.065)*10)/10)}s"
                countdown_surface = (36).render(repr_time, True, (255, 255, 255))
                screen.blit(countdown_surface, countdown_surface.get_rect(center=(Config.SCREEN_WIDTH / 2, Config.SCREEN_HEIGHT / 4)))
            elif time_from_start < 0.5:
                repr_zero = f"0.0s"
                countdown_surface = (36).render(repr_zero, True, (255, 255, 255))
                countdown_surface.set_alpha((0.5-time_from_start)*2*255)
                screen.blit(countdown_surface, countdown_surface.get_rect(center=(Config.SCREEN_WIDTH / 2, Config.SCREEN_HEIGHT / 4)))

            # handle mouse clicks because self.handle_event doesn't get called for mouse clicks
            if pygame.mouse.get_pressed()[0] and not self.mouse_down:
                self.misses = self.world.handle_keypress(time_from_start, self.misses)
                self.mouse_down = True
            elif not pygame.mouse.get_pressed()[0]:
                self.mouse_down = False
            
            # draw accuracy
            # noinspection PyBroadException
            try:
                n_bounces = len(self.world.past_bounces)
                n_total_bounces = len(self.world.past_bounces)+len(self.world.future_bounces)
                if n_bounces > 0:
                    n_misses = self.misses
                    acc = round((n_bounces-n_misses)/n_bounces*100, 2)
                    acct = round((n_total_bounces-n_misses)/n_total_bounces*100, 2)
                    # clamp to 0-100
                    acc = max(0, min(100, acc))
                    acct = max(0, min(100, acct))
                    acc_text = (24).render(f"Accuracy: {acc}%", True, (255, 255, 255))
                    acct_text = (24).render(f"Total Accuracy: {acct}%", True, (255, 255, 255))
                    topleft1 = self.world.scorekeeper.life_bar_rect.move(0, 10).bottomleft
                    screen.blit(acc_text, acc_text.get_rect(topleft=topleft1))
                    screen.blit(acct_text, acct_text.get_rect(topleft=acc_text.get_rect(topleft=topleft1).move(0, 10).bottomleft))
            except ZeroDivisionError:
                pass
            except Exception:
                pass

            # failure message
            if self.world.square.died:
                # calculate accuracy
                n_bounces = len(self.world.past_bounces)
                n_total_bounces = len(self.world.past_bounces)+len(self.world.future_bounces)
                # clamp bounces to 1-infinity
                n_bounces = max(1, n_bounces)
                n_total_bounces = max(1, n_total_bounces)
                n_misses = self.misses

                acc = round((n_bounces-n_misses)/n_bounces*100, 2)
                acct = round((n_total_bounces-n_misses)/n_total_bounces*100, 2)

                # clamp to 0-100
                acc = max(0, min(100, acc))
                acct = max(0, min(100, acct))

                try_again_text = (36).render("Press escape to go back.", True, (255, 255, 255))
                acc_text = (36).render(f"Accuracy: {acc}%", True, (255, 255, 255))
                acct_text = (36).render(f"Total Accuracy: {acct}%", True, (255, 255, 255))
                screen.blit(try_again_text, try_again_text.get_rect(center=(Config.SCREEN_WIDTH / 2, Config.SCREEN_HEIGHT / 4)))
                screen.blit(acc_text, acc_text.get_rect(center=(Config.SCREEN_WIDTH / 2, Config.SCREEN_HEIGHT / 4 + 50)))
                screen.blit(acct_text, acct_text.get_rect(center=(Config.SCREEN_WIDTH / 2, Config.SCREEN_HEIGHT / 4 + 100)))

        if not self.camera.locked_on_square:
            screen.blit(self.camera_ctrl_text, (10, 10))

    def handle_event(self, event: pygame.event.Event):
        if not self.active:
            return False

        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE:
                return True
            if event.key == pygame.K_TAB:
                self.camera.locked_on_square = not self.camera.locked_on_square
            if not Config.theatre_mode:
                if self.camera.locked_on_square:
                    time_from_start = self.world.time-Config.start_playing_delay/1000+Config.music_offset/1000
                    if time_from_start < -0.2:
                        return
                    arrows_n_space = (pygame.K_SPACE, pygame.K_LEFT, pygame.K_RIGHT, pygame.K_UP, pygame.K_DOWN)

                    if 97+26 > event.key >= 97 or event.key in arrows_n_space:  # press a to z key or space or arrows
                        self.misses = self.world.handle_keypress(time_from_start, self.misses)
                   
def main():
    # patch to fix mouse on high dpi displays
    if "Windows" in get_os():
        from ctypes import windll
        windll.user32.SetProcessDPIAware()
    # patch to fix opengl error on mac
    elif "Darwin" in get_os():
        pygame.display.gl_set_attribute(pygame.GL_CONTEXT_MAJOR_VERSION, 3)
        pygame.display.gl_set_attribute(pygame.GL_CONTEXT_MINOR_VERSION, 3)
        pygame.display.gl_set_attribute(pygame.GL_CONTEXT_PROFILE_MASK, pygame.GL_CONTEXT_PROFILE_CORE)
        pygame.display.gl_set_attribute(pygame.GL_CONTEXT_FORWARD_COMPATIBLE_FLAG, True)

    # pygame and other boilerplate
    n_frames = 0
    pygame.mixer.music.load("./assets/mainmenu.mp3")
    pygame.mixer.music.set_volume(Config.volume / 100)
    pygame.mixer.music.play(loops=-1, start=2)

    clock = pygame.time.Clock()

    flags = pygame.HWACCEL | pygame.HWSURFACE | pygame.OPENGL | pygame.DOUBLEBUF
    do_vsync = 1
    if "Linux" in get_os():
        do_vsync = 0  # otherwise error of "regular vsync for OpenGL not available" at least that's what i got under wsl
    # noinspection PyUnusedLocal
    if [Config.SCREEN_WIDTH, Config.SCREEN_HEIGHT] == [pygame.display.Info().current_w,pygame.display.Info().current_h]:
        flags |= pygame.FULLSCREEN
    
    real_screen = pygame.display.set_mode(
        [Config.SCREEN_WIDTH, Config.SCREEN_HEIGHT],
        flags,
        vsync=do_vsync
    )
    screen = pygame.Surface([Config.SCREEN_WIDTH, Config.SCREEN_HEIGHT])

    # noinspection PyBroadException
    try:
        pygame.display.set_caption("Midi Playground")
        pygame.display.set_icon(pygame.image.load("./assets/icon.png").convert_alpha())
    except Exception as e:
        print(e)

    # moderngl stuff
    ctx = moderngl.create_context()
    Config.ctx = ctx

    quad_buffer = ctx.buffer(data=array('f', [
        # position, uv coords
        -1.0, 1.0, 0.0, 0.0,  # topleft
        1.0, 1.0, 1.0, 0.0,  # topright
        -1.0, -1.0, 0.0, 1.0,  # bottomleft
        1.0, -1.0, 1.0, 1.0  # bottomright
    ]))

    vert_shader = '''
    #version 330 core
    
    in vec2 vert;
    in vec2 texcoord;
    out vec2 uvs;
    
    void main() {
        uvs = texcoord;
        gl_Position = vec4(vert.x, vert.y, 0.0, 1.0);
    }
    '''

    with open(f"./assets/shaders/{Config.shader_file_name}") as shader_file:
        frag_shader = shader_file.read()

    glsl_program = ctx.program(vertex_shader=vert_shader, fragment_shader=frag_shader)
    render_object = ctx.vertex_array(glsl_program, [(quad_buffer, '2f 2f', 'vert', 'texcoord')])

    Config.glsl_program = glsl_program
    Config.render_object = render_object
    Config.screen = screen

    game = Game()

    # game loop
    running = True
    while running:
        n_frames += 1
        # thanks to TheCodingCrafter for the implementation
        if Config.theme == "rainbow":
            to_set_as_rainbow = pygame.Color((0, 0, 0))
            to_set_as_rainbow2 = pygame.Color((0, 0, 0))
            to_set_as_rainbow.hsva = (((pygame.time.get_ticks() / 1000) * Config.rainbow_speed) % 360, 100, 75, 100)
            to_set_as_rainbow2.hsva = ((((pygame.time.get_ticks() / 1000) * Config.rainbow_speed) + 180) % 360, 100, 75, 100)
            get_colors()["background"] = to_set_as_rainbow
            get_colors()["hallway"] = to_set_as_rainbow2
            get_colors()["square"][0] = to_set_as_rainbow

        screen.fill(get_colors()["background"])
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False
            if event.type == pygame.KEYDOWN:
                # artificial lag spike for debugging purposes
                if event.key == pygame.K_F12:
                    total = 0
                    for _ in range(10_000_000):
                        total += 1
                
        
                game.active = True
                if msg := game.start_song(screen):
                    pygame.mixer.music.load("./assets/mainmenu.mp3")
                    pygame.mixer.music.set_volume(Config.volume / 100)
                    pygame.mixer.music.play(loops=-1, start=2)

            # handle game events
            if game.handle_event(event):
                game.active = False

        # draw stuff here
        game.draw(screen, n_frames)

        update_screen(screen, glsl_program, render_object)

        Config.dt = clock.tick(FRAMERATE) / 1000
    pygame.quit()
    save_to_file()


if __name__ == '__main__':
    main()