public class Assets {

    public static AssetManager internalManager = new AssetManager(new InternalFileHandleResolver());
    public static AssetManager externalManager = new AssetManager(new ExternalFileHandleResolver());

    static {
        externalManager.setLoader(List.class, new SimplifiedBeatmapLoader(new ExternalFileHandleResolver()));
    }

    public static SimpleSong selectedSong;
    public static SongFileInfo selectedBeatmap;
    public static SimpleSongGroup selectedGroup;

    public static TextureAtlas atlas;

    public static Skin menuSkin;
    public static Sound badSound;
    public static Sound goodSound;
    public static Sound greatSound;
    public static Sound perfectSound;

    public static BitmapFont font;
    public static BitmapFont songFont;

    public static Texture mainMenuBackgroundTexture;
    public static Texture holdBG;

    public static Array<SimpleSongGroup> songGroup;

    // In here we'll put everything that needs to be loaded in this format:
    // manager.load("file location in assets", fileType.class);
    //
    // libGDX AssetManager currently supports: Pixmap, Texture, BitmapFont,
    //     TextureAtlas, TiledAtlas, TiledMapRenderer, Music and Sound.
    public static void queueLoading() {
        internalManager.load("textures/textures.pack.atlas", TextureAtlas.class);
        internalManager.load("hitsounds/bad.mp3", Sound.class);
        internalManager.load("hitsounds/good.mp3", Sound.class);
        internalManager.load("hitsounds/great.mp3", Sound.class);
        internalManager.load("hitsounds/perfect.mp3", Sound.class);
        internalManager.load("bigimages/main_menu_background.jpg", Texture.class);
        internalManager.load("images/hold_background.png", Texture.class);
        internalManager.load("fonts/combo-font.fnt", BitmapFont.class);
        internalManager.load("fonts/song-font.fnt", BitmapFont.class);
        reloadBeatmaps();
    }

    // thanks to libgdx, the manager will not actually load maps which were already loaded,
    // so if the same file comes again, it will be skipped
    public static void reloadBeatmaps() {
        if (Gdx.files.absolute(Gdx.files.getExternalStoragePath() + "beatmaps/datafiles").exists()) {
            for (String fileName : Gdx.files.absolute(Gdx.files.getExternalStoragePath() + "beatmaps/datafiles").file().list()) {
                String fullPath = Gdx.files.getExternalStoragePath() + "beatmaps/datafiles/" + fileName;
                // if for any reason the user placed .osu/.osz files in the datafiles, we process them
                if (Gdx.files.absolute(fullPath).isDirectory() || (!fileName.endsWith(".rs") && !fileName.endsWith(".osz") && !fileName.endsWith(".osu")))
                    continue;

                externalManager.load("beatmaps/datafiles/" + fileName, List.class);
            }
            // process osu files from the beatmaps folder
            for (String fileName : Gdx.files.absolute(Gdx.files.getExternalStoragePath() + "beatmaps/").file().list()) {
                String fullPath = Gdx.files.getExternalStoragePath() + "beatmaps/" + fileName;
                if (Gdx.files.absolute(fullPath).isDirectory() || (!fileName.endsWith(".osz") && !fileName.endsWith(".osu")))
                    continue;
                externalManager.load("beatmaps/" + fileName, List.class);
            }
        } else {
            (Gdx.files.absolute(Gdx.files.getExternalStoragePath() + "beatmaps")).mkdirs();
            (Gdx.files.absolute(Gdx.files.getExternalStoragePath() + "beatmaps/datafiles")).mkdirs();
            (Gdx.files.absolute(Gdx.files.getExternalStoragePath() + "beatmaps/soundfiles")).mkdirs();
        }
    }

    // unlike the simple reload, in the hard reload we unload everything from the external manager
    // and force a reload of the beatmaps - this will cause .osz files which weren't extracted
    // to be processed, .osu files to be converted and music files within the .osz packages
    // to be copied over to the /beatmaps/soundfiles/ folder.
    public static void hardReloadBeatmaps() {
        selectedSong = null;
        selectedBeatmap = null;
        selectedGroup = null;
        externalManager.clear();
        reloadBeatmaps();
    }

    //In here we'll create our skin, so we only have to create it once.
    public static void setMenuSkin() {
        if (menuSkin == null)
            menuSkin = new Skin(Gdx.files.internal("skins/menuSkin.json"), internalManager.get("textures/textures.pack.atlas", TextureAtlas.class));
    }

    public static void setTextures() {
        if (atlas == null)
            atlas = internalManager.get("textures/textures.pack.atlas");

        if (mainMenuBackgroundTexture == null)
            mainMenuBackgroundTexture = internalManager.get("bigimages/main_menu_background.jpg");

        if (holdBG == null)
            holdBG = internalManager.get("images/hold_background.png");
    }

    public static void setFonts() {
        if (font == null) {
            font = internalManager.get("fonts/combo-font.fnt");
        }
        if (songFont == null) {
            songFont = internalManager.get("fonts/song-font.fnt");
        }

    }

    public static void setHitsounds() {
        if (badSound == null)
            badSound = internalManager.get("hitsounds/bad.mp3");
        if (goodSound == null)
            goodSound = internalManager.get("hitsounds/good.mp3");
        if (greatSound == null)
            greatSound = internalManager.get("hitsounds/great.mp3");
        if (perfectSound == null)
            perfectSound = internalManager.get("hitsounds/perfect.mp3");
    }

