/*
     Description: 质量分数相关的工具函数

     Copyright : All right reserved by ICT

     Author : Zhang Zhonghai
     Date : 2025/12/25
*/
#pragma once

#include <cassert>
#include <cmath>
#include <cstdint>
#include <climits>
#include <vector>

#include "util/math/math_utils.h"

using std::vector;

struct QualityUtils {

    static void StaticInit() {
        for (int i = 0; i <= MAX_QUAL; i++) {
            qualToErrorProbCache[i] = qualToErrorProb((double)i);
            qualToProbLog10Cache[i] = std::log10(1.0 - qualToErrorProbCache[i]);
        }
    }

    /**
     * Maximum quality score that can be encoded in a SAM/BAM file
     */

    static constexpr uint8_t MAX_SAM_QUAL_SCORE = 93;
    /**
     * bams containing quals above this value are extremely suspicious and we should warn the user
     */
    static constexpr uint8_t MAX_REASONABLE_Q_SCORE = 60;

    /**
     * conversion factor from phred scaled quality to log error probability and vice versa
     */

    static const double PHRED_TO_LOG_PROB_MULTIPLIER;

    static const double LOG_PROB_TO_PHRED_MULTIPLIER;

    static const double MIN_LOG10_SCALED_QUAL;

    static const double MIN_PHRED_SCALED_QUAL;


    /**
     * The lowest quality score for a base that is considered reasonable for statistical analysis.  This is
     * because Q 6 => you stand a 25% of being right, which means all bases are equally likely
     */

    static constexpr uint8_t MIN_USABLE_Q_SCORE = 6;

    static constexpr int MAPPING_QUALITY_UNAVAILABLE = 255;

    /**
     * Maximum sense quality value.
     */

    static constexpr int MAX_QUAL = 254;

    /**
     * Cached values for qual as uint8_t calculations so they are very fast
     */
    static double qualToErrorProbCache[MAX_QUAL + 1];
    static double qualToProbLog10Cache[MAX_QUAL + 1];

    // ----------------------------------------------------------------------
    //
    // These are all functions to convert a phred-scaled quality score to a probability
    //
    // ----------------------------------------------------------------------

    /**
     * Convert a phred-scaled quality score to its probability of being true (Q30 => 0.999)
     *
     * This is the Phred-style conversion, *not* the Illumina-style conversion.
     *
     * Because the input is a discretized byte value, this function uses a cache so is very efficient
     *
     * WARNING -- because this function takes a byte for maxQual, you must be careful in converting
     * integers to byte.  The appropriate way to do this is ((byte)(myInt & 0xFF))
     *
     * @param qual a quality score (0-255)
     * @return a probability (0.0-1.0)
     */
    static double qualToProb(const uint8_t qual) { return 1.0 - qualToErrorProb(qual); }

    /**
     * Convert a phred-scaled quality score to its probability of being true (Q30 => 0.999)
     *
     * This is the Phred-style conversion, *not* the Illumina-style conversion.
     *
     * Because the input is a double value, this function must call Math.pow so can be quite expensive
     *
     * @param qual a phred-scaled quality score encoded as a double.  Can be non-integer values (30.5)
     * @return a probability (0.0-1.0)
     */
    static double qualToProb(const double qual) {
        // Utils.validateArg(qual >= 0.0, ()->"qual must be >= 0.0 but got " + qual);
        return 1.0 - qualToErrorProb(qual);
    }

    /**
     * Convert a phred-scaled quality score to its log10 probability of being true (Q30 => log10(0.999))
     *
     * This is the Phred-style conversion, *not* the Illumina-style conversion.
     *
     * WARNING -- because this function takes a byte for maxQual, you must be careful in converting
     * integers to byte.  The appropriate way to do this is ((byte)(myInt & 0xFF))
     *
     * @param qual a phred-scaled quality score encoded as a double.  Can be non-integer values (30.5)
     * @return a probability (0.0-1.0)
     */
    static double qualToProbLog10(const uint8_t qual) {
        return qualToProbLog10Cache[(int)qual & 0xff];  // Map: 127 -> 127; -128 -> 128; -1 -> 255; etc.
    }

