import bisect
import math
import sqlite3
from collections import deque
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.texture import tex
from libs.tja import (
    Balloon,
    Drumroll,
    Note,
    TJAParser,
    apply_modifiers,
    calculate_base_score,
)
from libs.transition import Transition
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,
    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.screen_init = False
        self.movie = None
        self.song_music = None
        self.end_ms = 0
        self.start_delay = 1000
        self.song_started = False

    def load_sounds(self):
        sounds_dir = Path("Sounds")
        if global_data.hit_sound == -1:
            self.sound_don = audio.load_sound(Path('none.wav'))
            self.sound_kat = audio.load_sound(Path('none.wav'))
        if global_data.hit_sound == 0:
            self.sound_don = audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound) / "don.wav")
            self.sound_kat = audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound) / "ka.wav")
        else:
            self.sound_don = audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound) / "don.ogg")
            self.sound_kat = audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound) / "ka.ogg")
        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_kusudama_pop = audio.load_sound(sounds_dir / "kusudama_pop.ogg")
        self.sound_result_transition = audio.load_sound(sounds_dir / "result" / "VO_RESULT [1].ogg")

    def init_tja(self, song: Path, difficulty: int):
        if song == Path(''):
            self.start_ms = get_current_ms()
            self.tja = None
        else:
            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'])
            if self.tja.metadata.wave.exists() and self.tja.metadata.wave.is_file():
                self.song_music = audio.load_music_stream(self.tja.metadata.wave)
                audio.normalize_music_stream(self.song_music, 0.1935)

        self.player_1 = Player(self, global_data.player_num, difficulty)
        if self.tja is not None:
            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
            tex.load_screen_textures('game')
            self.background = Background(self.width, self.height, global_data.player_num)
            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 = ResultTransition(global_data.player_num)
            if self.tja is not None:
                subtitle = self.tja.metadata.subtitle.get(global_data.config['general']['language'].lower(), '')
            else:
                subtitle = ''
            self.transition = Transition(session_data.song_title, subtitle, is_second=True)
            self.transition.start()

    def on_screen_end(self, next_screen):
        self.screen_init = False
        tex.unload_textures()
        if self.song_music is not None:
            audio.unload_music_stream(self.song_music)
        del self.song_music
        self.song_started = False
        self.end_ms = 0
        self.movie = None
        self.background.unload()
        return next_screen

    def write_score(self):
        if self.tja is None:
            return
        if global_data.modifiers.auto:
            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]:
                if result is None:
                    session_data.prev_score = 0
                else:
                    session_data.prev_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()
        self.transition.update(get_current_ms())
        self.current_ms = get_current_ms() - self.start_ms
        if self.tja is not None:
            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_music_stream_playing(self.song_music):
                        audio.play_music_stream(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)
        self.song_info.update(get_current_ms())

        if self.song_music is not None:
            audio.update_music_stream(self.song_music)

        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:
                    if not self.result_transition.is_started:
                        self.result_transition.start()
                        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):
            if self.song_music is not None:
                audio.stop_music_stream(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):
            if self.song_music is not None:
                audio.stop_music_stream(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)
        self.song_info.draw()
        self.transition.draw()
        self.result_transition.draw()

    def draw_3d(self):
        self.player_1.draw_3d()

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 = str(player_number)
        self.difficulty = difficulty
        self.visual_offset = global_data.config["general"]["visual_offset"]

        if game_screen.tja is not None:
            self.play_notes, self.draw_note_list, self.draw_bar_list = game_screen.tja.notes_to_position(self.difficulty)
            self.play_notes, self.draw_note_list, self.draw_bar_list = apply_modifiers(self.play_notes, self.draw_note_list, self.draw_bar_list)
        else:
            self.play_notes, self.draw_note_list, self.draw_bar_list = deque(), deque(), deque()
        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.kusudama_anim: Optional[KusudamaAnimation] = 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()

        if game_screen.tja is not None:
            stars = game_screen.tja.metadata.course_data[self.difficulty].level
        else:
            stars = 0
        self.gauge = Gauge(self.player_number, self.difficulty, stars, 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_x(self, width: int, current_ms: float, load_ms: float, pixels_per_frame: float) -> int:
        return int(width + pixels_per_frame * (60 / 1000) * (load_ms - current_ms) - 64) - self.visual_offset

    def get_position_y(self, current_ms: float, load_ms: float, pixels_per_frame: float, pixels_per_frame_x) -> int:
        return int((pixels_per_frame * (60 / 1000) * (load_ms - current_ms)) + (((1280 - GameScreen.JUDGE_X) * pixels_per_frame) / pixels_per_frame_x))

    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_x(game_screen.width, game_screen.current_ms, bar.hit_ms, bar.pixels_per_frame_x)
            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 or note.type == 9:
                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_x(game_screen.width, game_screen.current_ms, note.hit_ms, note.pixels_per_frame_x)
        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, note: Note):
        self.play_notes.popleft()
        index = note.index
        if note.type == 7:
            self.play_notes.popleft()

        if note.type < 7:
            self.combo += 1
            if self.combo > self.max_combo:
                self.max_combo = self.combo

        if note.type != 9:
            self.draw_arc_list.append(NoteArc(note.type, get_current_ms(), 1, note.type == 3 or note.type == 4) or note.type == 7)

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

    def check_drumroll(self, drum_type: int):
        self.draw_arc_list.append(NoteArc(drum_type, get_current_ms(), 1, 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(self.player_number, 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 note.is_kusudama:
            self.check_kusudama(game_screen, note)
            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(self.player_number, 100))
        if self.curr_balloon_count == note.count:
            self.is_balloon = False
            note.popped = True
            self.balloon_anim.update(get_current_ms(), self.curr_balloon_count, note.popped)
            audio.play_sound(game_screen.sound_balloon_pop)
            self.note_correct(self.play_notes[0])

    def check_kusudama(self, game_screen: GameScreen, note: Balloon):
        if self.kusudama_anim is None:
            self.kusudama_anim = KusudamaAnimation(note.count)
        self.curr_balloon_count += 1
        self.total_drumroll += 1
        self.score += 100
        self.base_score_list.append(ScoreCounterAnimation(self.player_number, 100))
        if self.curr_balloon_count == note.count:
            audio.play_sound(game_screen.sound_kusudama_pop)
            self.is_balloon = False
            note.popped = True

    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(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.player_number, self.base_score))
                self.note_correct(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(self.player_number, 10 * math.floor(self.base_score / 2 / 10)))
                self.note_correct(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):
        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(get_current_ms(), self.curr_drumroll_count)

    def balloon_manager(self):
        if self.balloon_anim is not None:
            self.balloon_anim.update(get_current_ms(), self.curr_balloon_count, not self.is_balloon)
            if self.balloon_anim.is_finished:
                self.balloon_anim = None
        if self.kusudama_anim is not None:
            self.kusudama_anim.update(get_current_ms(), not self.is_balloon)
            self.kusudama_anim.update_count(self.curr_balloon_count)
            if self.kusudama_anim.is_finished:
                self.kusudama_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))

                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.modifiers.auto:
            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:
                    type = 3
                else:
                    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(get_current_ms(), self.combo)
        self.drumroll_counter_manager()
        self.animation_manager(self.draw_judge_list)
        self.balloon_manager()
        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.note_type, 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())

    def draw_drumroll(self, game_screen: GameScreen, head: Drumroll, current_eighth: int):
        start_position = self.get_position_x(game_screen.width, game_screen.current_ms, head.load_ms, head.pixels_per_frame_x)
        tail = next((note for note in self.current_notes_draw[1:] if note.type == 8 and note.index > head.index), self.current_notes_draw[1])
        is_big = int(head.type == 6)
        end_position = self.get_position_x(game_screen.width, game_screen.current_ms, tail.load_ms, tail.pixels_per_frame_x)
        length = end_position - start_position
        color = ray.Color(255, head.color, head.color, 255)
        if head.display:
            tex.draw_texture('notes', "8", frame=is_big, x=start_position+64, y=192, x2=length-64-32, color=color)
            if is_big:
                tex.draw_texture('notes', "drumroll_big_tail", x=end_position, y=192, color=color)
            else:
                tex.draw_texture('notes', "drumroll_tail", x=end_position, y=192, color=color)
            tex.draw_texture('notes', str(head.type), frame=current_eighth % 2, x=start_position, y=192, color=color)

        tex.draw_texture('notes', 'moji_drumroll_mid', x=start_position + 60, y=323, x2=length)
        tex.draw_texture('notes', 'moji', frame=head.moji, x=(start_position - (168//2)) + 64, y=323)
        tex.draw_texture('notes', 'moji', frame=tail.moji, x=(end_position - (168//2)) + 32, y=323)

    def draw_balloon(self, game_screen: GameScreen, head: Balloon, current_eighth: int):
        offset = 12
        start_position = self.get_position_x(game_screen.width, game_screen.current_ms, head.load_ms, head.pixels_per_frame_x)
        tail = next((note for note in self.current_notes_draw[1:] if note.type == 8 and note.index > head.index), self.current_notes_draw[1])
        end_position = self.get_position_x(game_screen.width, game_screen.current_ms, tail.load_ms, tail.pixels_per_frame_x)
        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
        if head.display:
            tex.draw_texture('notes', str(head.type), frame=current_eighth % 2, x=position-offset, y=192)
        tex.draw_texture('notes', '10', frame=current_eighth % 2, x=position-offset+128, y=192)

    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
            x_position = self.get_position_x(game_screen.width, game_screen.current_ms, bar.load_ms, bar.pixels_per_frame_x)
            y_position = self.get_position_y(game_screen.current_ms, bar.load_ms, bar.pixels_per_frame_y, bar.pixels_per_frame_x)
            tex.draw_texture('notes', str(bar.type), x=x_position+60, y=y_position+190)

    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
            x_position = self.get_position_x(game_screen.width, game_screen.current_ms, note.load_ms, note.pixels_per_frame_x)
            y_position = self.get_position_y(game_screen.current_ms, note.load_ms, note.pixels_per_frame_y, note.pixels_per_frame_x)
            if isinstance(note, Drumroll):
                self.draw_drumroll(game_screen, note, current_eighth)
            elif isinstance(note, Balloon) and not note.is_kusudama:
                self.draw_balloon(game_screen, note, current_eighth)
                tex.draw_texture('notes', 'moji', frame=note.moji, x=x_position - (168//2) + 64, y=323 + y_position)
            else:
                if note.display:
                    tex.draw_texture('notes', str(note.type), frame=current_eighth % 2, x=x_position, y=y_position+192, center=True)
                tex.draw_texture('notes', 'moji', frame=note.moji, x=x_position - (168//2) + 64, y=323 + y_position)

    def draw_modifiers(self):
        tex.draw_texture('lane', 'mod_shinuchi')
        if global_data.modifiers.speed >= 4:
            tex.draw_texture('lane', 'mod_yonbai')
        elif global_data.modifiers.speed >= 3:
            tex.draw_texture('lane', 'mod_sanbai')
        elif global_data.modifiers.speed > 1:
            tex.draw_texture('lane', 'mod_baisaku')
        if global_data.modifiers.display:
            tex.draw_texture('lane', 'mod_doron')
        if global_data.modifiers.inverse:
            tex.draw_texture('lane', 'mod_abekobe')
        if global_data.modifiers.random == 2:
            tex.draw_texture('lane', 'mod_detarame')
        elif global_data.modifiers.random == 1:
            tex.draw_texture('lane', 'mod_kimagure')

    def draw(self, game_screen: GameScreen):
        tex.draw_texture('lane', 'lane_background')
        self.gauge.draw()
        if self.lane_hit_effect is not None:
            self.lane_hit_effect.draw()
        tex.draw_texture('lane', 'lane_hit_circle')
        for anim in self.draw_judge_list:
            anim.draw()
        self.draw_bars(game_screen)
        self.draw_notes(game_screen)
        tex.draw_texture('lane', f'{self.player_number}p_lane_cover')
        tex.draw_texture('lane', 'drum')
        if global_data.modifiers.auto:
            tex.draw_texture('lane', 'auto_icon')
        for anim in self.draw_drum_hit_list:
            anim.draw()
        self.combo_display.draw()
        tex.draw_texture('lane', 'lane_score_cover')
        tex.draw_texture('lane', f'{self.player_number}p_icon')
        tex.draw_texture('lane', 'lane_difficulty', frame=self.difficulty)
        self.draw_modifiers()
        if self.drumroll_counter is not None:
            self.drumroll_counter.draw()
        for anim in self.draw_arc_list:
            anim.draw()
        for anim in self.gauge_hit_effect:
            anim.draw()
        if self.balloon_anim is not None:
            self.balloon_anim.draw()
        if self.kusudama_anim is not None:
            self.kusudama_anim.draw()
        self.score_counter.draw()
        for anim in self.base_score_list:
            anim.draw()
        #ray.draw_circle(game_screen.width//2, game_screen.height, 300, ray.ORANGE)

    def draw_3d(self):
        pass

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_1.start()
        self.fade_animation_2 = Animation.create_fade(316 - 233.3, delay=233.3)
        self.fade_animation_2.start()
        self.move_animation = Animation.create_move(83, total_distance=15, start_position=144)
        self.move_animation.start()
        self.texture_animation = Animation.create_texture_change(100, textures=[(33, 50, 0), (50, 83, 1), (83, 100, 2), (100, float('inf'), 3)])
        self.texture_animation.start()

    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):
        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:
                tex.draw_texture('hit_effect', 'hit_effect_good_big', color=color)
                tex.draw_texture('hit_effect', 'outer_good_big', frame=index, color=hit_color)
            else:
                tex.draw_texture('hit_effect', 'hit_effect_good', color=color)
                tex.draw_texture('hit_effect', 'outer_good', frame=index, color=hit_color)
            tex.draw_texture('hit_effect', 'judge_good', y=y, color=color)
        elif self.type == 'OK':
            if self.big:
                tex.draw_texture('hit_effect', 'hit_effect_ok_big', color=color)
                tex.draw_texture('hit_effect', 'outer_ok_big', frame=index, color=hit_color)
            else:
                tex.draw_texture('hit_effect', 'hit_effect_ok', color=color)
                tex.draw_texture('hit_effect', 'outer_ok', frame=index, color=hit_color)
            tex.draw_texture('hit_effect', 'judge_ok', y=y, color=color)
        elif self.type == 'BAD':
            tex.draw_texture('hit_effect', 'judge_bad', y=y, color=color)

class LaneHitEffect:
    def __init__(self, type: str):
        self.type = type
        self.fade = tex.get_animation(0, is_copy=True)
        self.fade.start()
        self.is_finished = False

    def update(self, current_ms: float):
        self.fade.update(current_ms)
        if self.fade.is_finished:
            self.is_finished = True

    def draw(self):
        if self.type == 'GOOD':
            tex.draw_texture('lane', 'lane_hit_effect', frame=2, fade=self.fade.attribute)
        elif self.type == 'DON':
            tex.draw_texture('lane', 'lane_hit_effect', frame=0, fade=self.fade.attribute)
        elif self.type == 'KAT':
            tex.draw_texture('lane', 'lane_hit_effect', frame=1, fade=self.fade.attribute)

class DrumHitEffect:
    def __init__(self, type: str, side: str):
        self.type = type
        self.side = side
        self.is_finished = False
        self.fade = tex.get_animation(1, is_copy=True)
        self.fade.start()

    def update(self, current_ms: float):
        self.fade.update(current_ms)
        if self.fade.is_finished:
            self.is_finished = True

    def draw(self):
        if self.type == 'DON':
            if self.side == 'L':
                tex.draw_texture('lane', 'drum_don_l', fade=self.fade.attribute)
            elif self.side == 'R':
                tex.draw_texture('lane', 'drum_don_r', fade=self.fade.attribute)
        elif self.type == 'KAT':
            if self.side == 'L':
                tex.draw_texture('lane', 'drum_kat_l', fade=self.fade.attribute)
            elif self.side == 'R':
                tex.draw_texture('lane', 'drum_kat_r', fade=self.fade.attribute)

class GaugeHitEffect:
    def __init__(self, note_type: int, big: bool):
        self.note_type = note_type
        self.is_big = big
        self.texture_change = tex.get_animation(2, is_copy=True)
        self.texture_change.start()
        self.circle_fadein = Animation.create_fade(133, initial_opacity=0.0, final_opacity=1.0, delay=16.67)
        self.circle_fadein.start()
        self.resize = Animation.create_texture_resize(233, delay=self.texture_change.duration, initial_size=0.75, final_size=1.15)
        self.resize.start()
        self.fade_out = Animation.create_fade(66, delay=233)
        self.fade_out.start()
        self.rotation = Animation.create_fade(300, delay=116.67, initial_opacity=0.0, final_opacity=1.0)
        self.rotation.start()
        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.rotation.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):
        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
        dest_width = 152 * self.resize.attribute
        dest_height = 152 * self.resize.attribute
        origin = ray.Vector2(dest_width / 2, dest_height / 2)
        rotation = self.rotation.attribute*100
        tex.draw_texture('gauge', 'hit_effect', frame=self.texture_change.attribute, x2=-152 + (152 * self.resize.attribute), y2=-152 + (152 * self.resize.attribute), color=ray.fade(texture_color, self.fade_out.attribute), origin=origin, rotation=rotation, center=True)
        tex.draw_texture('notes', str(self.note_type), x=1158, y=101, fade=self.fade_out.attribute)
        if self.is_big:
            tex.draw_texture('gauge', 'hit_effect_circle_big', color=self.color)
        else:
            tex.draw_texture('gauge', 'hit_effect_circle', color=self.color)

class NoteArc:
    def __init__(self, note_type: int, current_ms: float, player_number: int, big: bool):
        self.note_type = note_type
        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):
        tex.draw_texture('notes', str(self.note_type), x=self.x_i, y=self.y_i)

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 = tex.get_animation(8)
        self.fade_animation.start()
        self.stretch_animation = tex.get_animation(9)

    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.start()

    def update(self, 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):
        color = ray.fade(ray.WHITE, self.fade_animation.attribute)
        tex.draw_texture('drumroll_counter', 'bubble', color=color)
        counter = str(self.drumroll_count)
        total_width = len(counter) * 52
        for i in range(len(counter)):
            tex.draw_texture('drumroll_counter', 'counter', color=color, frame=int(counter[i]), x=-(total_width//2)+(i*52), y=-self.stretch_animation.attribute, y2=self.stretch_animation.attribute)

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.stretch_animation = tex.get_animation(6)
        self.fade_animation = tex.get_animation(7)
        self.fade_animation.start()

    def update_count(self, balloon_count: int):
        if self.balloon_count != balloon_count:
            self.balloon_count = balloon_count
            self.stretch_animation.start()

    def update(self, 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):
        if self.is_popped:
            tex.draw_texture('balloon', 'pop', frame=7, color=self.color)
        elif self.balloon_count >= 1:
            balloon_index = min(6, (self.balloon_count - 1) * 6 // self.balloon_total)
            tex.draw_texture('balloon', 'pop', frame=balloon_index, color=self.color)
        if self.balloon_count > 0:
            tex.draw_texture('balloon', 'bubble')
            counter = str(max(0, self.balloon_total - self.balloon_count + 1))
            total_width = len(counter) * 52
            for i in range(len(counter)):
                tex.draw_texture('balloon', 'counter', frame=int(counter[i]), color=self.color, x=-(total_width // 2) + (i * 52), y=-self.stretch_animation.attribute, y2=self.stretch_animation.attribute)

class KusudamaAnimation:
    def __init__(self, balloon_total: int):
        self.balloon_total = balloon_total
        self.move_down = tex.get_animation(11)
        self.move_up = tex.get_animation(12)
        self.renda_move_up = tex.get_animation(13)
        self.renda_move_down = tex.get_animation(18)
        self.renda_fade_in = tex.get_animation(14)
        self.renda_fade_out = tex.get_animation(20)
        self.stretch_animation = tex.get_animation(15)
        self.breathing = tex.get_animation(16)
        self.renda_breathe = tex.get_animation(17)
        self.open = tex.get_animation(19)
        self.fade_out = tex.get_animation(21)
        self.balloon_count = 0
        self.is_popped = False
        self.is_finished = False
        self.move_down.start()
        self.move_up.start()
        self.renda_move_up.start()
        self.renda_move_down.start()
        self.renda_fade_in.start()
        self.renda_breathe.start()

        self.open.reset()
        self.renda_fade_out.reset()
        self.fade_out.reset()

    def update_count(self, balloon_count: int):
        if self.balloon_count != balloon_count:
            self.balloon_count = balloon_count
            self.stretch_animation.start()
            self.breathing.start()

    def update(self, current_ms, is_popped: bool):
        if is_popped and not self.is_popped:
            self.is_popped = True
            self.open.start()
            self.renda_fade_out.start()
            self.fade_out.start()
        self.move_down.update(current_ms)
        self.move_up.update(current_ms)
        self.renda_move_up.update(current_ms)
        self.renda_move_down.update(current_ms)
        self.renda_fade_in.update(current_ms)
        self.renda_fade_out.update(current_ms)
        self.fade_out.update(current_ms)
        self.stretch_animation.update(current_ms)
        self.breathing.update(current_ms)
        self.renda_breathe.update(current_ms)
        self.open.update(current_ms)
        if self.renda_breathe.is_finished:
            self.renda_breathe.restart()
        self.is_finished = self.fade_out.is_finished
    def draw(self):
        y = self.move_down.attribute - self.move_up.attribute
        renda_y = -self.renda_move_up.attribute + self.renda_move_down.attribute + self.renda_breathe.attribute
        tex.draw_texture('kusudama', 'kusudama', frame=self.open.attribute, y=y, scale=self.breathing.attribute, center=True, fade=self.fade_out.attribute)
        tex.draw_texture('kusudama', 'renda', y=renda_y, fade=min(self.renda_fade_in.attribute, self.renda_fade_out.attribute))

        if self.move_up.is_finished and not self.is_popped:
            counter = str(max(0, self.balloon_total - self.balloon_count))
            if counter == '0':
                return
            total_width = len(counter) * 150
            for i in range(len(counter)):
                tex.draw_texture('kusudama', 'counter', frame=int(counter[i]), x=-(total_width // 2) + (i * 150), y=-self.stretch_animation.attribute, y2=self.stretch_animation.attribute)

class Combo:
    def __init__(self, combo: int, current_ms: float):
        self.combo = combo
        self.stretch_animation = tex.get_animation(5)
        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, combo: int):
        if self.combo != combo:
            self.combo = combo
            self.stretch_animation.start()

    def update(self, current_ms: float, combo: int):
        self.update_count(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):
        counter = str(self.combo)
        if self.combo < 3:
            return
        if self.combo < 100:
            margin = 30
            total_width = len(counter) * margin
            tex.draw_texture('combo', 'combo')
            for i in range(len(counter)):
                tex.draw_texture('combo', 'counter', frame=int(counter[i]), x=-(total_width // 2) + (i * margin), y=-self.stretch_animation.attribute, y2=self.stretch_animation.attribute)
        else:
            margin = 35
            total_width = len(counter) * margin
            tex.draw_texture('combo', 'combo_100')
            for i in range(len(counter)):
                tex.draw_texture('combo', 'counter_100', frame=int(counter[i]), x=-(total_width // 2) + (i * margin), y=-self.stretch_animation.attribute, y2=self.stretch_animation.attribute)
            glimmer_positions = [(225, 210), (200, 230), (250, 230)]
            for j, (x, y) in enumerate(glimmer_positions):
                for i in range(3):
                    tex.draw_texture('combo', 'gleam', x=x+(i*30), y=y+self.glimmer_dict[j], color=self.color[j])

class ScoreCounter:
    def __init__(self, score: int):
        self.score = score
        self.stretch = tex.get_animation(4)

    def update_count(self, score: int):
        if self.score != score:
            self.score = score
            self.stretch.start()

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

    def draw(self):
        counter = str(self.score)
        x, y = 150, 185
        margin = 20
        total_width = len(counter) * margin
        start_x = x - total_width
        for i in range(len(counter)):
            tex.draw_texture('lane', 'score_number', frame=int(counter[i]), x=start_x + (i * margin), y=y - self.stretch.attribute, y2=self.stretch.attribute)

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

        if player_num == '2':
            self.color = ray.fade(ray.Color(84, 250, 238, 255), 1.0)
        else:
            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(self.color, self.fade_animation_2.attribute)
        else:
            self.color = ray.fade(self.color, 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):
        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
        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
            tex.draw_texture('lane', 'score_number', frame=int(counter[i]), x=start_x + (i * margin), y=y, color=self.color)

class SongInfo:
    def __init__(self, song_name: str, genre: str):
        self.song_name = song_name
        self.genre = genre
        self.song_title = OutlinedText(song_name, 40, ray.WHITE, ray.BLACK, outline_thickness=5)
        self.fade = tex.get_animation(3)
        self.fade.start()

    def update(self, current_ms: float):
        self.fade.update(current_ms)
        if self.fade.is_finished:
            self.fade.restart()

    def draw(self):
        tex.draw_texture('song_info', 'song_num', fade=self.fade.attribute, frame=global_data.songs_played % 4)

        text_x = 1252 - self.song_title.texture.width
        text_y = 50 - self.song_title.texture.height//2
        dest = ray.Rectangle(text_x, text_y, self.song_title.texture.width, self.song_title.texture.height)
        self.song_title.draw(self.song_title.default_src, dest, ray.Vector2(0, 0), 0, ray.fade(ray.WHITE, 1 - self.fade.attribute))

class ResultTransition:
    def __init__(self, player_num: int):
        self.player_num = player_num
        self.move = global_data.tex.get_animation(5)
        self.move.reset()
        self.is_finished = False
        self.is_started = False

    def start(self):
        self.move.start()

    def update(self, current_ms: float):
        self.move.update(current_ms)
        self.is_started = self.move.is_started
        self.is_finished = self.move.is_finished

    def draw(self):
        x = 0
        screen_width = 1280
        while x < screen_width:
            global_data.tex.draw_texture('result_transition', f'{str(self.player_num)}p_shutter', frame=0, x=x, y=-720 + self.move.attribute)
            global_data.tex.draw_texture('result_transition', f'{str(self.player_num)}p_shutter', frame=0, x=x, y=720 - self.move.attribute)
            global_data.tex.draw_texture('result_transition', f'{str(self.player_num)}p_shutter_footer', x=x, y=-432 + self.move.attribute)
            global_data.tex.draw_texture('result_transition', f'{str(self.player_num)}p_shutter_footer', x=x, y=1008 - self.move.attribute)
            x += 256

class Gauge:
    def __init__(self, player_num: str, difficulty: int, level: int, total_notes: int):
        self.player_num = player_num
        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 = tex.get_animation(10)
        self.rainbow_fade_in = None
        self.rainbow_animation = None

    def add_good(self):
        self.gauge_update_anim.start()
        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.start()
        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):
        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)
            self.rainbow_fade_in.start()
        self.gauge_update_anim.update(current_ms)

        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)])
            self.rainbow_animation.start()
        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):
        tex.draw_texture('gauge', 'border')
        tex.draw_texture('gauge', f'{self.player_num}p_unfilled')
        gauge_length = int(self.gauge_length)
        for i in range(gauge_length):
            if i == 68:
                tex.draw_texture('gauge', 'bar_clear_transition', x=i*8)
            elif i > 68:
                tex.draw_texture('gauge', 'bar_clear_top', x=i*8)
                tex.draw_texture('gauge', 'bar_clear_bottom', x=i*8)
            else:
                tex.draw_texture('gauge', f'{self.player_num}p_bar', x=i*8)
        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:
                tex.draw_texture('gauge', 'rainbow', frame=self.rainbow_animation.attribute-1, fade=self.rainbow_fade_in.attribute)
            tex.draw_texture('gauge', 'rainbow', frame=self.rainbow_animation.attribute, fade=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:
                tex.draw_texture('gauge', 'bar_clear_transition_fade', x=gauge_length*8, fade=self.gauge_update_anim.attribute)
            elif gauge_length > 69:
                tex.draw_texture('gauge', 'bar_clear_fade', x=gauge_length*8, fade=self.gauge_update_anim.attribute)
            else:
                tex.draw_texture('gauge', f'{self.player_num}p_bar_fade', x=gauge_length*8, fade=self.gauge_update_anim.attribute)
        tex.draw_texture('gauge', 'overlay', fade=0.15)
        if gauge_length >= 69:
            tex.draw_texture('gauge', 'clear')
            tex.draw_texture('gauge', 'tamashii')
        else:
            tex.draw_texture('gauge', 'clear_dark')
            tex.draw_texture('gauge', 'tamashii_dark')
