package com.foxx.androidcast.sender;

/*********************************************************************
 * CodecCatalog.java
 * Created at: Fri 15 May 2026 20:37:44 +0200
 * Updated at: Wed 20 May 2026 15:17:13 +0200 by Anton Afanasyeu <a.afanasieff@gmail.com>
 * Commit: 5d8e82d2e60a21fff3138d2a394ee4e8b4c6dcb8
 * class CodecCatalog
 * Contributors:
 *    - Anton Afanasyeu <a.afanasieff@gmail.com> (6 commits, 295 lines)
 *    - Cursor Agent (project assistant)
 * Digest: SHA256	46817d6e28afd269c00e5c8699d553b6938eeedbaca7b5f6ac3ce8f4a8021887
**********************************************************************/
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import android.media.MediaFormat;
import android.os.Build;

import com.foxx.androidcast.CastSettings;
import com.foxx.androidcast.media.CodecPriorityCatalog;
import com.foxx.androidcast.media.CodecTier;
import com.foxx.androidcast.media.codec.jni.NativeCodecBridge;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;

/** Discovers device video encoder/decoder MIME types and UI codec choices with priority scores. */
public final class CodecCatalog {
    private static volatile CapabilitySnapshot capabilities;

    private CodecCatalog() {}

    public static final class VideoCodecChoice {
        public final CastSettings.VideoCodec codec;
        public final boolean selectable;
        /** Lower = higher priority in UI and AUTO negotiation hints. */
        public final int priorityScore;

        public VideoCodecChoice(CastSettings.VideoCodec codec, boolean selectable, int priorityScore) {
            this.codec = codec;
            this.selectable = selectable;
            this.priorityScore = priorityScore;
        }
    }

    public static final class AudioCodecChoice {
        public final CastSettings.AudioCodec codec;
        public final boolean selectable;
        public final int priorityScore;

        public AudioCodecChoice(CastSettings.AudioCodec codec, boolean selectable, int priorityScore) {
            this.codec = codec;
            this.selectable = selectable;
            this.priorityScore = priorityScore;
        }
    }

    /** Selectable codecs for settings spinners (AUTO first, then by ascending priority score). */
    public static List<VideoCodecChoice> videoCodecChoicesForUi(CastSettings settings) {
        ensureCapabilities();
        List<VideoCodecChoice> out = new ArrayList<>();
        out.add(new VideoCodecChoice(CastSettings.VideoCodec.AUTO, true, 0));
        Set<String> enc = capabilities.hardwareEncoderMimes;
        if (enc.contains(MediaFormat.MIMETYPE_VIDEO_AVC)) {
            out.add(scoredVideo(CastSettings.VideoCodec.H264, true, settings));
        }
        if (enc.contains(MediaFormat.MIMETYPE_VIDEO_HEVC)) {
            out.add(scoredVideo(CastSettings.VideoCodec.H265, true, settings));
        }
        if (capabilities.softwareEncoderMimes.contains(MediaFormat.MIMETYPE_VIDEO_VP8)) {
            out.add(scoredVideo(CastSettings.VideoCodec.LIBVPX_VP8, true, settings));
        }
        if (capabilities.softwareEncoderMimes.contains(MediaFormat.MIMETYPE_VIDEO_VP9)) {
            out.add(scoredVideo(CastSettings.VideoCodec.LIBVPX_VP9, true, settings));
        }
        out.add(scoredVideo(CastSettings.VideoCodec.PASSTHROUGH, true, settings));
        sortVideoChoices(out);
        return out;
    }

    public static List<VideoCodecChoice> videoCodecChoicesForUi() {
        return videoCodecChoicesForUi(null);
    }

    public static List<AudioCodecChoice> audioCodecChoicesForUi(CastSettings settings) {
        List<AudioCodecChoice> out = new ArrayList<>();
        out.add(new AudioCodecChoice(CastSettings.AudioCodec.AUTO, true, 0));
        // Selectable even without native .so — stub path uses AAC + OPUS/SPEEX backend label (validation).
        out.add(scoredAudio(CastSettings.AudioCodec.OPUS, true, settings));
        out.add(scoredAudio(CastSettings.AudioCodec.AAC, true, settings));
        out.add(scoredAudio(CastSettings.AudioCodec.SPEEX, true, settings));
        out.add(scoredAudio(CastSettings.AudioCodec.PASSTHROUGH_DEBUG, true, settings));
        sortAudioChoices(out);
        return out;
    }

