package com.foxx.androidcast.media;

/*********************************************************************
 * CodecPriorityCatalog.java
 * Created at: Mon 18 May 2026 18:59:14 +0200
 * Updated at: Wed 20 May 2026 15:17:13 +0200 by Anton Afanasyeu <a.afanasieff@gmail.com>
 * Commit: 5d8e82d2e60a21fff3138d2a394ee4e8b4c6dcb8
 * class CodecPriorityCatalog
 * Contributors:
 *    - Anton Afanasyeu <a.afanasieff@gmail.com> (3 commits, 542 lines)
 *    - Cursor Agent (project assistant)
 * Digest: SHA256	b14ada272d847b895ab45abaf659441b25bdc4eb634821d4d37827cfa1bc1db4
**********************************************************************/
import android.content.Context;
import android.media.MediaFormat;
import android.util.Log;

import com.foxx.androidcast.CastSettings;
import com.foxx.androidcast.media.codec.jni.NativeCodecBridge;
import com.foxx.androidcast.network.CastCodecFlags;
import com.foxx.androidcast.sender.CodecCatalog;

import org.json.JSONObject;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;

import androidx.annotation.Nullable;

/**
 * Runtime codec priority: tier base (0 / 500 / 1000) + component hooks.
 * Lower {@link #priorityScore} = higher preference in AUTO negotiation and settings ordering.
 */
public final class CodecPriorityCatalog {
    private static final String TAG = "CodecPriority";
    private static final String ASSET = "codecs.json";

    private static final String KEY_INTERNAL = "internal_codec_score";
    private static final String KEY_FEC = "no_inband_fec_score";
    private static final String KEY_PROTECTION = "no_inband_protection_score";
    private static final String KEY_OVERRIDE = "priority_override_score";
    private static final String KEY_BITRATE = "bitrate_score";

    private static volatile JSONObject rootJson = new JSONObject();
    private static volatile Context appContext;

    private CodecPriorityCatalog() {}

    public static void init(Context context) {
        appContext = context.getApplicationContext();
        rootJson = loadJson(appContext);
    }

    /** Reload {@code assets/codecs.json} and refresh local codec capability cache. */
    public static String reload(Context context) {
        Context app = context.getApplicationContext();
        appContext = app;
        JSONObject loaded = loadJson(app);
        rootJson = loaded;
        CodecCatalog.invalidateCapabilities();
        int overrides = countJsonOverrides(loaded);
        String summary = "codecs.json reloaded (" + overrides + " override field(s))";
        Log.i(TAG, summary);
        return summary;
    }

    public static int priorityScore(CodecPriorityId id, CodecTier tier, CastSettings settings) {
        return breakdown(id, tier, settings).total;
    }

    public static ScoreBreakdown breakdown(CodecPriorityId id, CodecTier tier, CastSettings settings) {
        if (id == null || tier == null) {
            return new ScoreBreakdown(Integer.MAX_VALUE, 0, 0, 0, 0, 0, 0);
        }
        int internal = component(id, KEY_INTERNAL, defaultInternal(id));
        int fec = component(id, KEY_FEC, defaultFec(id));
        int protection = component(id, KEY_PROTECTION, defaultProtection(id, settings));
        int override = component(id, KEY_OVERRIDE, 0);
        int bitrate = component(id, KEY_BITRATE, defaultBitrate(id));
        int total = tier.baseScore + internal + fec + protection + override + bitrate;
        return new ScoreBreakdown(total, tier.baseScore, internal, fec, protection, override, bitrate);
    }

    public static int priorityScoreForVideoMime(String mime, CodecTier tier, CastSettings settings) {
        CodecPriorityId id = CodecPriorityId.fromVideoMime(mime);
        if (id == null) {
            return CodecTier.PASSTHROUGH.baseScore + 100;
        }
        return priorityScore(id, tier, settings);
    }

    public static int priorityScore(CastSettings.VideoCodec codec, CastSettings settings) {
        CodecPriorityId id = CodecPriorityId.fromVideoPreference(codec);
        if (id == null) {
            return 0;
        }
        CodecTier tier = tierForVideoPreference(codec, id);
        return priorityScore(id, tier, settings);
    }

    public static int priorityScore(CastSettings.AudioCodec codec, CastSettings settings) {
        CodecPriorityId id = CodecPriorityId.fromAudioPreference(codec);
        if (id == null) {
            return 0;
        }
        CodecTier tier = tierForAudioPreference(codec);
        return priorityScore(id, tier, settings);
    }

