#if HWTEST
using System;
using System.Collections.Generic;

namespace TIASshot {

    /// <summary>
    /// 実機テストの判定しきい値と手順のパラメータ（HWTEST 限定）．
    /// 【暫定値】判定しきい値は最初の実機実行で実測してから決め直す（PLAN_06）．
    /// 結果ファイルには必ず測定値を出すため，暫定値で FAIL しても測定値から決め直せる．
    /// </summary>
    internal static class HwTestThresholds {

        // ---- 判定しきい値（暫定） ----

        /// <summary>preview: フレームレートの下限(fps)．DFK 33UX178 の実測 16.86〜17.71 fps に対して余裕を持たせた値</summary>
        public const double MinPreviewFps = 10.0;

        /// <summary>
        /// exposure-gain: 露光を 1 段（時間で半分）下げたときの平均輝度の比（下げた / 基準）の上限．
        /// 消灯値（黒の下限）は light より前のため未測定で，単純比で見る（黒の下限の分だけ比は 1 に寄る．DFK 33UX178 の実測は 1/60→1/64 s で 0.94 ≒ 60/64）
        /// </summary>
        public const double ExposureLowerMaxRatio = 0.8;

        /// <summary>exposure-gain: 露光を 1 段（時間で 2 倍）上げたときの平均輝度の比（上げた / 基準）の下限（単純比）</summary>
        public const double ExposureRaiseMinRatio = 1.25;

        /// <summary>exposure-gain: ゲインを上げ下げしたときに期待する平均輝度の変化量の下限（8bit 階調）</summary>
        public const double MinBrightnessDelta = 3.0;

        /// <summary>飽和とみなす画素値（8bit 階調）．画素のいずれかのチャネルがこれ以上なら飽和画素として数える</summary>
        public const int SaturatedPixelLevel = 254;

        /// <summary>
        /// exposure-gain: 飽和画素の割合（0〜1）がこれを超えたフレームは飽和とみなす（暫定 1%）．
        /// 平均輝度だけでは明るい色票の飽和を見逃すため（DFK 33UX178 の実測: 平均 227.8 で明るい色票が飽和）
        /// </summary>
        public const double MaxSaturatedFraction = 0.01;

        /// <summary>exposure-gain: これ以下の平均輝度は真っ暗とみなし，下げる向きの確認を省く（8bit 階調）</summary>
        public const double DarkLevel = 5.0;

        /// <summary>exposure-gain: 基準にする平均輝度の中間の範囲の下限（8bit 階調．暫定）．これ未満は暗すぎるとして露光を上げる向きに探す</summary>
        public const double MidBrightnessLow = 30.0;

        /// <summary>exposure-gain: 基準にする平均輝度の中間の範囲の上限（8bit 階調．暫定）．これを超えると明るすぎるとして露光を下げる向きに探す</summary>
        public const double MidBrightnessHigh = 220.0;

        /// <summary>exposure-gain: 元の値に戻した後の平均輝度と最初の平均輝度の差の許容（超えたら WARN．判定には使わない）</summary>
        public const double RestoreTolerance = 10.0;

        /// <summary>exposure-gain: 露光・ゲインを元の値に戻したときの読み戻しの相対誤差の許容（超えたら FAIL）</summary>
        public const double RestoreRelativeTolerance = 0.01;

        /// <summary>
        /// exposure-gain: ゲインの読み戻しの絶対誤差の許容（設定範囲の幅に対する割合）．
        /// ゲインの元の値は 0（dB）のことがあり，相対誤差だけでは判定できないため
        /// </summary>
        public const double GainRestoreRangeFraction = 0.001;

        /// <summary>light: 点灯時と消灯時の平均輝度の差の下限（8bit 階調）．DFK 33UX178 の実測は約 196</summary>
        public const double MinLightDelta = 100.0;

        /// <summary>close-preview: 閉じる処理（後始末）の所要時間の上限．DFK 33UX178 の実測は 144 ms</summary>
        public static readonly TimeSpan CloseLimit = TimeSpan.FromMilliseconds(1000);

        /// <summary>close-preview: 閉じた後の観測時間内に届いてよいフレーム数の上限（停止処理中に流れていた分）</summary>
        public const int MaxFramesAfterClose = 2;

        /// <summary>reconnect: 再接続後に届くべきフレーム数の下限</summary>
        public const int ReconnectMinFrames = 5;

        // ---- 手順のパラメータ ----

        /// <summary>最初のフレームが届くまでの待機の上限</summary>
        public static readonly TimeSpan FirstFrameTimeout = TimeSpan.FromSeconds(10);

        /// <summary>preview: フレームレートを測る前の慣らし時間</summary>
        public static readonly TimeSpan PreviewWarmup = TimeSpan.FromSeconds(1);

        /// <summary>preview: フレームレートを測る時間</summary>
        public static readonly TimeSpan PreviewMeasure = TimeSpan.FromSeconds(3);

        /// <summary>設定（露光・ゲイン・光源）を変えた後，フレームに反映されるのを待つ時間</summary>
        public static readonly TimeSpan SettleTime = TimeSpan.FromMilliseconds(1500);

        /// <summary>平均輝度の 1 回の測定に使うフレーム数</summary>
        public const int SamplesPerMeasurement = 8;

        /// <summary>平均輝度の 1 回の測定の待機の上限</summary>
        public static readonly TimeSpan SampleTimeout = TimeSpan.FromSeconds(15);