    public static List<AudioCodecChoice> audioCodecChoicesForUi() {
        return audioCodecChoicesForUi(null);
    }

    /** Advertised audio encoder lines for diagnostics (MIME + tier + priority). */
    public static List<String> localAdvertisedAudioEncoderLines(CastSettings settings) {
        CastSettings s = settings != null ? settings : new CastSettings();
        List<String> out = new ArrayList<>();
        out.add(audioAdvertisedLine(CastSettings.AudioCodec.AAC, MediaFormat.MIMETYPE_AUDIO_AAC, s));
        out.add(audioAdvertisedLine(CastSettings.AudioCodec.OPUS, "audio/opus", s));
        out.add(audioAdvertisedLine(CastSettings.AudioCodec.SPEEX, "audio/speex", s));
        out.add(audioAdvertisedLine(CastSettings.AudioCodec.PASSTHROUGH_DEBUG, "audio/raw", s));
        return out;
    }

    public static List<String> localAdvertisedAudioDecoderLines(CastSettings settings) {
        return localAdvertisedAudioEncoderLines(settings);
    }

    /** Audio codec names advertised on wire (priority order; excludes AUTO). */
    public static List<String> localAdvertisedAudioCodecsInOrder(CastSettings settings) {
        List<String> out = new ArrayList<>();
        for (AudioCodecChoice c : audioCodecChoicesForUi(settings)) {
            if (c.codec == CastSettings.AudioCodec.AUTO) {
                continue;
            }
            out.add(c.codec.name());
        }
        return out;
    }

    /** Video MIME types advertised on wire (encoder catalog order). */
    public static List<String> localAdvertisedVideoMimesInOrder() {
        return localEncoderMimes();
    }

    /** Video codec labels for diagnostics (priority order; excludes AUTO). */
    public static List<String> localAdvertisedVideoCodecsInOrder(CastSettings settings) {
        List<String> out = new ArrayList<>();
        for (VideoCodecChoice c : videoCodecChoicesForUi(settings)) {
            if (c.codec == CastSettings.VideoCodec.AUTO) {
                continue;
            }
            out.add(c.codec.name());
        }
        return out;
    }

    private static String audioAdvertisedLine(CastSettings.AudioCodec codec, String mime, CastSettings settings) {
        CodecTier tier = com.foxx.androidcast.media.CodecPriorityCatalog.tierForAudioPreference(codec);
        int score = com.foxx.androidcast.media.CodecPriorityCatalog.priorityScore(codec, settings);
        String backend = audioBackendLabel(codec);
        return String.format(Locale.US, "  %s [%s] pri=%d · %s", mime, tier.name(), score, backend);
    }

    private static String audioBackendLabel(CastSettings.AudioCodec codec) {
        switch (codec) {
            case OPUS:
                return NativeCodecBridge.isOpusAvailable() ? "Opus native enc" : "Opus stub→AAC enc";
            case SPEEX:
                return NativeCodecBridge.isSpeexAvailable() ? "Speex native enc" : "Speex stub→AAC enc";
            case PASSTHROUGH_DEBUG:
                return "PCM passthrough (debug)";
            case AAC:
            default:
                return "MediaCodec AAC enc";
        }
    }

    public static List<String> localNativeSoftwareVideoLines(CastSettings settings) {
        CastSettings s = settings != null ? settings : new CastSettings();
        List<String> out = new ArrayList<>();
        if (!NativeCodecBridge.isLibvpxAvailable()) {
            return out;
        }
        out.add(nativeVideoLine(MediaFormat.MIMETYPE_VIDEO_VP8, CastSettings.VideoCodec.LIBVPX_VP8, s, "enc"));
        out.add(nativeVideoLine(MediaFormat.MIMETYPE_VIDEO_VP9, CastSettings.VideoCodec.LIBVPX_VP9, s, "enc"));
        return out;
    }