    /** Best MIME for AUTO: lowest score among codecs both sides can use. */
    @Nullable
    public static String negotiateBestVideoMime(List<String> senderEncoders, List<String> receiverDecoders,
            CastSettings settings) {
        NegotiationDecision decision = decideBestVideoMime(senderEncoders, receiverDecoders, settings);
        return decision != null ? decision.mime : null;
    }

    /** AUTO negotiation with ranked candidates for logging and diagnostics. */
    @Nullable
    public static NegotiationDecision decideBestVideoMime(List<String> senderEncoders,
            List<String> receiverDecoders, CastSettings settings) {
        List<String> common = commonMimes(senderEncoders, receiverDecoders);
        if (common.isEmpty()) {
            return null;
        }
        List<NegotiationCandidate> candidates = new ArrayList<>();
        for (String mime : common) {
            candidates.add(buildCandidate(mime, settings));
        }
        Collections.sort(candidates, Comparator.comparingInt(c -> c.combinedScore));
        NegotiationCandidate best = candidates.get(0);
        return new NegotiationDecision(best, candidates);
    }

    public static void logNegotiationDecision(String role, NegotiationDecision decision) {
        if (decision == null) {
            return;
        }
        Log.i(TAG, role + " codec AUTO: selected " + decision.logLine());
        for (NegotiationCandidate c : decision.candidates) {
            boolean picked = c.mime != null && c.mime.equals(decision.mime);
            Log.i(TAG, role + " codec candidate: " + c.logLine(picked));
        }
    }

    public static void logExplicitNegotiation(String role, String mime, CastSettings.VideoCodec preference,
            CastSettings settings) {
        int score = combinedVideoMimeScore(mime, settings);
        Log.i(TAG, role + " codec explicit " + preference.name() + ": "
                + displayMime(mime) + " (" + mime + ") score=" + score
                + " · " + formatVideoMimeBreakdown(mime, settings));
    }

    public static void logReceivedCodec(String role, String mime, CastSettings settings) {
        int score = combinedVideoMimeScore(mime, settings);
        Log.i(TAG, role + " codec selected by peer: "
                + displayMime(mime) + " (" + mime + ") score=" + score
                + " · " + formatVideoMimeBreakdown(mime, settings));
    }

    public static String formatVideoMimeBreakdown(String mime, CastSettings settings) {
        CodecPriorityId id = CodecPriorityId.fromVideoMime(mime);
        if (id == null) {
            return mime + " unknown";
        }
        CodecTier localDec = CodecCatalog.effectiveTierForVideoMime(mime, false);
        ScoreBreakdown dec = breakdown(id, localDec, settings);
        int peerEncScore = assumedRemoteEncoderScore(mime, settings);
        return String.format(Locale.US,
                "combined=%d dec=%d (tier %d +%d+%d+%d+%d+%d) peerEnc=%d",
                Math.max(dec.total, peerEncScore),
                dec.total, dec.tierBase, dec.internal, dec.fec, dec.protection, dec.override, dec.bitrate,
                peerEncScore);
    }

    private static NegotiationCandidate buildCandidate(String mime, CastSettings settings) {
        int localDecoder = videoMimeScoreOnDevice(mime, false, settings);
        int peerEncoder = assumedRemoteEncoderScore(mime, settings);
        int combined = Math.max(localDecoder, peerEncoder);
        return new NegotiationCandidate(mime, combined, localDecoder, peerEncoder,
                formatVideoMimeBreakdown(mime, settings));
    }

    /** Conservative score when sender and receiver may use different tiers for the same MIME. */
    public static int combinedVideoMimeScore(String mime, CastSettings settings) {
        int localEncoder = videoMimeScoreOnDevice(mime, true, settings);
        int localDecoder = videoMimeScoreOnDevice(mime, false, settings);
        int assumedPeerEncoder = assumedRemoteEncoderScore(mime, settings);
        return Math.max(Math.max(localEncoder, localDecoder), assumedPeerEncoder);
    }

    /**
     * Estimates encoder-side score for the remote peer during handshake (only MIME caps known).
     */
    public static int assumedRemoteEncoderScore(String mime, CastSettings remoteSettings) {
        CodecPriorityId id = CodecPriorityId.fromVideoMime(mime);
        if (id == null) {
            return CodecTier.PASSTHROUGH.baseScore + 100;
        }
        CodecTier tier = assumedEncoderTier(id, remoteSettings);
        return priorityScore(id, tier, remoteSettings);
    }