        /// <summary>平均輝度を求めるときの間引き（縦横とも何画素ごとに読むか）</summary>
        public const int PixelStep = 4;

        /// <summary>exposure-gain: 露光が 2^n の段（HwTestValueScale.Log2Step）のとき 1 段で動かす段数（±1 = 時間で 2 倍・半分）</summary>
        public const double ExposureLogStep = 1.0;

        /// <summary>exposure-gain: 基準の明るさを中間に寄せるために露光を動かす段数の上限（元の設定の測定は含めない）</summary>
        public const int BaselineSearchMaxSteps = 12;

        /// <summary>exposure-gain: 露光が線形の量（HwTestValueScale.Linear）のとき下げる倍率</summary>
        public const double ExposureDownFactor = 0.5;

        /// <summary>exposure-gain: 露光が線形の量（HwTestValueScale.Linear）のとき上げる倍率</summary>
        public const double ExposureUpFactor = 2.0;

        /// <summary>exposure-gain: ゲインを上げ下げする量（設定範囲の幅に対する割合）</summary>
        public const double GainStepFraction = 0.1;

        /// <summary>close-preview: 閉じた後にフレームが止まったかを見る時間</summary>
        public static readonly TimeSpan AfterCloseObserve = TimeSpan.FromSeconds(1);

        /// <summary>reconnect: 再接続後のフレームの待機の上限</summary>
        public static readonly TimeSpan ReconnectFrameTimeout = TimeSpan.FromSeconds(15);

        // ---- タイムアウト（各項目・全体） ----

        public static readonly TimeSpan ConnectTimeout = TimeSpan.FromSeconds(60);
        public static readonly TimeSpan PreviewTimeout = TimeSpan.FromSeconds(60);
        public static readonly TimeSpan ExposureGainTimeout = TimeSpan.FromSeconds(180);
        public static readonly TimeSpan LightTimeout = TimeSpan.FromSeconds(90);
        public static readonly TimeSpan ClosePreviewTimeout = TimeSpan.FromSeconds(30);
        public static readonly TimeSpan ReconnectTimeout = TimeSpan.FromSeconds(60);
        public static readonly TimeSpan CleanupTimeout = TimeSpan.FromSeconds(30);

        /// <summary>実機テスト全体の上限（各項目の上限の合計より長くする）</summary>
        public static readonly TimeSpan OverallTimeout = TimeSpan.FromMinutes(10);
    }

    /// <summary>ウォッチドッグの判定結果（HWTEST 限定）</summary>
    internal enum HwTestOverdue {
        /// <summary>上限内</summary>
        None,
        /// <summary>実行中の項目が項目の上限を超えた</summary>
        Item,
        /// <summary>全体の上限を超えた</summary>
        Overall,
    }

    /// <summary>
    /// 露光・ゲインの上げ下げに対する平均輝度の変化の向きの判定結果（HWTEST 限定）．
    /// </summary>
    internal sealed class HwTestDirectionResult {
        /// <summary>下げる向きを確認したか（設定が変わらなかった・元が真っ暗・下げた側が飽和の場合は確認しない）</summary>
        public bool LowerChecked;
        /// <summary>下げたときに十分暗くなったか</summary>
        public bool LowerOk;
        /// <summary>上げる向きを確認したか（設定が変わらなかった・元が飽和・上げた側が飽和の場合は確認しない）</summary>
        public bool RaiseChecked;
        /// <summary>上げたときに十分明るくなったか</summary>
        public bool RaiseOk;
        /// <summary>合格か（基準（HwTestDirectionCriteria.RequireAllChecked）に従う）</summary>
        public bool Pass;
        /// <summary>判定の説明（結果ファイルに出す）</summary>
        public string Summary;
    }

    /// <summary>
    /// 露光・ゲインの上げ下げの判定の基準（HWTEST 限定）．
    /// 比で見る（UseRatio）場合は 下げた / 基準 ≦ LowerLimit・上げた / 基準 ≧ RaiseLimit，
    /// 差で見る場合は 基準 − 下げた ≧ LowerLimit・上げた − 基準 ≧ RaiseLimit を期待どおりとする．
    /// </summary>
    internal sealed class HwTestDirectionCriteria {
        /// <summary>比で見るか（false なら差で見る）</summary>
        public bool UseRatio;
        /// <summary>下げる向きの限度（比なら上限，差なら下限）</summary>
        public double LowerLimit;
        /// <summary>上げる向きの限度（比・差とも下限）</summary>
        public double RaiseLimit;
        /// <summary>基準の平均輝度がこれ以下なら真っ暗とみなし，下げる向き（比なら上げる向きも）を確認しない</summary>
        public double DarkLevel;
        /// <summary>飽和画素の割合がこれを超えたら飽和とみなす</summary>
        public double MaxSaturatedFraction;
        /// <summary>
        /// true: 確認した向きがすべて期待どおりで合格（少なくとも一方を確認できること）．
        /// false: 一方の向きだけでも期待どおりなら合格（範囲の端で変化が小さくなりうるゲイン用）
        /// </summary>
        public bool RequireAllChecked;
    }

    /// <summary>1 フレームの測定値（平均輝度と飽和画素の割合）（HWTEST 限定）</summary>
    internal struct HwTestFrameStats {
        /// <summary>平均輝度（0〜255）</summary>
        public double Mean;
        /// <summary>飽和画素の割合（0〜1）</summary>
        public double SaturatedFraction;