    @SuppressWarnings("unchecked")
    public static void setSongs() {
        if (songGroup == null) {
            songGroup = new Array<>();
        } else {
            songGroup.clear();
        }

        Array<String> assets = externalManager.getAssetNames();
        Map<String, SimpleSongGroup> groupMap = new HashMap<>();

        for (String string : assets) {
            List<SongFileInfo> beatmaps = externalManager.get(string, List.class);
            if (!beatmaps.isEmpty()) {
                String resourceName = beatmaps.get(0).getResourceName();
                String musicFile = beatmaps.get(0).music_file;
                String songName = beatmaps.get(0).song_name;
                if (groupMap.get(resourceName) == null) {
                    SimpleSongGroup group = new SimpleSongGroup();
                    group.resource_name = resourceName;
                    group.music_file = musicFile;
                    group.song_name = songName;
                    group.songs = new Array<>();
                    groupMap.put(resourceName, group);
                }
                SimpleSongGroup group = groupMap.get(resourceName);
                for (SongFileInfo beatmap : beatmaps) {
                    group.songs.add(beatmap);
                }
                group.songs.sort();
            }
        }
        for (String key : groupMap.keySet()) {
            songGroup.add(groupMap.get(key));
        }
        songGroup.sort();
    }

    public static boolean update() {
        return internalManager.update() && externalManager.update();
    }

    public static float getProgress() {
        return (internalManager.getProgress() + externalManager.getProgress()) / 2;
    }

