/* SleepLib Day Class Implementation
 *
 * Copyright (c) 2019 The OSCAR Team
 * Copyright (c) 2011-2018 Mark Watkins <mark@jedimark.net>
 *
 * This file is subject to the terms and conditions of the GNU General Public
 * License. See the file COPYING in the main directory of the source code
 * for more details. */

#include <QMultiMap>

#include <algorithm>
#include <cmath>
#include <limits>
#include <QDebug>

#include "day.h"
#include "profiles.h"

Day::Day()
{
    d_firstsession = true;
    d_summaries_open = false;
    d_events_open = false;
    d_invalidate = true;

}
Day::~Day()
{
    for (auto & sess : sessions) {
        delete sess;
    }
}

void Day::updateCPAPCache()
{
    d_count.clear();
    d_sum.clear();
    OpenSummary();
    QList<ChannelID> channels = getSortedMachineChannels(MT_CPAP, schema::FLAG | schema::MINOR_FLAG | schema::SPAN);

    for (const auto code : channels) {
        d_count[code] = count(code);
        d_sum[code] = count(code);
        d_machhours[MT_CPAP] = hours(MT_CPAP);
    }
}

Session * Day::firstSession(MachineType type)
{
    for (auto & sess : sessions) {
        if (!sess->enabled()) continue;
        if (sess->type() == type) {
            return sess;
        }
    }
    return nullptr;
}

bool Day::addMachine(Machine *mach)
{
    invalidate();
    if (!machines.contains(mach->type())) {
        machines[mach->type()] = mach;
        return true;
    }
    return false;
}
Machine *Day::machine(MachineType type)
{
    auto it = machines.find(type);
    if (it != machines.end())
        return it.value();
    return nullptr;
}

QList<Session *> Day::getSessions(MachineType type, bool ignore_enabled)
{
    QList<Session *> newlist;

    for (auto & sess : sessions) {
        if (!ignore_enabled && !sess->enabled())
            continue;

        if (sess->type() == type)
            newlist.append(sess);
    }
    return newlist;
}

Session *Day::find(SessionID sessid)
{
    for (auto & sess : sessions) {
        if (sess->session() == sessid) {
            return sess;
        }
    }
    return nullptr;
}

Session *Day::find(SessionID sessid, MachineType mt)
{
    for (auto & sess : sessions) {
        if ((sess->session() == sessid) && (sess->s_machtype == mt)) {
            return sess;
        }
    }
    return nullptr;
}

void Day::addSession(Session *s)
{
    if (s == nullptr) {
        qDebug() << "addSession called with null session pointer";
        return;
    }
    invalidate();
    auto mi = machines.find(s->type());
    if (mi != machines.end()) {
        if (mi.value() != s->machine()) {
            qDebug() << "OSCAR can't add session" << s->session()
                     << "["+QDateTime::fromTime_t(s->session()).toString("MMM dd, yyyy hh:mm:ss")+"]"
                     << "from machine" << mi.value()->serial() << "to machine" << s->machine()->serial()
                     << "to this day record, as it already contains a different machine of the same MachineType" << s->type();
            return;
        }
    } else {
        machines[s->type()] = s->machine();
    }

    if (s->first() == 0) {
        qWarning() << "Day::addSession discarding session" << s->session()
                 << "from machine" << s->machine()->serial() << "with first=0";
        return;
    }
    
    for (auto & sess : sessions) {
        if (sess->session() == s->session() && sess->type() == s->type()) {
            // This usually indicates a problem in purging or cleanup somewhere,
            // unless there's a problem with a parser.
            qCritical() << "Day object" << this->date().toString() << "adding duplicate session" << s->session()
                     << "["+QDateTime::fromTime_t(s->session()).toString("MMM dd, yyyy hh:mm:ss")+"]";
            // Don't skip this one, since it might have replaced the original elsewhere already.
            //return;
        }
    }
    sessions.push_back(s);
}

EventDataType Day::calcMiddle(ChannelID code)
{
    int c = p_profile->general->prefCalcMiddle();

    if (c == 0) {
        return percentile(code, 0.5); // Median
    } else if (c == 1 ) {
        return wavg(code); // Weighted Average
    } else {
        return avg(code); // Average
    }
}
EventDataType Day::calcMax(ChannelID code)
{
    return p_profile->general->prefCalcMax() ? percentile(code, 0.995f) : Max(code);
}
EventDataType Day::calcPercentile(ChannelID code)
{
    double p = p_profile->general->prefCalcPercentile() / 100.0;
    return percentile(code, p);
}

QString Day::calcMiddleLabel(ChannelID code)
{
    int c = p_profile->general->prefCalcMiddle();
    if (c == 0) {
        return QObject::tr("%1 %2").arg(STR_TR_Median).arg(schema::channel[code].label());
    } else if (c == 1) {
        return QObject::tr("%1 %2").arg(STR_TR_WAvg).arg(schema::channel[code].label());
    } else {
        return QObject::tr("%1 %2").arg(STR_TR_Avg).arg(schema::channel[code].label());
    }
}
QString Day::calcMaxLabel(ChannelID code)
{
    return QObject::tr("%1 %2").arg(p_profile->general->prefCalcMax() ? QObject::tr("99.5%") : STR_TR_Max).arg(schema::channel[code].label());
}
QString Day::calcPercentileLabel(ChannelID code)
{
    return QObject::tr("%1% %2").arg(p_profile->general->prefCalcPercentile(),0, 'f',0).arg(schema::channel[code].label());
}

EventDataType Day::countInsideSpan(ChannelID span, ChannelID code)
{
    int count = 0;
    for (auto & sess : sessions) {
        if (sess->enabled()) {
            count += sess->countInsideSpan(span, code);
        }
    }
    return count;
}