        public HwTestFrameStats(double mean, double saturatedFraction) {
            Mean = mean;
            SaturatedFraction = saturatedFraction;
        }
    }

    /// <summary>複数フレームをまとめた 1 回の測定値（HWTEST 限定）</summary>
    internal struct HwTestMeasurement {
        /// <summary>平均輝度の平均（フレームが無ければ NaN）</summary>
        public double Mean;
        /// <summary>飽和画素の割合の最大（各フレームのうち最も飽和したもの．判定に使う．フレームが無ければ NaN）</summary>
        public double SaturatedFraction;
        /// <summary>飽和画素の割合の平均（参考．フレームが無ければ NaN）</summary>
        public double MeanSaturatedFraction;
        /// <summary>フレーム数</summary>
        public int Count;

        /// <summary>測定できなかった（フレームが無い）測定値</summary>
        public static readonly HwTestMeasurement NotMeasured = new HwTestMeasurement {
            Mean = double.NaN, SaturatedFraction = double.NaN, MeanSaturatedFraction = double.NaN, Count = 0,
        };
    }

    /// <summary>基準の明るさ探し（exposure-gain）の 1 段ごとの判断（HWTEST 限定）</summary>
    internal enum HwTestBaselineSearchDecision {
        /// <summary>平均輝度が中間の範囲に入り，飽和もしていない（この設定を基準にする）</summary>
        Found,
        /// <summary>同じ向きにもう 1 段動かす</summary>
        Continue,
        /// <summary>測定できなかった（平均輝度が NaN）</summary>
        StopNotMeasured,
        /// <summary>設定が範囲の端で変わらなくなった</summary>
        StopRangeEnd,
        /// <summary>段数の上限に達した</summary>
        StopMaxSteps,
        /// <summary>中間の範囲を飛び越えた（明るすぎ → 暗すぎ，またはその逆）</summary>
        StopOvershoot,
    }

    /// <summary>基準の明るさ探しの 1 段（設定値とその平均輝度・飽和画素の割合）（HWTEST 限定）</summary>
    internal struct HwTestBaselineSample {
        public double Setting;
        public double Mean;
        public double SaturatedFraction;

        public HwTestBaselineSample(double setting, double mean, double saturatedFraction) {
            Setting = setting;
            Mean = mean;
            SaturatedFraction = saturatedFraction;
        }
    }

    /// <summary>
    /// 実機テストの判断ロジック（HWTEST 限定．SDK・UI・スレッドに依存しない純粋関数．層①でテストする）．
    /// </summary>
    internal static class HwTestJudge {

        /// <summary>フレームレート(fps)．時間が 0 以下なら 0</summary>
        public static double FrameRate(long frames, TimeSpan elapsed) {
            if (elapsed <= TimeSpan.Zero || frames <= 0) return 0;
            return frames / elapsed.TotalSeconds;
        }

        /// <summary>フレームレートが下限以上か</summary>
        public static bool MeetsMinFrameRate(double fps, double minFps) {
            return !double.IsNaN(fps) && fps >= minFps;
        }

        /// <summary>
        /// 現在値に倍率を掛けた設定値を範囲内に収めて返す（露光の上げ下げに使う）．
        /// 現在値が 0 以下で倍率が 1 を超える場合は，範囲の幅の 5% だけ最小値から上げた値を返す（0 に掛けても変わらないため）．
        /// </summary>
        /// <exception cref="ArgumentException">最小値が最大値より大きい・非有限値を含む場合</exception>
        public static double ScaleSetting(double current, double min, double max, double factor) {
            ValidateRange(current, min, max);
            if (!IsFinite(factor) || factor <= 0) {
                throw new ArgumentException($"倍率が不正です: {factor}", nameof(factor));
            }
            double target;
            if (current > 0) {
                target = current * factor;
            } else if (factor > 1) {
                target = min + (max - min) * 0.05;
            } else {
                target = current;
            }
            return Clamp(target, min, max);
        }

        /// <summary>
        /// 露光を 1 段動かした設定値を範囲内に収めて返す．
        /// 2^n の段（Log2Step）は現在値に最も近い 2^n（log2 を四捨五入）から logStep 段動かした 2^(n±logStep)（時間で 2 倍・半分），
        /// 線形の量（Linear）は ScaleSetting で downFactor / upFactor を掛ける．
        /// </summary>
        /// <param name="direction">正なら上げる（明るくする）向き，負なら下げる（暗くする）向き．0 は不可</param>
        /// <exception cref="ArgumentException">direction が 0・範囲が不正・非有限値を含む・未知の種類の場合，Log2Step で現在値が 0 以下の場合</exception>
        public static double NextExposure(double current, double min, double max, HwTestValueScale scale, int direction,
                double logStep, double downFactor, double upFactor) {
            ValidateRange(current, min, max);
            if (direction == 0) throw new ArgumentException("向きが 0 です", nameof(direction));
            switch (scale) {
                case HwTestValueScale.Log2Step:
                    if (!IsFinite(logStep) || logStep <= 0) {
                        throw new ArgumentException($"段の量が不正です: {logStep}", nameof(logStep));
                    }
                    var n = NearestLog2Exponent(current);
                    return Clamp(Math.Pow(2, n + (direction > 0 ? logStep : -logStep)), min, max);
                case HwTestValueScale.Linear:
                    return ScaleSetting(current, min, max, direction > 0 ? upFactor : downFactor);
                default:
                    throw new ArgumentException($"未知の値の種類です: {scale}", nameof(scale));
            }
        }