    /**
     * Convert a log-probability to a phred-scaled value  ( log(0.001) => 30 )
     *
     * @param prob a log-probability
     * @return a phred-scaled value, not necessarily integral or bounded by {@code MAX_QUAL}
     */
    static double logProbToPhred(const double prob) { return prob * LOG_PROB_TO_PHRED_MULTIPLIER; }

    /**
     * Convert a phred-scaled quality score to its probability of being wrong (Q30 => 0.001)
     *
     * This is the Phred-style conversion, *not* the Illumina-style conversion.
     *
     * Because the input is a double value, this function must call Math.pow so can be quite expensive
     *
     * @param qual a phred-scaled quality score encoded as a double.  Can be non-integer values (30.5)
     * @return a probability (0.0-1.0)
     */

    static double qualToErrorProb(const double qual) {
        assert(qual >= 0.0);
        return std::pow(10.0, qual / -10.0);
    }

    /**v
     * Convert a phred-scaled quality score to its probability of being wrong (Q30 => 0.001)
     *
     * This is the Phred-style conversion, *not* the Illumina-style conversion.
     *
     * Because the input is a byte value, this function uses a cache so is very efficient
     *
     * WARNING -- because this function takes a byte for maxQual, you must be careful in converting
     * integers to byte.  The appropriate way to do this is ((byte)(myInt & 0xFF))
     *
     * @param qual a phred-scaled quality score encoded as a byte
     * @return a probability (0.0-1.0)
     */
    static double qualToErrorProb(const uint8_t qual) {
        return qualToErrorProbCache[(int)qual & 0xff];  // Map: 127 -> 127; -128 -> 128; -1 -> 255; etc.
    }

    /**
     * Convert a phred-scaled quality score to its log10 probability of being wrong (Q30 => log10(0.001))
     *
     * This is the Phred-style conversion, *not* the Illumina-style conversion.
     *
     * The calculation is extremely efficient
     *
     * WARNING -- because this function takes a byte for maxQual, you must be careful in converting
     * integers to byte.  The appropriate way to do this is ((byte)(myInt & 0xFF))
     *
     * @param qual a phred-scaled quality score encoded as a byte
     * @return a probability (0.0-1.0)
     */
    static double qualToErrorProbLog10(const uint8_t qual) { return qualToErrorProbLog10((double)(qual & 0xFF)); }

    /**
     * Convert a phred-scaled quality score to its log10 probability of being wrong (Q30 => log10(0.001))
     *
     * This is the Phred-style conversion, *not* the Illumina-style conversion.
     *
     * The calculation is extremely efficient
     *
     * @param qual a phred-scaled quality score encoded as a double
     * @return log of probability (0.0-1.0)
     */
    static double qualToErrorProbLog10(const double qual) {
        // Utils.validateArg(qual >= 0.0, ()->"qual must be >= 0.0 but got " + qual);
        return qual * -0.1;
    }

    // ----------------------------------------------------------------------
    //
    // Functions to convert a probability to a phred-scaled quality score
    //
    // ----------------------------------------------------------------------

    /**
     * Convert a probability of being wrong to a phred-scaled quality score (0.01 => 20).
     *
     * Note, this function caps the resulting quality score by the public static value MAX_SAM_QUAL_SCORE
     * and by 1 at the low-end.
     *
     * @param errorRate a probability (0.0-1.0) of being wrong (i.e., 0.01 is 1% change of being wrong)
     * @return a quality score (0-MAX_SAM_QUAL_SCORE)
     */
    static uint8_t errorProbToQual(const double errorRate) { return errorProbToQual(errorRate, MAX_SAM_QUAL_SCORE); }

    /**
     * Convert a probability of being wrong to a phred-scaled quality score (0.01 => 20).
     *
     * Note, this function caps the resulting quality score by the public static value MIN_REASONABLE_ERROR
     * and by 1 at the low-end.
     *
     * WARNING -- because this function takes a byte for maxQual, you must be careful in converting
     * integers to byte.  The appropriate way to do this is ((byte)(myInt & 0xFF))
     *
     * @param errorRate a probability (0.0-1.0) of being wrong (i.e., 0.01 is 1% change of being wrong)
     * @return a quality score (0-maxQual)
     */
    static uint8_t errorProbToQual(const double errorRate, const uint8_t maxQual) {
        // Utils.validateArg(MathUtils.isValidProbability(errorRate), ()->"errorRate must be good probability but got " + errorRate);
        const double d = std::round(-10.0 * std::log10(errorRate));
        if (d > maxQual) // 注意当d超过int表示的最大值时(如inf)，转变成int会变成负数
            return maxQual;
        return boundQual((int)d, maxQual);
    }