EventDataType Day::lookupValue(ChannelID code, qint64 time, bool square)
{
    // Remove drift from CPAP graphs so we get the right value...
    qint64 clockdrift = qint64(p_profile->cpap->clockDrift()) * 1000L;
    for (auto & sess : sessions) {
        if (sess->enabled()) {
            qint64 drift = (sess->type() == MT_CPAP?clockdrift:0);
            if ((time-drift > sess->first()) && (time-drift < sess->last())) {
                if (sess->channelExists(code)) {
                    return sess->SearchValue(code,time-drift,square);
                }
            }
        }
    }
    return 0;
}


EventDataType Day::timeAboveThreshold(ChannelID code, EventDataType threshold)
{
    EventDataType val = 0;

    for (auto & sess : sessions) {
        if (sess->enabled() && sess->m_availableChannels.contains(code)) {
            val += sess->timeAboveThreshold(code,threshold);
        }
    }
    return val;
}

EventDataType Day::timeBelowThreshold(ChannelID code, EventDataType threshold)
{
    EventDataType val = 0;

    for (auto & sess : sessions) {
        if (sess->enabled()) {
            val += sess->timeBelowThreshold(code,threshold);
        }
    }
    return val;
}


EventDataType Day::settings_sum(ChannelID code)
{
    EventDataType val = 0;

    for (auto & sess : sessions) {
        if (sess->enabled()) {
            auto set = sess->settings.find(code);
            if (set != sess->settings.end()) {
                val += set.value().toDouble();
            }
        }
    }
    return val;
}

EventDataType Day::settings_max(ChannelID code)
{
    EventDataType min = -std::numeric_limits<EventDataType>::max();
    EventDataType max = min;
    EventDataType value;

    for (auto & sess : sessions) {
        if (sess->enabled()) {
            value = sess->settings.value(code, min).toFloat();
            if (value > max) {
                max = value;
            }
        }
    }
    return max;
}

EventDataType Day::settings_min(ChannelID code)
{
    EventDataType max = std::numeric_limits<EventDataType>::max();
    EventDataType min = max;
    EventDataType value;

    for (auto & sess : sessions) {
        if (sess->enabled()) {
            value = sess->settings.value(code, max).toFloat();
            if (value < min) {
                min = value;
            }
        }
    }
    return min;
}

EventDataType Day::settings_avg(ChannelID code)
{
    EventDataType val = 0;
    int cnt = 0;

    for (auto & sess : sessions) {
        if (sess->enabled()) {
            auto set = sess->settings.find(code);

            if (set != sess->settings.end()) {
                val += set.value().toDouble();
                cnt++;
            }
        }
    }
    val = (cnt > 0) ? (val / EventDataType(cnt)) : val;

    return val;
}

EventDataType Day::settings_wavg(ChannelID code)
{
    double s0 = 0, s1 = 0, s2 = 0, tmp;

    for (auto & sess : sessions) {
        if (sess->enabled()) {
            auto set = sess->settings.find(code);

            if (set != sess->settings.end()) {
                if (code == CPAP_Mode && sess->type() != MT_CPAP) {
                    // There used to be a bug in gLineChart::SetDay that inserted a CPAP_Mode
                    // setting in any session that didn't already have one. That shouldn't
                    // happen any more, but leave this diagnostic message here in case it does.
                    qWarning() << sess->session() << "non-CPAP session with CPAP mode setting";
                    continue;
                }
                s0 = sess->hours();
                tmp = set.value().toDouble();
                s1 += tmp * s0;
                s2 += s0;
            }
        }
    }
    if (s2 == 0) { return 0; }

    tmp = (s1 / s2);
    return tmp;
}


EventDataType Day::percentile(ChannelID code, EventDataType percentile)
{
    // Cache this calculation?
    //    QHash<ChannelID, QHash<EventDataType, EventDataType> >::iterator pi;
    //    pi=perc_cache.find(code);
    //    if (pi!=perc_cache.end()) {
    //        QHash<EventDataType, EventDataType> & hsh=pi.value();
    //        QHash<EventDataType, EventDataType>::iterator hi=hsh.find(
    //        if (hi!=pi.value().end()) {
    //            return hi.value();
    //        }
    //    }

    QHash<EventStoreType, qint64> wmap; // weight map

    QHash<EventStoreType, qint64>::iterator wmapit;
    qint64 SN = 0;

    EventDataType lastgain = 0, gain = 0;
    // First Calculate count of all events
    bool timeweight;

    for (auto & sess : sessions) {
        if (!sess->enabled()) { continue; }

        auto ei = sess->m_valuesummary.find(code);
        if (ei == sess->m_valuesummary.end()) { continue; }

        auto tei = sess->m_timesummary.find(code);
        timeweight = (tei != sess->m_timesummary.end());
        gain = sess->m_gain[code];

        // Here's assuming gains don't change accross a days sessions
        // Can't assume this in any multi day calculations..
        if (lastgain > 0) {
            if (gain != lastgain) {
                qDebug() << "Gains differ across sessions: " << gain << lastgain;
            }
        }

        lastgain = gain;

        qint64 weight;

        //qint64 tval;
        if (timeweight) {
            wmap.reserve(wmap.size() + tei.value().size());

            for (auto it = tei.value().begin(), teival_end=tei.value().end(); it != teival_end; ++it) {
                weight = it.value();
                SN += weight;

                wmap[it.key()] += weight;
            }
        } else {
            wmap.reserve(wmap.size() + ei.value().size());
            for (auto it = ei.value().begin(), eival_end=ei.value().end(); it != eival_end; ++it) {
                weight = it.value();

                SN += weight;

                wmap[it.key()] += weight;
            }
        }
    }

    QVector<ValueCount> valcnt;

    valcnt.resize(wmap.size());
    // Build sorted list of value/counts

    auto wmap_end = wmap.end();
    int ii=0;
    for (auto it = wmap.begin(); it != wmap_end; ++it) {
        valcnt[ii++]=ValueCount(EventDataType(it.key()) * gain, it.value(), 0);
    }

    // sort by weight, then value
    //qSort(valcnt);
    std::sort(valcnt.begin(), valcnt.end());

    //double SN=100.0/double(N); // 100% / overall sum
    double p = 100.0 * percentile;

    double nth = double(SN) * percentile; // index of the position in the unweighted set would be
    double nthi = floor(nth);

    qint64 sum1 = 0, sum2 = 0;
    qint64 w1, w2 = 0;
    double v1 = 0, v2;

    int N = valcnt.size();
    int k = 0;

    for (k = 0; k < N; k++) {
        v1 = valcnt.at(k).value;
        w1 = valcnt.at(k).count;
        sum1 += w1;

        if (sum1 > nthi) {
            return v1;
        }

        if (sum1 == nthi) {
            break; // boundary condition
        }
    }

    if (k >= N) {
        return v1;
    }

    if (valcnt.size() == 1) {
        return valcnt[0].value;
    }
    v2 = valcnt[k + 1].value;
    w2 = valcnt[k + 1].count;
    sum2 = sum1 + w2;
    // value lies between v1 and v2

    double px = 100.0 / double(SN); // Percentile represented by one full value

    // calculate percentile ranks
    double p1 = px * (double(sum1) - (double(w1) / 2.0));
    double p2 = px * (double(sum2) - (double(w2) / 2.0));

    // calculate linear interpolation
    double v = v1 + ((p - p1) / (p2 - p1)) * (v2 - v1);

    return v;

    //                p1.....p.............p2
    //                37     55            70
}