        /// <summary>
        /// 基準探しの前に露光をきりのよい段に合わせた値を返す．
        /// 2^n の段（Log2Step）は現在値に最も近い 2^n（log2 を四捨五入）．それが範囲外なら範囲内で最も近い 2^n，
        /// 範囲内に 2^n が 1 つも無ければ現在値をそのまま返す．線形の量（Linear）は現在値をそのまま返す（合わせない）．
        /// </summary>
        /// <exception cref="ArgumentException">範囲が不正・非有限値を含む・未知の種類の場合，Log2Step で現在値が 0 以下の場合</exception>
        public static double AlignExposure(double current, double min, double max, HwTestValueScale scale) {
            ValidateRange(current, min, max);
            switch (scale) {
                case HwTestValueScale.Linear:
                    return current;
                case HwTestValueScale.Log2Step:
                    var n = NearestLog2Exponent(current);
                    if (max <= 0) return current;
                    // 範囲の端がちょうど 2^n のとき log の丸め誤差で 1 段ずれないよう，わずかに内側へ寄せる
                    var nMax = Math.Floor(Math.Log(max, 2) + 1e-9);
                    var nMin = min > 0 ? Math.Ceiling(Math.Log(min, 2) - 1e-9) : double.NegativeInfinity;
                    if (nMin > nMax) return current;   // 範囲内に 2^n が無い
                    if (n > nMax) n = nMax;
                    if (n < nMin) n = nMin;
                    return Clamp(Math.Pow(2, n), min, max);
                default:
                    throw new ArgumentException($"未知の値の種類です: {scale}", nameof(scale));
            }
        }

        /// <summary>値に最も近い 2^n の n（log2 の四捨五入．読み戻しの微小な丸めに影響されない）</summary>
        private static double NearestLog2Exponent(double value) {
            if (value <= 0) throw new ArgumentException($"2^n の段にできない値です（0 以下）: {value}", nameof(value));
            return Math.Round(Math.Log(value, 2), MidpointRounding.AwayFromZero);
        }

        /// <summary>平均輝度が中間の範囲（low 以上 high 以下）に入っているか．NaN は false</summary>
        public static bool IsMidBrightness(double mean, double low, double high) {
            return !double.IsNaN(mean) && mean >= low && mean <= high;
        }

        /// <summary>飽和画素の割合がしきい値を超えているか（飽和とみなすか）．NaN（測定できない）は false</summary>
        public static bool IsSaturated(double saturatedFraction, double maxSaturatedFraction) {
            return !double.IsNaN(saturatedFraction) && saturatedFraction > maxSaturatedFraction;
        }

        /// <summary>基準にできる明るさか（平均輝度が中間の範囲に入り，かつ飽和画素の割合がしきい値以下）</summary>
        public static bool IsBaselineCandidate(double mean, double saturatedFraction, double low, double high, double maxSaturatedFraction) {
            return IsMidBrightness(mean, low, high) && !double.IsNaN(saturatedFraction)
                && !IsSaturated(saturatedFraction, maxSaturatedFraction);
        }

        /// <summary>
        /// 基準の明るさを中間に寄せるために露光を動かす向き．high を超える・飽和している（飽和画素の割合がしきい値超え）なら -1（下げる），
        /// low 未満なら +1（上げる），基準にできる明るさ・測定できない（NaN）なら 0（動かさない）．
        /// </summary>
        public static int BaselineSearchDirection(double mean, double saturatedFraction, double low, double high, double maxSaturatedFraction) {
            if (double.IsNaN(mean)) return 0;
            if (mean > high || IsSaturated(saturatedFraction, maxSaturatedFraction)) return -1;
            if (mean < low) return 1;
            return 0;
        }

        /// <summary>
        /// 基準の明るさ探しで 1 段動かして測った後の判断．
        /// 基準にできる明るさ（中間の範囲かつ飽和していない）になれば Found．
        /// 測定できない・設定が変わらない（範囲の端）・中間の範囲を飛び越えた・段数の上限に達したら Stop*．
        /// それ以外は Continue（同じ向きにもう 1 段）．
        /// </summary>
        /// <param name="mean">動かした後の平均輝度</param>
        /// <param name="saturatedFraction">動かした後の飽和画素の割合</param>
        /// <param name="direction">探している向き（-1: 下げる，+1: 上げる）</param>
        /// <param name="settingChanged">動かした設定が実際に変わったか（読み戻しの比較）</param>
        /// <param name="stepsDone">これまでに動かした段数（今回を含む．1 以上）</param>
        /// <param name="maxSteps">段数の上限</param>
        public static HwTestBaselineSearchDecision DecideBaselineSearch(double mean, double saturatedFraction, int direction, bool settingChanged,
                int stepsDone, int maxSteps, double low, double high, double maxSaturatedFraction) {
            if (!settingChanged) return HwTestBaselineSearchDecision.StopRangeEnd;
            if (double.IsNaN(mean) || double.IsNaN(saturatedFraction)) return HwTestBaselineSearchDecision.StopNotMeasured;
            if (IsBaselineCandidate(mean, saturatedFraction, low, high, maxSaturatedFraction)) return HwTestBaselineSearchDecision.Found;
            var tooBright = mean > high || IsSaturated(saturatedFraction, maxSaturatedFraction);
            if ((direction < 0 && mean < low) || (direction > 0 && tooBright)) return HwTestBaselineSearchDecision.StopOvershoot;
            if (stepsDone >= maxSteps) return HwTestBaselineSearchDecision.StopMaxSteps;
            return HwTestBaselineSearchDecision.Continue;
        }

