import bisect
import math
import sqlite3
from pathlib import Path
from typing import Optional

import pyray as ray

from libs.animation import Animation
from libs.audio import audio
from libs.backgrounds import Background
from libs.tja import Balloon, Drumroll, Note, TJAParser, calculate_base_score
from libs.utils import (
    OutlinedText,
    get_current_ms,
    global_data,
    is_l_don_pressed,
    is_l_kat_pressed,
    is_r_don_pressed,
    is_r_kat_pressed,
    load_all_textures_from_zip,
    load_image_from_zip,
    load_texture_from_zip,
    session_data,
)
from libs.video import VideoPlayer


class GameScreen:
    JUDGE_X = 414
    def __init__(self, width: int, height: int):
        self.width = width
        self.height = height
        self.current_ms = 0
        self.result_transition = None
        self.transition = None
        self.song_info = None
        self.screen_init = False
        self.movie = None
        self.end_ms = 0
        self.start_delay = 1000
        self.song_started = False

    def load_textures(self):
        self.textures = load_all_textures_from_zip(Path('Graphics/lumendata/enso_system/common.zip'))
        zip_file = Path('Graphics/lumendata/enso_system/common.zip')

        image = load_image_from_zip(zip_file, 'lane_img00000.png')
        ray.image_resize(image, 948, 176)
        ray.unload_texture(self.textures['lane'][0])
        self.textures['lane'][0] = ray.load_texture_from_image(image)

        image = load_image_from_zip(zip_file, 'lane_hit_img00005.png')
        ray.image_resize(image, 951, 130)
        ray.unload_texture(self.textures['lane_hit'][5])
        self.textures['lane_hit'][5] = ray.load_texture_from_image(image)
        image = load_image_from_zip(zip_file, 'lane_hit_img00006.png')
        ray.image_resize(image, 951, 130)
        ray.unload_texture(self.textures['lane_hit'][6])
        self.textures['lane_hit'][6] = ray.load_texture_from_image(image)
        image = load_image_from_zip(zip_file, 'lane_hit_img00007.png')
        ray.image_resize(image, 951, 130)
        ray.unload_texture(self.textures['lane_hit'][7])
        self.textures['lane_hit'][7] = ray.load_texture_from_image(image)

        self.texture_combo_text = [load_texture_from_zip(zip_file, 'lane_obi_img00035.png'),
        load_texture_from_zip(zip_file, 'lane_obi_img00046.png')]
        self.texture_combo_numbers = []
        for i in range(36, 58):
            if i not in [46, 48]:
                filename = f'lane_obi_img{str(i).zfill(5)}.png'
                self.texture_combo_numbers.append(load_texture_from_zip(zip_file, filename))
        self.texture_combo_glimmer = load_texture_from_zip(zip_file, 'lane_obi_img00048.png')

        self.texture_se_moji = []
        for i in range(0, 17):
            filename = f'onp_moji_img{str(i).zfill(5)}.png'
            if i == 8:
                filename = 'onp_renda_moji_img00001.png'
            self.texture_se_moji.append(load_texture_from_zip(zip_file, filename))

        self.textures.update(load_all_textures_from_zip(Path('Graphics/lumendata/enso_system/base1p.zip')))
        self.textures.update(load_all_textures_from_zip(Path('Graphics/lumendata/enso_system/don1p.zip')))

    def load_sounds(self):
        sounds_dir = Path("Sounds")
        self.sound_don = audio.load_sound(sounds_dir / "inst_00_don.wav")
        self.sound_kat = audio.load_sound(sounds_dir / "inst_00_katsu.wav")
        self.sound_restart = audio.load_sound(sounds_dir / 'song_select' / 'Skip.ogg')
        self.sound_balloon_pop = audio.load_sound(sounds_dir / "balloon_pop.wav")
        self.sound_result_transition = audio.load_sound(sounds_dir / "result" / "VO_RESULT [1].ogg")
        self.sounds = [self.sound_don, self.sound_kat, self.sound_balloon_pop, self.sound_result_transition]

    def init_tja(self, song: Path, difficulty: int):
        #Map notes to textures
        self.note_type_list = [self.textures['lane_syousetsu'][0],
            self.textures['onp_don'], self.textures['onp_katsu'],
            self.textures['onp_don_dai'], self.textures['onp_katsu_dai'],
            [self.textures['onp_renda'][2], self.textures['onp_renda'][3]],
            [self.textures['onp_renda_dai'][2], self.textures['onp_renda_dai'][3]],
            [self.textures['onp_fusen'][1], self.textures['onp_fusen'][2]],
            self.textures['onp_renda'][0], self.textures['onp_renda'][1],
            self.textures['onp_renda_dai'][0], self.textures['onp_renda_dai'][1],
            self.textures['onp_fusen'][0]]

        self.tja = TJAParser(song, start_delay=self.start_delay, distance=self.width - GameScreen.JUDGE_X)
        if self.tja.metadata.bgmovie != Path() and self.tja.metadata.bgmovie.exists():
            self.movie = VideoPlayer(self.tja.metadata.bgmovie)
            self.movie.set_volume(0.0)
        else:
            self.movie = None
        session_data.song_title = self.tja.metadata.title.get(global_data.config['general']['language'].lower(), self.tja.metadata.title['en'])

        self.player_1 = Player(self, 1, difficulty)
        if not hasattr(self, 'song_music'):
            self.song_music = audio.load_sound(self.tja.metadata.wave)
            audio.normalize_sound(self.song_music, 0.1935)
        self.start_ms = (get_current_ms() - self.tja.metadata.offset*1000)

    def on_screen_start(self):
        if not self.screen_init:
            self.screen_init = True
            self.background = Background(self.width, self.height)
            self.load_textures()
            self.load_sounds()
            self.init_tja(global_data.selected_song, session_data.selected_difficulty)
            self.song_info = SongInfo(session_data.song_title, 'TEST')
            self.result_transition = None
            self.transition = Transition(self.height)

    def on_screen_end(self, next_screen):
        self.screen_init = False
        for zip in self.textures:
            for texture in self.textures[zip]:
                ray.unload_texture(texture)
        audio.unload_sound(self.song_music)
        del self.song_music
        self.song_started = False
        self.end_ms = 0
        self.movie = None
        return next_screen

    def write_score(self):
        if global_data.config['general']['autoplay']:
            return
        with sqlite3.connect('scores.db') as con:
            cursor = con.cursor()
            notes, _, bars = TJAParser.notes_to_position(TJAParser(self.tja.file_path), self.player_1.difficulty)
            hash = self.tja.hash_note_data(notes, bars)
            check_query = "SELECT score FROM Scores WHERE hash = ? LIMIT 1"
            cursor.execute(check_query, (hash,))
            result = cursor.fetchone()
            if result is None or session_data.result_score > result[0]:
                insert_query = '''
                INSERT OR REPLACE INTO Scores (hash, en_name, jp_name, diff, score, good, ok, bad, drumroll, combo, clear)
                VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
                '''
                data = (hash, self.tja.metadata.title['en'],
                        self.tja.metadata.title.get('ja', ''), self.player_1.difficulty,
                        session_data.result_score, session_data.result_good,
                        session_data.result_ok, session_data.result_bad,
                        session_data.result_total_drumroll, session_data.result_max_combo, int(self.player_1.gauge.gauge_length > self.player_1.gauge.clear_start[min(self.player_1.difficulty, 3)]))
                cursor.execute(insert_query, data)
                con.commit()

    def update(self):
        self.on_screen_start()
        if self.transition is not None:
            self.transition.update(get_current_ms())
        self.current_ms = get_current_ms() - self.start_ms
        if (self.current_ms >= self.tja.metadata.offset*1000 + self.start_delay - global_data.config["general"]["judge_offset"]) and not self.song_started:
            if self.song_music is not None:
                if not audio.is_sound_playing(self.song_music):
                    audio.play_sound(self.song_music)
                    print(f"Song started at {self.current_ms}")
            if self.movie is not None:
                self.movie.start(get_current_ms())
            self.song_started = True
        if self.movie is not None:
            self.movie.update()
        else:
            self.background.update(get_current_ms(), self.player_1.gauge.gauge_length > self.player_1.gauge.clear_start[min(self.player_1.difficulty, 3)])

        self.player_1.update(self)
        if self.song_info is not None:
            self.song_info.update(get_current_ms())

        if self.result_transition is not None:
            self.result_transition.update(get_current_ms())
            if self.result_transition.is_finished:
                return self.on_screen_end('RESULT')
        elif len(self.player_1.play_notes) == 0:
            session_data.result_score, session_data.result_good, session_data.result_ok, session_data.result_bad, session_data.result_max_combo, session_data.result_total_drumroll = self.player_1.get_result_score()
            session_data.result_gauge_length = self.player_1.gauge.gauge_length
            if self.end_ms != 0:
                if get_current_ms() >= self.end_ms + 8533.34:
                    self.result_transition = ResultTransition(self.height)
                    audio.play_sound(self.sound_result_transition)
            else:
                self.write_score()
                self.end_ms = get_current_ms()

        if ray.is_key_pressed(ray.KeyboardKey.KEY_F1):
            audio.stop_sound(self.song_music)
            self.init_tja(global_data.selected_song, session_data.selected_difficulty)
            audio.play_sound(self.sound_restart)
            self.song_started = False

        if ray.is_key_pressed(ray.KeyboardKey.KEY_ESCAPE):
            audio.stop_sound(self.song_music)
            return self.on_screen_end('SONG_SELECT')

    def draw(self):
        if self.movie is not None:
            self.movie.draw()
        else:
            self.background.draw()
        self.player_1.draw(self)
        if self.song_info is not None:
            self.song_info.draw(self)
        if self.transition is not None:
            self.transition.draw(self.height)
        if self.result_transition is not None:
            self.result_transition.draw(self.width, self.height, global_data.textures['shutter'][0], global_data.textures['shutter'][1])