EventDataType Day::p90(ChannelID code)
{
    return percentile(code, 0.90F);
}

EventDataType Day::rangeCount(ChannelID code, qint64 st, qint64 et)
{
    int cnt = 0;

    for (auto & sess : sessions) {
        if (sess->enabled()) {
            cnt += sess->rangeCount(code, st, et);
        }
    }

    return cnt;
}
EventDataType Day::rangeSum(ChannelID code, qint64 st, qint64 et)
{
    double val = 0;

    for (auto & sess : sessions) {
        if (sess->enabled()) {
            val += sess->rangeSum(code, st, et);
        }
    }

    return val;
}
EventDataType Day::rangeAvg(ChannelID code, qint64 st, qint64 et)
{
    double val = 0;
    int cnt = 0;

    for (auto & sess : sessions) {
        if (sess->enabled()) {
            val += sess->rangeSum(code, st, et);
            cnt += sess->rangeCount(code, st,et);
        }
    }
    if (cnt == 0) { return 0; }
    val /= double(cnt);

    return val;
}
EventDataType Day::rangeWavg(ChannelID code, qint64 st, qint64 et)
{
    double sum = 0;
    double cnt = 0;
    qint64 lasttime, time;
    double data, duration;

    for (auto & sess : sessions) {
        auto EVEC = sess->eventlist.find(code);
        if (EVEC == sess->eventlist.end()) continue;

        for (auto & el : EVEC.value()) {
            if (el->count() < 1) continue;
            lasttime = el->time(0);

            if (lasttime < st)
                lasttime = st;

            for (unsigned i=1; i<el->count(); i++)  {
                data = el->data(i);
                time = el->time(i);

                if (time < st) {
                    lasttime = st;
                    continue;
                }

                if (time > et) {
                    time = et;
                }

                duration = double(time - lasttime) / 1000.0;
                sum += data * duration;
                cnt += duration;

                if (time >= et) break;

                lasttime = time;
            }
        }
    }
    if (cnt < 0.000001)
        return 0;
    return sum / cnt;
}


// Boring non weighted percentile
EventDataType Day::rangePercentile(ChannelID code, float p, qint64 st, qint64 et)
{
    int count = rangeCount(code, st,et);
    QVector<EventDataType> list;
    list.resize(count);
    int idx = 0;
    qint64 time;

    for (auto & sess : sessions) {
        auto EVEC = sess->eventlist.find(code);
        if (EVEC == sess->eventlist.end()) continue;

        for (auto & el : EVEC.value()) {
            for (unsigned i=0; i<el->count(); i++)  {
                time = el->time(i);
                if ((time < st) || (time > et)) continue;
                list[idx++] = el->data(i);
            }
        }
    }

    // TODO: use nth_element instead..
    //qSort(list);
    std::sort(list.begin(), list.end());

    float b = float(idx) * p;
    int a = floor(b);
    int c = ceil(b);

    if ((a == c) || (c >= idx)) {
        return list[a];
    }

    EventDataType v1 = list[a];
    EventDataType v2 = list[c];

    EventDataType diff = v2 - v1;  // the whole               == C-A

    double ba = b - float(a);     // A....B...........C      == B-A

    double val = v1 + diff * ba;

    return val;
}

EventDataType Day::avg(ChannelID code)
{
    double val = 0;
    // Cache this?
    int cnt = 0;

    for (auto & sess : sessions) {
        if (sess->enabled()) {
            val += sess->sum(code);
            cnt += sess->count(code);
        }
    }
    if (cnt == 0) { return 0; }
    val /= double(cnt);

    return val;
}

EventDataType Day::sum(ChannelID code)
{
    // Cache this?
    EventDataType val = 0;

    if (code == AllAhiChannels) {
        for (int i = 0; i < ahiChannels.size(); i++)
            val += sum(ahiChannels.at(i));
        return val;
    }

    for (auto & sess : sessions) {
        if (sess->enabled() && sess->m_sum.contains(code)) {
            val += sess->sum(code);
        }
    }

    return val;
}