    public static List<String> localNativeSoftwareVideoDecoderLines(CastSettings settings) {
        CastSettings s = settings != null ? settings : new CastSettings();
        List<String> out = new ArrayList<>();
        if (!NativeCodecBridge.isLibvpxAvailable()) {
            return out;
        }
        out.add(nativeVideoLine(MediaFormat.MIMETYPE_VIDEO_VP8, CastSettings.VideoCodec.LIBVPX_VP8, s, "dec"));
        out.add(nativeVideoLine(MediaFormat.MIMETYPE_VIDEO_VP9, CastSettings.VideoCodec.LIBVPX_VP9, s, "dec"));
        return out;
    }

    private static String nativeVideoLine(String mime, CastSettings.VideoCodec codec, CastSettings settings,
            String role) {
        int score = com.foxx.androidcast.media.CodecPriorityCatalog.priorityScore(codec, settings);
        return String.format(Locale.US, "  [SW:%04d] MIME: '%s', libvpx JNI %s", score, mime, role);
    }

    public static List<CastSettings.VideoCodec> supportedChoices() {
        List<CastSettings.VideoCodec> out = new ArrayList<>();
        for (VideoCodecChoice c : videoCodecChoicesForUi()) {
            if (c.selectable) {
                out.add(c.codec);
            }
        }
        return out;
    }

    public static List<String> localEncoderMimes() {
        ensureCapabilities();
        return new ArrayList<>(capabilities.advertisedEncoderMimes);
    }

    public static List<String> localDecoderMimes() {
        ensureCapabilities();
        return new ArrayList<>(capabilities.advertisedDecoderMimes);
    }

    public static boolean hasHardwareCodec(String mime, boolean encoder) {
        ensureCapabilities();
        Set<String> set = encoder ? capabilities.hardwareEncoderMimes : capabilities.hardwareDecoderMimes;
        return containsIgnoreCase(set, mime);
    }

    public static boolean hasSoftwareCodec(String mime, boolean encoder) {
        ensureCapabilities();
        Set<String> set = encoder ? capabilities.softwareEncoderMimes : capabilities.softwareDecoderMimes;
        return containsIgnoreCase(set, mime);
    }

    /** Tier used for scoring on this device for a MIME (HW preferred over SW when both exist). */
    public static CodecTier effectiveTierForVideoMime(String mime, boolean encoder) {
        if (hasHardwareCodec(mime, encoder)) {
            return CodecTier.HARDWARE;
        }
        if (hasSoftwareCodec(mime, encoder)) {
            return CodecTier.SOFTWARE;
        }
        return CodecTier.PASSTHROUGH;
    }

    public static boolean canUseVideoMimeOnDevice(String mime, boolean encoder) {
        return hasHardwareCodec(mime, encoder) || hasSoftwareCodec(mime, encoder);
    }

    private static VideoCodecChoice scoredVideo(CastSettings.VideoCodec codec, boolean selectable,
            CastSettings settings) {
        int score = CodecPriorityCatalog.priorityScore(codec, settings);
        return new VideoCodecChoice(codec, selectable, score);
    }

    private static AudioCodecChoice scoredAudio(CastSettings.AudioCodec codec, boolean selectable,
            CastSettings settings) {
        int score = CodecPriorityCatalog.priorityScore(codec, settings);
        return new AudioCodecChoice(codec, selectable, score);
    }

    private static void sortVideoChoices(List<VideoCodecChoice> choices) {
        if (choices.size() <= 2) {
            return;
        }
        VideoCodecChoice auto = choices.get(0);
        List<VideoCodecChoice> rest = new ArrayList<>(choices.subList(1, choices.size()));
        Collections.sort(rest, Comparator.comparingInt(c -> c.priorityScore));
        choices.clear();
        choices.add(auto);
        choices.addAll(rest);
    }

    private static void sortAudioChoices(List<AudioCodecChoice> choices) {
        if (choices.size() <= 2) {
            return;
        }
        AudioCodecChoice auto = choices.get(0);
        List<AudioCodecChoice> rest = new ArrayList<>(choices.subList(1, choices.size()));
        Collections.sort(rest, Comparator.comparingInt(c -> c.priorityScore));
        choices.clear();
        choices.add(auto);
        choices.addAll(rest);
    }