        /// <summary>
        /// 基準の明るさ探しで中間に寄せられなかったときに基準にする段を選ぶ．
        /// 飽和していない段を飽和している段より優先し，その中で平均輝度が中間の範囲に最も近い段（同じ近さなら先の段）．
        /// 測定できなかった（平均輝度が NaN）段は選ばない．選べる段が無ければ -1．
        /// </summary>
        public static int ChooseBaselineIndex(IReadOnlyList<HwTestBaselineSample> samples, double low, double high, double maxSaturatedFraction) {
            if (samples == null) return -1;
            var best = -1;
            var bestSaturated = true;
            var bestDistance = double.PositiveInfinity;
            for (var i = 0; i < samples.Count; i++) {
                var mean = samples[i].Mean;
                if (double.IsNaN(mean)) continue;
                var saturated = IsSaturated(samples[i].SaturatedFraction, maxSaturatedFraction);
                var distance = mean < low ? low - mean : mean > high ? mean - high : 0;
                var better = best < 0
                    || (bestSaturated && !saturated)
                    || (bestSaturated == saturated && distance < bestDistance);
                if (better) {
                    best = i;
                    bestSaturated = saturated;
                    bestDistance = distance;
                }
            }
            return best;
        }

        /// <summary>
        /// 現在値に範囲の幅の割合を足した設定値を範囲内に収めて返す（ゲインの上げ下げに使う．負の割合で下げる）．
        /// </summary>
        /// <exception cref="ArgumentException">最小値が最大値より大きい・非有限値を含む場合</exception>
        public static double StepSetting(double current, double min, double max, double fractionOfRange) {
            ValidateRange(current, min, max);
            if (!IsFinite(fractionOfRange)) {
                throw new ArgumentException($"割合が不正です: {fractionOfRange}", nameof(fractionOfRange));
            }
            return Clamp(current + (max - min) * fractionOfRange, min, max);
        }

        /// <summary>設定値が変わったか（カメラから読み戻した値どうしの比較．整数値の丸めを考慮して相対誤差で見る）</summary>
        public static bool IsChanged(double before, double after) {
            var scale = Math.Max(1e-6, Math.Max(Math.Abs(before), Math.Abs(after)));
            return Math.Abs(after - before) / scale > 1e-4;
        }

        /// <summary>
        /// 設定値が期待した値と同じか（元の値に戻したときの読み戻しの確認）．
        /// 差が「相対誤差の許容 × 大きい方の絶対値」と「絶対誤差の許容」の大きい方以内なら同じとみなす．
        /// NaN を含む場合は false．
        /// </summary>
        /// <param name="expected">期待した値（元の値）</param>
        /// <param name="actual">読み戻した値</param>
        /// <param name="relativeTolerance">相対誤差の許容（0 以上．例 0.01 = 1%）</param>
        /// <param name="absoluteTolerance">絶対誤差の許容（0 以上．元の値が 0 になりうる設定用）</param>
        public static bool IsSameSetting(double expected, double actual, double relativeTolerance, double absoluteTolerance) {
            if (double.IsNaN(expected) || double.IsNaN(actual)) return false;
            var tolerance = Math.Max(Math.Max(0, relativeTolerance) * Math.Max(Math.Abs(expected), Math.Abs(actual)), Math.Max(0, absoluteTolerance));
            return Math.Abs(actual - expected) <= tolerance;
        }

        /// <summary>
        /// 複数フレームの測定値をまとめる（平均輝度は平均，飽和画素の割合は最大（判定用）と平均（参考））．空なら NotMeasured．
        /// </summary>
        public static HwTestMeasurement Summarize(IReadOnlyList<HwTestFrameStats> frames) {
            if (frames == null || frames.Count == 0) return HwTestMeasurement.NotMeasured;
            double sumMean = 0, sumSat = 0, maxSat = double.NegativeInfinity;
            foreach (var f in frames) {
                sumMean += f.Mean;
                sumSat += f.SaturatedFraction;
                if (f.SaturatedFraction > maxSat) maxSat = f.SaturatedFraction;
            }
            return new HwTestMeasurement {
                Mean = sumMean / frames.Count,
                SaturatedFraction = maxSat,
                MeanSaturatedFraction = sumSat / frames.Count,
                Count = frames.Count,
            };
        }