EventDataType Day::wavg(ChannelID code)
{
    double s0 = 0, s1 = 0, s2 = 0;
    qint64 d;

    for (auto & sess : sessions) {
        if (sess->enabled() && sess->m_wavg.contains(code)) {
            d = sess->length();
            s0 = double(d) / 3600000.0;

            if (s0 > 0) {
                s1 += sess->wavg(code) * s0;
                s2 += s0;
            }
        }
    }

    if (s2 == 0) {
        return 0;
    }

    return (s1 / s2);
}

// Total session time in milliseconds
qint64 Day::total_time()
{
    qint64 d_totaltime = 0;
    QMultiMap<qint64, bool> range;
    //range.reserve(size()*2);

    // Remember sessions may overlap..

    qint64 first, last;

    for (auto & sess : sessions) {
        int slicesize = sess->m_slices.size();

        if (sess->enabled() && (sess->type() != MT_JOURNAL)) {
            first = sess->first();
            last = sess->last();

            if (slicesize == 0) {
                // This algorithm relies on non zero length, and correctly ordered sessions
                if (last > first) {
                    range.insert(first, 0);
                    range.insert(last, 1);
                    d_totaltime += sess->length();
                    if (sess->length() == 0) {
                        qWarning() << sess->s_session << "0 length session";
                    }
                }
            } else {
                for (auto & slice : sess->m_slices) {
                    if (slice.status == MaskOn) {
                        range.insert(slice.start, 0);
                        range.insert(slice.end, 1);
                        d_totaltime += slice.end - slice.start;
                        if (slice.end - slice.start == 0) {
                            qWarning() << sess->s_session << "0 length slice";
                        }
                    }
                }
            }
        }
    }

    bool b;
    int nest = 0;
    qint64 ti = 0;
    qint64 total = 0;

    // This is my implementation of a typical "brace counting" algorithm mentioned here:
    // http://stackoverflow.com/questions/7468948/problem-calculating-overlapping-date-ranges

    auto rend = range.end();
    for (auto rit = range.begin(); rit != rend; ++rit) {
        b = rit.value();

        if (!b) {
            if (!ti) {
                ti = rit.key();
            }

            nest++;
        } else {
            if (--nest <= 0) {
                total += rit.key() - ti;
                ti = 0;
            }
        }
    }

    if (total != d_totaltime) {
        // They can overlap.. tough.
//        qDebug() << "Sessions Times overlaps!" << total << d_totaltime;
    }

    return total; //d_totaltime;
}

// Total session time in milliseconds, only considering machinetype
qint64 Day::total_time(MachineType type)
{
    qint64 d_totaltime = 0;
    QMultiMap<qint64, bool> range;
    //range.reserve(size()*2);

    // Remember sessions may overlap..

    qint64 first, last;
    for (auto & sess : sessions) {
        int slicesize = sess->m_slices.size();

        if (sess->enabled() && (sess->type() == type)) {
            first = sess->first();
            last = sess->last();

            // This algorithm relies on non zero length, and correctly ordered sessions
            if (slicesize == 0) {
                if (last > first) {
                    range.insert(first, 0);
                    range.insert(last, 1);
                    d_totaltime += sess->length();
                    if (sess->length() == 0) {
                        qWarning() << sess->s_session << "0 length session";
                    }
                }
            } else {
                for (const auto & slice : sess->m_slices) {
                    if (slice.status == MaskOn) {
                        range.insert(slice.start, 0);
                        range.insert(slice.end, 1);
                        d_totaltime += slice.end - slice.start;
                        if (slice.end - slice.start == 0) {
                            qWarning() << sess->s_session << "0 length slice";
                        }
                    }
                }
            }
        }
    }

    bool b;
    int nest = 0;
    qint64 ti = 0;
    qint64 total = 0;

    // This is my implementation of a typical "brace counting" algorithm mentioned here:
    // http://stackoverflow.com/questions/7468948/problem-calculating-overlapping-date-ranges

    auto rend = range.end();
    for (auto rit = range.begin(); rit != rend; ++rit) {
        b = rit.value();

        if (!b) {
            if (!ti) {
                ti = rit.key();
            }

            nest++;
        } else {
            if (--nest <= 0) {
                total += rit.key() - ti;
                ti = 0;
            }
        }
    }

    if (total != d_totaltime) {
        // They can overlap.. tough.
//        qDebug() << "Sessions Times overlaps!" << total << d_totaltime;
    }

    return total; //d_totaltime;
}

ChannelID Day::getPressureChannelID() {
    // TODO: This is an awful hack that depends on the enum ordering of the generic CPAP_Mode channel.
    // See the comment in getCPAPModeStr().
    // Determined the preferred pressure channel (CPAP_IPAP or CPAP_Pressure)
    CPAPMode cpapmode = (CPAPMode)(int)settings_max(CPAP_Mode);

    // TODO: PRS1 ventilators in CPAP mode report IPAP rather than pressure...but their pressure setting channel is CPAP_Pressure,
    // so this currently gets fixed in the welcome screen manually.
    // And why would ASV or AVAPS have Pressure channels?
    QList<ChannelID> preferredIDs = { CPAP_Pressure, CPAP_PressureSet, CPAP_IPAP, CPAP_IPAPSet };
    if (cpapmode == MODE_ASV || cpapmode == MODE_ASV_VARIABLE_EPAP || cpapmode == MODE_AVAPS ||
        cpapmode == MODE_BILEVEL_FIXED || cpapmode == MODE_BILEVEL_AUTO_FIXED_PS || cpapmode == MODE_BILEVEL_AUTO_VARIABLE_PS) {
        preferredIDs = { CPAP_IPAP, CPAP_IPAPSet, CPAP_Pressure, CPAP_PressureSet };
    }

    for (auto & preferredID : preferredIDs) {
        // If preferred channel has data, return it
        if (channelHasData(preferredID)) {
//          if ( cpapmode == MODE_AVAPS )
//              qDebug() << QString("Found pressure channel 0x%1").arg(preferredID, 4, 16, QChar('0'));
            return preferredID;
        }
    }
    
    qDebug() << "No pressure channel for " << getCPAPModeStr();
    return NoChannel;
}