    private static CodecTier assumedEncoderTier(CodecPriorityId id, CastSettings remote) {
        CastSettings.VideoCodec pref = remote != null ? remote.getVideoCodec() : CastSettings.VideoCodec.AUTO;
        if (pref == CastSettings.VideoCodec.PASSTHROUGH) {
            return CodecTier.PASSTHROUGH;
        }
        if (pref == CastSettings.VideoCodec.LIBVPX_VP8 && id == CodecPriorityId.VP8) {
            return CodecTier.SOFTWARE;
        }
        if (pref == CastSettings.VideoCodec.LIBVPX_VP9 && id == CodecPriorityId.VP9) {
            return CodecTier.SOFTWARE;
        }
        if (id == CodecPriorityId.VP8 || id == CodecPriorityId.VP9) {
            return CodecTier.HARDWARE;
        }
        return CodecTier.HARDWARE;
    }

    public static int videoMimeScoreOnDevice(String mime, boolean encoder, CastSettings settings) {
        CodecPriorityId id = CodecPriorityId.fromVideoMime(mime);
        if (id == null) {
            return CodecTier.PASSTHROUGH.baseScore + 100;
        }
        CodecTier tier = CodecCatalog.effectiveTierForVideoMime(mime, encoder);
        return priorityScore(id, tier, settings);
    }

    public static CodecTier tierForVideoPreference(CastSettings.VideoCodec preference, CodecPriorityId id) {
        if (preference == CastSettings.VideoCodec.PASSTHROUGH) {
            return CodecTier.PASSTHROUGH;
        }
        if (preference == CastSettings.VideoCodec.LIBVPX_VP8
                || preference == CastSettings.VideoCodec.LIBVPX_VP9) {
            return CodecTier.SOFTWARE;
        }
        if (id == CodecPriorityId.VP8 || id == CodecPriorityId.VP9) {
            return CodecTier.SOFTWARE;
        }
        return CodecTier.HARDWARE;
    }

    public static CodecTier tierForAudioPreference(CastSettings.AudioCodec preference) {
        if (preference == CastSettings.AudioCodec.SPEEX) {
            return NativeCodecBridge.isSpeexAvailable() ? CodecTier.HARDWARE : CodecTier.PASSTHROUGH;
        }
        if (preference == CastSettings.AudioCodec.OPUS) {
            return NativeCodecBridge.isOpusAvailable() ? CodecTier.HARDWARE : CodecTier.PASSTHROUGH;
        }
        if (preference == CastSettings.AudioCodec.PASSTHROUGH_DEBUG) {
            return CodecTier.PASSTHROUGH;
        }
        return CodecTier.HARDWARE;
    }

    /** AUTO: lowest score among audio codecs both peers support on this device. */
    @Nullable
    public static AudioNegotiationDecision decideBestAudioCodec(int senderMask, int receiverMask,
            CastSettings settings) {
        int common = senderMask & receiverMask;
        List<AudioNegotiationCandidate> candidates = new ArrayList<>();
        for (CastSettings.AudioCodec codec : manualAudioPreferences()) {
            if (!CastCodecFlags.maskSupports(common, codec)) {
                continue;
            }
            if (!CastCodecFlags.isLocallyAvailable(codec)) {
                continue;
            }
            int score = audioCodecScoreOnDevice(codec, settings);
            candidates.add(new AudioNegotiationCandidate(codec, score,
                    formatAudioBreakdown(codec, settings)));
        }
        if (candidates.isEmpty()) {
            return null;
        }
        Collections.sort(candidates, Comparator.comparingInt(c -> c.score));
        return new AudioNegotiationDecision(candidates.get(0), candidates);
    }

    public static int audioCodecScoreOnDevice(CastSettings.AudioCodec codec, CastSettings settings) {
        CodecPriorityId id = CodecPriorityId.fromAudioPreference(codec);
        if (id == null) {
            return Integer.MAX_VALUE;
        }
        CodecTier tier = tierForAudioPreference(codec);
        return priorityScore(id, tier, settings);
    }

    public static void logAudioNegotiationDecision(String role, AudioNegotiationDecision decision) {
        if (decision == null) {
            return;
        }
        Log.i(TAG, role + " audio AUTO: selected " + decision.logLine());
        for (AudioNegotiationCandidate c : decision.candidates) {
            boolean picked = c.codec == decision.codec;
            Log.i(TAG, role + " audio candidate: " + c.logLine(picked));
        }
    }