    /**
     * @see #errorProbToQual(double, byte) with proper conversion of maxQual integer to a byte
     */
    static uint8_t errorProbToQual(const double prob, const int maxQual) {
        // Utils.validateArg(maxQual >= 0 && maxQual <= 255, ()->"maxQual must be between 0-255 but got " + maxQual);
        return errorProbToQual(prob, (uint8_t)(maxQual & 0xFF));
    }

    /**
     * Convert a probability of being right to a phred-scaled quality score (0.99 => 20).
     *
     * Note, this function caps the resulting quality score by the public static value MAX_SAM_QUAL_SCORE
     * and by 1 at the low-end.
     *
     * @param prob a probability (0.0-1.0) of being right
     * @return a quality score (0-MAX_SAM_QUAL_SCORE)
     */
    static uint8_t trueProbToQual(const double prob) { return trueProbToQual(prob, MAX_SAM_QUAL_SCORE); }

    /**
     * Convert a probability of being right to a phred-scaled quality score (0.99 => 20).
     *
     * Note, this function caps the resulting quality score by the min probability allowed (EPS).
     * So for example, if prob is 1e-6, which would imply a Q-score of 60, and EPS is 1e-4,
     * the result of this function is actually Q40.
     *
     * Note that the resulting quality score, regardless of EPS, is capped by MAX_SAM_QUAL_SCORE and
     * bounded on the low-side by 1.
     *
     * WARNING -- because this function takes a byte for maxQual, you must be careful in converting
     * integers to byte.  The appropriate way to do this is ((byte)(myInt & 0xFF))
     *
     * @param trueProb a probability (0.0-1.0) of being right
     * @param maxQual the maximum quality score we are allowed to emit here, regardless of the error rate
     * @return a phred-scaled quality score (0-maxQualScore) as a byte
     */
    static uint8_t trueProbToQual(const double trueProb, const uint8_t maxQual) {
        // Utils.validateArg(MathUtils.isValidProbability(trueProb), ()->"trueProb must be good probability but got " + trueProb);
        const double lp = std::round(-10.0 * MathUtils::log10OneMinusX(trueProb));
        return boundQual((int)lp, maxQual);
    }

    /**
     * @see #trueProbToQual(double, byte) with proper conversion of maxQual to a byte
     */
    static uint8_t trueProbToQual(const double prob, const int maxQual) {
        // Utils.validateArg(maxQual >= 0 && maxQual <= 255, ()->"maxQual must be between 0-255 but got " + maxQual);
        return trueProbToQual(prob, (uint8_t)(maxQual & 0xFF));
    }

    /**
     * Convert a probability of being right to a phred-scaled quality score of being wrong as a double
     *
     * This is a very generic method, that simply computes a phred-scaled double quality
     * score given an error rate.  It has the same precision as a normal double operation
     *
     * @param trueRate the probability of being right (0.0-1.0)
     * @return a phred-scaled version of the error rate implied by trueRate
     */
    static double phredScaleCorrectRate(const double trueRate) { return phredScaleLog10ErrorRate(MathUtils::log10OneMinusX(trueRate)); }

    /**
     * Convert a probability of being wrong to a phred-scaled quality score as a double
     *
     * This is a very generic method, that simply computes a phred-scaled double quality
     * score given an error rate.  It has the same precision as a normal double operation
     *
     * @param errorRate the probability of being wrong (0.0-1.0)
     * @return a phred-scaled version of the error rate
     */
    static double phredScaleErrorRate(const double errorRate) { return phredScaleLog10ErrorRate(std::log10(errorRate)); }