bool Day::hasEnabledSessions()
{
    for (auto & sess : sessions) {
        if (sess->enabled()) {
            return true;
        }
    }
    return false;
}

bool Day::hasEnabledSessions(MachineType type)
{
    for (auto & sess : sessions) {
        if ((sess->type() == type) && sess->enabled()) {
            return true;
        }
    }
    return false;
}

/*EventDataType Day::percentile(ChannelID code,double percent)
{
    double val=0;
    int cnt=0;

    for (auto & sess : sessions) {
        if (sess->eventlist.find(code)!=sess->eventlist.end()) {
            val+=sess->percentile(code,percent);
            cnt++;
        }
    }
    if (cnt==0) return 0;
    return EventDataType(val/cnt);

}*/

qint64 Day::first(ChannelID code)
{
    qint64 date = 0;
    qint64 tmp;

    for (auto & sess : sessions) {
        if (sess->enabled()) {
            tmp = sess->first(code);

            if (!tmp) { continue; }

            if (!date) {
                date = tmp;
            } else {
                if (tmp < date) { date = tmp; }
            }
        }
    }

    return date;
}

qint64 Day::last(ChannelID code)
{
    qint64 date = 0;
    qint64 tmp;

    for (auto & sess : sessions) {
        if (sess->enabled()) {
            tmp = sess->last(code);

            if (!tmp) { continue; }

            if (!date) {
                date = tmp;
            } else {
                if (tmp > date) { date = tmp; }
            }
        }
    }

    return date;
}

EventDataType Day::Min(ChannelID code)
{
    EventDataType min = 0;
    EventDataType tmp;
    bool first = true;

    for (auto & sess : sessions) {
        if (sess->enabled() && sess->m_min.contains(code)) {

            tmp = sess->Min(code);

            if (first) {
                min = tmp;
                first = false;
            } else {
                if (tmp < min) { min = tmp; }
            }
        }
    }

    return min;
}

EventDataType Day::physMin(ChannelID code)
{
    EventDataType min = 0;
    EventDataType tmp;
    bool first = true;

    for (auto & sess : sessions) {
        if (sess->enabled() && sess->m_min.contains(code)) {

            tmp = sess->physMin(code);

            if (first) {
                min = tmp;
                first = false;
            } else {
                if (tmp < min) { min = tmp; }
            }
        }
    }

    return min;
}

bool Day::hasData(ChannelID code, SummaryType type)
{
    bool has = false;

    for (auto & sess : sessions) {
        if (sess->type() == MT_JOURNAL) continue;

        if (sess->enabled()) {
            switch (type) {
            //        case ST_90P:
            //            has=sess->m_90p.contains(code);
            //            break;
            case ST_PERC:
                has = sess->m_valuesummary.contains(code);
                break;

            case ST_MIN:
                has = sess->m_min.contains(code);
                break;

            case ST_MAX:
                has = sess->m_max.contains(code);
                break;

            case ST_CNT:
                has = sess->m_cnt.contains(code);
                break;

            case ST_AVG:
                has = sess->m_avg.contains(code);
                break;

            case ST_WAVG:
                has = sess->m_wavg.contains(code);
                break;

            case ST_CPH:
                has = sess->m_cph.contains(code);
                break;

            case ST_SPH:
                has = sess->m_sph.contains(code);
                break;

            case ST_FIRST:
                has = sess->m_firstchan.contains(code);
                break;

            case ST_LAST:
                has = sess->m_lastchan.contains(code);
                break;

            case ST_SUM:
                has = sess->m_sum.contains(code);
                break;

            default:
                break;
            }

            if (has) { break; }
        }
    }

    return has;
}

EventDataType Day::Max(ChannelID code)
{
    EventDataType max = 0;
    EventDataType tmp;
    bool first = true;

    for (auto & sess : sessions) {
        if (sess->enabled() && sess->m_max.contains(code)) {

            tmp = sess->Max(code);

            if (first) {
                max = tmp;
                first = false;
            } else {
                if (tmp > max) { max = tmp; }
            }
        }
    }
    return max;
}

EventDataType Day::physMax(ChannelID code)
{
    EventDataType max = 0;
    EventDataType tmp;
    bool first = true;

    for (auto & sess : sessions) {
        if (sess->enabled() && sess->m_max.contains(code)) {
            tmp = sess->physMax(code);

            if (first) {
                max = tmp;
                first = false;
            } else {
                if (tmp > max) { max = tmp; }
            }
        }
    }
    return max;
}
EventDataType Day::cph(ChannelID code)
{
    double sum = 0;

    for (auto & sess : sessions) {
        if (sess->enabled() && sess->m_cnt.contains(code)) {
            sum += sess->count(code);
        }
    }
    sum /= hours();
    return sum;
}

EventDataType Day::sph(ChannelID code)
{
    EventDataType sum = 0;
    EventDataType h = 0;

    for (auto & sess : sessions) {
        if (sess->enabled() && sess->m_sum.contains(code)) {
            sum += sess->sum(code) / 3600.0;
        }
    }

    h = hours();
    sum = (100.0 / h) * sum;
    return sum;
}