    public static String formatAudioBreakdown(CastSettings.AudioCodec codec, CastSettings settings) {
        CodecPriorityId id = CodecPriorityId.fromAudioPreference(codec);
        if (id == null) {
            return codec.name();
        }
        ScoreBreakdown b = breakdown(id, tierForAudioPreference(codec), settings);
        return String.format(Locale.US, "%s score=%d tier=%d +%d+%d+%d+%d+%d",
                codec.name(), b.total, b.tierBase, b.internal, b.fec, b.protection, b.override, b.bitrate);
    }

    /** Manual settings order (after AUTO): Opus, AAC, Speex, PCM passthrough. */
    public static CastSettings.AudioCodec[] manualAudioPreferences() {
        return new CastSettings.AudioCodec[] {
                CastSettings.AudioCodec.OPUS,
                CastSettings.AudioCodec.AAC,
                CastSettings.AudioCodec.SPEEX,
                CastSettings.AudioCodec.PASSTHROUGH_DEBUG,
        };
    }

    public static final class AudioNegotiationCandidate {
        public final CastSettings.AudioCodec codec;
        public final int score;
        public final String detail;

        AudioNegotiationCandidate(CastSettings.AudioCodec codec, int score, String detail) {
            this.codec = codec;
            this.score = score;
            this.detail = detail;
        }

        String logLine(boolean selected) {
            return (selected ? "* " : "  ") + codec.name() + " score=" + score + " · " + detail;
        }
    }

    public static final class AudioNegotiationDecision {
        public final CastSettings.AudioCodec codec;
        public final int score;
        public final List<AudioNegotiationCandidate> candidates;

        AudioNegotiationDecision(AudioNegotiationCandidate selected,
                List<AudioNegotiationCandidate> candidates) {
            this.codec = selected.codec;
            this.score = selected.score;
            this.candidates = candidates;
        }

        String logLine() {
            return codec.name() + " score=" + score;
        }
    }

    public static <T> List<T> sortByPriority(List<ScoredItem<T>> items) {
        List<ScoredItem<T>> copy = new ArrayList<>(items);
        Collections.sort(copy, Comparator.comparingInt(a -> a.priorityScore));
        List<T> out = new ArrayList<>(copy.size());
        for (ScoredItem<T> item : copy) {
            out.add(item.value);
        }
        return out;
    }

    public static final class ScoredItem<T> {
        public final T value;
        public final int priorityScore;

        public ScoredItem(T value, int priorityScore) {
            this.value = value;
            this.priorityScore = priorityScore;
        }
    }

    public static final class ScoreBreakdown {
        public final int total;
        public final int tierBase;
        public final int internal;
        public final int fec;
        public final int protection;
        public final int override;
        public final int bitrate;

        ScoreBreakdown(int total, int tierBase, int internal, int fec, int protection, int override,
                int bitrate) {
            this.total = total;
            this.tierBase = tierBase;
            this.internal = internal;
            this.fec = fec;
            this.protection = protection;
            this.override = override;
            this.bitrate = bitrate;
        }
    }

    public static final class NegotiationCandidate {
        public final String mime;
        public final int combinedScore;
        public final int localDecoderScore;
        public final int peerEncoderScore;
        public final String detail;

        NegotiationCandidate(String mime, int combinedScore, int localDecoderScore, int peerEncoderScore,
                String detail) {
            this.mime = mime;
            this.combinedScore = combinedScore;
            this.localDecoderScore = localDecoderScore;
            this.peerEncoderScore = peerEncoderScore;
            this.detail = detail;
        }

        String logLine(boolean selected) {
            return (selected ? "* " : "  ")
                    + displayMime(mime) + " score=" + combinedScore
                    + " (dec=" + localDecoderScore + " peerEnc=" + peerEncoderScore + ") " + detail;
        }
    }

    public static final class NegotiationDecision {
        public final String mime;
        public final int score;
        public final NegotiationCandidate selected;
        public final List<NegotiationCandidate> candidates;

        NegotiationDecision(NegotiationCandidate selected, List<NegotiationCandidate> candidates) {
            this.selected = selected;
            this.candidates = candidates;
            this.mime = selected.mime;
            this.score = selected.combinedScore;
        }

        String logLine() {
            return displayMime(mime) + " (" + mime + ") score=" + score + " · " + selected.detail;
        }
    }

    private static String displayMime(String mime) {
        if (mime == null) {
            return "?";
        }
        if (MediaFormat.MIMETYPE_VIDEO_AVC.equals(mime)) {
            return "H.264";
        }
        if (MediaFormat.MIMETYPE_VIDEO_HEVC.equals(mime)) {
            return "H.265";
        }
        if (MediaFormat.MIMETYPE_VIDEO_VP9.equals(mime)) {
            return "VP9";
        }
        if (MediaFormat.MIMETYPE_VIDEO_VP8.equals(mime)) {
            return "VP8";
        }
        int slash = mime.lastIndexOf('/');
        return slash >= 0 ? mime.substring(slash + 1).toUpperCase(Locale.US) : mime;
    }