class Player:
    TIMING_GOOD = 25.0250015258789
    TIMING_OK = 75.0750045776367
    TIMING_BAD = 108.441665649414

    def __init__(self, game_screen: GameScreen, player_number: int, difficulty: int):

        self.player_number = player_number
        self.difficulty = difficulty
        self.visual_offset = global_data.config["general"]["visual_offset"]

        self.play_notes, self.draw_note_list, self.draw_bar_list = game_screen.tja.notes_to_position(self.difficulty)
        self.total_notes = len([note for note in self.play_notes if 0 < note.type < 5])
        self.base_score = calculate_base_score(self.play_notes)

        #Note management
        self.current_bars: list[Note] = []
        self.current_notes_draw: list[Note | Drumroll | Balloon] = []
        self.is_drumroll = False
        self.curr_drumroll_count = 0
        self.is_balloon = False
        self.curr_balloon_count = 0
        self.balloon_index = 0

        #Score management
        self.good_count = 0
        self.ok_count = 0
        self.bad_count = 0
        self.combo = 0
        self.score = 0
        self.max_combo = 0
        self.total_drumroll = 0

        self.arc_points = 25

        self.draw_judge_list: list[Judgement] = []
        self.lane_hit_effect: Optional[LaneHitEffect] = None
        self.draw_arc_list: list[NoteArc] = []
        self.draw_drum_hit_list: list[DrumHitEffect] = []
        self.drumroll_counter: Optional[DrumrollCounter] = None
        self.balloon_anim: Optional[BalloonAnimation] = None
        self.base_score_list: list[ScoreCounterAnimation] = []
        self.combo_display = Combo(self.combo, get_current_ms())
        self.score_counter = ScoreCounter(self.score)

        self.input_log: dict[float, tuple] = dict()

        self.gauge = Gauge(self.difficulty, game_screen.tja.metadata.course_data[self.difficulty].level, self.total_notes)
        self.gauge_hit_effect: list[GaugeHitEffect] = []

        self.autoplay_hit_side = 'L'
        self.last_subdivision = -1

    def get_result_score(self):
        return self.score, self.good_count, self.ok_count, self.bad_count, self.total_drumroll, self.max_combo

    def get_position(self, game_screen: GameScreen, ms: float, pixels_per_frame: float) -> int:
        return int(game_screen.width + pixels_per_frame * 60 / 1000 * (ms - game_screen.current_ms) - 64) - self.visual_offset

    def animation_manager(self, animation_list: list):
        if len(animation_list) <= 0:
            return

        for i in range(len(animation_list)-1, -1, -1):
            animation = animation_list[i]
            animation.update(get_current_ms())
            if animation.is_finished:
                animation_list.pop(i)

    def bar_manager(self, game_screen: GameScreen):
        #Add bar to current_bars list if it is ready to be shown on screen
        if len(self.draw_bar_list) > 0 and game_screen.current_ms > self.draw_bar_list[0].load_ms:
            self.current_bars.append(self.draw_bar_list.popleft())

        #If a bar is off screen, remove it
        if len(self.current_bars) == 0:
            return

        for i in range(len(self.current_bars)-1, -1, -1):
            bar = self.current_bars[i]
            position = self.get_position(game_screen, bar.hit_ms, bar.pixels_per_frame)
            if position < GameScreen.JUDGE_X + 650:
                self.current_bars.pop(i)

    def play_note_manager(self, game_screen: GameScreen):
        if len(self.play_notes) == 0:
            return

        note = self.play_notes[0]
        if note.hit_ms + Player.TIMING_BAD < game_screen.current_ms:
            if 0 < note.type <= 4:
                self.combo = 0
                self.bad_count += 1
                self.gauge.add_bad()
                self.play_notes.popleft()
            elif note.type != 8:
                tail = self.play_notes[1]
                if tail.hit_ms <= game_screen.current_ms:
                    self.play_notes.popleft()
                    self.play_notes.popleft()
                    self.is_drumroll = False
                    self.is_balloon = False
            else:
                if len(self.play_notes) == 1:
                    self.play_notes.popleft()
        elif (note.hit_ms <= game_screen.current_ms):
            if note.type == 5 or note.type == 6:
                self.is_drumroll = True
            elif note.type == 7:
                self.is_balloon = True

    def draw_note_manager(self, game_screen: GameScreen):
        if len(self.draw_note_list) > 0 and game_screen.current_ms + 1000 >= self.draw_note_list[0].load_ms:
            current_note = self.draw_note_list.popleft()
            if 5 <= current_note.type <= 7:
                bisect.insort_left(self.current_notes_draw, current_note, key=lambda x: x.index)
                try:
                    tail_note = next(note for note in self.draw_note_list if note.type == 8)
                    bisect.insort_left(self.current_notes_draw, tail_note, key=lambda x: x.index)
                    self.draw_note_list.remove(tail_note)
                except Exception as e:
                    raise(e)
            else:
                bisect.insort_left(self.current_notes_draw, current_note, key=lambda x: x.index)

        if len(self.current_notes_draw) == 0:
            return

        if isinstance(self.current_notes_draw[0], Drumroll) and 255 > self.current_notes_draw[0].color > 0:
            self.current_notes_draw[0].color += 1

        note = self.current_notes_draw[0]
        if note.type in {5, 6, 7} and len(self.current_notes_draw) > 1:
            note = self.current_notes_draw[1]
        position = self.get_position(game_screen, note.hit_ms, note.pixels_per_frame)
        if position < GameScreen.JUDGE_X + 650:
            self.current_notes_draw.pop(0)

    def note_manager(self, game_screen: GameScreen):
        self.bar_manager(game_screen)
        self.play_note_manager(game_screen)
        self.draw_note_manager(game_screen)

    def note_correct(self, game_screen: GameScreen, note: Note):
        self.play_notes.popleft()
        index = note.index
        if note.type == 7:
            note_type = game_screen.note_type_list[3][0]
            self.play_notes.popleft()
        else:
            note_type = game_screen.note_type_list[note.type][0]

        self.combo += 1
        if self.combo > self.max_combo:
            self.max_combo = self.combo

        self.draw_arc_list.append(NoteArc(note_type, get_current_ms(), self.player_number, note.type == 3 or note.type == 4) or note.type == 7)
        #game_screen.background.chibis.append(game_screen.background.Chibi())

        if note in self.current_notes_draw:
            index = self.current_notes_draw.index(note)
            self.current_notes_draw.pop(index)

    def check_drumroll(self, game_screen: GameScreen, drum_type: int):
        note_type = game_screen.note_type_list[drum_type][0]
        self.draw_arc_list.append(NoteArc(note_type, get_current_ms(), self.player_number, drum_type == 3 or drum_type == 4))
        self.curr_drumroll_count += 1
        self.total_drumroll += 1
        self.score += 100
        self.base_score_list.append(ScoreCounterAnimation(100))
        if not isinstance(self.current_notes_draw[0], Drumroll):
            return
        self.current_notes_draw[0].color = max(0, 255 - (self.curr_drumroll_count * 10))

    def check_balloon(self, game_screen: GameScreen, drum_type: int, note: Balloon):
        if drum_type != 1:
            return
        if self.balloon_anim is None:
            self.balloon_anim = BalloonAnimation(get_current_ms(), note.count)
        self.curr_balloon_count += 1
        self.total_drumroll += 1
        self.score += 100
        self.base_score_list.append(ScoreCounterAnimation(100))
        if self.curr_balloon_count == note.count:
            self.is_balloon = False
            note.popped = True
            self.balloon_anim.update(game_screen, get_current_ms(), self.curr_balloon_count, note.popped)
            audio.play_sound(game_screen.sound_balloon_pop)
            self.note_correct(game_screen, self.play_notes[0])

    def check_note(self, game_screen: GameScreen, drum_type: int):
        if len(self.play_notes) == 0:
            return

        curr_note = self.play_notes[0]
        if self.is_drumroll:
            self.check_drumroll(game_screen, drum_type)
        elif self.is_balloon:
            if not isinstance(curr_note, Balloon):
                raise Exception("Balloon mode entered but current note is not balloon")
            self.check_balloon(game_screen, drum_type, curr_note)
        else:
            self.curr_drumroll_count = 0
            self.curr_balloon_count = 0
            curr_note = next(
                (note for note in self.play_notes if note.type not in {5, 6, 7, 8}),
                None  # Default if no matching note is found
            )
            if curr_note is None:
                return
            #If the wrong key was hit, stop checking
            if drum_type == 1 and curr_note.type not in {1, 3}:
                return
            if drum_type == 2 and curr_note.type not in {2, 4}:
                return
            #If the note is too far away, stop checking
            if game_screen.current_ms > (curr_note.hit_ms + Player.TIMING_BAD):
                return
            big = curr_note.type == 3 or curr_note.type == 4
            if (curr_note.hit_ms - Player.TIMING_GOOD) <= game_screen.current_ms <= (curr_note.hit_ms + Player.TIMING_GOOD):
                self.draw_judge_list.append(Judgement('GOOD', big, ms_display=game_screen.current_ms - curr_note.hit_ms))
                self.lane_hit_effect = LaneHitEffect('GOOD')
                self.good_count += 1
                self.score += self.base_score
                self.base_score_list.append(ScoreCounterAnimation(self.base_score))
                self.note_correct(game_screen, curr_note)
                self.gauge.add_good()

            elif (curr_note.hit_ms - Player.TIMING_OK) <= game_screen.current_ms <= (curr_note.hit_ms + Player.TIMING_OK):
                self.draw_judge_list.append(Judgement('OK', big, ms_display=game_screen.current_ms - curr_note.hit_ms))
                self.ok_count += 1
                self.score += 10 * math.floor(self.base_score / 2 / 10)
                self.base_score_list.append(ScoreCounterAnimation(10 * math.floor(self.base_score / 2 / 10)))
                self.note_correct(game_screen, curr_note)
                self.gauge.add_ok()

            elif (curr_note.hit_ms - Player.TIMING_BAD) <= game_screen.current_ms <= (curr_note.hit_ms + Player.TIMING_BAD):
                self.draw_judge_list.append(Judgement('BAD', big, ms_display=game_screen.current_ms - curr_note.hit_ms))
                self.bad_count += 1
                self.combo = 0
                self.play_notes.popleft()
                self.gauge.add_bad()

    def drumroll_counter_manager(self, game_screen: GameScreen):
        if self.is_drumroll and self.curr_drumroll_count > 0 and self.drumroll_counter is None:
            self.drumroll_counter = DrumrollCounter(get_current_ms())

        if self.drumroll_counter is not None:
            if self.drumroll_counter.is_finished and not self.is_drumroll:
                self.drumroll_counter = None
            else:
                self.drumroll_counter.update(game_screen, get_current_ms(), self.curr_drumroll_count)

    def balloon_manager(self, game_screen: GameScreen):
        if self.balloon_anim is not None:
            self.balloon_anim.update(game_screen, get_current_ms(), self.curr_balloon_count, not self.is_balloon)
            if self.balloon_anim.is_finished:
                self.balloon_anim = None

    def handle_input(self, game_screen: GameScreen):
        input_checks = [
            (is_l_don_pressed, 'DON', 'L', game_screen.sound_don),
            (is_r_don_pressed, 'DON', 'R', game_screen.sound_don),
            (is_l_kat_pressed, 'KAT', 'L', game_screen.sound_kat),
            (is_r_kat_pressed, 'KAT', 'R', game_screen.sound_kat)
        ]
        for check_func, note_type, side, sound in input_checks:
            if check_func():
                self.lane_hit_effect = LaneHitEffect(note_type)
                self.draw_drum_hit_list.append(DrumHitEffect(note_type, side))

                if global_data.config["general"]["sfx"]:
                    audio.play_sound(sound)

                self.check_note(game_screen, 1 if note_type == 'DON' else 2)
                self.input_log[game_screen.current_ms] = (note_type, side)

    def autoplay_manager(self, game_screen: GameScreen):
        if not global_data.config["general"]["autoplay"]:
            return
        if len(self.play_notes) == 0:
            return
        note = self.play_notes[0]
        if self.is_drumroll or self.is_balloon:
            if self.play_notes[0].bpm == 0:
                subdivision_in_ms = 0
            else:
                subdivision_in_ms = game_screen.current_ms // ((60000 * 4 / self.play_notes[0].bpm) / 24)
            if subdivision_in_ms > self.last_subdivision:
                self.last_subdivision = subdivision_in_ms
                hit_type = 'DON'
                self.lane_hit_effect = LaneHitEffect(hit_type)
                if self.autoplay_hit_side == 'L':
                    self.autoplay_hit_side = 'R'
                else:
                    self.autoplay_hit_side = 'L'
                self.draw_drum_hit_list.append(DrumHitEffect(hit_type, self.autoplay_hit_side))
                audio.play_sound(game_screen.sound_don)
                type = note.type
                if type == 6 or type == 9:
                    type = 3
                elif type == 5 or type == 7:
                    type = 1
                self.check_note(game_screen, type)
        else:
            while game_screen.current_ms >= note.hit_ms and note.type <= 4:
                hit_type = 'DON'
                if note.type == 2 or note.type == 4:
                    hit_type = 'KAT'
                self.lane_hit_effect = LaneHitEffect(hit_type)
                if self.autoplay_hit_side == 'L':
                    self.autoplay_hit_side = 'R'
                else:
                    self.autoplay_hit_side = 'L'
                self.draw_drum_hit_list.append(DrumHitEffect(hit_type, self.autoplay_hit_side))
                sound = game_screen.sound_don if hit_type == "DON" else game_screen.sound_kat
                audio.play_sound(sound)
                type = note.type
                if type == 6 or type == 9:
                    type = 3
                elif type == 5 or type == 7:
                    type = 1
                self.check_note(game_screen, type)
                if len(self.play_notes) > 0:
                    note = self.play_notes[0]
                else:
                    break


    def update(self, game_screen: GameScreen):
        self.note_manager(game_screen)
        self.combo_display.update(game_screen, get_current_ms(), self.combo)
        self.drumroll_counter_manager(game_screen)
        self.animation_manager(self.draw_judge_list)
        self.balloon_manager(game_screen)
        if self.lane_hit_effect is not None:
            self.lane_hit_effect.update(get_current_ms())
        self.animation_manager(self.draw_drum_hit_list)
        for anim in self.draw_arc_list:
            anim.update(get_current_ms())
            if anim.is_finished:
                self.gauge_hit_effect.append(GaugeHitEffect(anim.texture, anim.is_big))
                self.draw_arc_list.remove(anim)
        self.animation_manager(self.gauge_hit_effect)
        self.animation_manager(self.base_score_list)
        self.score_counter.update(get_current_ms(), self.score)
        self.autoplay_manager(game_screen)
        self.handle_input(game_screen)

        self.gauge.update(get_current_ms(), self.good_count, self.ok_count, self.bad_count, self.total_notes)

    def draw_drumroll(self, game_screen: GameScreen, head: Drumroll, current_eighth: int):
        start_position = self.get_position(game_screen, head.load_ms, head.pixels_per_frame)
        tail = next((note for note in self.current_notes_draw[1:] if note.type == 8 and note.index > head.index), None)
        if tail is None:
            raise Exception("Tail for Balloon not found")
        is_big = int(head.type == 6) * 2
        end_position = self.get_position(game_screen, tail.load_ms, tail.pixels_per_frame)
        length = (end_position - start_position - 50)
        if length <= 0:
            end_position += 50
        source_rect = ray.Rectangle(0,0,game_screen.note_type_list[8].width, game_screen.note_type_list[8].height)
        dest_rect = ray.Rectangle(start_position+64, 192, length, game_screen.note_type_list[1][0].height)
        color = ray.Color(255, head.color, head.color, 255)
        ray.draw_texture_pro(game_screen.note_type_list[8 + is_big], source_rect, dest_rect, ray.Vector2(0,0), 0, color)
        ray.draw_texture(game_screen.note_type_list[9 + is_big], end_position, 192, color)
        ray.draw_texture(game_screen.note_type_list[head.type][current_eighth % 2], start_position, 192, color)

        source_rect = ray.Rectangle(0,0,game_screen.texture_se_moji[8].width,game_screen.texture_se_moji[8].height)
        dest_rect = ray.Rectangle(start_position - (game_screen.texture_se_moji[8].width // 2) + 64, 323, length,game_screen.texture_se_moji[8].height)
        ray.draw_texture_pro(game_screen.texture_se_moji[8], source_rect, dest_rect, ray.Vector2(0,0), 0, ray.WHITE)
        moji_texture = game_screen.texture_se_moji[head.moji]
        ray.draw_texture(moji_texture, start_position - (moji_texture.width//2) + 64, 323, ray.WHITE)
        moji_texture = game_screen.texture_se_moji[tail.moji]
        ray.draw_texture(moji_texture, (end_position - (moji_texture.width//2)) + 32, 323, ray.WHITE)

    def draw_balloon(self, game_screen: GameScreen, head: Balloon, current_eighth: int):
        offset = 12
        start_position = self.get_position(game_screen, head.load_ms, head.pixels_per_frame)
        tail = next((note for note in self.current_notes_draw[1:] if note.type == 8 and note.index > head.index), None)
        if tail is None:
            raise Exception("Tail for Balloon not found")
        end_position = self.get_position(game_screen, tail.load_ms, tail.pixels_per_frame)
        pause_position = 349
        if game_screen.current_ms >= tail.hit_ms:
            position = end_position
        elif game_screen.current_ms >= head.hit_ms:
            position = pause_position
        else:
            position = start_position
        ray.draw_texture(game_screen.note_type_list[head.type][current_eighth % 2], position-offset, 192, ray.WHITE)
        ray.draw_texture(game_screen.note_type_list[12], position-offset+128, 192, ray.WHITE)

    def draw_bars(self, game_screen: GameScreen):
        if len(self.current_bars) <= 0:
            return

        for bar in reversed(self.current_bars):
            if not bar.display:
                continue
            position = self.get_position(game_screen, bar.load_ms, bar.pixels_per_frame)
            ray.draw_texture(game_screen.note_type_list[bar.type], position+60, 190, ray.WHITE)

    def draw_notes(self, game_screen: GameScreen):
        if len(self.current_notes_draw) <= 0:
            return

        if len(self.current_bars) > 0:
            if self.current_bars[0].bpm == 0:
                eighth_in_ms = 0
            else:
                eighth_in_ms = (60000 * 4 / self.current_bars[0].bpm) / 8
        else:
            if self.current_notes_draw[0].bpm == 0:
                eighth_in_ms = 0
            else:
                eighth_in_ms = (60000 * 4 / self.current_notes_draw[0].bpm) / 8
        current_eighth = 0
        if self.combo >= 50 and eighth_in_ms != 0:
            current_eighth = int((game_screen.current_ms - game_screen.start_ms) // eighth_in_ms)

        for note in reversed(self.current_notes_draw):
            if self.is_balloon and note == self.current_notes_draw[0]:
                continue
            if note.type == 8:
                continue
            position = self.get_position(game_screen, note.load_ms, note.pixels_per_frame)
            if isinstance(note, Drumroll):
                self.draw_drumroll(game_screen, note, current_eighth)
            elif isinstance(note, Balloon):
                self.draw_balloon(game_screen, note, current_eighth)
                moji_texture = game_screen.texture_se_moji[note.moji]
                ray.draw_texture(moji_texture, position - (moji_texture.width//2) + 64, 323, ray.WHITE)
            else:
                ray.draw_texture(game_screen.note_type_list[note.type][current_eighth % 2], position, 192, ray.WHITE)
                moji_texture = game_screen.texture_se_moji[note.moji]
                ray.draw_texture(moji_texture, position - (moji_texture.width//2) + 64, 323, ray.WHITE)
            #ray.draw_text(str(note.index), position+64, 192, 25, ray.GREEN)

    def draw(self, game_screen: GameScreen):
        ray.draw_texture(game_screen.textures['lane'][0], 332, 184, ray.WHITE)
        self.gauge.draw(game_screen.textures['gage_don_1p_hard'])
        if self.lane_hit_effect is not None:
            self.lane_hit_effect.draw(game_screen.textures['lane_hit'])
        ray.draw_texture(game_screen.textures['lane_hit'][17], 342, 184, ray.WHITE)
        for anim in self.draw_judge_list:
            anim.draw(game_screen.textures['lane_hit'], game_screen.textures['lane_hit_effect'])

        #ray.draw_texture(game_screen.textures['onp_kiseki_don_1p'][0], 350, 192, ray.WHITE)
        #ray.draw_texture(game_screen.textures['onp_kiseki_don_1p'][22], 332, -84, ray.WHITE)
        #ray.draw_texture(game_screen.textures['onp_kiseki_don_1p'][6], 1187 - 29, 130 - 29, ray.WHITE)

        self.draw_bars(game_screen)
        self.draw_notes(game_screen)
        ray.draw_texture(game_screen.textures['lane_obi'][0], 0, 184, ray.WHITE)
        ray.draw_texture(game_screen.textures['lane_obi'][14], 211, 206, ray.WHITE)
        if global_data.config["general"]["autoplay"]:
            ray.draw_texture(game_screen.textures['lane_obi'][58], 0, 290, ray.WHITE)
        for anim in self.draw_drum_hit_list:
            anim.draw(game_screen)
        self.combo_display.draw(game_screen)
        ray.draw_texture(game_screen.textures['lane_obi'][3], 0, 184, ray.WHITE)
        ray.draw_texture(game_screen.textures['lane_obi'][19], 0, 225, ray.WHITE)
        ray.draw_texture(game_screen.textures['lane_obi'][self.difficulty+21], 50, 222, ray.WHITE)
        if self.drumroll_counter is not None:
            self.drumroll_counter.draw(game_screen)
        for anim in self.draw_arc_list:
            anim.draw(game_screen)
        for anim in self.gauge_hit_effect:
            anim.draw(game_screen)
        if self.balloon_anim is not None:
            self.balloon_anim.draw(game_screen)
        self.score_counter.draw(game_screen)
        for anim in self.base_score_list:
            anim.draw(game_screen)
        #ray.draw_circle(game_screen.width//2, game_screen.height, 300, ray.ORANGE)

class Judgement:
    def __init__(self, type: str, big: bool, ms_display: Optional[float]=None):
        self.type = type
        self.big = big
        self.is_finished = False
        self.curr_hit_ms = None
        if ms_display is not None:
            self.curr_hit_ms = str(round(ms_display, 2))

        self.fade_animation_1 = Animation.create_fade(132, initial_opacity=0.5, delay=100)
        self.fade_animation_2 = Animation.create_fade(316 - 233.3, delay=233.3)
        self.move_animation = Animation.create_move(83, total_distance=15, start_position=144)
        self.texture_animation = Animation.create_texture_change(100, textures=[(33, 50, 1), (50, 83, 2), (83, 100, 3), (100, float('inf'), 4)])

    def update(self, current_ms):
        self.fade_animation_1.update(current_ms)
        self.fade_animation_2.update(current_ms)
        self.move_animation.update(current_ms)
        self.texture_animation.update(current_ms)

        if self.fade_animation_2.is_finished:
            self.is_finished = True

    def draw(self, textures_1: list[ray.Texture], textures_2: list[ray.Texture]):
        y = self.move_animation.attribute
        index = int(self.texture_animation.attribute)
        hit_color = ray.fade(ray.WHITE, self.fade_animation_1.attribute)
        color = ray.fade(ray.WHITE, self.fade_animation_2.attribute)
        if self.curr_hit_ms is not None:
            if float(self.curr_hit_ms) < -(global_data.config['general']['hard_judge']):
                color = ray.fade(ray.BLUE, self.fade_animation_2.attribute)
            elif float(self.curr_hit_ms) > (global_data.config['general']['hard_judge']):
                color = ray.fade(ray.RED, self.fade_animation_2.attribute)
        if self.type == 'GOOD':
            if self.big:
                ray.draw_texture(textures_1[21], 342, 184, color)
                ray.draw_texture(textures_2[index+11], 304, 143, hit_color)
            else:
                ray.draw_texture(textures_1[19], 342, 184, color)
                ray.draw_texture(textures_2[index+5], 304, 143, hit_color)
            ray.draw_texture(textures_2[9], 370, int(y), color)
        elif self.type == 'OK':
            if self.big:
                ray.draw_texture(textures_1[20], 342, 184, color)
                ray.draw_texture(textures_2[index+16], 304, 143, hit_color)
            else:
                ray.draw_texture(textures_1[18], 342, 184, color)
                ray.draw_texture(textures_2[index], 304, 143, hit_color)
            ray.draw_texture(textures_2[4], 370, int(y), color)
        elif self.type == 'BAD':
            ray.draw_texture(textures_2[10], 370, int(y), color)

class LaneHitEffect:
    def __init__(self, type: str):
        self.type = type
        self.color = ray.fade(ray.WHITE, 0.5)
        self.fade = Animation.create_fade(150, delay=83, initial_opacity=0.5)
        self.is_finished = False

    def update(self, current_ms: float):
        self.fade.update(current_ms)
        fade_opacity = self.fade.attribute
        self.color = ray.fade(ray.WHITE, fade_opacity)
        if self.fade.is_finished:
            self.is_finished = True

    def draw(self, textures: list[ray.Texture]):
        if self.type == 'GOOD':
            ray.draw_texture(textures[7], 328, 192, self.color)
        elif self.type == 'DON':
            ray.draw_texture(textures[5], 328, 192, self.color)
        elif self.type == 'KAT':
            ray.draw_texture(textures[6], 328, 192, self.color)

class DrumHitEffect:
    def __init__(self, type: str, side: str):
        self.type = type
        self.side = side
        self.color = ray.fade(ray.WHITE, 1)
        self.is_finished = False
        self.fade = Animation.create_fade(100, delay=67)

    def update(self, current_ms: float):
        self.fade.update(current_ms)
        fade_opacity = self.fade.attribute
        self.color = ray.fade(ray.WHITE, fade_opacity)
        if self.fade.is_finished:
            self.is_finished = True

    def draw(self, game_screen):
        x, y = 211, 206
        if self.type == 'DON':
            if self.side == 'L':
                ray.draw_texture(game_screen.textures['lane_obi'][16], x, y, self.color)
            elif self.side == 'R':
                ray.draw_texture(game_screen.textures['lane_obi'][15], x, y, self.color)
        elif self.type == 'KAT':
            if self.side == 'L':
                ray.draw_texture(game_screen.textures['lane_obi'][18], x, y, self.color)
            elif self.side == 'R':
                ray.draw_texture(game_screen.textures['lane_obi'][17], x, y, self.color)

class GaugeHitEffect:
    def __init__(self, note_texture: ray.Texture, big: bool):
        self.note_texture = note_texture
        self.is_big = big
        self.texture_change = Animation.create_texture_change(116.67, textures=[(0, 33.33, 1), (33.33, 66.66, 2), (66.66, float('inf'), 3)])
        self.circle_fadein = Animation.create_fade(133, initial_opacity=0.0, final_opacity=1.0, delay=16.67)
        self.resize = Animation.create_texture_resize(233, delay=self.texture_change.duration, initial_size=0.75, final_size=1.15)
        self.fade_out = Animation.create_fade(66, delay=233)
        self.test = Animation.create_fade(300, delay=116.67, initial_opacity=0.0, final_opacity=1.0)
        self.color = ray.fade(ray.YELLOW, self.circle_fadein.attribute)
        self.is_finished = False
    def update(self, current_ms):
        self.texture_change.update(current_ms)
        self.circle_fadein.update(current_ms)
        self.fade_out.update(current_ms)
        self.resize.update(current_ms)
        self.test.update(current_ms)
        color = ray.YELLOW
        if self.circle_fadein.is_finished:
            color = ray.WHITE
        self.color = ray.fade(color, min(self.fade_out.attribute, self.circle_fadein.attribute))
        if self.fade_out.is_finished:
            self.is_finished = True
    def draw(self, game_screen):
        texture = game_screen.textures['onp_kiseki_don_1p'][self.texture_change.attribute]
        color_map = {0.70: ray.WHITE, 0.80: ray.YELLOW, 0.90: ray.ORANGE, 1.00: ray.RED}
        texture_color = ray.WHITE
        for upper_bound, color in color_map.items():
            lower_bound = list(color_map.keys())[list(color_map.keys()).index(upper_bound) - 1] if list(color_map.keys()).index(upper_bound) > 0 else 0.70

            if lower_bound <= self.resize.attribute <= upper_bound:
                texture_color = color
            elif self.resize.attribute >= upper_bound:
                texture_color = ray.RED
        original_x = 1223
        original_y = 164
        source = ray.Rectangle(0, 0, texture.width, texture.height)
        dest_width = texture.width * self.resize.attribute
        dest_height = texture.height * self.resize.attribute
        dest = ray.Rectangle(original_x, original_y, dest_width, dest_height)
        origin = ray.Vector2(dest_width / 2, dest_height / 2)
        ray.draw_texture_pro(texture, source, dest, origin, self.test.attribute*100, ray.fade(texture_color, self.fade_out.attribute))
        ray.draw_texture(self.note_texture, 1187 - 29, 130 - 29, ray.fade(ray.WHITE, self.fade_out.attribute))
        if self.is_big:
            ray.draw_texture(game_screen.textures['onp_kiseki_don_1p'][20], 1187 - 29, 130 - 29, self.color)
        else:
            ray.draw_texture(game_screen.textures['onp_kiseki_don_1p'][4], 1187 - 29, 130 - 29, self.color)

class NoteArc:
    def __init__(self, note_texture: ray.Texture, current_ms: float, player_number: int, big: bool):
        self.texture = note_texture
        self.is_big = big
        self.arc_points = 22
        self.create_ms = current_ms
        self.player_number = player_number
        curve_height = 425

        self.start_x, self.start_y = 350, 192
        self.end_x, self.end_y = 1158, 101

        if self.player_number == 1:
            # Control point influences the curve shape
            self.control_x = (self.start_x + self.end_x) // 2
            self.control_y = min(self.start_y, self.end_y) - curve_height  # Arc upward
        else:
            # For player 2 (assumed to be a downward arc)
            self.control_x = (self.start_x + self.end_x) // 2
            self.control_y = max(self.start_y, self.end_y) + curve_height  # Arc downward

        self.x_i = self.start_x
        self.y_i = self.start_y
        self.is_finished = False

        num_precalc_points = 100  # More points for better approximation
        self.path_points = []
        self.path_distances = [0.0]  # Cumulative distance at each point

        prev_x, prev_y = self.start_x, self.start_y
        total_distance = 0.0

        for i in range(1, num_precalc_points + 1):
            t = i / num_precalc_points
            x = int((1-t)**2 * self.start_x + 2*(1-t)*t * self.control_x + t**2 * self.end_x)
            y = int((1-t)**2 * self.start_y + 2*(1-t)*t * self.control_y + t**2 * self.end_y)

            # Calculate distance from previous point
            dx = x - prev_x
            dy = y - prev_y
            distance = math.sqrt(dx*dx + dy*dy)
            total_distance += distance

            self.path_points.append((x, y))
            self.path_distances.append(total_distance)

            prev_x, prev_y = x, y

        self.total_path_length = total_distance
        self.x_i = self.start_x
        self.y_i = self.start_y
        self.is_finished = False

    def update(self, current_ms: float):
        if self.x_i >= self.end_x:
            self.is_finished = True
            self.x_i = self.end_x
            self.y_i = self.end_y
            return

        ms_since_call = (current_ms - self.create_ms) / 16.67
        ms_since_call = max(0, min(ms_since_call, self.arc_points))

        # Calculate desired distance along the path (constant speed)
        target_distance = (ms_since_call / self.arc_points) * self.total_path_length

        # Find the closest pre-calculated points
        index = 0
        while index < len(self.path_distances) - 1 and self.path_distances[index + 1] < target_distance:
            index += 1

        # Interpolate between the points
        if index < len(self.path_distances) - 1:
            d1 = self.path_distances[index]
            d2 = self.path_distances[index + 1]
            if d2 > d1:  # Avoid division by zero
                fraction = (target_distance - d1) / (d2 - d1)
                x1, y1 = self.path_points[index - 1] if index > 0 else (self.start_x, self.start_y)
                x2, y2 = self.path_points[index]
                self.x_i = int(x1 + fraction * (x2 - x1))
                self.y_i = int(y1 + fraction * (y2 - y1))
        else:
            # At the end of the path
            self.x_i = self.end_x
            self.y_i = self.end_y

    def draw(self, game_screen):
        ray.draw_texture(self.texture, self.x_i, self.y_i, ray.WHITE)

class DrumrollCounter:
    def __init__(self, current_ms: float):
        self.create_ms = current_ms
        self.is_finished = False
        self.total_duration = 1349
        self.drumroll_count = 0
        self.fade_animation = Animation.create_fade(166, delay=self.total_duration - 166)
        self.stretch_animation = Animation.create_text_stretch(0)

    def update_count(self, count: int, elapsed_time: float):
        self.total_duration = elapsed_time + 1349
        self.fade_animation.delay = self.total_duration - 166
        if self.drumroll_count != count:
            self.drumroll_count = count
            self.stretch_animation = Animation.create_text_stretch(50)

    def update(self, game_screen: GameScreen, current_ms: float, drumroll_count: int):
        self.stretch_animation.update(current_ms)
        self.fade_animation.update(current_ms)

        elapsed_time = current_ms - self.create_ms
        if drumroll_count != 0:
            self.update_count(drumroll_count, elapsed_time)
        if self.fade_animation.is_finished:
            self.is_finished = True

    def draw(self, game_screen: GameScreen):
        color = ray.fade(ray.WHITE, self.fade_animation.attribute)
        ray.draw_texture(game_screen.textures['renda_num'][0], 200, 0, color)
        counter = str(self.drumroll_count)
        total_width = len(counter) * 52
        start_x = 344 - (total_width // 2)
        source_rect = ray.Rectangle(0, 0, game_screen.textures['renda_num'][1].width, game_screen.textures['renda_num'][1].height)
        for i in range(len(counter)):
            dest_rect = ray.Rectangle(start_x + (i * 52), 50 - self.stretch_animation.attribute, game_screen.textures['renda_num'][1].width, game_screen.textures['renda_num'][1].height + self.stretch_animation.attribute)
            ray.draw_texture_pro(game_screen.textures['renda_num'][int(counter[i])+1], source_rect, dest_rect, ray.Vector2(0,0), 0, color)

class BalloonAnimation:
    def __init__(self, current_ms: float, balloon_total: int):
        self.create_ms = current_ms
        self.is_finished = False
        self.total_duration = 83.33
        self.color = ray.fade(ray.WHITE, 1.0)
        self.balloon_count = 0
        self.balloon_total = balloon_total
        self.is_popped = False
        self.fade_animation = Animation.create_fade(166)
        self.stretch_animation = Animation.create_text_stretch(0)

    def update_count(self, balloon_count: int):
        if self.balloon_count != balloon_count:
            self.balloon_count = balloon_count
            self.stretch_animation = Animation.create_text_stretch(50)

    def update(self, game_screen: GameScreen, current_ms: float, balloon_count: int, is_popped: bool):
        self.update_count(balloon_count)
        self.stretch_animation.update(current_ms)
        self.is_popped = is_popped

        elapsed_time = current_ms - self.create_ms
        if self.is_popped:
            self.fade_animation.update(current_ms)
            self.color = ray.fade(ray.WHITE, self.fade_animation.attribute)
        else:
            self.total_duration = elapsed_time + 166
            self.fade_animation.delay = self.total_duration - 166
        if self.fade_animation.is_finished:
            self.is_finished = True

    def draw(self, game_screen: GameScreen):
        if self.is_popped:
            ray.draw_texture(game_screen.textures['action_fusen_1p'][18], 460, 130, self.color)
        elif self.balloon_count >= 1:
            balloon_index = min(7, (self.balloon_count - 1) * 7 // self.balloon_total)
            ray.draw_texture(game_screen.textures['action_fusen_1p'][balloon_index+11], 460, 130, self.color)
        if self.balloon_count > 0:
            ray.draw_texture(game_screen.textures['action_fusen_1p'][0], 414, 40, ray.WHITE)
            counter = str(max(0, self.balloon_total - self.balloon_count + 1))
            x, y = 493, 68
            margin = 52
            total_width = len(counter) * margin
            start_x = x - (total_width // 2)
            source_rect = ray.Rectangle(0, 0, game_screen.textures['action_fusen_1p'][1].width, game_screen.textures['action_fusen_1p'][1].height)
            for i in range(len(counter)):
                dest_rect = ray.Rectangle(start_x + (i * margin), y - self.stretch_animation.attribute, game_screen.textures['action_fusen_1p'][1].width, game_screen.textures['action_fusen_1p'][1].height + self.stretch_animation.attribute)
                ray.draw_texture_pro(game_screen.textures['action_fusen_1p'][int(counter[i])+1], source_rect, dest_rect, ray.Vector2(0,0), 0, self.color)

class Combo:
    def __init__(self, combo: int, current_ms: float):
        self.combo = combo
        self.stretch_animation = Animation.create_text_stretch(0)
        self.color = [ray.fade(ray.WHITE, 1), ray.fade(ray.WHITE, 1), ray.fade(ray.WHITE, 1)]
        self.glimmer_dict = {0: 0, 1: 0, 2: 0}
        self.total_time = 250
        self.cycle_time = self.total_time * 2
        self.start_times = [
                    current_ms,
                    current_ms + (2 / 3) * self.cycle_time,
                    current_ms + (4 / 3) * self.cycle_time
                ]

    def update_count(self, current_ms: float, combo: int):
        if self.combo != combo:
            self.combo = combo
            self.stretch_animation = Animation.create_text_stretch(50)

    def update(self, game_screen: GameScreen, current_ms: float, combo: int):
        self.update_count(current_ms, combo)
        self.stretch_animation.update(current_ms)

        for i in range(3):
            elapsed_time = current_ms - self.start_times[i]
            if elapsed_time > self.cycle_time:
                cycles_completed = elapsed_time // self.cycle_time
                self.start_times[i] += cycles_completed * self.cycle_time
                elapsed_time = current_ms - self.start_times[i]
            if elapsed_time <= self.total_time:
                self.glimmer_dict[i] = -int(elapsed_time // 16.67)
                fade_start_time = self.total_time - 164
                if elapsed_time >= fade_start_time:
                    fade = 1 - (elapsed_time - fade_start_time) / 164
                else:
                    fade = 1
            else:
                self.glimmer_dict[i] = 0
                fade = 0
            self.color[i] = ray.fade(ray.WHITE, fade)

    def draw(self, game_screen: GameScreen):
        if self.combo < 3:
            return
        if self.combo < 100:
            text_color = 0
            margin = 30
        else:
            text_color = 1
            margin = 35
        ray.draw_texture(game_screen.texture_combo_text[text_color], 234, 265, ray.WHITE)
        counter = str(self.combo)
        total_width = len(counter) * margin
        x, y = 262, 220
        start_x = x - (total_width // 2)
        source_rect = ray.Rectangle(0, 0, game_screen.texture_combo_numbers[0].width, game_screen.texture_combo_numbers[0].height)
        for i in range(len(counter)):
            dest_rect = ray.Rectangle(start_x + (i * margin), y - self.stretch_animation.attribute, game_screen.texture_combo_numbers[0].width, game_screen.texture_combo_numbers[0].height + self.stretch_animation.attribute)
            ray.draw_texture_pro(game_screen.texture_combo_numbers[int(counter[i]) + (text_color*10)], source_rect, dest_rect, ray.Vector2(0,0), 0, ray.WHITE)
        glimmer_positions = [(225, 210), (200, 230), (250, 230)]
        if self.combo >= 100:
            for j, (x, y) in enumerate(glimmer_positions):
                for i in range(3):
                    ray.draw_texture(game_screen.texture_combo_glimmer, x + (i * 30), y + self.glimmer_dict[j], self.color[j])

class ScoreCounter:
    def __init__(self, score: int):
        self.score = score
        self.stretch_animation = Animation.create_text_stretch(0)

    def update_count(self, current_ms: float, score: int):
        if self.score != score:
            self.score = score
            self.stretch_animation = Animation.create_text_stretch(50)

    def update(self, current_ms: float, score: int):
        self.update_count(current_ms, score)
        if self.score > 0:
            self.stretch_animation.update(current_ms)

    def draw(self, game_screen: GameScreen):
        counter = str(self.score)
        x, y = 150, 185
        margin = 20
        total_width = len(counter) * margin
        start_x = x - total_width
        source_rect = ray.Rectangle(0, 0, game_screen.textures['lane_obi'][4].width, game_screen.textures['lane_obi'][4].height)
        for i in range(len(counter)):
            dest_rect = ray.Rectangle(start_x + (i * margin), y - self.stretch_animation.attribute, game_screen.textures['lane_obi'][4].width, game_screen.textures['lane_obi'][4].height + self.stretch_animation.attribute)
            ray.draw_texture_pro(game_screen.textures['lane_obi'][int(counter[i])+4], source_rect, dest_rect, ray.Vector2(0,0), 0, ray.WHITE)

class ScoreCounterAnimation:
    def __init__(self, counter: int):
        self.counter = counter
        self.fade_animation_1 = Animation.create_fade(50, initial_opacity=0.0, final_opacity=1.0)
        self.move_animation_1 = Animation.create_move(80, total_distance=-20, start_position=175)
        self.fade_animation_2 = Animation.create_fade(80, delay=366.74)
        self.move_animation_2 = Animation.create_move(66, total_distance=5, start_position=145, delay=80)
        self.move_animation_3 = Animation.create_move(66, delay=279.36, total_distance=-2, start_position=146)
        self.move_animation_4 = Animation.create_move(80, delay=366.74, total_distance=10, start_position=148)

        self.color = ray.fade(ray.Color(254, 102, 0, 255), 1.0)
        self.is_finished = False
        self.y_pos_list = []

    def update(self, current_ms: float):
        self.fade_animation_1.update(current_ms)
        self.move_animation_1.update(current_ms)
        self.move_animation_2.update(current_ms)
        self.move_animation_3.update(current_ms)
        self.move_animation_4.update(current_ms)
        self.fade_animation_2.update(current_ms)

        if self.fade_animation_1.is_finished:
            self.color = ray.fade(ray.Color(254, 102, 0, 255), self.fade_animation_2.attribute)
        else:
            self.color = ray.fade(ray.Color(254, 102, 0, 255), self.fade_animation_1.attribute)
        if self.fade_animation_2.is_finished:
            self.is_finished = True
        self.y_pos_list = []
        for i in range(1, len(str(self.counter))+1):
            self.y_pos_list.append(self.move_animation_4.attribute + i*5)

    def draw(self, game_screen: GameScreen):
        if self.move_animation_1.is_finished:
            x = self.move_animation_2.attribute
        else:
            x = self.move_animation_1.attribute
        if x == 0:
            return
        counter = str(self.counter)
        margin = 20
        total_width = len(counter) * margin
        start_x = x - total_width
        source_rect = ray.Rectangle(0, 0, game_screen.textures['score_add_1p'][0].width, game_screen.textures['score_add_1p'][0].height)
        for i in range(len(counter)):
            if self.move_animation_3.is_finished:
                y = self.y_pos_list[i]
            elif self.move_animation_2.is_finished:
                y = self.move_animation_3.attribute
            else:
                y = 148
            dest_rect = ray.Rectangle(start_x + (i * margin), y, game_screen.textures['score_add_1p'][0].width, game_screen.textures['score_add_1p'][0].height)
            ray.draw_texture_pro(game_screen.textures['score_add_1p'][int(counter[i])], source_rect, dest_rect, ray.Vector2(0,0), 0, self.color)

class SongInfo:
    FADE_DURATION = 366
    DISPLAY_DURATION = 1666

    def __init__(self, song_name: str, genre: str):
        self.song_name = song_name
        self.genre = genre
        self.song_title = OutlinedText(
            song_name, 40, ray.Color(255, 255, 255, 255), ray.Color(0, 0, 0, 255), outline_thickness=5
        )
        self.fade_in = Animation.create_fade(self.FADE_DURATION, initial_opacity=0.0, final_opacity=1.0)
        self.fade_out = Animation.create_fade(self.FADE_DURATION, delay=self.DISPLAY_DURATION)
        self.fade_fake = Animation.create_fade(0, delay=self.DISPLAY_DURATION*2 + self.FADE_DURATION)

    def update(self, current_ms: float):
        self.fade_in.update(current_ms)
        self.fade_out.update(current_ms)
        self.fade_fake.update(current_ms)

        if not self.fade_in.is_finished:
            self.song_num_fade = ray.fade(ray.WHITE, self.fade_in.attribute)
            self.song_name_fade = ray.fade(ray.WHITE, 1 - self.fade_in.attribute)
        else:
            self.song_num_fade = ray.fade(ray.WHITE, self.fade_out.attribute)
            self.song_name_fade = ray.fade(ray.WHITE, 1 - self.fade_out.attribute)

        if self.fade_fake.is_finished:
            self._reset_animations(current_ms)

    def _reset_animations(self, current_ms: float):
        self.fade_in = Animation.create_fade(self.FADE_DURATION, initial_opacity=0.0, final_opacity=1.0)
        self.fade_out = Animation.create_fade(self.FADE_DURATION, delay=self.DISPLAY_DURATION)
        self.fade_fake = Animation.create_fade(0, delay=self.DISPLAY_DURATION*2 + self.FADE_DURATION)

    def draw(self, game_screen: GameScreen):
        song_texture_index = (global_data.songs_played % 4) + 8
        ray.draw_texture(
            game_screen.textures['song_info'][song_texture_index],
            1132, 25,
            self.song_num_fade
        )

        text_x = 1252 - self.song_title.texture.width
        text_y = int(50 - self.song_title.texture.height / 2)
        src = ray.Rectangle(0, 0, self.song_title.texture.width, self.song_title.texture.height)
        dest = ray.Rectangle(text_x, text_y, self.song_title.texture.width, self.song_title.texture.height)
        self.song_title.draw(src, dest, ray.Vector2(0, 0), 0, self.song_name_fade)

class Transition:
    def __init__(self, screen_height: int) -> None:
        self.is_finished = False
        self.rainbow_up = Animation.create_move(266, start_position=0, total_distance=screen_height + global_data.textures['scene_change_rainbow'][2].height, ease_in='cubic')
        self.chara_down = None
    def update(self, current_time_ms: float):
        self.rainbow_up.update(current_time_ms)
        if self.rainbow_up.is_finished and self.chara_down is None:
            self.chara_down = Animation.create_move(33, start_position=0, total_distance=30)

        if self.chara_down is not None:
            self.chara_down.update(current_time_ms)
            self.is_finished = self.chara_down.is_finished

    def draw(self, screen_height: int):
        ray.draw_texture(global_data.textures['scene_change_rainbow'][1], 0, screen_height - int(self.rainbow_up.attribute), ray.WHITE)
        texture = global_data.textures['scene_change_rainbow'][0]
        src = ray.Rectangle(0, 0, texture.width, texture.height)
        dest = ray.Rectangle(0, -int(self.rainbow_up.attribute), texture.width, screen_height)
        ray.draw_texture_pro(texture, src, dest, ray.Vector2(0, 0), 0, ray.WHITE)
        texture = global_data.textures['scene_change_rainbow'][3]
        offset = 0
        if self.chara_down is not None:
            offset = int(self.chara_down.attribute)
        ray.draw_texture(texture, 76, -int(self.rainbow_up.attribute*3) - offset, ray.WHITE)

class ResultTransition:
    def __init__(self, screen_height: int):
        self.move = Animation.create_move(983.33, start_position=0, total_distance=screen_height//2, ease_out='quadratic')

        self.is_finished = False

    def update(self, current_ms: float):
        self.move.update(current_ms)

        if self.move.is_finished:
            self.is_finished = True

    def draw(self, screen_width: int, screen_height: int, texture_1: ray.Texture, texture_2: ray.Texture):
        x = 0
        while x < screen_width:
            ray.draw_texture(texture_1, x, (0 - texture_1.height) + int(self.move.attribute), ray.WHITE)
            ray.draw_texture(texture_1, x, (screen_height) - int(self.move.attribute), ray.WHITE)
            x += texture_1.width
        x = 0
        while x < screen_width:
            ray.draw_texture(texture_2, x, (0 - texture_2.height//2) - (texture_1.height//2) + int(self.move.attribute), ray.WHITE)
            ray.draw_texture(texture_2, x, (screen_height) + (texture_1.height//2) - (texture_2.height//2) - int(self.move.attribute), ray.WHITE)
            x += texture_2.width

class Gauge:
    GAUGE_MAX = 87
    def __init__(self, difficulty: int, level: int, total_notes: int):
        self.gauge_length = 0
        self.previous_length = 0
        self.total_notes = total_notes
        self.difficulty = min(3, difficulty)
        self.clear_start = [0, 0, 68, 68]
        self.level = min(10, level)
        self.table = [
            [
                None,
                {"clear_rate": 36.0, "ok_multiplier": 0.75, "bad_multiplier": -0.5},
                {"clear_rate": 38.0, "ok_multiplier": 0.75, "bad_multiplier": -0.5},
                {"clear_rate": 38.0, "ok_multiplier": 0.75, "bad_multiplier": -0.5},
                {"clear_rate": 44.0, "ok_multiplier": 0.75, "bad_multiplier": -0.5},
                {"clear_rate": 44.0, "ok_multiplier": 0.75, "bad_multiplier": -0.5},
            ],
            [
                None,
                {"clear_rate": 45.939, "ok_multiplier": 0.75, "bad_multiplier": -0.5},
                {"clear_rate": 45.939, "ok_multiplier": 0.75, "bad_multiplier": -0.5},
                {"clear_rate": 48.676, "ok_multiplier": 0.75, "bad_multiplier": -0.5},
                {"clear_rate": 49.232, "ok_multiplier": 0.75, "bad_multiplier": -0.75},
                {"clear_rate": 52.5, "ok_multiplier": 0.75, "bad_multiplier": -1.0},
                {"clear_rate": 52.5, "ok_multiplier": 0.75, "bad_multiplier": -1.0},
                {"clear_rate": 52.5, "ok_multiplier": 0.75, "bad_multiplier": -1.0},
            ],
            [
                None,
                {"clear_rate": 54.325, "ok_multiplier": 0.75, "bad_multiplier": -0.75},
                {"clear_rate": 54.325, "ok_multiplier": 0.75, "bad_multiplier": -0.75},
                {"clear_rate": 50.774, "ok_multiplier": 0.75, "bad_multiplier": -1.0},
                {"clear_rate": 48.410, "ok_multiplier": 0.75, "bad_multiplier": -1.17},
                {"clear_rate": 47.246, "ok_multiplier": 0.75, "bad_multiplier": -1.25},
                {"clear_rate": 48.120, "ok_multiplier": 0.75, "bad_multiplier": -1.25},
                {"clear_rate": 48.120, "ok_multiplier": 0.75, "bad_multiplier": -1.25},
                {"clear_rate": 48.120, "ok_multiplier": 0.75, "bad_multiplier": -1.25},
            ],
            [
                None,
                {"clear_rate": 56.603, "ok_multiplier": 0.5, "bad_multiplier": -1.6},
                {"clear_rate": 56.603, "ok_multiplier": 0.5, "bad_multiplier": -1.6},
                {"clear_rate": 56.603, "ok_multiplier": 0.5, "bad_multiplier": -1.6},
                {"clear_rate": 56.603, "ok_multiplier": 0.5, "bad_multiplier": -1.6},
                {"clear_rate": 56.603, "ok_multiplier": 0.5, "bad_multiplier": -1.6},
                {"clear_rate": 56.603, "ok_multiplier": 0.5, "bad_multiplier": -1.6},
                {"clear_rate": 56.603, "ok_multiplier": 0.5, "bad_multiplier": -1.6},
                {"clear_rate": 56.0, "ok_multiplier": 0.5, "bad_multiplier": -2.0},
                {"clear_rate": 61.428, "ok_multiplier": 0.5, "bad_multiplier": -2.0},
                {"clear_rate": 61.428, "ok_multiplier": 0.5, "bad_multiplier": -2.0},
            ]
        ]
        self.gauge_update_anim = None
        self.rainbow_fade_in = None
        self.rainbow_animation = None

    def add_good(self):
        self.gauge_update_anim = Animation.create_fade(450)
        self.previous_length = int(self.gauge_length)
        self.gauge_length += (1 / self.total_notes) * (100 * (self.clear_start[self.difficulty] / self.table[self.difficulty][self.level]["clear_rate"]))
        if self.gauge_length > 87:
            self.gauge_length = 87

    def add_ok(self):
        self.gauge_update_anim = Animation.create_fade(450)
        self.previous_length = int(self.gauge_length)
        self.gauge_length += ((1 * self.table[self.difficulty][self.level]["ok_multiplier"]) / self.total_notes) * (100 * (self.clear_start[self.difficulty] / self.table[self.difficulty][self.level]["clear_rate"]))
        if self.gauge_length > 87:
            self.gauge_length = 87

    def add_bad(self):
        self.previous_length = int(self.gauge_length)
        self.gauge_length += ((1 * self.table[self.difficulty][self.level]["bad_multiplier"]) / self.total_notes) * (100 * (self.clear_start[self.difficulty] / self.table[self.difficulty][self.level]["clear_rate"]))
        if self.gauge_length < 0:
            self.gauge_length = 0

    def update(self, current_ms: float, good_count: int, ok_count: int, bad_count: int, total_notes: int):
        if self.gauge_length == 87 and self.rainbow_fade_in is None:
            self.rainbow_fade_in = Animation.create_fade(450, initial_opacity=0.0, final_opacity=1.0)

        if self.gauge_update_anim is not None:
            self.gauge_update_anim.update(current_ms)
            if self.gauge_update_anim.is_finished:
                self.gauge_update_anim = None

        if self.rainbow_fade_in is not None:
            self.rainbow_fade_in.update(current_ms)

        if self.rainbow_animation is None:
            self.rainbow_animation = Animation.create_texture_change((16.67*8) * 3, textures=[((16.67 * 3) * i, (16.67 * 3) * (i + 1), i) for i in range(8)])
        else:
            self.rainbow_animation.update(current_ms)
            if self.rainbow_animation.is_finished or self.gauge_length < 87:
                self.rainbow_animation = None

    def draw(self, textures: list[ray.Texture]):
        ray.draw_texture(textures[0], 327, 132, ray.WHITE)
        ray.draw_texture(textures[1], 483, 124, ray.WHITE)
        gauge_length = int(self.gauge_length)
        for i in range(gauge_length):
            if i == 68:
                ray.draw_texture(textures[16], 491 + (i*textures[13].width), 160 - 24, ray.WHITE)
            elif i > 68:
                ray.draw_texture(textures[15], 491 + (i*textures[13].width) + 2, 160 - 22, ray.WHITE)
                ray.draw_texture(textures[20], 491 + (i*textures[13].width) + 2, 160, ray.WHITE)
            else:
                ray.draw_texture(textures[13], 491 + (i*textures[13].width), 160, ray.WHITE)
        if gauge_length == 87 and self.rainbow_fade_in is not None and self.rainbow_animation is not None:
            if 0 < self.rainbow_animation.attribute < 8:
                ray.draw_texture(textures[1 + int(self.rainbow_animation.attribute)], 483, 124, ray.fade(ray.WHITE, self.rainbow_fade_in.attribute))
            ray.draw_texture(textures[2 + int(self.rainbow_animation.attribute)], 483, 124, ray.fade(ray.WHITE, self.rainbow_fade_in.attribute))
        if self.gauge_update_anim is not None and gauge_length < 88 and gauge_length != self.previous_length:
            if gauge_length == 69:
                ray.draw_texture(textures[17], 491 + (gauge_length*textures[13].width) - 13, 160 - 8 - 24, ray.fade(ray.WHITE, self.gauge_update_anim.attribute))
            elif gauge_length > 69:
                ray.draw_texture(textures[21], 491 + (gauge_length*textures[13].width) - 13, 160 - 8 - 22, ray.fade(ray.WHITE, self.gauge_update_anim.attribute))
            else:
                ray.draw_texture(textures[14], 491 + (gauge_length*textures[13].width) - 13, 160 - 8, ray.fade(ray.WHITE, self.gauge_update_anim.attribute))
        ray.draw_texture(textures[10], 483, 124, ray.fade(ray.WHITE, 0.15))
        if gauge_length >= 70:
            ray.draw_texture(textures[18], 1038, 141, ray.WHITE)
            ray.draw_texture(textures[19], 1187, 130, ray.WHITE)
        else:
            ray.draw_texture(textures[11], 1038, 141, ray.WHITE)
            ray.draw_texture(textures[12], 1187, 130, ray.WHITE)