EventDataType Day::count(ChannelID code)
{
    EventDataType total = 0;

    if (code == AllAhiChannels) {
        for (int i = 0; i < ahiChannels.size(); i++)
            total += count(ahiChannels.at(i));
        return total;
    }

    for (auto & sess : sessions) {
        if (sess->enabled() && sess->m_cnt.contains(code)) {
            total += sess->count(code);
        }
    }
    return total;
}

bool Day::noSettings(Machine * mach)
{
    for (auto & sess : sessions) {
        if ((mach == nullptr) && sess->noSettings()) {
            // If this day generally has just summary data.
            return true;
        } else if ((mach == sess->machine())  && sess->noSettings()) {
            // Focus only on machine mach
            return true;
        }
    }
    return false;
}

bool Day::summaryOnly(Machine * mach)
{
    for (auto & sess : sessions) {
        if ((mach == nullptr) && sess->summaryOnly()) {
            // If this day generally has just summary data.
            return true;
        } else if ((mach == sess->machine())  && sess->summaryOnly()) {
            // Focus only on machine mach
            return true;
        }
    }
    return false;
}

bool Day::settingExists(ChannelID id)
{
    for (auto & sess : sessions) {
        if (sess->enabled()) {
            auto set = sess->settings.find(id);
            if (set != sess->settings.end()) {
                return true;
            }
        }
    }
    return false;
}

bool Day::eventsLoaded()
{
    for (auto & sess : sessions) {
        if (sess->eventsLoaded()) {
            return true;
        }
    }

    return false;
}

bool Day::channelExists(ChannelID id)
{
    for (auto & sess : sessions) {
        if (sess->enabled() && sess->eventlist.contains(id)) {
            return true;
        }
    }

    return false;
}
bool Day::hasEvents() {
    for (auto & sess : sessions) {
        if (sess->eventlist.size() > 0) return true;
    }
    return false;
}

bool Day::channelHasData(ChannelID id)
{
    for (auto & sess : sessions) {
        if (sess->enabled()) {
            if (sess->m_cnt.contains(id)) {
                return true;
            }

            if (sess->eventlist.contains(id)) {
                return true;
            }

            if (sess->m_valuesummary.contains(id)) {
                return true;
            }
        }
    }

    return false;
}

void Day::OpenEvents()
{
    for (auto & sess : sessions) {
        if (sess->type() != MT_JOURNAL)
            sess->OpenEvents();
    }
    d_events_open = true;
}

void Day::OpenSummary()
{
    if (d_summaries_open) return;
    for (auto & sess : sessions) {
        sess->LoadSummary();
    }
    d_summaries_open = true;
}


void Day::CloseEvents()
{
    for (auto & sess : sessions) {
        sess->TrashEvents();
    }
    d_events_open = false;
}

QList<ChannelID> Day::getSortedMachineChannels(MachineType type, quint32 chantype)
{
    QList<ChannelID> available;
    auto mi_end = machines.end();
    for (auto mi = machines.begin(); mi != mi_end; mi++) {
        if (mi.key() != type) continue;
        available.append(mi.value()->availableChannels(chantype));
    }

    QMultiMap<int, ChannelID> order;

    for (const auto code : available) {
        order.insert(schema::channel[code].order(), code);
    }

    QList<ChannelID> channels;
    for (auto it = order.begin(); it != order.end(); ++it) {
        ChannelID code = it.value();
        channels.append(code);
    }
    return channels;
}


QList<ChannelID> Day::getSortedMachineChannels(quint32 chantype)
{
    QList<ChannelID> available;
    auto mi_end = machines.end();
    for (auto mi = machines.begin(); mi != mi_end; mi++) {
        if (mi.key() == MT_JOURNAL) continue;
        available.append(mi.value()->availableChannels(chantype));
    }

    QMultiMap<int, ChannelID> order;

    for (auto code : available) {
        order.insert(schema::channel[code].order(), code);
    }

    QList<ChannelID> channels;
    for (auto it = order.begin(); it != order.end(); ++it) {
        ChannelID code = it.value();
        channels.append(code);
    }
    return channels;
}

qint64 Day::first(MachineType type)
{
    qint64 date = 0;
    qint64 tmp;

    for (auto & sess : sessions) {
        if ((sess->type() == type) && sess->enabled()) {
            tmp = sess->first();

            if (!tmp) { continue; }

            if (!date) {
                date = tmp;
            } else {
                if (tmp < date) { date = tmp; }
            }
        }
    }
    return date;
}

qint64 Day::first()
{
    qint64 date = 0;
    qint64 tmp;

    for (auto & sess : sessions) {
        if (sess->type() == MT_JOURNAL) continue;
        if (sess->enabled()) {
            tmp = sess->first();
            if (!tmp) { continue; }
            if (!date) {
                date = tmp;
            } else {
                if (tmp < date) { date = tmp; }
            }
        }
    }
    return date;
}

//! \brief Returns the last session time of this day
qint64 Day::last()
{
    qint64 date = 0;
    qint64 tmp;

    for (auto & sess : sessions) {
        if (sess->type() == MT_JOURNAL) continue;
        if (sess->enabled()) {
            tmp = sess->last();
            if (!tmp) { continue; }
            if (!date) {
                date = tmp;
            } else {
                if (tmp > date) { date = tmp; }
            }
        }
    }
    return date;
}

qint64 Day::last(MachineType type)
{
    qint64 date = 0;
    qint64 tmp;

    for (auto & sess : sessions) {
        if ((sess->type() == type) && sess->enabled()) {
            tmp = sess->last();
            if (!tmp) { continue; }
            if (!date) {
                date = tmp;
            } else {
                if (tmp > date) { date = tmp; }
            }
        }
    }
    return date;
}