    private static List<String> commonMimes(List<String> enc, List<String> dec) {
        List<String> out = new ArrayList<>();
        if (enc == null || dec == null) {
            return out;
        }
        for (String mime : enc) {
            if (mime != null && containsIgnoreCase(dec, mime)) {
                out.add(mime);
            }
        }
        return out;
    }

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

    private static int component(CodecPriorityId id, String key, int compileDefault) {
        try {
            JSONObject section = sectionFor(id);
            if (section == null) {
                return compileDefault;
            }
            JSONObject codec = section.optJSONObject(id.jsonKey);
            if (codec != null && codec.has(key)) {
                return codec.optInt(key, compileDefault);
            }
        } catch (Exception ignored) {
        }
        return compileDefault;
    }

    private static JSONObject sectionFor(CodecPriorityId id) {
        if (rootJson == null) {
            return null;
        }
        switch (id) {
            case H264:
            case H265:
            case VP8:
            case VP9:
            case PASSTHROUGH:
                return rootJson.optJSONObject("video");
            case AAC:
            case OPUS:
            case SPEEX:
            case PCM:
            default:
                return rootJson.optJSONObject("audio");
        }
    }

    private static int defaultInternal(CodecPriorityId id) {
        switch (id) {
            case H264:
            case VP9:
                return 0;
            case H265:
            case OPUS:
                return 1;
            case VP8:
            case AAC:
                return 2;
            case SPEEX:
                return 5;
            case PCM:
            case PASSTHROUGH:
            default:
                return 10;
        }
    }

    private static int defaultFec(CodecPriorityId id) {
        switch (id) {
            case H264:
            case OPUS:
                return 0;
            case H265:
            case VP8:
            case AAC:
                return 1;
            case VP9:
                return 2;
            case SPEEX:
                return 5;
            default:
                return 1;
        }
    }

    private static int defaultProtection(CodecPriorityId id, CastSettings settings) {
        switch (id) {
            case VP8:
            case VP9:
                return vpxInbandProtectionScore(settings);
            case H264:
            case H265:
                return 1;
            case OPUS:
                return 0;
            case SPEEX:
                return 5;
            default:
                return 1;
        }
    }

    /** 0 when libvpx in-band protection is available and enabled; 2 otherwise. */
    private static int vpxInbandProtectionScore(CastSettings settings) {
        if (!NativeCodecBridge.isLibvpxAvailable()) {
            return 2;
        }
        if (settings == null) {
            return 0;
        }
        CastSettings.StreamProtection mode = settings.getStreamProtection();
        if (mode == null || mode == CastSettings.StreamProtection.AUTO
                || mode == CastSettings.StreamProtection.NONE) {
            return 2;
        }
        return 0;
    }

    private static int defaultBitrate(CodecPriorityId id) {
        switch (id) {
            case H265:
            case VP9:
            case OPUS:
                return 0;
            case H264:
            case AAC:
                return 1;
            case VP8:
                return 2;
            default:
                return 10;
        }
    }

    private static int countJsonOverrides(JSONObject root) {
        int n = 0;
        n += countSectionOverrides(root.optJSONObject("video"));
        n += countSectionOverrides(root.optJSONObject("audio"));
        return n;
    }

    private static int countSectionOverrides(JSONObject section) {
        if (section == null) {
            return 0;
        }
        int n = 0;
        java.util.Iterator<String> keys = section.keys();
        while (keys.hasNext()) {
            JSONObject codec = section.optJSONObject(keys.next());
            if (codec == null) {
                continue;
            }
            for (String k : new String[] {KEY_INTERNAL, KEY_FEC, KEY_PROTECTION, KEY_OVERRIDE, KEY_BITRATE}) {
                if (codec.has(k)) {
                    n++;
                }
            }
        }
        return n;
    }

    private static JSONObject loadJson(Context context) {
        try (BufferedReader r = new BufferedReader(new InputStreamReader(
                context.getAssets().open(ASSET), StandardCharsets.UTF_8))) {
            StringBuilder sb = new StringBuilder();
            String line;
            while ((line = r.readLine()) != null) {
                sb.append(line);
            }
            return new JSONObject(sb.toString());
        } catch (Exception e) {
            Log.w(TAG, "Using compile-time codec scores only (" + e.getMessage() + ")");
            return new JSONObject();
        }
    }
}