        /// <summary>
        /// 画素データの平均輝度（先頭 3 チャネル（BGR）の平均．1〜2 チャネルの形式は先頭チャネル）と，
        /// 飽和画素（読んだチャネルのいずれかが saturationLevel 以上の画素）の割合を求める．
        /// rows は「間引いて読んだ行」を詰めて並べたもの（1 行 rowStride バイト，rowCount 行）．
        /// 各行の中は pixelStep 画素ごとに読む．
        /// </summary>
        /// <param name="rows">間引いた行を詰めた画素データ</param>
        /// <param name="rowCount">行数</param>
        /// <param name="width">1 行の画素数</param>
        /// <param name="rowStride">rows 内の 1 行のバイト数（width * bytesPerPixel 以上）</param>
        /// <param name="bytesPerPixel">1 画素のバイト数（1〜4）</param>
        /// <param name="pixelStep">行の中で何画素ごとに読むか（1 以上）</param>
        /// <param name="saturationLevel">飽和とみなす画素値（0〜255）</param>
        /// <returns>平均輝度（0〜255）と飽和画素の割合（0〜1）</returns>
        /// <exception cref="ArgumentException">引数の組み合わせが不正な場合</exception>
        public static HwTestFrameStats FrameStats(byte[] rows, int rowCount, int width, int rowStride, int bytesPerPixel, int pixelStep,
                int saturationLevel) {
            if (rows == null) throw new ArgumentNullException(nameof(rows));
            if (rowCount <= 0 || width <= 0) throw new ArgumentException($"画素がありません（{width}x{rowCount}）");
            if (bytesPerPixel < 1 || bytesPerPixel > 4) throw new ArgumentException($"1 画素のバイト数が不正です: {bytesPerPixel}", nameof(bytesPerPixel));
            if (pixelStep < 1) throw new ArgumentException($"間引きが不正です: {pixelStep}", nameof(pixelStep));
            if (saturationLevel < 0 || saturationLevel > 255) throw new ArgumentException($"飽和とみなす画素値が不正です: {saturationLevel}", nameof(saturationLevel));
            if (rowStride < width * bytesPerPixel) throw new ArgumentException($"1 行のバイト数が不足しています: {rowStride} < {width * bytesPerPixel}", nameof(rowStride));
            if ((long)rowStride * rowCount > rows.Length) throw new ArgumentException($"画素データが不足しています: {rows.Length} < {(long)rowStride * rowCount}", nameof(rows));

            var channels = bytesPerPixel >= 3 ? 3 : 1;
            long sum = 0;
            long count = 0;
            long pixels = 0;
            long saturated = 0;
            for (var r = 0; r < rowCount; r++) {
                var rowStart = r * rowStride;
                for (var x = 0; x < width; x += pixelStep) {
                    var p = rowStart + x * bytesPerPixel;
                    var isSaturated = false;
                    for (var c = 0; c < channels; c++) {
                        var v = rows[p + c];
                        sum += v;
                        if (v >= saturationLevel) isSaturated = true;
                    }
                    count += channels;
                    pixels++;
                    if (isSaturated) saturated++;
                }
            }
            return new HwTestFrameStats((double)sum / count, (double)saturated / pixels);
        }

        /// <summary>露光の上げ下げの判定基準（比で見る．確認した向きはすべて期待どおりであること）</summary>
        public static HwTestDirectionCriteria ExposureCriteria() {
            return new HwTestDirectionCriteria {
                UseRatio = true,
                LowerLimit = HwTestThresholds.ExposureLowerMaxRatio,
                RaiseLimit = HwTestThresholds.ExposureRaiseMinRatio,
                DarkLevel = HwTestThresholds.DarkLevel,
                MaxSaturatedFraction = HwTestThresholds.MaxSaturatedFraction,
                RequireAllChecked = true,
            };
        }

        /// <summary>
        /// ゲインの上げ下げの判定基準（差で見る．一方の向きだけでも期待どおりなら合格）．
        /// 下げる側は範囲の下端にかかって変化が小さいことがあるため（DFK 33UX178 の実測: 10→0 で差 -3.2）
        /// </summary>
        public static HwTestDirectionCriteria GainCriteria() {
            return new HwTestDirectionCriteria {
                UseRatio = false,
                LowerLimit = HwTestThresholds.MinBrightnessDelta,
                RaiseLimit = HwTestThresholds.MinBrightnessDelta,
                DarkLevel = HwTestThresholds.DarkLevel,
                MaxSaturatedFraction = HwTestThresholds.MaxSaturatedFraction,
                RequireAllChecked = false,
            };
        }

