// master_meter.cpp — see master_meter.h.

#include "core/instrument/ui/master_meter.h"

namespace reasampler::instrument::ui {

bool meterTickNumeralled(int db) {
    // Every OTHER 6 dB tick, which is the 0/−12/−24/−36/−48/−60 set the scale is specified as.
    return db % (2 * static_cast<int>(kMeterTickStepDb)) == 0;
}

MeterRects meterRects(const Rect& column, LaneSplit split) {
    MeterRects r;
    if (column.width < kMeterColumnW || column.height <= 0) return r;
    r.labels = Rect::ltrb(column.x, column.y, column.x + kMeterLabelW, column.bottom());
    const int fieldLeft = column.x + kMeterLabelW + kMeterLabelGap;
    r.field = Rect::ltrb(fieldLeft, column.y, fieldLeft + kMeterFieldW, column.bottom());
    if (split == LaneSplit::Single) {
        r.barA = r.field;
        return r;
    }
    const int barW = (kMeterFieldW - kMeterBarGap) / 2;
    r.barA = Rect::ltrb(fieldLeft, column.y, fieldLeft + barW, column.bottom());
    const int bLeft = r.barA.right() + kMeterBarGap;
    r.barB = Rect::ltrb(bLeft, column.y, bLeft + barW, column.bottom());
    return r;
}

bool meterFastPathEligible(const MeterRects& m, const Rect& dirty) {
    return !m.field.empty() && dirty.x >= m.field.x && dirty.y >= m.field.y &&
           dirty.right() <= m.field.right() && dirty.bottom() <= m.field.bottom();
}

namespace {
bool rectWithin(const Rect& outer, const Rect& inner) {
    return inner.empty() ||
           (inner.x >= outer.x && inner.right() <= outer.right() && inner.y >= outer.y &&
            inner.bottom() <= outer.bottom());
}
}  // namespace

bool meterBarsWithinField(const MeterRects& m) {
    if (m.field.empty()) return m.barA.empty() && m.barB.empty();
    return rectWithin(m.field, m.barA) && rectWithin(m.field, m.barB);
}

Rect meterNumeralRect(const Rect& labels, int y) {
    if (labels.empty()) return {};
    int top = y - 5;
    if (top < labels.y) top = labels.y;
    int bottom = top + 10;
    if (bottom > labels.bottom()) {
        bottom = labels.bottom();
        top = bottom - 10 < labels.y ? labels.y : bottom - 10;
    }
    return Rect::ltrb(labels.x, top, labels.right(), bottom);
}

int meterDbToY(const Rect& field, double db) {
    const double norm = engine::meterNormFromDb(db);
    const int y = field.bottom() - static_cast<int>(norm * field.height + 0.5);
    if (y < field.y) return field.y;
    if (y > field.bottom()) return field.bottom();
    return y;
}

MasterMeterUi advanceMasterMeter(MasterMeterUi prev, const MasterMeterBlock& block,
                                 double elapsedSeconds) {
    MasterMeterUi next;
    next.left = engine::advanceMeter(prev.left, block.peakL, elapsedSeconds);
    next.right = engine::advanceMeter(prev.right, block.peakR, elapsedSeconds);
    // ORed in unconditionally. Now that the peaks accumulate, advanceMeter's own >= 0 dBFS
    // check sees the same window and would latch too — but the published flag stays the
    // definitive one, and it is the half clearMasterBusClip resets.
    if (block.clip) {
        next.left.clip = true;
        next.right.clip = true;
    }

    const double dt = (elapsedSeconds > 0.0) ? elapsedSeconds : 0.0;
    const double reduction = -engine::meterDbFromLinear(block.minGain);
    // Same hold-then-release shape as the peak tick, for the same reason: at the UI period the
    // lamp actually runs at, a bare decay retires a catch before it has been drawn twice.
    if (reduction >= prev.reductionDb) {
        next.reductionDb = reduction;
        next.reductionHoldSeconds = engine::kMeterPeakHoldSeconds;
    } else {
        next.reductionDb = prev.reductionDb;
        next.reductionHoldSeconds = prev.reductionHoldSeconds - dt;
        if (next.reductionHoldSeconds < 0.0) {
            // Spend the overshoot as fall time so the release does not quantize to whichever
            // UI frame the hold happened to expire on.
            const double fallen =
                next.reductionDb - engine::kMeterFallDbPerSecond * -next.reductionHoldSeconds;
            next.reductionDb = fallen > reduction ? fallen : reduction;
            next.reductionHoldSeconds = 0.0;
        }
    }
    if (next.reductionDb < 0.0) next.reductionDb = 0.0;
    return next;
}

bool meterClipped(const MasterMeterUi& m) { return m.left.clip || m.right.clip; }

MasterMeterUi clearMasterMeterClip(MasterMeterUi prev) {
    MasterMeterUi next = prev;
    next.left = engine::clearMeterClip(prev.left);
    next.right = engine::clearMeterClip(prev.right);
    return next;
}

engine::MeterState meterSingleLaneState(const MasterMeterUi& m) {
    engine::MeterState s;
    s.levelDb = m.left.levelDb > m.right.levelDb ? m.left.levelDb : m.right.levelDb;
    s.holdDb = m.left.holdDb > m.right.holdDb ? m.left.holdDb : m.right.holdDb;
    s.holdRemainingSeconds = m.left.holdRemainingSeconds > m.right.holdRemainingSeconds
                                 ? m.left.holdRemainingSeconds
                                 : m.right.holdRemainingSeconds;
    s.clip = m.left.clip || m.right.clip;
    return s;
}

bool grLampLit(const MasterMeterUi& m) { return m.reductionDb >= kGrLampFloorDb; }

bool meterDrawEqual(const MasterMeterUi& a, const MasterMeterUi& b) {
    return a.left.levelDb == b.left.levelDb && a.right.levelDb == b.right.levelDb &&
           a.left.holdDb == b.left.holdDb && a.right.holdDb == b.right.holdDb &&
           meterClipped(a) == meterClipped(b) && grLampLit(a) == grLampLit(b);
}

}  // namespace reasampler::instrument::ui