    /** Clears cached HW/SW probe (call after {@link com.foxx.androidcast.media.CodecPriorityCatalog#reload}). */
    public static void invalidateCapabilities() {
        capabilities = null;
    }

    private static void ensureCapabilities() {
        if (capabilities != null) {
            return;
        }
        synchronized (CodecCatalog.class) {
            if (capabilities != null) {
                return;
            }
            capabilities = probeCapabilities();
        }
    }

    private static CapabilitySnapshot probeCapabilities() {
        Set<String> hwEnc = probeHardwareMimes(true);
        Set<String> hwDec = probeHardwareMimes(false);
        Set<String> swEnc = new LinkedHashSet<>();
        Set<String> swDec = new LinkedHashSet<>();
        if (NativeCodecBridge.isLibvpxAvailable()) {
            swEnc.add(MediaFormat.MIMETYPE_VIDEO_VP8);
            swEnc.add(MediaFormat.MIMETYPE_VIDEO_VP9);
            swDec.add(MediaFormat.MIMETYPE_VIDEO_VP8);
            swDec.add(MediaFormat.MIMETYPE_VIDEO_VP9);
        }
        Set<String> advertisedEnc = new LinkedHashSet<>(hwEnc);
        advertisedEnc.addAll(swEnc);
        if (advertisedEnc.isEmpty()) {
            advertisedEnc.add(MediaFormat.MIMETYPE_VIDEO_AVC);
        }
        Set<String> advertisedDec = new LinkedHashSet<>(hwDec);
        advertisedDec.addAll(swDec);
        if (advertisedDec.isEmpty()) {
            advertisedDec.add(MediaFormat.MIMETYPE_VIDEO_AVC);
        }
        return new CapabilitySnapshot(hwEnc, hwDec, swEnc, swDec, advertisedEnc, advertisedDec);
    }

    private static Set<String> probeHardwareMimes(boolean encoder) {
        Set<String> out = new LinkedHashSet<>();
        String[] want = {
                MediaFormat.MIMETYPE_VIDEO_AVC,
                MediaFormat.MIMETYPE_VIDEO_HEVC,
                MediaFormat.MIMETYPE_VIDEO_VP9,
                MediaFormat.MIMETYPE_VIDEO_VP8,
        };
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
            for (MediaCodecInfo info : list.getCodecInfos()) {
                if (info.isEncoder() != encoder) {
                    continue;
                }
                for (String type : info.getSupportedTypes()) {
                    for (String w : want) {
                        if (w.equalsIgnoreCase(type)) {
                            out.add(w);
                        }
                    }
                }
            }
        } else {
            int n = MediaCodecList.getCodecCount();
            for (int i = 0; i < n; i++) {
                MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i);
                if (info.isEncoder() != encoder) {
                    continue;
                }
                for (String type : info.getSupportedTypes()) {
                    for (String w : want) {
                        if (w.equalsIgnoreCase(type)) {
                            out.add(w);
                        }
                    }
                }
            }
        }
        return out;
    }

    private static boolean containsIgnoreCase(Set<String> set, String mime) {
        if (mime == null) {
            return false;
        }
        for (String s : set) {
            if (s != null && s.equalsIgnoreCase(mime)) {
                return true;
            }
        }
        return false;
    }

    private static final class CapabilitySnapshot {
        final Set<String> hardwareEncoderMimes;
        final Set<String> hardwareDecoderMimes;
        final Set<String> softwareEncoderMimes;
        final Set<String> softwareDecoderMimes;
        final Set<String> advertisedEncoderMimes;
        final Set<String> advertisedDecoderMimes;

        CapabilitySnapshot(Set<String> hardwareEncoderMimes, Set<String> hardwareDecoderMimes,
                Set<String> softwareEncoderMimes, Set<String> softwareDecoderMimes,
                Set<String> advertisedEncoderMimes, Set<String> advertisedDecoderMimes) {
            this.hardwareEncoderMimes = hardwareEncoderMimes;
            this.hardwareDecoderMimes = hardwareDecoderMimes;
            this.softwareEncoderMimes = softwareEncoderMimes;
            this.softwareDecoderMimes = softwareDecoderMimes;
            this.advertisedEncoderMimes = advertisedEncoderMimes;
            this.advertisedDecoderMimes = advertisedDecoderMimes;
        }
    }
}