        /// <summary>
        /// 設定を下げた・上げたときの平均輝度の変化を判定する．
        /// 下げる向きは「設定が実際に変わった（loweredApplied）かつ測定でき，基準が真っ暗でなく，下げた側が飽和していない」とき確認する．
        /// 上げる向きは「設定が実際に変わった（raisedApplied）かつ測定でき，基準が飽和していない（飽和画素の割合がしきい値以下）」とき確認する
        /// （比で見る場合は基準が真っ暗でないことも要る）．ただし上げた側が飽和している場合は，比・差にかかわらず確認しない
        /// （飽和した画素は明るさの変化を正しく表さず，期待どおりに見えても頭打ちで偶然越えただけのことがあるため）．
        /// 合否は criteria.RequireAllChecked に従う（true: 確認した向きがすべて期待どおりで，少なくとも一方を確認できた．
        /// false: 少なくとも一方の向きが期待どおり）．
        /// </summary>
        /// <exception cref="ArgumentNullException">criteria が null の場合</exception>
        public static HwTestDirectionResult JudgeDirection(HwTestMeasurement baseline, HwTestMeasurement lowered, HwTestMeasurement raised,
                bool loweredApplied, bool raisedApplied, HwTestDirectionCriteria criteria) {
            if (criteria == null) throw new ArgumentNullException(nameof(criteria));
            var b = baseline.Mean;
            var baselineMeasured = !double.IsNaN(b);
            var baselineDark = baselineMeasured && b <= criteria.DarkLevel;
            var baselineSaturated = IsSaturated(baseline.SaturatedFraction, criteria.MaxSaturatedFraction);

            var result = new HwTestDirectionResult();
            var lowerMeasurable = baselineMeasured && loweredApplied && !double.IsNaN(lowered.Mean) && !baselineDark;
            var lowerSaturated = lowerMeasurable && IsSaturated(lowered.SaturatedFraction, criteria.MaxSaturatedFraction);
            result.LowerChecked = lowerMeasurable && !lowerSaturated;
            result.LowerOk = result.LowerChecked && (criteria.UseRatio
                ? lowered.Mean / b <= criteria.LowerLimit
                : b - lowered.Mean >= criteria.LowerLimit);

            var raiseMeasurable = baselineMeasured && raisedApplied && !double.IsNaN(raised.Mean) && !baselineSaturated
                && (!criteria.UseRatio || !baselineDark);
            var raiseSaturated = raiseMeasurable && IsSaturated(raised.SaturatedFraction, criteria.MaxSaturatedFraction);
            result.RaiseChecked = raiseMeasurable && !raiseSaturated;
            result.RaiseOk = result.RaiseChecked && (criteria.UseRatio
                ? raised.Mean / b >= criteria.RaiseLimit
                : raised.Mean - b >= criteria.RaiseLimit);

            var anyChecked = result.LowerChecked || result.RaiseChecked;
            result.Pass = criteria.RequireAllChecked
                ? anyChecked && (!result.LowerChecked || result.LowerOk) && (!result.RaiseChecked || result.RaiseOk)
                : result.LowerOk || result.RaiseOk;

            var parts = new List<string> {
                lowerSaturated
                    ? DescribeSaturatedSide("下げる", b, lowered)
                    : DescribeSide("下げる", result.LowerChecked, result.LowerOk, loweredApplied, b, lowered.Mean, criteria,
                        baselineDark ? "基準が真っ暗" : null),
                raiseSaturated
                    ? DescribeSaturatedSide("上げる", b, raised)
                    : DescribeSide("上げる", result.RaiseChecked, result.RaiseOk, raisedApplied, b, raised.Mean, criteria,
                        baselineSaturated ? "基準が飽和" : baselineDark ? "基準が真っ暗" : null),
            };
            if (!anyChecked) parts.Add("どちらの向きも確認できません");
            else if (!criteria.RequireAllChecked && result.Pass && !(result.LowerOk && result.RaiseOk)) parts.Add("一方の向きが期待どおりのため合格");
            result.Summary = string.Join("／", parts);
            return result;
        }

        private static string DescribeSide(string label, bool isChecked, bool ok, bool applied, double baseline, double value,
                HwTestDirectionCriteria criteria, string skipReason) {
            var limit = label == "下げる" ? criteria.LowerLimit : criteria.RaiseLimit;
            if (isChecked) {
                var verdict = ok ? "期待どおり" : "期待と異なる";
                return criteria.UseRatio
                    ? $"{label}: 比 {value / baseline:0.000}（{baseline:0.00}→{value:0.00}．{(label == "下げる" ? "≦" : "≧")} {limit:G3} を期待．{verdict}）"
                    : $"{label}: 差 {value - baseline:+0.0;-0.0;0.0}（{baseline:0.00}→{value:0.00}．{(label == "下げる" ? "−" : "+")}{limit:G3} 以上を期待．{verdict}）";
            }
            if (!applied) return $"{label}: 設定が変わらなかったため確認しない";
            if (double.IsNaN(value) || double.IsNaN(baseline)) return $"{label}: 測定できなかった";
            return $"{label}: {skipReason ?? "確認できない"}のため確認しない（{baseline:0.00}→{value:0.00}）";
        }

        /// <summary>動かした側が飽和していて確認しない向きの説明（飽和画素の割合は判定に使う最大値）</summary>
        private static string DescribeSaturatedSide(string label, double baseline, HwTestMeasurement side) {
            return $"{label}: 飽和のため確認しない（飽和割合 {side.SaturatedFraction * 100:0.00}%．{baseline:0.00}→{side.Mean:0.00}）";
        }

        /// <summary>
        /// 読み戻した値の元の値に対する相対誤差（|読み戻し − 元| / |元|）．
        /// NaN を含む場合は NaN，元が 0 の場合は読み戻しも 0 なら 0・そうでなければ正の無限大．
        /// </summary>
        public static double RelativeError(double expected, double actual) {
            if (double.IsNaN(expected) || double.IsNaN(actual)) return double.NaN;
            var diff = Math.Abs(actual - expected);
            if (expected == 0) return diff == 0 ? 0 : double.PositiveInfinity;
            return diff / Math.Abs(expected);
        }

        /// <summary>
        /// 元に戻した設定値の読み戻しの説明（丸めない値（往復できる "R" 書式）と相対誤差（%））．
        /// 例: 「露光 読み戻し 0.0166666666666667（元 0.0166666666666667．相対誤差 0%）」
        /// </summary>
        public static string DescribeRestoredValue(string name, double expected, double actual) {
            var inv = System.Globalization.CultureInfo.InvariantCulture;
            var err = RelativeError(expected, actual);
            var errText = double.IsNaN(err) ? "測定できない"
                : double.IsInfinity(err) ? "—（元が 0）"
                : (err * 100).ToString("0.######", inv) + "%";
            return $"{name} 読み戻し {actual.ToString("R", inv)}（元 {expected.ToString("R", inv)}．相対誤差 {errText}）";
        }

