/*
 * Decompiled with CFR 0.152.
 * 
 * Could not load the following classes:
 *  com.google.gson.JsonArray
 *  com.google.gson.JsonElement
 *  com.google.gson.JsonObject
 *  com.google.gson.JsonParser
 *  meteordevelopment.orbit.EventHandler
 *  net.minecraft.class_332
 *  org.lwjgl.glfw.GLFW
 */
package meteordevelopment.meteorclient.systems.modules.misc;

import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.awt.Color;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.invoke.CallSite;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileVisitOption;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.UnsupportedAudioFileException;
import meteordevelopment.meteorclient.events.render.Render2DEvent;
import meteordevelopment.meteorclient.events.world.TickEvent;
import meteordevelopment.meteorclient.settings.BoolSetting;
import meteordevelopment.meteorclient.settings.ColorSetting;
import meteordevelopment.meteorclient.settings.DoubleSetting;
import meteordevelopment.meteorclient.settings.Setting;
import meteordevelopment.meteorclient.settings.SettingGroup;
import meteordevelopment.meteorclient.systems.modules.Categories;
import meteordevelopment.meteorclient.systems.modules.Module;
import meteordevelopment.meteorclient.utils.render.RenderUtils;
import meteordevelopment.meteorclient.utils.render.color.SettingColor;
import meteordevelopment.orbit.EventHandler;
import net.minecraft.class_332;
import org.lwjgl.glfw.GLFW;