    public static void setSelectedSong(SimpleSong song) {
        selectedSong = song;
    }
} public class GlobalConfiguration {

    // package info
    public static String appVersionName;

    public static int songVolume;
    public static int feedbackVolume;
    // offset in milliseconds
    public static int offset;
    public static int inputOffset;
    public static int noteSpeed;
    public static int overallDifficulty;
    // path to beatmaps
    public static String pathToBeatmaps;
    public static boolean playHintSounds;
    // sorting related
    public static int sortMode;
    public static int sortOrder;
    // randomizer config
    public static int randomMode;
    // sync config
    public static int syncMode;

    // other - per session configs
    public static boolean random;

    // not stored/remembered variables:
    public static Float playbackRate;
    public static Integer playbackMode;
    public static Float aTime;
    public static Float bTime;

    public final static int BASE_HEIGHT = 720;

    public static void loadConfiguration() {
        Preferences prefs = Gdx.app.getPreferences("sif_train_config");
        offset = prefs.getInteger("offset", 0);
        inputOffset = prefs.getInteger("input_offset", 0);
        songVolume = prefs.getInteger("song_vol", 100);
        feedbackVolume = prefs.getInteger("feedback_vol", 100);
        pathToBeatmaps = prefs.getString("path_to_beatmaps", Gdx.files.getExternalStoragePath() + "beatmaps");
        playHintSounds = prefs.getBoolean("play_hint_sounds", false);
        noteSpeed = prefs.getInteger("note_speed", 6);
        overallDifficulty = prefs.getInteger("overall_difficulty", 7);
        // default to song name sorting
        sortMode = prefs.getInteger("sorting_mode", SongUtils.SORTING_MODE_SONG_NAME);
        sortOrder = prefs.getInteger("sorting_order", SongUtils.SORTING_MODE_ASCENDING);
        // default to the new mode
        randomMode = prefs.getInteger("random_mode", SongUtils.RANDOM_MODE_NEW);
        // sync mode
        syncMode = prefs.getInteger("sync_mode", SongUtils.SYNC_MODE_1);

    }

    public static void storeConfiguration() {
        Preferences prefs = Gdx.app.getPreferences("sif_train_config");
        prefs.putInteger("offset", offset);
        prefs.putInteger("input_offset", inputOffset);
        prefs.putInteger("song_vol", songVolume);
        prefs.putInteger("feedback_vol", feedbackVolume);
        prefs.putString("path_to_beatmaps", pathToBeatmaps);
        prefs.putBoolean("play_hint_sounds", playHintSounds);
        prefs.putInteger("note_speed", noteSpeed);
        prefs.putInteger("overall_difficulty", overallDifficulty);
        prefs.putInteger("sorting_mode", sortMode);
        prefs.putInteger("random_mode", randomMode);
        prefs.putInteger("sorting_order", sortOrder);
        prefs.putInteger("sync_mode", syncMode);
        prefs.flush();
    }
} public class SimpleNotesInfo implements Comparable<SimpleNotesInfo>{
    public Double timing_sec;
    public Integer effect;
    public Double effect_value;
    public Integer position;


    @Override
    public int compareTo(SimpleNotesInfo o) {
        if (!o.timing_sec.equals(timing_sec))
            return Double.compare(timing_sec, o.timing_sec);
        return SongUtils.compare(position, o.position);
    }
} public class CircleMark implements Comparable<CircleMark> {

    public float getSize() {
        return size;
    }

    public int getEffectMask() {
        return effect;
    }

    public SimpleNotesInfo getNote() {
        return note;
    }

    public Vector2 getHoldReleasePosition() {
        return holdReleasePosition;
    }

    @Override
    public int compareTo(CircleMark o) {
        if (this.spawnTime != o.spawnTime) {
            return Float.compare(spawnTime, o.spawnTime);
        }
        return SongUtils.compare(destination, o.destination);

    }

    public enum Accuracy {
        NONE, MISS, BAD, GOOD, GREAT, PERFECT

    }

    public boolean visible;
    public boolean holdBeamVisible;
    public boolean waiting;
    public boolean holding;
    public boolean miss;
    public boolean endVisible;
    public boolean waitingStart;
    public boolean waitingEnd;
    public boolean processed;
    public boolean hold;
    public boolean soundPlayed;
    public boolean sound2Played;

    public boolean left;
    Vector2 origin = new Vector2();
    Vector2 position = new Vector2();
    Vector2 holdReleasePosition = new Vector2();
    Vector2 velocity = new Vector2();

    public Integer notePosition;
    public Integer destination = 0;
    private Double speed;
    SimpleNotesInfo note;

    private float spawnTime;
    private float despawnTime;
    private float startWaitTime;
    private float endWaitTime;
    private float holdEndSpawnTime;
    private float holdEndDespawnTime;
    private float holdEndStartWaitTime;
    private float holdEndEndWaitTime;

    public float alpha = 1f;
    public float alpha2 = 1f;
    public Integer effect;
    // only for holds
    private float size;
    private float size2;


    public CircleMark(float x, float y, SimpleNotesInfo note, Double noteSpeed, float delay) {
        float timing = (float) (delay + note.timing_sec * 1f + GlobalConfiguration.offset * 1f / 1000f);
        notePosition = note.position;
        this.origin.x = x;
        this.origin.y = y;
        this.position.x = x;
        this.position.y = y;
        this.holdReleasePosition.x = x;
        this.holdReleasePosition.y = y;
        this.note = note;
        this.effect = note.effect;
        this.hold = (note.effect & SongUtils.NOTE_TYPE_HOLD) != 0;
        // position goes 9-8-...-2-1
        this.destination = note.position - 1;
        this.speed = noteSpeed;
        this.spawnTime = (float) (timing - speed);
        this.startWaitTime = (float) (timing - SongUtils.overallDiffBad[GlobalConfiguration.overallDifficulty] / 1000f);
        this.endWaitTime = (float) (timing + SongUtils.overallDiffBad[GlobalConfiguration.overallDifficulty] / 1000f);
        this.despawnTime = timing * 1.0f;
        this.size = 0.1f;
        this.size2 = 0f;
        if (hold) {
            this.holdEndSpawnTime = (float) (timing + note.effect_value - speed);
            this.holdEndStartWaitTime = (float) (timing + note.effect_value - SongUtils.overallDiffBad[GlobalConfiguration.overallDifficulty] / 1000f);
            this.holdEndEndWaitTime = (float) (timing + note.effect_value + SongUtils.overallDiffBad[GlobalConfiguration.overallDifficulty] / 1000f);
            this.holdEndDespawnTime = (float) (timing + note.effect_value);

        }
        previousTime = 0f;
        previousSystemTime = 0L;

        initializeVelocity();
        initializeStates();
    }

    private boolean firstHit;
    private boolean secondHit;

    public Accuracy accuracyStart;
    public Accuracy accuracyEnd;

    public Float accuracyHitStartTime;
    public Float accuracyHitEndTime;

    public void updateDestination(int newDestination) {
        this.destination = newDestination;
        this.notePosition = newDestination + 1;
        // reset the velocity vectors;
        initializeVelocity();
    }


    private void initializeStates() {
        visible = false;
        holdBeamVisible = false;
        waiting = false;
        miss = false;
        holding = false;
        endVisible = false;
        waitingStart = false;
        waitingEnd = false;
        processed = false;
        soundPlayed = false;
        sound2Played = false;
        alpha = 1f;
        alpha2 = 1f;
    }

    public void reset()
    {
        this.accuracyStart = null;
        this.accuracyEnd = null;
        this.accuracyHitStartTime = null;
        this.accuracyHitEndTime = null;
        this.position.x = this.origin.x;
        this.position.y = this.origin.y;
        this.holdReleasePosition.x = this.origin.x;
        this.holdReleasePosition.y = this.origin.y;
        this.size = 0.1f;
        this.size2 = 0f;
        firstHit = false;
        secondHit = false;
        initializeStates();
    }

    private void initializeVelocity() {
        float xVel = (float) Math.cos((destination) * Math.PI / 8);
        float yVel = -(float) Math.sin((destination) * Math.PI / 8);
        velocity.x = (float) (xVel * 249 / speed);
        velocity.y = (float) (yVel * 249 / speed);
    }

    public boolean isDone() {
        return miss || (firstHit && secondHit);
    }

    float previousTime;
    long previousSystemTime;

    public void update(float time) {
        if (miss || (firstHit && secondHit)) {
            if (visible) {
                visible = false;
                holdBeamVisible = false;
            }
            if (endVisible) {
                endVisible = false;
            }
            return;
        }
        updateFirst(time);
        if (hold) {
            updateSecond(time);
        }
        processMiss(time);
        previousTime = time;
        previousSystemTime = System.currentTimeMillis();
    }


    private void updateFirst(float time) {

        if (spawnTime <= time && despawnTime > time && !visible) {
            visible = true;
            holdBeamVisible = true;
        }

        if (spawnTime >= time && visible) {
            visible = false;
            holdBeamVisible = false;
        }


        if (visible && despawnTime <= time) {
            if (GlobalConfiguration.playHintSounds && !soundPlayed) {
                Assets.perfectSound.play(GlobalConfiguration.feedbackVolume / 200f);
                soundPlayed = true;
            }

            if (holding) {
                alpha = 1f;
            } else {
                alpha = MathUtils.clamp((endWaitTime - time) / (endWaitTime - despawnTime), 0f, 1f);
                if (alpha == 0f) {
                    visible = false;
                    holdBeamVisible = false;
                }

            }
        }

        if (visible) {
            float scl = time - spawnTime;

            if (holding) {
                scl = speed.floatValue();
                updateSize(Math.max(0f, despawnTime - time));
            } else
                updateSize(despawnTime - time);

            position.set(origin.cpy().add(velocity.cpy().scl(scl)));
        }
        if (startWaitTime <= time && endWaitTime > time && !waiting) {
            waiting = true;
            waitingStart = true;
        }
    }

    private void updateSecond(float time) {

        if (holdEndSpawnTime <= time && holdEndDespawnTime > time && !endVisible) {
            endVisible = true;
            waitingEnd = true;
        }

        if (holdEndSpawnTime >= time && endVisible)
            endVisible = false;

        if (endVisible && holdEndDespawnTime <= time) {
            if (GlobalConfiguration.playHintSounds && !sound2Played) {
                Assets.perfectSound.play(GlobalConfiguration.feedbackVolume / 200f);
                sound2Played = true;
            }

            alpha2 = MathUtils.clamp((holdEndEndWaitTime - time) / (holdEndEndWaitTime - holdEndDespawnTime), 0f, 1f);
            if (alpha2 == 0f)
                endVisible = false;
        }

        if (endVisible) {
            updateSize2(holdEndDespawnTime - time);
            holdReleasePosition.add(velocity.cpy().scl(time - previousTime));
        }

        if (holdEndStartWaitTime <= time && holdEndEndWaitTime > time && !waitingEnd && waiting) {
            waitingEnd = true;
        }
    }

    private void processMiss(float time) {
        // miss if we miss the first note
        if (hold && !firstHit && !holding && endWaitTime <= time && waitingStart && !miss) {
            waiting = false;
            endVisible = false;
            miss = true;
            accuracyStart = Accuracy.MISS;
            accuracyEnd = Accuracy.MISS;
            // System.out.println("MISS-001: didn't hit the note");
        } else if (!hold && endWaitTime <= time && waitingStart && !miss) {
            waiting = false;
            endVisible = false;
            miss = true;
            accuracyStart = Accuracy.MISS;
            //System.out.println("MISS-002: didn't hit the note");
        }
        if (hold && !miss) {
            // miss if we hold for too long
            if (holdEndEndWaitTime <= time && waitingEnd && !miss) {
                //System.out.println("MISS-003: held for too long");
                miss = true;
                waitingEnd = false;
                holding = false;
                endVisible = false;
                waiting = false;
                accuracyEnd = Accuracy.MISS;
            }
            // miss if we release before we start waiting
            if (firstHit && holdEndSpawnTime > time && !holding && !miss) {
                accuracyEnd = Accuracy.MISS;
                waiting = false;
                endVisible = false;
                miss = true;
                //System.out.println("MISS-004: released hold too early");
            }
        }
    }

    public float getSize2() {
        return this.size2;
    }

    public Accuracy hit() {
        float delta = (System.currentTimeMillis() - previousSystemTime) / 1000f;
        float hit = previousTime + delta - despawnTime - GlobalConfiguration.inputOffset / 1000f;

        Accuracy accuracy = Results.getAccuracyFor(hit);
        // If the note was tapped too early, we ignore the tap
        if (despawnTime > previousTime && accuracy == Accuracy.MISS) {
            return Accuracy.NONE;
        }
        accuracyHitStartTime = hit;
        if (hold) {
            holding = true;
            accuracyStart = accuracy;
            firstHit = true;
        } else {
            accuracyStart = accuracy;
            accuracyEnd = Accuracy.NONE;
            firstHit = true;
            secondHit = true;
            processed = true;
            waiting = false;
            visible = false;
            holdBeamVisible = false;
        }
        waitingStart = false;
        // calculate hit accuracy
        return accuracyStart;
    }

    public Accuracy release() {
        if (!firstHit) {
            return Accuracy.NONE;
        }
        secondHit = true;
        float delta = (System.currentTimeMillis() - previousSystemTime) / 1000f;
        float hit = previousTime + delta - holdEndDespawnTime - GlobalConfiguration.inputOffset / 1000f;
        waitingEnd = false;
        holding = false;
        endVisible = false;
        holdBeamVisible = false;
        waiting = false;
        accuracyEnd = Results.getAccuracyFor(hit);
        if (accuracyEnd == Accuracy.MISS) {
            processed = true;
            //System.out.println("MISS-005: Released hold too early");
        } else {
            accuracyHitEndTime = hit;
        }
        return accuracyEnd;
    }

    private void updateSize(float despawnTime) {
        float progress = (float) ((speed - despawnTime) / speed);
        this.size = 0.1f + progress * 0.9f;
    }

    private void updateSize2(float despawnTime) {
        float progress = (float) ((speed - despawnTime) / speed);
        this.size2 = progress;
    }

    public Vector2 getPosition() {
        return position;
    }
} public class SongUtils {
    public final static String[] DIFF = {"EASY", "NORMAL", "HARD", "EXPERT"};
    public final static Integer NOTE_TYPE_NORMAL = 0b00000001;
    public final static Integer NOTE_TYPE_TOKEN = 0b00000010;
    public final static Integer NOTE_TYPE_HOLD = 0b00000100;
    public final static Integer NOTE_TYPE_SPECIAL = 0b00001000;
    public final static Integer NOTE_TYPE_SIMULT_START = 0b00010000;
    public final static Integer NOTE_TYPE_SIMULT_END = 0b00100000;
    public final static Integer NOTE_TYPE_HOLD_END = 0b01000000;

    public final static Integer SORTING_MODE_FILE_NAME = 0;
    public final static Integer SORTING_MODE_SONG_NAME = 1;

    public final static Integer SORTING_MODE_ASCENDING = 0;
    public final static Integer SORTING_MODE_DESCENDING = 1;

    public final static Integer RANDOM_MODE_OLD = 0;
    public final static Integer RANDOM_MODE_NEW = 1;
    public final static Integer RANDOM_MODE_KEEP_SIDES = 2;
    public final static Integer RANDOM_MODE_MIRRORED_KEEP_SIDES = 3;

    public final static Integer SYNC_MODE_1 = 0;
    public final static Integer SYNC_MODE_2 = 1;
    public final static Integer SYNC_MODE_3 = 2;
    public final static Integer SYNC_DISABLED = 3;

    public final static Integer GAME_MODE_NORMAL = 0;
    public final static Integer GAME_MODE_ABREPEAT = 1;

    public final static String[] randomModes = {"Old mode", "New mode", "Keep Sides mode", "Mirrored Keep Sides mode", "Simple mode", "Extreme mode"};
    public final static String[] syncModes = {"Default", "Constant Sync", "Initial Sync", "Disabled"};

    public final static Long[] noteSpeeds = {1800L, 1680L, 1560L, 1440L, 1320L, 1200L, 1050L, 900L, 750L, 600L, 450L};
    public final static Double[] overallDiffPerfect = {79.5, 73.5, 67.5, 61.5, 56.5, 49.5, 43.5, 37.5, 31.5, 25.5, 19.5}; // -6
    public final static Double[] overallDiffGreat = {139.5, 131.5, 123.5, 115.5, 107.5, 99.5, 91.5, 83.5, 75.5, 67.5, 59.5}; // -8
    public final static Double[] overallDiffNice = {199.5, 189.5, 179.5, 169.5, 159.5, 149.5, 139.5, 129.5, 119.5, 109.5, 99.5}; // - 10
    public final static Double[] overallDiffBad = {249.5, 237.5, 225.5, 213.5, 201.5, 189.5, 177.5, 165.5, 153.5, 141.5, 129.5};// - 12

    public static Long getSpeedFromConfig(Integer noteSpeed) {
        return noteSpeeds[noteSpeed];
    }

    public static String getDifficulty(Integer difficulty) {
        return DIFF[difficulty - 1];
    }

    static double[] speedForApproachRate = {1.8, 1.68, 1.56, 1.44, 1.32, 1.2, 1.05, 0.9, 0.75, 0.6, 0.45};

    public static Double getDefaultNoteSpeedForApproachRate(int i) {
        return speedForApproachRate[i];
    }

    static int[][] positionForMode = {
            // 1:
            {5},
            // 2
            {6, 4},
            // 3
            {7, 5, 3},
            // 4
            {8, 6, 4, 2},
            // 5
            {7, 6, 5, 4, 3},
            // 6
            {8, 7, 6, 4, 3, 2},
            // 7
            {8, 7, 6, 5, 4, 3, 2},
            // 8
            {9, 8, 7, 6, 4, 3, 2, 1},
            // 9
            {9, 8, 7, 6, 5, 4, 3, 2, 1}
    };

    public static int getPositionForMode(int mode, int position) {
        return positionForMode[mode - 1][position];
    }

    public static int compare(int x, int y) {
        return (x < y) ? -1 : ((x == y) ? 0 : 1);
    }
} public class ExtremeRandomizer extends Randomizer {

    /*
     * Info: this is the only randomizer which works with ANY BEATMAP (even those with more than 2 notes at the same time!)
     */
    public void randomize(Array<CircleMark> marks) {
        // sort marks by timing
        marks.sort();

        double threshold = SongUtils.getDefaultNoteSpeedForApproachRate(GlobalConfiguration.noteSpeed) / 32.0;

        for (int i = 0 ; i < marks.size; i++)
        {
            CircleMark mark = marks.get(i);
            Integer pos = getFreePosition(mark.getNote().timing_sec);
            noteToReleaseTime.put(pos, mark.getNote().timing_sec + (mark.hold ? mark.getNote().effect_value : 0) + threshold);
            mark.updateDestination(pos);
        }
    }

    private Integer getFreePosition(Double timing_sec) {
        Integer position = (int)(Math.random() * 100) % 9;
        while (inUse(position, timing_sec) )
        {
            position = (int)(Math.random() * 100) % 9;
        }
        return position;
    }

} public class KeepSidesRandomizer extends Randomizer {

    /*
     * Info: by default beatmaps will use 2 notes at the same time at most. If there's more, things can get nasty and messy.
     */
    public void randomize(Array<CircleMark> marks) {
        // sort marks by timing
        marks.sort();

        for (CircleMark mark : marks)
        {
            // we don't process notes which spawn in the middle
            if (mark.getNote().position == 5)
            {
                continue;
            }
            boolean left = isLeft(mark);
            Integer newPosition = getPositionWithoutMiddle(left);
            while (inUse(newPosition, mark.getNote().timing_sec))
            {
                newPosition = getPosition(left);
            }
            mark.updateDestination(newPosition);
            noteToReleaseTime.put(newPosition, mark.getNote().timing_sec + BUFFER_TIME);
        }
    }
} public class MirroredKeepSidesRandomizer extends Randomizer {

    /*
     * Info: by default beatmaps will use 2 notes at the same time at most. If there's more, things can get nasty and messy.
     */
    public void randomize(Array<CircleMark> marks) {
        // sort marks by timing
        marks.sort();

        for (CircleMark mark : marks)
        {
            // we don't process notes which spawn in the middle
            if (mark.getNote().position == 5)
            {
                continue;
            }
            boolean left = isLeft(mark);
            Integer newPosition = getPositionWithoutMiddle(!left);
            while (inUse(newPosition, mark.getNote().timing_sec))
            {
                newPosition = getPosition(!left);
            }
            mark.updateDestination(newPosition);
            noteToReleaseTime.put(newPosition, mark.getNote().timing_sec + BUFFER_TIME);
        }
    }
} public class NewAlgorithmRandomizer extends Randomizer {

    private boolean left;
    private boolean holding;
    private double holdEndTime;
    // 5 ms of buffer to prevent simultaneous notes on the same side

    /*
     * Info: by default beatmaps will use 2 notes at the same time at most. If there's more, things can get nasty and messy.
     */
    public void randomize(Array<CircleMark> marks) {
        marks.sort();

        double threshold = SongUtils.getDefaultNoteSpeedForApproachRate(GlobalConfiguration.noteSpeed) / 4.0;

        holding = false;
        // set the position for each note
        for (int i = 0; i < marks.size; i++) {
            CircleMark mark = marks.get(i);
            if (mark.getNote().timing_sec > holdEndTime) {
                holding = false;
            }
            boolean isLeft = Math.random() > 0.5;

            // this note is a hold
            if (mark.hold) {
                randomizeHold(marks, i, isLeft, threshold);
            } else
            // not a hold
            {
                randomizeNotHold(marks, i, isLeft, threshold);
            }
        }
    }

    private void randomizeHold(Array<CircleMark> marks, int i, boolean isLeft, double threshold) {
        CircleMark mark = marks.get(i);

        // we're holding!
        if (holding) {
            // note during hold which ends after the current end time
            // swap times.
            if (mark.getNote().timing_sec + mark.getNote().effect_value > holdEndTime) {
                holdEndTime = mark.getNote().timing_sec + mark.getNote().effect_value + BUFFER_TIME;
                left = !left;
                Integer pos = getPositionWithoutMiddle(left);
                mark.updateDestination(pos);
                mark.left = left;
                // new hold is shorter than the previous one
                // just add it.
            } else {
                isLeft = !left;
                Integer pos = getPositionWithoutMiddle(isLeft);
                mark.updateDestination(pos);
                mark.left = isLeft;
            }
            // we got a hold and we're not holding
        } else {
            holding = true;
            double previousHoldEndTime = holdEndTime;
            holdEndTime = mark.getNote().timing_sec + mark.getNote().effect_value + BUFFER_TIME;

            // check the previous note for side calculation - just in case
            if (i >= 1) {
                CircleMark previous = marks.get(i - 1);
                // we're no longer holding, but the new hold spawns close to the previous released hold
                if (mark.getNote().timing_sec - previousHoldEndTime < threshold) {
                    left = !left;
                    Integer pos = getPositionWithoutMiddle(left);
                    mark.updateDestination(pos);
                    mark.left = left;

                }
                // notes close together must go to different sides
                else if (mark.getNote().timing_sec - previous.getNote().timing_sec < threshold) {
                    isLeft = !previous.left;
                    Integer pos = getPositionWithoutMiddle(isLeft);
                    mark.updateDestination(pos);
                    mark.left = isLeft;
                    left = isLeft;
                    // if the new note is released really close to the last hold, make sure to release on the other side
                } else {
                    isLeft = !left;
                    Integer pos = getPositionWithoutMiddle(isLeft);
                    mark.updateDestination(pos);
                    mark.left = isLeft;
                    left = isLeft;

                }
                // if the note is the first one, we just pick a random side and spawn it there.
            } else {
                left = isLeft;
                Integer pos = getPositionWithoutMiddle(left);
                mark.updateDestination(pos);
                mark.left = isLeft;
                left = isLeft;
            }
        }
    }

    private void randomizeNotHold(Array<CircleMark> marks, int i, boolean isLeft, double threshold) {
        CircleMark mark = marks.get(i);

        // we're holding
        if (holding) {
            // pick the other side
            isLeft = !left;
            // if one side is holding, notes will never spawn in the middle lane
            Integer pos = getPositionWithoutMiddle(isLeft, mark.getNote().timing_sec);
            mark.updateDestination(pos);
            mark.left = isLeft;
            // we're not holding
        } else {
            // and this is not the first note
            if (i >= 1) {
                CircleMark previous = marks.get(i - 1);
                // if the notes are close together, use different sides
                if (mark.getNote().timing_sec - previous.getNote().timing_sec < threshold) {
                    isLeft = !previous.left;
                    // if notes are too close together, we don't pick up the center
                    // this also applies to simultaneous notes (distance will obviously be < average)
                    Integer pos = getPositionWithoutMiddle(isLeft, mark.getNote().timing_sec);
                    mark.updateDestination(pos);
                    mark.left = isLeft;
                    // we're not holding but there was a hold released close - swap sides
                } else if (mark.getNote().timing_sec - holdEndTime < threshold) {
                    // they're far away and we may choose a side randomly
                    isLeft = !left;
                    Integer pos = getPositionWithoutMiddle(isLeft, mark.getNote().timing_sec);
                    mark.updateDestination(pos);
                    mark.left = isLeft;
                    // if they're not really close, just pick a random side
                } else if (hasSimultEndBeforeCurrentNote(marks, i, i-1)) {
                    isLeft = !left;
                    Integer pos = getPositionWithoutMiddle(isLeft, mark.getNote().timing_sec);
                    mark.updateDestination(pos);
                    mark.left = isLeft;

                } else {
                    Integer pos = getPosition(isLeft);
                    // if the note is multi - don't pick the center
                    if ((mark.effect & (SongUtils.NOTE_TYPE_SIMULT_END | SongUtils.NOTE_TYPE_SIMULT_START)) != 0) {
                        pos = getPositionWithoutMiddle(isLeft, mark.getNote().timing_sec);
                    }
                    mark.updateDestination(pos);
                    mark.left = isLeft;
                }
                // if this is the first note, just pick a random side
            } else {
                Integer pos = getPosition(isLeft, mark.getNote().timing_sec);
                // if the first note is a simult, choose a random side - no center
                if ((mark.effect & (SongUtils.NOTE_TYPE_SIMULT_START | SongUtils.NOTE_TYPE_SIMULT_END)) != 0)
                {
                    pos = getPositionWithoutMiddle(isLeft, mark.getNote().timing_sec);
                }
                mark.updateDestination(pos);
                mark.left = isLeft;
            }
        }
    }

    public Integer getPosition(boolean isLeft, Double timing) {
        Integer newPosition = getPosition(isLeft);
        // try to get a spot on one side
        if (hasSpots(isLeft, timing))
        {
            while (inUse(newPosition, timing)) {
                newPosition = getPosition(isLeft);
            }
        } else
        // if we couldn't get a spot because all are on cooldown, use a spot from the other side
        {
            if (hasSpots(!isLeft, timing)){
                while (inUse(newPosition, timing)) {
                    newPosition = getPosition(!isLeft);
                }
            }
            // if the spots are all in use, use the center.
            else
            {
                newPosition = 4;
            }
        }
        noteToReleaseTime.put(newPosition, timing + BUFFER_TIME);
        return newPosition;
    }

    public Integer getPositionWithoutMiddle(boolean isLeft, Double timing) {
        Integer newPosition = getPositionWithoutMiddle(isLeft);
        if (hasSpots(isLeft, timing))
        {
            while (inUse(newPosition, timing)) {
                newPosition = getPositionWithoutMiddle(isLeft);
            }
        } else
        {
            if (hasSpots(!isLeft, timing)){
                while (inUse(newPosition, timing)) {
                    newPosition = getPositionWithoutMiddle(!isLeft);
                }
            }
            else
            {
                newPosition = 5;
            }
        }
        noteToReleaseTime.put(newPosition, timing + BUFFER_TIME);
        return newPosition;
    }

} public class OldAlgorithmRandomizer extends Randomizer {

    private double holdEndTime;

    /*
     * Info: by default beatmaps will use 2 notes at the same time at most. If there's more, things can get nasty and messy.
     */
    public void randomize(Array<CircleMark> marks) {
        // sort marks by timing
        marks.sort();

        double threshold = SongUtils.getDefaultNoteSpeedForApproachRate(GlobalConfiguration.noteSpeed) / 4.0;

        // set the position for each note
        for (int i = 0; i < marks.size; i++) {
            CircleMark mark = marks.get(i);
            if (mark.hold)
            {
                // if the note is a hold, we store the ending time and ignore any notes which appear until the hold ends
                if (holdEndTime < mark.getNote().timing_sec + mark.getNote().effect_value)
                {
                    holdEndTime = mark.getNote().timing_sec + mark.getNote().effect_value + BUFFER_TIME;
                    continue;
                }

            }

            // we give notes a bit of leeway for hold release to prevent having a note on the same side and almost simultaneously after the hold was released.
            if (mark.getNote().timing_sec < holdEndTime + threshold)
            {
                continue;
            }

            // this note is a hold or a double don't randomize it
            if (!mark.hold && (mark.effect & (SongUtils.NOTE_TYPE_SIMULT_START | SongUtils.NOTE_TYPE_SIMULT_END)) == 0 ) {
                Integer pos = getRandomPosition();
                mark.updateDestination(pos);
            }
        }
    }

} public abstract class Randomizer {

    static final double BUFFER_TIME = 0.005d;

    public abstract void randomize(Array<CircleMark> marks);

    protected Map<Integer, Double> noteToReleaseTime = new HashMap<>();

    protected Integer getPosition(boolean isLeft) {
        // left = 4-8
        if (isLeft) {
            return 4 + (int) (Math.random() * 100) % 5;
        } else
            // right = 0-4
            return (int) (Math.random() * 100) % 5;
    }

    // holds don't spawn in the middle
    protected Integer getPositionWithoutMiddle(boolean isLeft) {
        // left = 5-8
        if (isLeft) {
            return 5 + (int) (Math.random() * 100) % 4;
        } else
            // right = 0-3
            return (int) (Math.random() * 100) % 4;
    }

    protected Integer getRandomPosition() {
        return (int)(Math.random() * 100) % 9;
    }

    protected boolean isLeft(CircleMark mark) {
        return mark.getNote().position > 5;
    }

    protected Boolean hasSimultEndBeforeCurrentNote(Array<CircleMark> marks, Integer currentIndex, Integer lookupIndex)
    {
        CircleMark lookup = marks.get(lookupIndex);
        Double lookupStartTime = lookup.getNote().timing_sec;
        Double lookupHoldEndTime = lookup.getNote().timing_sec + lookup.getNote().effect_value;

        CircleMark current = marks.get(currentIndex);
        Double currentTime = current.getNote().timing_sec;

        for (int i = 0; i < marks.size; i++)
        {
            if (i == currentIndex)
                continue;

            if (i == lookupIndex)
                continue;

            CircleMark mark = marks.get(i);
            if (mark.getNote().timing_sec >= currentTime)
                continue;
            // if both are holds, we only check ends
            // if one is a hold, we check ends vs tap
            // if both are taps we check both
            if (lookup.hold)
            {
                // 2 holds
                if (mark.hold)
                {
                    if (mark.getNote().timing_sec + mark.getNote().effect_value == lookupHoldEndTime)
                    {
                        return true;
                    }
                }
                else
                {
                    // 1 hold 1 tap
                    if (mark.getNote().timing_sec.equals(lookupHoldEndTime))
                    {
                        return true;
                    }
                }
            }
            else
            {
                // 1 hold 1 tap
                if (mark.hold)
                {
                    if (mark.getNote().timing_sec + mark.getNote().effect_value == lookupStartTime)
                    {
                        return true;
                    }
                }
                else
                {
                    // 2 taps
                    if (mark.getNote().timing_sec.equals(lookupStartTime))
                    {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    protected Boolean inUse(Integer position, Double timing_sec) {
        return noteToReleaseTime.get(position) != null && noteToReleaseTime.get(position) >= timing_sec;
    }

    protected Boolean hasSpots(Boolean isLeft, Double timing)
    {
        for (int i = isLeft ? 5 : 0; i <= (isLeft ? 8 : 3); i++)
        {
            if (noteToReleaseTime.get(i) == null || noteToReleaseTime.get(i) < timing)
                return true;
        }
        return false;
    }
} public class SimpleRandomizer extends Randomizer {

    private double holdEndTime;
    private boolean left;

    /*
     * Info: by default beatmaps will use 2 notes at the same time at most. If there's more, things can get nasty and messy.
     */
    public void randomize(Array<CircleMark> marks) {
        marks.sort();

        double threshold = SongUtils.getDefaultNoteSpeedForApproachRate(GlobalConfiguration.noteSpeed) / 4.0;

        // set the position for each note
        for (int i = 0; i < marks.size; i++) {
            CircleMark mark = marks.get(i);

            if (mark.hold) {
                // if the note is a hold, we store the ending time and ignore any notes which appear until the hold ends
                if (holdEndTime < mark.getNote().timing_sec + mark.getNote().effect_value) {
                    holdEndTime = mark.getNote().timing_sec + mark.getNote().effect_value + BUFFER_TIME;
                    left = isLeft(mark);
                    continue;
                }

            }

            // we give notes a bit of leeway for hold release to prevent having a note on the same side and almost simultaneously after the hold was released.
            if (mark.getNote().timing_sec < holdEndTime) {
                continue;
            }

            boolean isLeft = Math.random() > 0.5;

            // this note is a hold or a double don't randomize it
            if (!mark.hold && (mark.effect & (SongUtils.NOTE_TYPE_SIMULT_START | SongUtils.NOTE_TYPE_SIMULT_END)) == 0) {
                randomizeNotHold(marks, i, isLeft, threshold);
            }
        }
    }

    private void randomizeNotHold(Array<CircleMark> marks, int i, boolean isLeft, double threshold) {
        CircleMark mark = marks.get(i);

        // if this is not the first note
        if (i >= 1) {
            // we check against the previous note
            CircleMark previous = marks.get(i - 1);
            // if the notes are close together, use different sides
            if (mark.getNote().timing_sec - previous.getNote().timing_sec < threshold) {
                isLeft = !previous.left;
                // if notes are too close together, we don't pick up the center
                Integer pos = getPositionWithoutMiddle(isLeft);
                mark.updateDestination(pos);
                mark.left = isLeft;
            } else if (mark.getNote().timing_sec - holdEndTime < threshold) {
                // they're far away and we may choose a side randomly
                isLeft = !left;
                Integer pos = getPositionWithoutMiddle(isLeft);
                mark.updateDestination(pos);
                mark.left = isLeft;
                // if they're not really close, just pick a random side
            } else {
                Integer pos = getPosition(isLeft);
                mark.updateDestination(pos);
                mark.left = isLeft;
            }
            // if this is the first note, just pick a random side
        } else {
            Integer pos = getPosition(isLeft);
            mark.updateDestination(pos);
            mark.left = isLeft;
        }
    }
}
import com.badlogic.gdx.utils.Array;
import com.fteams.siftrain.assets.Assets;
import com.fteams.siftrain.assets.GlobalConfiguration;
import com.fteams.siftrain.entities.SimpleNotesInfo;
import com.fteams.siftrain.objects.AccuracyMarker;
import com.fteams.siftrain.objects.AccuracyPopup;
import com.fteams.siftrain.objects.CircleMark;
import com.fteams.siftrain.objects.TapZone;
import com.fteams.siftrain.util.SongUtils;
import com.fteams.siftrain.util.random.ExtremeRandomizer;
import com.fteams.siftrain.util.random.KeepSidesRandomizer;
import com.fteams.siftrain.util.random.MirroredKeepSidesRandomizer;
import com.fteams.siftrain.util.random.NewAlgorithmRandomizer;
import com.fteams.siftrain.util.random.OldAlgorithmRandomizer;
import com.fteams.siftrain.util.random.Randomizer;
import com.fteams.siftrain.util.random.SimpleRandomizer;
package com.fteams.siftrain;


public class World {

    int width;
    int height;
    /**
     * The 9 zones the user can tap
     */
    Array<TapZone> zones = new Array<>();
    /**
     * The notes which spawn from the center
     */
    Array<CircleMark> marks = new Array<>();

    public int combo;
    public boolean started;
    public int offsetX;
    public int offsetY;

    private Array<AccuracyMarker> accuracyMarkers;
    private Array<AccuracyPopup> accuracyPopups;

    public boolean paused;

    // Getters -----------
    public Array<TapZone> getZones() {
        return zones;
    }

    // --------------------

    public World() {
        createWorld();
    }

    public float delay;

    private void createWorld() {
        float x = 0f;
        float y = 0f;
        Double noteSpeed = SongUtils.getDefaultNoteSpeedForApproachRate(GlobalConfiguration.noteSpeed);

        delay = Assets.selectedSong.lead_in != null ? Assets.selectedSong.lead_in : noteSpeed.floatValue();

        for (SimpleNotesInfo notesInfo : Assets.selectedSong.song_info.get(0).notes) {

            SimpleNotesInfo copy = copy(notesInfo);
            if (GlobalConfiguration.playbackMode != null && GlobalConfiguration.playbackMode.equals(SongUtils.GAME_MODE_ABREPEAT)) {
                if (GlobalConfiguration.aTime != null) {
                    if (copy.timing_sec < GlobalConfiguration.aTime) {
                        continue;
                    }
                }
                if (GlobalConfiguration.bTime != null) {
                    if (copy.timing_sec > GlobalConfiguration.bTime + 2f) {
                        continue;
                    }
                }
            }
            if (GlobalConfiguration.playbackRate != null) {
                copy.timing_sec = copy.timing_sec / GlobalConfiguration.playbackRate;
                if ((copy.effect & SongUtils.NOTE_TYPE_HOLD) != 0 && copy.effect_value != null) {
                    copy.effect_value = copy.effect_value / GlobalConfiguration.playbackRate;
                }
            }
            CircleMark mark = new CircleMark(x, y, copy, noteSpeed, delay);
            marks.add(mark);
        }
        marks.sort();

        if (GlobalConfiguration.random) {
            switch (GlobalConfiguration.randomMode) {
                case 0: {
                    Randomizer oldAlgorithmRandomizer = new OldAlgorithmRandomizer();