bool Day::removeSession(Session *sess)
{
    sess->machine()->sessionlist.remove(sess->session());
    MachineType mt = sess->type();
    bool b = sessions.removeAll(sess) > 0;
    if (!searchMachine(mt)) {
        machines.remove(mt);
    }
    return b;
}
bool Day::searchMachine(MachineType mt) {
    for (auto & sess : sessions) {
        if (sess->type() == mt)
            return true;
    }
    return false;
}

bool Day::hasMachine(Machine * mach)
{
    for (auto & sess : sessions) {
        if (sess->machine() == mach)
            return true;
    }
    return false;
}

void Day::removeMachine(Machine * mach)
{
    // Yell about and fix any dangling references rather than possibly crashing later.
    //
    // This has no functional use and can be removed when the data structures are cleaned up
    // with better encapsulation and fewer unnecessary references between each other.
    
    QList<Session*> list = sessions;  // make a copy so the iterator doesn't get broken by removals
    for (auto & sess : list) {
        if (sess->machine() == mach) {
            // This indicates a problem with the Machine class not tracking all of its sessions, for
            // example if there's a duplicate session ID.
            qCritical() << "Day object" << this->date().toString()
                        << "session" << sess->session() << "refers to machine" << mach->serial();
            removeSession(sess);
        }
    }
    
    for (auto & m : machines.keys()) {
        if (machines[m] == mach) {
            // This indicates a problem internal to the Day class, since removeSession should remove
            // machines from this list if there are no longer any sessions pointing to it.
            qCritical() << "Day object" << this->date().toString()
                        << "refers to machine" << mach->serial();
            machines.remove(m);
        }
    }
}

int Day::getCPAPMode()
{
    // NOTE: This needs to return the generic mode, unlike getCPAPModeStr().
    // This function is used only to determine whether to use advanced graphs,
    // which refer to the generic mode.
    /*
    Machine * mach = machine(MT_CPAP);
    if (!mach) return 0;

    CPAPLoader * loader = qobject_cast<CPAPLoader *>(mach->loader());

    ChannelID modechan = loader->CPAPModeChannel();
    */
    ChannelID modechan = CPAP_Mode;

//    schema::Channel & chan = schema::channel[modechan];

    // TODO: This is an awful hack that depends on the enum ordering of the machine-specific CPAP mode.
    // See the comment in getCPAPModeStr().
    int mode = (CPAPMode)(int)qRound(settings_wavg(modechan));

    return mode;
}

QString Day::getCPAPModeStr()
{
    Machine * mach = machine(MT_CPAP);
    if (!mach) return STR_MessageBox_Error;

    CPAPLoader * loader = qobject_cast<CPAPLoader *>(mach->loader());

    ChannelID modechan = loader->CPAPModeChannel();

    schema::Channel & chan = schema::channel[modechan];

    // TODO: This is an awful hack that depends on the enum ordering of the machine-specific CPAP mode.
    // Instead, we should calculate how long each mode was in operation and
    // determine the one that was running the longest, along with the settings
    // while that mode was in operation.
    int mode = (CPAPMode)(int)qRound(settings_wavg(modechan));

    return chan.option(mode);

//    if (mode == MODE_CPAP) {
//        return QObject::tr("Fixed");
//    } else if (mode == MODE_APAP) {
//        return QObject::tr("Auto");
//    } else if (mode == MODE_BILEVEL_FIXED ) {
//        return QObject::tr("Fixed Bi-Level");
//    } else if (mode == MODE_BILEVEL_AUTO_FIXED_PS) {
//        return QObject::tr("Auto Bi-Level (Fixed PS)");
//    } else if (mode == MODE_BILEVEL_AUTO_VARIABLE_PS) {
//        return QObject::tr("Auto Bi-Level (Variable PS)");
//    }  else if (mode == MODE_ASV) {
//        return QObject::tr("ASV Fixed EPAP");
//    } else if (mode == MODE_ASV_VARIABLE_EPAP) {
//        return QObject::tr("ASV Variable EPAP");
//    }
//    return STR_TR_Unknown;
}

QString Day::getPressureRelief()
{
    Machine * mach = machine(MT_CPAP);
    if (!mach) return STR_MessageBox_Error;

    CPAPLoader * loader = qobject_cast<CPAPLoader *>(mach->loader());

    if (!loader) return STR_MessageBox_Error;

    QString pr_str;

    ChannelID pr_level_chan = loader->PresReliefLevel();
    ChannelID pr_mode_chan = loader->PresReliefMode();

    // Separate calculation for SleepStyle machines
    if (mach->info.loadername == "SleepStyle") {
        pr_str = loader->PresReliefLabel();

        int pr_level = -1;
        if (pr_level_chan != NoChannel && settingExists(pr_level_chan)) {
            pr_level = qRound(settings_wavg(pr_level_chan));
        }
        if (pr_level == -1)
            return STR_TR_None;

        if ((pr_mode_chan != NoChannel) && settingExists(pr_mode_chan)) {
            schema::Channel & chan = schema::channel[pr_level_chan];
            QString level = chan.option(pr_level);
            if (level.isEmpty()) {
                level = QString().number(pr_level) + " " + chan.units();
            if (settings_min(pr_level_chan) != settings_max(pr_level_chan))
                level = QObject::tr("varies");
            }
            pr_str += QString(" %1").arg(level);
        }

        return pr_str;
    }

    if ((pr_mode_chan != NoChannel) && settingExists(pr_mode_chan)) {
        // TODO: This is an awful hack that depends on the enum ordering of the pressure relief mode.
        // See the comment in getCPAPModeStr().
        int pr_mode = qRound(settings_wavg(pr_mode_chan));
        pr_str = QObject::tr("%1%2").arg(loader->PresReliefLabel()).arg(schema::channel[pr_mode_chan].option(pr_mode));

        int pr_level = -1;
        if (pr_level_chan != NoChannel && settingExists(pr_level_chan)) {
            pr_level = qRound(settings_wavg(pr_level_chan));
        }

        if (pr_level >= 0) {
            // TODO: Ideally the formatting of LOOKUP datatypes should be done in only one place.
            schema::Channel & chan = schema::channel[pr_level_chan];
            QString level = chan.option(pr_level);
            if (level.isEmpty()) {
                level = QString().number(pr_level) + " " + chan.units();;
            }
            pr_str += QString(" %1").arg(level);
        }
    } else pr_str = STR_TR_None;
    return pr_str;
}