public class MusicPlayer2
extends Module {
    private static final int CONTROL_BUTTON_HEIGHT = 12;
    private static final int CONTROL_BUTTON_GAP = 4;
    private static final int PLAY_BUTTON_WIDTH = 38;
    private static final int SKIP_BUTTON_WIDTH = 32;
    private final SettingGroup sgGeneral;
    private final Setting<Boolean> overlay;
    private final Setting<Double> xPos;
    private final Setting<Double> yPos;
    private final Setting<Boolean> shuffle;
    private final Setting<SettingColor> accentColor;
    private final Setting<Double> volume;
    private static final String MANIFEST_URL = "https://pub-6a5d87d6801b41c983c2736e9d0e88ce.r2.dev/radio/manifest.json";
    private final List<File> songQueue;
    private final Random random;
    private volatile boolean isPlaying;
    private volatile boolean paused;
    private volatile boolean loading;
    private Thread audioThread;
    private Thread cloudSyncThread;
    private Thread startupThread;
    private Clip currentClip;
    private int currentSongIndex;
    private String currentSongName;
    private final List<File> songDirectories;
    private long lastRescanMs;
    private long lastCloudSyncMs;
    private boolean bundledExtractAttempted;
    private volatile long playbackSession;
    private volatile boolean awaitingFirstPlayback;
    private volatile boolean worldPlaybackInitialized;
    private final Set<String> failedSongs;
    private boolean dragging;
    private boolean adjustingVolume;
    private boolean leftMouseDownLast;
    private int dragOffsetX;
    private int dragOffsetY;
    private UiInteraction uiInteraction;

    public MusicPlayer2() {
        super(Categories.Misc, "music-player-2", "Advanced radio with cloud sync and local playback.");
        this.sgGeneral = this.settings.getDefaultGroup();
        this.overlay = this.sgGeneral.add(((BoolSetting.Builder)((BoolSetting.Builder)((BoolSetting.Builder)new BoolSetting.Builder().name("overlay")).description("Show the radio overlay.")).defaultValue(true)).build());
        this.xPos = this.sgGeneral.add(((DoubleSetting.Builder)((DoubleSetting.Builder)new DoubleSetting.Builder().name("x")).description("X position of the overlay.")).defaultValue(14.0).min(0.0).sliderMax(1920.0).build());
        this.yPos = this.sgGeneral.add(((DoubleSetting.Builder)((DoubleSetting.Builder)new DoubleSetting.Builder().name("y")).description("Y position of the overlay.")).defaultValue(110.0).min(0.0).sliderMax(1080.0).build());
        this.shuffle = this.sgGeneral.add(((BoolSetting.Builder)((BoolSetting.Builder)((BoolSetting.Builder)new BoolSetting.Builder().name("shuffle")).description("Shuffle the songs.")).defaultValue(true)).build());
        this.accentColor = this.sgGeneral.add(((ColorSetting.Builder)((ColorSetting.Builder)new ColorSetting.Builder().name("accent-color")).description("Accent color for the music player.")).defaultValue(new SettingColor(0, 255, 180, 255)).build());
        this.volume = this.sgGeneral.add(((DoubleSetting.Builder)((DoubleSetting.Builder)new DoubleSetting.Builder().name("volume")).description("Volume of the music.")).defaultValue(5.0).min(0.0).max(100.0).sliderMax(100.0).build());
        this.songQueue = new ArrayList<File>();
        this.random = new Random();
        this.isPlaying = false;
        this.paused = false;
        this.loading = false;
        this.currentSongIndex = 0;
        this.currentSongName = "Idle";
        this.songDirectories = new ArrayList<File>();
        this.lastRescanMs = 0L;
        this.lastCloudSyncMs = 0L;
        this.bundledExtractAttempted = false;
        this.playbackSession = 0L;
        this.awaitingFirstPlayback = false;
        this.worldPlaybackInitialized = false;
        this.failedSongs = Collections.newSetFromMap(new ConcurrentHashMap());
        this.dragging = false;
        this.adjustingVolume = false;
        this.leftMouseDownLast = false;
        this.dragOffsetX = 0;
        this.dragOffsetY = 0;
        this.uiInteraction = UiInteraction.NONE;
    }

    @Override
    public void onActivate() {
        ++this.playbackSession;
        this.worldPlaybackInitialized = false;
        this.awaitingFirstPlayback = false;
        this.loading = false;
        this.paused = false;
        this.currentSongName = "Join a server";
        this.failedSongs.clear();
        this.lastCloudSyncMs = 0L;
        this.ensureSongsDirectory();
        this.startPlaybackWhenWorldReady();
    }

    @Override
    public void onDeactivate() {
        ++this.playbackSession;
        this.worldPlaybackInitialized = false;
        this.awaitingFirstPlayback = false;
        this.loading = false;
        this.paused = false;
        this.failedSongs.clear();
        this.dragging = false;
        this.adjustingVolume = false;
        this.leftMouseDownLast = false;
        this.uiInteraction = UiInteraction.NONE;
        this.lastCloudSyncMs = 0L;
        this.stopBackgroundWorkers();
    }

    @EventHandler
    private void onTick(TickEvent.Post event) {
        if (!this.isInPlayableWorld()) {
            if (this.worldPlaybackInitialized || this.loading || this.isPlaying || this.awaitingFirstPlayback) {
                this.stopBackgroundWorkers();
                this.worldPlaybackInitialized = false;
            }
            this.awaitingFirstPlayback = false;
            this.loading = false;
            this.currentSongName = "Join a server";
        } else if (!this.worldPlaybackInitialized) {
            this.startPlaybackWhenWorldReady();
        } else {
            long now;
            Clip clip = this.currentClip;
            if (clip != null && clip.isOpen()) {
                this.setVolume(clip, this.volume.get().floatValue());
            }
            if (this.awaitingFirstPlayback) {
                if (clip != null && clip.isOpen() && clip.isRunning()) {
                    this.awaitingFirstPlayback = false;
                    this.loading = false;
                } else {
                    this.loading = true;
                }
            }
            if ((now = System.currentTimeMillis()) - this.lastRescanMs >= 8000L) {
                this.lastRescanMs = now;
                int before = this.getQueueSize();
                this.loadSongs();
                if (!(before != 0 || this.getQueueSize() <= 0 || this.audioThread != null && this.audioThread.isAlive())) {
                    this.startPlayback();
                }
            }
            if (!(now - this.lastCloudSyncMs < 20000L || this.cloudSyncThread != null && this.cloudSyncThread.isAlive())) {
                this.startCloudSync(false);
            }
        }
    }

    private boolean isInPlayableWorld() {
        return this.mc != null && this.mc.field_1687 != null && this.mc.field_1724 != null;
    }

    private void startPlaybackWhenWorldReady() {
        if (this.isActive() && this.isInPlayableWorld()) {
            this.worldPlaybackInitialized = true;
            this.awaitingFirstPlayback = true;
            this.loading = true;
            this.currentSongName = "Loading...";
            this.failedSongs.clear();
            this.startupLoadAsync();
        }
    }

    @EventHandler
    private void onRender2D(Render2DEvent event) {
        int fillW;
        String title;
        if (!this.overlay.get().booleanValue() || this.mc.field_1724 == null) {
            return;
        }
        class_332 context = event.drawContext;
        int x = this.xPos.get().intValue();
        int y = this.yPos.get().intValue();
        int width = 206;
        int height = 72;
        int volumeX = x + 10;
        int volumeY = y + 20;
        int volumeW = width - 20;
        int volumeH = 5;
        this.handleUiInput(x, y, width, height, volumeX, volumeY, volumeW, volumeH);
        x = this.xPos.get().intValue();
        y = this.yPos.get().intValue();
        volumeX = x + 10;
        volumeY = y + 20;
        meteordevelopment.meteorclient.utils.render.color.Color accent = this.accentColor.get();
        meteordevelopment.meteorclient.utils.render.color.Color card = new meteordevelopment.meteorclient.utils.render.color.Color(28, 30, 36, 180);
        meteordevelopment.meteorclient.utils.render.color.Color border = new meteordevelopment.meteorclient.utils.render.color.Color(accent.r, accent.g, accent.b, 120);
        meteordevelopment.meteorclient.utils.render.color.Color subText = new meteordevelopment.meteorclient.utils.render.color.Color(186, 190, 198, 255);
        meteordevelopment.meteorclient.utils.render.color.Color barBg = new meteordevelopment.meteorclient.utils.render.color.Color(60, 65, 74, 220);
        meteordevelopment.meteorclient.utils.render.color.Color volumeFillColor = new meteordevelopment.meteorclient.utils.render.color.Color(120, 205, 168, 255);
        RenderUtils.renderRoundedQuad(null, new Color(card.r, card.g, card.b, card.a), x, y, x + width, y + height, 8.0, 20.0);
        RenderUtils.renderRoundedOutline(context, new Color(border.r, border.g, border.b, border.a), x, y, x + width, y + height, 8.0, 8.0, 8.0, 8.0, 1.0, 20.0);
        Object volumeLabel = "Volume " + this.volume.get().intValue() + "%";
        volumeLabel = this.truncateText((String)volumeLabel, width - 20);
        context.method_51433(this.mc.field_1772, (String)volumeLabel, x + 10, y + 9, subText.getPacked(), false);
        RenderUtils.renderRoundedQuad(null, new Color(barBg.r, barBg.g, barBg.b, barBg.a), volumeX, volumeY, volumeX + volumeW, volumeY + volumeH, 2.0, 10.0);
        int volumeFill = Math.max(0, Math.min(volumeW, (int)((float)volumeW * (this.volume.get().floatValue() / 100.0f))));
        if (volumeFill > 0) {
            RenderUtils.renderRoundedQuad(null, new Color(volumeFillColor.r, volumeFillColor.g, volumeFillColor.b, volumeFillColor.a), volumeX, volumeY, volumeX + volumeFill, volumeY + volumeH, 2.0, 10.0);
        }
        int textX = x + 10;
        int textW = width - 20;
        String string = title = this.currentSongName != null && !this.currentSongName.isEmpty() ? this.currentSongName : "Idle";
        String status = this.loading ? "Loading" : (this.getQueueSize() == 0 ? "No songs" : (this.paused ? "Paused" : (this.isPlaying ? "Playing" : "Stopped")));
        title = this.getSlowMarqueeText(title, textW, System.currentTimeMillis());
        status = this.truncateText(status, textW);
        context.method_51433(this.mc.field_1772, title, textX, y + 31, accent.getPacked(), false);
        context.method_51433(this.mc.field_1772, status, textX, y + 42, subText.getPacked(), false);
        int buttonY = y + 6;
        int skipX = x + width - 10 - 32;
        int playX = skipX - 4 - 38;
        this.renderControlButton(context, playX, buttonY, 38, 12, this.paused ? "Play" : "Pause", accent);
        this.renderControlButton(context, skipX, buttonY, 32, 12, "Skip", accent);
        float progress = this.getProgressRatio();
        int barY = y + 55;
        int barH = 4;
        RenderUtils.renderRoundedQuad(null, new Color(barBg.r, barBg.g, barBg.b, barBg.a), textX, barY, textX + textW, barY + barH, 2.0, 10.0);
        if (progress > 0.0f && (fillW = Math.max(0, Math.min(textW, (int)((float)textW * progress)))) > 0) {
            RenderUtils.renderRoundedQuad(null, new Color(accent.r, accent.g, accent.b, accent.a), textX, barY, textX + fillW, barY + barH, 2.0, 10.0);
        }
        Object timeText = this.formatTime(this.getCurrentSeconds()) + " / " + this.formatTime(this.getTotalSeconds());
        timeText = this.truncateText((String)timeText, textW);
        context.method_51433(this.mc.field_1772, (String)timeText, textX, y + 62, subText.getPacked(), false);
    }

    private void ensureSongsDirectory() {
        this.songDirectories.clear();
        File primary = this.getPrimarySongsDir();
        this.ensureHiddenDirectory(primary);
        this.songDirectories.add(primary);
    }

    private void startupLoadAsync() {
        if (this.startupThread == null || !this.startupThread.isAlive()) {
            long session = this.playbackSession;
            this.startupThread = new Thread(() -> {
                try {
                    if (!this.isActive() || session != this.playbackSession || !this.isInPlayableWorld()) {
                        return;
                    }
                    this.extractBundledTracksIfPresent();
                    this.migrateLegacySongs(this.getPrimarySongsDir());
                    this.provisionDefaultTrackIfMissing();
                    this.loadSongs();
                    if (!this.isActive() || session != this.playbackSession || !this.isInPlayableWorld()) {
                        return;
                    }
                    if (this.getQueueSize() > 0) {
                        this.startPlayback();
                        this.startCloudSync(false);
                    } else {
                        this.startCloudSync(true);
                    }
                }
                catch (Throwable throwable) {
                    // empty catch block
                }
            }, "Radio-StartupLoad");
            this.startupThread.setDaemon(true);
            this.startupThread.start();
        }
    }

    private void provisionDefaultTrackIfMissing() {
        File target;
        File[] wavs;
        File primary = this.getPrimarySongsDir();
        if (!primary.exists()) {
            primary.mkdirs();
        }
        if (!((wavs = primary.listFiles((dir, name) -> name != null && name.toLowerCase().endsWith(".wav"))) != null && wavs.length > 0 || (target = new File(primary, "radio_default.wav")).exists() && target.length() > 0L)) {
            try {
                byte[] wavData = this.generateSineWav(44100, 2, 220.0, 0.22);
                Files.write(target.toPath(), wavData, new OpenOption[0]);
            }
            catch (Exception exception) {
                // empty catch block
            }
        }
    }

    private File getPrimarySongsDir() {
        File base = new File(this.mc.field_1697, "divine-radio");
        if (!base.exists()) {
            base.mkdirs();
        }
        return base;
    }

    private void ensureHiddenDirectory(File dir) {
        try {
            if (!dir.exists()) {
                dir.mkdirs();
            }
            if (System.getProperty("os.name", "").toLowerCase().contains("win")) {
                Files.setAttribute(dir.toPath(), "dos:hidden", true, new LinkOption[0]);
            }
        }
        catch (Exception exception) {
            // empty catch block
        }
    }

    private void migrateLegacySongs(File targetDir) {
        ArrayList<File> legacyDirs = new ArrayList<File>();
        legacyDirs.add(new File(this.mc.field_1697, "meteor-client/radio"));
        legacyDirs.add(new File(this.mc.field_1697, "radio"));
        for (File legacy : legacyDirs) {
            if (legacy == null || !legacy.exists() || legacy.equals(targetDir)) continue;
            try {
                Files.walk(legacy.toPath(), new FileVisitOption[0]).filter(p -> Files.isRegularFile(p, new LinkOption[0])).filter(p -> this.isSupportedAudioFile(p.toString())).forEach(p -> {
                    try {
                        Path rel = legacy.toPath().relativize((Path)p);
                        File out = new File(targetDir, rel.toString());
                        File parent = out.getParentFile();
                        if (parent != null && !parent.exists()) {
                            parent.mkdirs();
                        }
                        if (!out.exists() || out.length() == 0L) {
                            Files.copy(p, out.toPath(), StandardCopyOption.REPLACE_EXISTING);
                        }
                    }
                    catch (Exception exception) {
                        // empty catch block
                    }
                });
            }
            catch (Exception exception) {}
        }
    }

    private void extractBundledTracksIfPresent() {
        if (!this.bundledExtractAttempted) {
            this.bundledExtractAttempted = true;
            File targetDir = this.getPrimarySongsDir();
            try {
                URL codeSource = MusicPlayer2.class.getProtectionDomain().getCodeSource().getLocation();
                if (codeSource == null) {
                    return;
                }
                File sourceFile = new File(codeSource.toURI());
                if (!sourceFile.exists()) {
                    return;
                }
                if (sourceFile.isFile() && sourceFile.getName().toLowerCase().endsWith(".jar")) {
                    this.extractFromJar(sourceFile, targetDir);
                }
            }
            catch (Exception exception) {
                // empty catch block
            }
        }
    }

    private void extractFromJar(File jarFile, File targetDir) {
        try (JarFile jf = new JarFile(jarFile);){
            Enumeration<JarEntry> entries = jf.entries();
            while (entries.hasMoreElements()) {
                String shortName;
                JarEntry entry = entries.nextElement();
                String name = entry.getName();
                if (entry.isDirectory() || !name.toLowerCase().endsWith(".wav") || !name.startsWith("assets/meteor-client/radio/") || (shortName = name.substring("assets/meteor-client/radio/".length())).isEmpty()) continue;
                File out = new File(targetDir, shortName.replace('/', File.separatorChar));
                File parent = out.getParentFile();
                if (parent != null && !parent.exists()) {
                    parent.mkdirs();
                }
                if (out.exists() && out.length() > 0L) continue;
                InputStream in = jf.getInputStream(entry);
                try {
                    Files.copy(in, out.toPath(), StandardCopyOption.REPLACE_EXISTING);
                }
                finally {
                    if (in == null) continue;
                    in.close();
                }
            }
        }
        catch (Exception exception) {
            // empty catch block
        }
    }

    private byte[] generateSineWav(int sampleRate, int seconds, double frequency, double amplitude) throws IOException {
        int channels = 1;
        int bitsPerSample = 16;
        int bytesPerSample = bitsPerSample / 8;
        int totalSamples = sampleRate * seconds;
        int dataSize = totalSamples * channels * bytesPerSample;
        ByteArrayOutputStream baos = new ByteArrayOutputStream(44 + dataSize);
        DataOutputStream out = new DataOutputStream(baos);
        out.writeBytes("RIFF");
        this.writeLEInt(out, 36 + dataSize);
        out.writeBytes("WAVE");
        out.writeBytes("fmt ");
        this.writeLEInt(out, 16);
        this.writeLEShort(out, (short)1);
        this.writeLEShort(out, (short)channels);
        this.writeLEInt(out, sampleRate);
        this.writeLEInt(out, sampleRate * channels * bytesPerSample);
        this.writeLEShort(out, (short)(channels * bytesPerSample));
        this.writeLEShort(out, (short)bitsPerSample);
        out.writeBytes("data");
        this.writeLEInt(out, dataSize);
        double attack = 0.02;
        double release = 0.06;
        for (int i = 0; i < totalSamples; ++i) {
            double t = (double)i / (double)sampleRate;
            double env = 1.0;
            if (t < attack) {
                env = t / attack;
            } else if (t > (double)seconds - release) {
                env = Math.max(0.0, ((double)seconds - t) / release);
            }
            double sample = Math.sin(Math.PI * 2 * frequency * t) * amplitude * env;
            short s = (short)Math.max(Short.MIN_VALUE, Math.min(Short.MAX_VALUE, (int)(sample * 32767.0)));
            this.writeLEShort(out, s);
        }
        out.flush();
        return baos.toByteArray();
    }

    private void writeLEShort(DataOutputStream out, short value) throws IOException {
        out.writeByte(value & 0xFF);
        out.writeByte(value >> 8 & 0xFF);
    }

    private void writeLEInt(DataOutputStream out, int value) throws IOException {
        out.writeByte(value & 0xFF);
        out.writeByte(value >> 8 & 0xFF);
        out.writeByte(value >> 16 & 0xFF);
        out.writeByte(value >> 24 & 0xFF);
    }

    /*
     * WARNING - Removed try catching itself - possible behaviour change.
     */
    private void loadSongs() {
        LinkedHashSet<String> seen = new LinkedHashSet<String>();
        ArrayList<File> found = new ArrayList<File>();
        for (File dir : this.songDirectories) {
            this.scanDirectory(dir, seen, found);
        }
        found.sort(Comparator.comparing(f -> f.getName().toLowerCase(Locale.ROOT)));
        LinkedHashSet<String> seenSongIds = new LinkedHashSet<String>();
        ArrayList<File> deduped = new ArrayList<File>();
        for (File file : found) {
            String songId = this.songIdentity(file);
            if (!seenSongIds.add(songId)) continue;
            deduped.add(file);
        }
        List<File> list = this.songQueue;
        synchronized (list) {
            this.songQueue.clear();
            this.songQueue.addAll(deduped);
            if (this.currentSongIndex >= this.songQueue.size()) {
                this.currentSongIndex = 0;
            }
        }
    }

    private void scanDirectory(File dir, Set<String> seen, List<File> out) {
        if (dir != null && dir.exists()) {
            try {
                Files.walk(dir.toPath(), new FileVisitOption[0]).filter(p -> Files.isRegularFile(p, new LinkOption[0])).filter(p -> this.isSupportedAudioFile(p.toString())).forEach(p -> {
                    String path = p.toFile().getAbsolutePath();
                    if (seen.add(path)) {
                        out.add(p.toFile());
                    }
                });
            }
            catch (IOException iOException) {
                // empty catch block
            }
        }
    }

    /*
     * WARNING - Removed try catching itself - possible behaviour change.
     */
    private int getQueueSize() {
        List<File> list = this.songQueue;
        synchronized (list) {
            return this.songQueue.size();
        }
    }

    private boolean isSupportedAudioFile(String path) {
        String lower = path.toLowerCase();
        return lower.endsWith(".wav") || lower.endsWith(".flac");
    }

    private String songIdentity(File file) {
        return this.normalizeSongIdentity(this.stripSongExtension(file.getName()));
    }

    private String stripSongExtension(String name) {
        if (name == null) {
            return "";
        }
        int dot = name.lastIndexOf(46);
        return dot > 0 ? name.substring(0, dot) : name;
    }

    private String normalizeSongIdentity(String input) {
        String before;
        boolean changed;
        if (input == null) {
            return "";
        }
        String value = input.toLowerCase(Locale.ROOT).trim();
        value = value.replace('_', ' ').replace('-', ' ');
        value = value.replaceAll("\\s+", " ");
        do {
            changed = false;
            before = value;
            value = value.replaceAll("\\(copy\\)$", "").trim();
            value = value.replaceAll("\\scopy$", "").trim();
            value = value.replaceAll("\\(\\d+\\)$", "").trim();
            value = value.replaceAll("\\s\\d+$", "").trim();
        } while (changed = !before.equals(value = value.replaceAll("\\s+", " ").trim()));
        return value;
    }

    private String deriveSongFileName(String rawName, String rawUrl) {
        String candidate;
        String string = candidate = rawName == null ? "" : rawName.trim();
        if (candidate.isEmpty()) {
            candidate = this.extractFileNameFromUrl(rawUrl);
        }
        if ((candidate = this.sanitizeSongFilename(candidate)).isEmpty()) {
            return "";
        }
        if (this.isSupportedAudioFile(candidate)) {
            return candidate;
        }
        String fromUrl = this.extractFileNameFromUrl(rawUrl);
        if (!fromUrl.isEmpty()) {
            String lower = fromUrl.toLowerCase(Locale.ROOT);
            if (lower.endsWith(".wav")) {
                return candidate + ".wav";
            }
            if (lower.endsWith(".flac")) {
                return candidate + ".flac";
            }
        }
        return "";
    }

    private String extractFileNameFromUrl(String rawUrl) {
        if (rawUrl == null || rawUrl.isBlank()) {
            return "";
        }
        try {
            URI uri = URI.create(rawUrl);
            String path = uri.getPath();
            if (path != null && !path.isBlank()) {
                int slash = path.lastIndexOf(47);
                String name = slash >= 0 ? path.substring(slash + 1) : path;
                return URLDecoder.decode(name, StandardCharsets.UTF_8);
            }
        }
        catch (Exception exception) {
            // empty catch block
        }
        return "";
    }

    private String sanitizeSongFilename(String name) {
        if (name == null || name.isBlank()) {
            return "";
        }
        String clean = name.trim().replace('\\', '_').replace('/', '_').replace(':', '_').replace('*', '_').replace('?', '_').replace('\"', '_').replace('<', '_').replace('>', '_').replace('|', '_');
        while (clean.startsWith(".")) {
            clean = clean.substring(1);
        }
        return clean;
    }

    private void startCloudSync(boolean immediateNeed) {
        if ((this.cloudSyncThread == null || !this.cloudSyncThread.isAlive()) && this.isActive() && this.isInPlayableWorld()) {
            this.lastCloudSyncMs = System.currentTimeMillis();
            long session = this.playbackSession;
            this.cloudSyncThread = new Thread(() -> {
                block8: {
                    try {
                        List<CloudSong> songs = this.fetchManifestSongs(MANIFEST_URL);
                        if (songs.isEmpty()) {
                            if (this.getQueueSize() == 0 && this.isInPlayableWorld()) {
                                this.awaitingFirstPlayback = false;
                                this.loading = false;
                                this.currentSongName = "No songs available";
                            }
                            return;
                        }
                        boolean startedFirst = false;
                        for (CloudSong song : songs) {
                            if (!this.isActive() || session != this.playbackSession || !this.isInPlayableWorld()) break;
                            boolean ok = this.ensureCloudSong(song);
                            if (!ok || startedFirst) continue;
                            startedFirst = true;
                            this.loadSongs();
                            if (!this.isActive() || session != this.playbackSession || !this.isInPlayableWorld() || this.isPlaying || this.getQueueSize() <= 0) continue;
                            this.startPlayback();
                        }
                        this.loadSongs();
                        if (this.isActive() && session == this.playbackSession && this.isInPlayableWorld() && !this.isPlaying && this.getQueueSize() > 0) {
                            this.startPlayback();
                        } else if (this.getQueueSize() == 0 && immediateNeed) {
                            this.awaitingFirstPlayback = false;
                            this.loading = false;
                            this.currentSongName = "No songs available";
                        }
                    }
                    catch (Throwable ignored) {
                        if (this.getQueueSize() != 0 || !this.isInPlayableWorld()) break block8;
                        this.awaitingFirstPlayback = false;
                        this.loading = false;
                        this.currentSongName = "No songs available";
                    }
                }
            }, "Radio-CloudSync");
            this.cloudSyncThread.setDaemon(true);
            this.cloudSyncThread.start();
        }
    }

    /*
     * WARNING - Removed try catching itself - possible behaviour change.
     */
    private List<CloudSong> fetchManifestSongs(String urlText) {
        ArrayList<CloudSong> out = new ArrayList<CloudSong>();
        LinkedHashSet<CallSite> seenEntries = new LinkedHashSet<CallSite>();
        HttpURLConnection conn = null;
        try {
            conn = (HttpURLConnection)URI.create(urlText).toURL().openConnection();
            conn.setRequestMethod("GET");
            conn.setConnectTimeout(8000);
            conn.setReadTimeout(12000);
            conn.setRequestProperty("User-Agent", "Meteor-Radio/1.0");
            if (conn.getResponseCode() != 200) {
                ArrayList<CloudSong> arrayList = out;
                return arrayList;
            }
            try (InputStream is = conn.getInputStream();){
                String json = new String(is.readAllBytes(), StandardCharsets.UTF_8);
                if (!json.isEmpty() && json.charAt(0) == '\ufeff') {
                    json = json.substring(1);
                }
                JsonElement rootEl = JsonParser.parseString((String)json.strip());
                JsonArray files = null;
                if (rootEl.isJsonArray()) {
                    files = rootEl.getAsJsonArray();
                } else if (rootEl.isJsonObject()) {
                    JsonObject root = rootEl.getAsJsonObject();
                    JsonArray jsonArray = files = root.has("files") ? root.getAsJsonArray("files") : root.getAsJsonArray("songs");
                }
                if (files == null) {
                    ArrayList<CloudSong> arrayList = out;
                    return arrayList;
                }
                for (JsonElement el : files) {
                    String key;
                    String fileName;
                    String sha;
                    String name;
                    if (!el.isJsonObject()) continue;
                    JsonObject obj = el.getAsJsonObject();
                    String string = obj.has("name") ? obj.get("name").getAsString() : (name = obj.has("filename") ? obj.get("filename").getAsString() : "");
                    String songUrl = obj.has("url") ? obj.get("url").getAsString() : (obj.has("link") ? obj.get("link").getAsString() : "");
                    String string2 = sha = obj.has("sha256") ? obj.get("sha256").getAsString() : "";
                    if (songUrl.isEmpty() || (fileName = this.deriveSongFileName(name, songUrl)).isEmpty() || !this.isSupportedAudioFile(fileName) || !seenEntries.add((CallSite)((Object)(key = sha != null && !sha.isBlank() ? "sha:" + sha.trim().toLowerCase(Locale.ROOT) : "song:" + this.normalizeSongIdentity(this.stripSongExtension(fileName)))))) continue;
                    out.add(new CloudSong(fileName, songUrl, sha));
                }
            }
        }
        catch (Exception exception) {
        }
        finally {
            if (conn != null) {
                conn.disconnect();
            }
        }
        return out;
    }

    /*
     * WARNING - Removed try catching itself - possible behaviour change.
     */
    private boolean ensureCloudSong(CloudSong song) {
        File base = this.getPrimarySongsDir();
        String fileName = this.sanitizeSongFilename(song.name);
        if (fileName.isEmpty()) {
            return false;
        }
        File out = new File(base, fileName);
        if (out.exists() && out.length() > 0L && this.matchesHashIfProvided(out, song.sha256)) {
            return true;
        }
        File tmp = new File(base, fileName + ".part");
        HttpURLConnection conn = null;
        try {
            conn = (HttpURLConnection)URI.create(song.url).toURL().openConnection();
            conn.setRequestMethod("GET");
            conn.setConnectTimeout(10000);
            conn.setReadTimeout(20000);
            conn.setRequestProperty("User-Agent", "Meteor-Radio/1.0");
            if (conn.getResponseCode() != 200) {
                boolean bl = false;
                return bl;
            }
            try (InputStream in = conn.getInputStream();){
                Files.copy(in, tmp.toPath(), StandardCopyOption.REPLACE_EXISTING);
            }
            if (tmp.exists() && tmp.length() > 0L) {
                if (!this.matchesHashIfProvided(tmp, song.sha256)) {
                    tmp.delete();
                    boolean bl = false;
                    return bl;
                }
                Files.move(tmp.toPath(), out.toPath(), StandardCopyOption.REPLACE_EXISTING);
                boolean bl = true;
                return bl;
            }
        }
        catch (Exception exception) {
        }
        finally {
            if (conn != null) {
                conn.disconnect();
            }
            if (tmp.exists()) {
                tmp.delete();
            }
        }
        return false;
    }

    private boolean matchesHashIfProvided(File file, String expectedSha256) {
        if (expectedSha256 == null || expectedSha256.isBlank()) {
            return true;
        }
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            try (FileInputStream fis = new FileInputStream(file);){
                int r;
                byte[] buf = new byte[8192];
                while ((r = fis.read(buf)) > 0) {
                    digest.update(buf, 0, r);
                }
            }
            StringBuilder sb = new StringBuilder();
            for (byte b : digest.digest()) {
                sb.append(String.format("%02x", b));
            }
            return sb.toString().equalsIgnoreCase(expectedSha256.trim());
        }
        catch (Exception ignored) {
            return false;
        }
    }

    private void startPlayback() {
        if (this.isActive() && this.isInPlayableWorld()) {
            long session = this.playbackSession;
            this.loading = true;
            this.isPlaying = true;
            if (this.audioThread == null || !this.audioThread.isAlive()) {
                this.audioThread = new Thread(() -> {
                    try {
                        File songFile;
                        int consecutiveFailures = 0;
                        while (this.isPlaying && this.isActive() && this.isInPlayableWorld() && session == this.playbackSession && this.getQueueSize() > 0 && (songFile = this.pickNextSong()) != null) {
                            if (!this.playSong(songFile)) {
                                if (++consecutiveFailures >= Math.max(1, this.getQueueSize())) {
                                    this.loading = false;
                                    this.currentSongName = "No playable songs";
                                    this.isPlaying = false;
                                    break;
                                }
                                Thread.sleep(650L);
                                continue;
                            }
                            consecutiveFailures = 0;
                            this.waitForSongEnd();
                        }
                        if (this.isPlaying && this.getQueueSize() == 0 && this.isActive() && this.isInPlayableWorld() && session == this.playbackSession) {
                            this.loading = false;
                            this.currentSongName = "No songs found";
                        }
                    }
                    catch (Throwable ignored) {
                        this.loading = false;
                        this.currentSongName = "Playback error";
                    }
                }, "Radio-Playback");
                this.audioThread.setDaemon(true);
                this.audioThread.start();
            }
        }
    }

    /*
     * WARNING - Removed try catching itself - possible behaviour change.
     */
    private File pickNextSong() {
        List<File> list = this.songQueue;
        synchronized (list) {
            if (this.songQueue.isEmpty()) {
                return null;
            }
            if (this.shuffle.get().booleanValue()) {
                ArrayList<File> candidates = new ArrayList<File>();
                for (File file : this.songQueue) {
                    if (file == null || this.failedSongs.contains(file.getAbsolutePath())) continue;
                    candidates.add(file);
                }
                if (candidates.isEmpty()) {
                    return null;
                }
                return (File)candidates.get(this.random.nextInt(candidates.size()));
            }
            int size = this.songQueue.size();
            for (int i = 0; i < size; ++i) {
                if (this.currentSongIndex >= size) {
                    this.currentSongIndex = 0;
                }
                File file = this.songQueue.get(this.currentSongIndex);
                this.currentSongIndex = (this.currentSongIndex + 1) % size;
                if (file == null || this.failedSongs.contains(file.getAbsolutePath())) continue;
                return file;
            }
            return null;
        }
    }

    private void waitForSongEnd() {
        try {
            long startedAt = System.currentTimeMillis();
            while (this.isPlaying) {
                if (this.paused) {
                    Thread.sleep(100L);
                    continue;
                }
                Clip clip = this.currentClip;
                if (clip != null && clip.isOpen()) {
                    long total = clip.getMicrosecondLength();
                    long pos = clip.getMicrosecondPosition();
                    if ((total <= 0L || pos < total - 5000L) && (clip.isRunning() || this.paused || System.currentTimeMillis() - startedAt <= 1200L)) {
                        Thread.sleep(100L);
                        continue;
                    }
                }
                break;
            }
        }
        catch (Throwable throwable) {
            // empty catch block
        }
    }

    /*
     * Enabled aggressive exception aggregation
     */
    private synchronized boolean playSong(File file) {
        String name;
        if (!this.isActive() || !this.isInPlayableWorld()) {
            return false;
        }
        this.stopCurrentClipOnly();
        this.currentSongName = name = this.stripSongExtension(file.getName());
        try {
            AudioInputStream sourceStream = this.openAudioStreamWithFallback(file);
            if (sourceStream == null) {
                this.loading = true;
                this.currentSongName = "Cannot play: " + name;
                return false;
            }
            try (AudioInputStream audioInputStream = sourceStream;){
                boolean bl;
                block24: {
                    Clip clip;
                    AudioInputStream playableStream;
                    block20: {
                        boolean bl2;
                        block23: {
                            block21: {
                                boolean bl3;
                                block22: {
                                    AudioFormat src = sourceStream.getFormat();
                                    AudioFormat pcm = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, src.getSampleRate(), 16, src.getChannels(), src.getChannels() * 2, src.getSampleRate(), false);
                                    playableStream = AudioSystem.getAudioInputStream(pcm, sourceStream);
                                    try {
                                        clip = AudioSystem.getClip();
                                        clip.open(playableStream);
                                        this.setVolume(clip, this.volume.get().floatValue());
                                        clip.setFramePosition(0);
                                        clip.start();
                                        Thread.sleep(120L);
                                        if (!clip.isRunning()) break block20;
                                        if (!this.isActive() || !this.isInPlayableWorld()) break block21;
                                        this.currentClip = clip;
                                        this.paused = false;
                                        this.loading = false;
                                        this.failedSongs.remove(file.getAbsolutePath());
                                        bl3 = true;
                                        if (playableStream == null) break block22;
                                    }
                                    catch (Throwable throwable) {
                                        if (playableStream != null) {
                                            try {
                                                playableStream.close();
                                            }
                                            catch (Throwable throwable2) {
                                                throwable.addSuppressed(throwable2);
                                            }
                                        }
                                        throw throwable;
                                    }
                                    playableStream.close();
                                }
                                return bl3;
                            }
                            clip.stop();
                            clip.close();
                            bl2 = false;
                            if (playableStream == null) break block23;
                            playableStream.close();
                        }
                        return bl2;
                    }
                    clip.stop();
                    clip.close();
                    this.loading = true;
                    this.currentSongName = "Cannot play: " + name;
                    this.failedSongs.add(file.getAbsolutePath());
                    bl = false;
                    if (playableStream == null) break block24;
                    playableStream.close();
                }
                return bl;
            }
        }
        catch (Exception e) {
            this.loading = true;
            this.currentSongName = "Cannot play: " + name;
            this.failedSongs.add(file.getAbsolutePath());
            return false;
        }
    }

    private AudioInputStream openAudioStreamWithFallback(File file) throws IOException, UnsupportedAudioFileException {
        try {
            return AudioSystem.getAudioInputStream(file);
        }
        catch (IOException | UnsupportedAudioFileException e) {
            String[] readerClasses;
            if (!file.getName().toLowerCase().endsWith(".flac")) {
                throw e;
            }
            for (String readerName : readerClasses = new String[]{"org.jflac.sound.spi.FlacAudioFileReader", "org.kc7bfi.jflac.sound.spi.FlacAudioFileReader"}) {
                try {
                    Class<?> readerClass = Class.forName(readerName);
                    Object reader = readerClass.getDeclaredConstructor(new Class[0]).newInstance(new Object[0]);
                    Object stream = readerClass.getMethod("getAudioInputStream", File.class).invoke(reader, file);
                    if (!(stream instanceof AudioInputStream)) continue;
                    return (AudioInputStream)stream;
                }
                catch (Exception exception) {
                    // empty catch block
                }
            }
            throw e;
        }
    }

    private synchronized void stopCurrentClipOnly() {
        if (this.currentClip != null) {
            try {
                this.currentClip.stop();
                this.currentClip.close();
            }
            catch (Exception exception) {
                // empty catch block
            }
            this.currentClip = null;
        }
    }

    private void stopBackgroundWorkers() {
        this.isPlaying = false;
        this.paused = false;
        this.loading = false;
        this.stopCurrentClipOnly();
        if (this.audioThread != null) {
            this.audioThread.interrupt();
            this.audioThread = null;
        }
        if (this.cloudSyncThread != null) {
            this.cloudSyncThread.interrupt();
            this.cloudSyncThread = null;
        }
        if (this.startupThread != null) {
            this.startupThread.interrupt();
            this.startupThread = null;
        }
    }

    private void setVolume(Clip clip, float vol) {
        if (clip != null && clip.isControlSupported(FloatControl.Type.MASTER_GAIN)) {
            FloatControl control = (FloatControl)clip.getControl(FloatControl.Type.MASTER_GAIN);
            if (vol <= 0.0f) {
                control.setValue(control.getMinimum());
            } else {
                float gain = (float)(20.0 * Math.log10((double)vol / 100.0));
                control.setValue(Math.max(control.getMinimum(), Math.min(control.getMaximum(), gain)));
            }
        }
    }

    private float getProgressRatio() {
        Clip clip = this.currentClip;
        if (clip != null && clip.isOpen()) {
            long total = clip.getMicrosecondLength();
            long current = clip.getMicrosecondPosition();
            return total <= 0L ? 0.0f : Math.max(0.0f, Math.min(1.0f, (float)current / (float)total));
        }
        return 0.0f;
    }

    private float getCurrentSeconds() {
        Clip clip = this.currentClip;
        return clip != null && clip.isOpen() ? (float)clip.getMicrosecondPosition() / 1000000.0f : 0.0f;
    }

    private float getTotalSeconds() {
        Clip clip = this.currentClip;
        return clip != null && clip.isOpen() ? (float)clip.getMicrosecondLength() / 1000000.0f : 0.0f;
    }

    private String formatTime(float sec) {
        int m = (int)(sec / 60.0f);
        int s = (int)(sec % 60.0f);
        return String.format("%d:%02d", m, s);
    }

    private String truncateText(String text, int maxWidth) {
        if (text == null || text.isEmpty()) {
            return "";
        }
        if (this.mc.field_1772.method_1727(text) <= maxWidth) {
            return text;
        }
        String ellipsis = "...";
        int ellipsisWidth = this.mc.field_1772.method_1727(ellipsis);
        if (ellipsisWidth >= maxWidth) {
            return this.mc.field_1772.method_27523(text, maxWidth);
        }
        return this.mc.field_1772.method_27523(text, maxWidth - ellipsisWidth) + ellipsis;
    }

    private void renderControlButton(class_332 context, int x, int y, int w, int h, String label, meteordevelopment.meteorclient.utils.render.color.Color accent) {
        meteordevelopment.meteorclient.utils.render.color.Color bg = new meteordevelopment.meteorclient.utils.render.color.Color(45, 50, 62, 214);
        meteordevelopment.meteorclient.utils.render.color.Color border = new meteordevelopment.meteorclient.utils.render.color.Color(accent.r, accent.g, accent.b, 130);
        RenderUtils.renderRoundedQuad(null, new Color(bg.r, bg.g, bg.b, bg.a), x, y, x + w, y + h, 3.0, 10.0);
        RenderUtils.renderRoundedOutline(context, new Color(border.r, border.g, border.b, border.a), x, y, x + w, y + h, 3.0, 3.0, 3.0, 3.0, 1.0, 10.0);
        String visible = this.mc.field_1772.method_27523(label, Math.max(2, w - 4));
        int tx = x + (w - this.mc.field_1772.method_1727(visible)) / 2;
        Objects.requireNonNull(this.mc.field_1772);
        int ty = y + (h - 9) / 2;
        context.method_51433(this.mc.field_1772, visible, tx, ty, new meteordevelopment.meteorclient.utils.render.color.Color(230, 234, 242, 255).getPacked(), false);
    }

    private void handleUiInput(int x, int y, int boxW, int boxH, int volumeX, int volumeY, int volumeW, int volumeH) {
        if (this.mc.field_1755 == null) {
            this.dragging = false;
            this.adjustingVolume = false;
            this.leftMouseDownLast = false;
            this.uiInteraction = UiInteraction.NONE;
            return;
        }
        long window = this.mc.method_22683().method_4490();
        double scale = this.mc.method_22683().method_4495();
        double mouseX = this.mc.field_1729.method_1603() / scale;
        double mouseY = this.mc.field_1729.method_1604() / scale;
        boolean leftDown = GLFW.glfwGetMouseButton((long)window, (int)0) == 1;
        boolean justPressed = leftDown && !this.leftMouseDownLast;
        boolean justReleased = !leftDown && this.leftMouseDownLast;
        int buttonY = y + 6;
        int skipX = x + boxW - 10 - 32;
        int playX = skipX - 4 - 38;
        if (justPressed) {
            if (this.isOver(mouseX, mouseY, playX, buttonY, 38, 12)) {
                this.togglePauseResume();
            } else if (this.isOver(mouseX, mouseY, skipX, buttonY, 32, 12)) {
                this.skipCurrentSong();
            } else if (this.isOver(mouseX, mouseY, volumeX - 2, volumeY - 2, volumeW + 4, volumeH + 4)) {
                this.uiInteraction = UiInteraction.VOLUME;
                this.adjustingVolume = true;
                this.applyVolume(mouseX, volumeX, volumeW);
            } else if (this.isOver(mouseX, mouseY, x, y, boxW, boxH)) {
                this.uiInteraction = UiInteraction.DRAGGING;
                this.dragging = true;
                this.dragOffsetX = (int)(mouseX - (double)x);
                this.dragOffsetY = (int)(mouseY - (double)y);
            }
        }
        if (leftDown) {
            if (this.uiInteraction == UiInteraction.VOLUME) {
                this.applyVolume(mouseX, volumeX, volumeW);
            } else if (this.uiInteraction == UiInteraction.DRAGGING) {
                int sw = this.mc.method_22683().method_4486();
                int sh = this.mc.method_22683().method_4502();
                this.xPos.set(Double.valueOf(Math.max(0, Math.min((int)mouseX - this.dragOffsetX, sw - boxW))));
                this.yPos.set(Double.valueOf(Math.max(0, Math.min((int)mouseY - this.dragOffsetY, sh - boxH))));
            }
        }
        if (justReleased) {
            this.dragging = false;
            this.adjustingVolume = false;
            this.uiInteraction = UiInteraction.NONE;
        }
        this.leftMouseDownLast = leftDown;
    }

    private void applyVolume(double mouseX, int volumeX, int volumeW) {
        this.volume.set(Math.max(0.0, Math.min(1.0, (mouseX - (double)volumeX) / (double)volumeW)) * 100.0);
    }

    private boolean isOver(double mx, double my, int x, int y, int w, int h) {
        return mx >= (double)x && mx <= (double)(x + w) && my >= (double)y && my <= (double)(y + h);
    }

    private void togglePauseResume() {
        if (this.currentClip != null && this.currentClip.isOpen()) {
            if (this.paused) {
                this.currentClip.start();
                this.paused = false;
            } else {
                this.currentClip.stop();
                this.paused = true;
            }
        } else if (!(this.loading || this.getQueueSize() <= 0 || this.audioThread != null && this.audioThread.isAlive())) {
            this.paused = false;
            this.startPlayback();
        }
    }

    private void skipCurrentSong() {
        this.paused = false;
        this.loading = true;
        this.stopCurrentClipOnly();
    }

    private String getSlowMarqueeText(String text, int maxWidth, long nowMs) {
        if (text == null || text.isEmpty() || this.mc.field_1772.method_1727(text) <= maxWidth) {
            return text;
        }
        String spacer = "   ";
        String roll = text + spacer + text;
        int step = (int)(nowMs / 450L % (long)(text.length() + spacer.length()));
        return this.mc.field_1772.method_27523(roll.substring(Math.max(0, Math.min(step, roll.length() - 1))), maxWidth);
    }

    private static enum UiInteraction {
        NONE,
        DRAGGING,
        VOLUME;

    }

    private static class CloudSong {
        final String name;
        final String url;
        final String sha256;

        CloudSong(String name, String url, String sha256) {
            this.name = name;
            this.url = url;
            this.sha256 = sha256;
        }
    }
}