    /**
     * Convert a log10 probability of being wrong to a phred-scaled quality score as a double
     *
     * This is a very generic method, that simply computes a phred-scaled double quality
     * score given an error rate.  It has the same precision as a normal double operation
     *
     * @param errorRateLog10 the log10 probability of being wrong (0.0-1.0).  Can be -Infinity, in which case
     *                       the result is MIN_PHRED_SCALED_QUAL
     * @return a phred-scaled version of the error rate
     */
    static double phredScaleLog10ErrorRate(const double errorRateLog10) {
        // Utils.validateArg(MathUtils.isValidLog10Probability(errorRateLog10), ()->"errorRateLog10 must be good probability but got " + errorRateLog10);

        // abs is necessary for edge base with errorRateLog10 = 0 producing -0.0 doubles
        return std::abs(-10.0 * std::max(errorRateLog10, MIN_LOG10_SCALED_QUAL));
    }

    /**
     * Convert a log10 probability of being right to a phred-scaled quality score of being wrong as a double
     *
     * This is a very generic method, that simply computes a phred-scaled double quality
     * score given an error rate.  It has the same precision as a normal double operation
     *
     * @param trueRateLog10 the log10 probability of being right (0.0-1.0).  Can be -Infinity to indicate
     *                      that the result is impossible in which MIN_PHRED_SCALED_QUAL is returned
     * @return a phred-scaled version of the error rate implied by trueRate
     */
    static double phredScaleLog10CorrectRate(const double trueRateLog10) { return phredScaleCorrectRate(std::pow(10.0, trueRateLog10)); }

    // ----------------------------------------------------------------------
    //
    // Routines to bound a quality score to a reasonable range
    //
    // ----------------------------------------------------------------------

    /**
     * Return a quality score that bounds qual by MAX_SAM_QUAL_SCORE and 1
     *
     * @param qual the uncapped quality score as an integer
     * @return the bounded quality score
     */
    static uint8_t boundQual(int qual) { return boundQual(qual, MAX_SAM_QUAL_SCORE); }

    /**
     * Return a quality score that bounds qual by maxQual and 1
     *
     * WARNING -- because this function takes a byte for maxQual, you must be careful in converting
     * integers to byte.  The appropriate way to do this is ((byte)(myInt & 0xFF))
     *
     * @param qual the uncapped quality score as an integer.  Can be < 0 (which may indicate an error in the
     *             client code), which will be brought back to 1, but this isn't an error, as some
     *             routines may use this functionality (BaseRecalibrator, for example)
     * @param maxQual the maximum quality score, must be less < 255
     * @return the bounded quality score
     */
    static uint8_t boundQual(const int qual, const uint8_t maxQual) { return (uint8_t)(std::max(std::min(qual, maxQual & 0xFF), 1) & 0xFF); }

    /**
     * Calculate the sum of phred scores.
     * @param phreds the phred score values.
     * @return the sum.
     */
    static double phredSum(const vector<double>& phreds) {
        switch (phreds.size()) {
        case 0:
            return DBL_MAX;
        case 1:
            return phreds[0];
        case 2:
            return phredSum(phreds[0], phreds[1]);
        case 3:
            return phredSum(phreds[0], phreds[1], phreds[2]);
        default:
            vector<double> log10Vals(phreds.size()); // todo: 优化加速
            for (int i = 0; i < log10Vals.size(); i++) {
                log10Vals[i] = phreds[i] * -0.1;
            }
            return -10 * MathUtils::log10SumLog10(log10Vals);
        }
    }

    /**
     * Calculate the sum of two phred scores.
     * <p>
     *     As any sum, this operation is commutative.
     * </p>
     * @param a first phred score value.
     * @param b second phred score value.
     * @return the sum.
     */
    static double phredSum(const double a, const double b) { return -10 * MathUtils::log10SumLog10(a * -.1, b * -.1); }

    /**
     * Calculate the sum of three phred scores.
     * <p>
     *     As any sum, this operation is commutative.
     * </p>
     * @param a first phred score value.
     * @param b second phred score value.
     * @param c thrid phred score value.
     * @return the sum.
     */
    static double phredSum(const double a, const double b, const double c) { return -10 * MathUtils::log10SumLog10(a * -.1, b * -.1, c * -.1); }
};