QString Day::validPressure(float pressure) {
    if (fabsf(pressure) == std::numeric_limits<EventDataType>::max())
        return QObject::tr("n/a");
    return QString("%1").arg(pressure, 0, 'f', 1);
}

QString Day::getPressureSettings()
{
    if (machine(MT_CPAP) == nullptr) {
        qCritical("getPressureSettings called with no CPAP machine record");
        return QString();
    }

    // TODO: This is an awful hack that depends on the enum ordering of the generic CPAP_Mode channel.
    // See the comment in getCPAPModeStr().
    CPAPMode mode = (CPAPMode)(int)settings_max(CPAP_Mode);
    QString units = schema::channel[CPAP_Pressure].units();

    if (mode == MODE_CPAP) {
        return QObject::tr("Fixed %1 (%2)").arg(validPressure(settings_min(CPAP_Pressure))).
                                            arg(units);
    } else if (mode == MODE_APAP) {
        return QObject::tr("Min %1 Max %2 (%3)").arg(validPressure(settings_min(CPAP_PressureMin))).
                                                 arg(validPressure(settings_max(CPAP_PressureMax))).
                                                 arg(units);
    } else if (mode == MODE_BILEVEL_FIXED ) {
        return QObject::tr("EPAP %1 IPAP %2 (%3)").arg(validPressure(settings_min(CPAP_EPAP))).
                                                   arg(validPressure(settings_max(CPAP_IPAP))).
                                                   arg(units);
    } else if (mode == MODE_BILEVEL_AUTO_FIXED_PS) {
        return QObject::tr("PS %1 over %2-%3 (%4)").arg(validPressure(settings_max(CPAP_PS))).
                                                    arg(validPressure(settings_min(CPAP_EPAPLo))).
                                                    arg(validPressure(settings_max(CPAP_IPAPHi))).
                                                    arg(units);
    } else if (mode == MODE_BILEVEL_AUTO_VARIABLE_PS) {
        return QObject::tr("Min EPAP %1 Max IPAP %2 PS %3-%4 (%5)").arg(validPressure(settings_min(CPAP_EPAPLo))).
                arg(validPressure(settings_max(CPAP_IPAPHi))).
                arg(validPressure(settings_min(CPAP_PSMin))).
                arg(validPressure(settings_max(CPAP_PSMax))).arg(units);
    } else if (mode == MODE_ASV) {
        return QObject::tr("EPAP %1 PS %2-%3 (%4)").arg(validPressure(settings_min(CPAP_EPAP))).
                arg(validPressure(settings_min(CPAP_PSMin))).
                arg(validPressure(settings_max(CPAP_PSMax))).arg(units);
    } else if (mode == MODE_ASV_VARIABLE_EPAP) {
        return QObject::tr("Min EPAP %1 Max IPAP %2 PS %3-%4 (%5)").
                arg(validPressure(settings_min(CPAP_EPAPLo))).
                arg(validPressure(settings_max(CPAP_IPAPHi))).
                arg(validPressure(settings_max(CPAP_PSMin))).
                arg(validPressure(settings_min(CPAP_PSMax))).
                arg(units);
    } else if (mode == MODE_AVAPS) {
//      qDebug() << "AVAPS: EPAP" << settings_min(CPAP_EPAP) << "IPAP min" << settings_max(CPAP_IPAPLo) <<
//                  "IPAP max" << settings_max(CPAP_IPAPHi);
        QString retStr;
        if (settings_min(CPAP_EPAPLo) == settings_max(CPAP_EPAPHi))
            retStr = QObject::tr("EPAP %1 IPAP %2-%3 (%4)").
                arg(validPressure(settings_min(CPAP_EPAP))).
                arg(validPressure(settings_max(CPAP_IPAPLo))).
                arg(validPressure(settings_max(CPAP_IPAPHi))).
                arg(units);
        else 
            retStr = QObject::tr("EPAP %1-%2 IPAP %3-%4 (%5)").
                arg(validPressure(settings_min(CPAP_EPAPLo))).
                arg(validPressure(settings_min(CPAP_EPAPHi))).
                arg(validPressure(settings_max(CPAP_IPAPLo))).
                arg(validPressure(settings_max(CPAP_IPAPHi))).
                arg(units);

//      qDebug() << "AVAPS mode:" << retStr;
        return retStr;
    }

    return STR_TR_Unknown;
}


EventDataType Day::calc(ChannelID code, ChannelCalcType type)
{
    EventDataType value;

    switch(type) {
    case Calc_Min:
        value = Min(code);
        break;
    case Calc_Middle:
        value = calcMiddle(code);
        break;
    case Calc_Perc:
        value = calcPercentile(code);
        break;
    case Calc_Max:
        value = calcMax(code);
        break;
    case Calc_UpperThresh:
        value = schema::channel[code].upperThreshold();
        break;
    case Calc_LowerThresh:
        value = schema::channel[code].lowerThreshold();
        break;
    case Calc_Zero:
    default:
        value = 0;
        break;
    };
    return value;
}