        /// <summary>点灯時と消灯時の平均輝度の差が下限以上か</summary>
        public static bool JudgeLight(double onMean, double offMean, double minDelta) {
            if (double.IsNaN(onMean) || double.IsNaN(offMean)) return false;
            return onMean - offMean >= minDelta;
        }

        /// <summary>所要時間が上限以内か</summary>
        public static bool WithinLimit(TimeSpan elapsed, TimeSpan limit) {
            return elapsed <= limit;
        }

        /// <summary>元に戻した後の平均輝度が最初の平均輝度から許容内か</summary>
        public static bool IsRestored(double baseline, double after, double tolerance) {
            if (double.IsNaN(baseline) || double.IsNaN(after)) return false;
            return Math.Abs(after - baseline) <= tolerance;
        }

        /// <summary>
        /// ウォッチドッグの判定．全体の上限を先に見る（全体を超えたら項目の状況によらず打ち切る）．
        /// 実行中の項目が無い（itemLimit が null）ときは項目の上限を見ない．
        /// </summary>
        public static HwTestOverdue FindOverdue(TimeSpan overallElapsed, TimeSpan overallLimit,
                TimeSpan itemElapsed, TimeSpan? itemLimit) {
            if (overallElapsed > overallLimit) return HwTestOverdue.Overall;
            if (itemLimit.HasValue && itemElapsed > itemLimit.Value) return HwTestOverdue.Item;
            return HwTestOverdue.None;
        }

        /// <summary>
        /// 実機テストの間，カメラのフレームコールバックでの校正処理を止めるか．
        /// チャート不要の検査（--with-chart なし）ではカメラの前にチャート（または白板）をはめたまま
        /// 露光・ゲイン・光源を検査するため，校正（ホワイトバランスの調整等）が走ると測定が乱れる．そこで止める．
        /// チャートが必要な検査（--with-chart．校正そのものを検査する）では止めない．
        /// </summary>
        /// <param name="withChart">--with-chart が指定されたか</param>
        public static bool ShouldSuppressCalibration(bool withChart) {
            return !withChart;
        }

        /// <summary>
        /// 校正を止めているはずなのに校正が始まった（または済んだ）か．真なら校正の停止が効いていない（不合格）．
        /// 校正を止めていない場合は常に偽（校正が走るのは想定どおり）．
        /// </summary>
        /// <param name="suppressed">校正を止めているか（ShouldSuppressCalibration の結果）</param>
        /// <param name="isCalibrating">校正中か（CameraBase.IsCalibrating）</param>
        /// <param name="isCalibrated">校正済みか（CameraBase.HwTestIsCalibrated）</param>
        public static bool IsCalibrationLeaked(bool suppressed, bool isCalibrating, bool isCalibrated) {
            return suppressed && (isCalibrating || isCalibrated);
        }

        /// <summary>
        /// 実機テストの終了コード．指定機種が見つからない（実機テスト自体を行えない）ときは 2，
        /// 不合格の項目があれば 1，すべて合格なら 0（タイムアウトの 3 はウォッチドッグが返す）．
        /// </summary>
        public static int DecideExitCode(bool cameraNotFound, int failedCount) {
            if (cameraNotFound) return HwTestExitCodes.Error;
            return failedCount > 0 ? HwTestExitCodes.Fail : HwTestExitCodes.Pass;
        }

        /// <summary>
        /// 作業フォルダ（%TEMP%\TIASshot-hwtest\work-*）を残すか．--hwtest-keep の指定があれば常に残す．
        /// 不合格（1）・タイムアウト（3）は証拠として残し，合格（0）と前提を満たさずに終わった場合（2: 引数不正・指定機種が見つからない等）は消す．
        /// </summary>
        /// <param name="keepRequested">--hwtest-keep が指定されたか</param>
        /// <param name="exitCode">実機テストの終了コード（HwTestExitCodes）</param>
        public static bool ShouldKeepWorkDir(bool keepRequested, int exitCode) {
            if (keepRequested) return true;
            return exitCode == HwTestExitCodes.Fail || exitCode == HwTestExitCodes.Timeout;
        }

        private static void ValidateRange(double current, double min, double max) {
            if (!IsFinite(current) || !IsFinite(min) || !IsFinite(max)) {
                throw new ArgumentException($"設定値・範囲に非有限値が含まれます: {current}（{min}〜{max}）");
            }
            if (min > max) throw new ArgumentException($"範囲が不正です: {min}〜{max}");
        }

        private static bool IsFinite(double value) {
            return !double.IsNaN(value) && !double.IsInfinity(value);
        }

        /// <summary>値を min〜max に収める（NaN はそのまま返す）．機種ごとのアダプタが設定値を範囲内に収めるのにも使う</summary>
        internal static double Clamp(double value, double min, double max) {
            if (value < min) return min;
            if (value > max) return max;
            return value;
        }
    }

    /// <summary>実機テストの終了コード（HWTEST 限定）</summary>
    internal static class HwTestExitCodes {
        public const int Pass = 0;
        public const int Fail = 1;
        public const int Error = 2;
        public const int Timeout = 3;
    }
}
#endif
