/* SleepLib PRS1 Loader Parser Implementation
 *
 * Copyright (c) 2019-2021 The OSCAR Team
 * Portions 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 "prs1_parser.h"
#include "prs1_loader.h"
#include "rawdata.h"


const PRS1ParsedEventType PRS1TidalVolumeEvent::TYPE;
const PRS1ParsedEventType PRS1SnoresAtPressureEvent::TYPE;

const PRS1ParsedEventType PRS1TimedBreathEvent::TYPE;
const PRS1ParsedEventType PRS1ObstructiveApneaEvent::TYPE;
const PRS1ParsedEventType PRS1ClearAirwayEvent::TYPE;
const PRS1ParsedEventType PRS1FlowLimitationEvent::TYPE;
const PRS1ParsedEventType PRS1PeriodicBreathingEvent::TYPE;
const PRS1ParsedEventType PRS1LargeLeakEvent::TYPE;
const PRS1ParsedEventType PRS1VariableBreathingEvent::TYPE;
const PRS1ParsedEventType PRS1HypopneaEvent::TYPE;
const PRS1ParsedEventType PRS1TotalLeakEvent::TYPE;
const PRS1ParsedEventType PRS1LeakEvent::TYPE;
const PRS1ParsedEventType PRS1AutoPressureSetEvent::TYPE;
const PRS1ParsedEventType PRS1PressureSetEvent::TYPE;
const PRS1ParsedEventType PRS1IPAPSetEvent::TYPE;
const PRS1ParsedEventType PRS1EPAPSetEvent::TYPE;
const PRS1ParsedEventType PRS1PressureAverageEvent::TYPE;
const PRS1ParsedEventType PRS1FlexPressureAverageEvent::TYPE;
const PRS1ParsedEventType PRS1IPAPAverageEvent::TYPE;
const PRS1ParsedEventType PRS1IPAPHighEvent::TYPE;
const PRS1ParsedEventType PRS1IPAPLowEvent::TYPE;
const PRS1ParsedEventType PRS1EPAPAverageEvent::TYPE;
const PRS1ParsedEventType PRS1RespiratoryRateEvent::TYPE;
const PRS1ParsedEventType PRS1PatientTriggeredBreathsEvent::TYPE;
const PRS1ParsedEventType PRS1MinuteVentilationEvent::TYPE;
const PRS1ParsedEventType PRS1SnoreEvent::TYPE;
const PRS1ParsedEventType PRS1VibratorySnoreEvent::TYPE;
const PRS1ParsedEventType PRS1PressurePulseEvent::TYPE;
const PRS1ParsedEventType PRS1RERAEvent::TYPE;
const PRS1ParsedEventType PRS1FlowRateEvent::TYPE;
const PRS1ParsedEventType PRS1Test1Event::TYPE;
const PRS1ParsedEventType PRS1Test2Event::TYPE;
const PRS1ParsedEventType PRS1HypopneaCount::TYPE;
const PRS1ParsedEventType PRS1ClearAirwayCount::TYPE;
const PRS1ParsedEventType PRS1ObstructiveApneaCount::TYPE;
//const PRS1ParsedEventType PRS1DisconnectAlarmEvent::TYPE;
const PRS1ParsedEventType PRS1ApneaAlarmEvent::TYPE;
//const PRS1ParsedEventType PRS1LowMinuteVentilationAlarmEvent::TYPE;


//********************************************************************************************
// MARK: Render parsed events as text

static QString hex(int i)
{
    return QString("0x") + QString::number(i, 16).toUpper();
}

#define ENUMSTRING(ENUM) case ENUM: s = QStringLiteral(#ENUM); break
QString PRS1ParsedEvent::typeName() const
{
    PRS1ParsedEventType t = m_type;
    QString s;
    switch (t) {
        ENUMSTRING(EV_PRS1_RAW);
        ENUMSTRING(EV_PRS1_UNKNOWN);
        ENUMSTRING(EV_PRS1_TB);
        ENUMSTRING(EV_PRS1_OA);
        ENUMSTRING(EV_PRS1_CA);
        ENUMSTRING(EV_PRS1_FL);
        ENUMSTRING(EV_PRS1_PB);
        ENUMSTRING(EV_PRS1_LL);
        ENUMSTRING(EV_PRS1_VB);
        ENUMSTRING(EV_PRS1_HY);
        ENUMSTRING(EV_PRS1_OA_COUNT);
        ENUMSTRING(EV_PRS1_CA_COUNT);
        ENUMSTRING(EV_PRS1_HY_COUNT);
        ENUMSTRING(EV_PRS1_TOTLEAK);
        ENUMSTRING(EV_PRS1_LEAK);
        ENUMSTRING(EV_PRS1_AUTO_PRESSURE_SET);
        ENUMSTRING(EV_PRS1_PRESSURE_SET);
        ENUMSTRING(EV_PRS1_IPAP_SET);
        ENUMSTRING(EV_PRS1_EPAP_SET);
        ENUMSTRING(EV_PRS1_PRESSURE_AVG);
        ENUMSTRING(EV_PRS1_FLEX_PRESSURE_AVG);
        ENUMSTRING(EV_PRS1_IPAP_AVG);
        ENUMSTRING(EV_PRS1_IPAPLOW);
        ENUMSTRING(EV_PRS1_IPAPHIGH);
        ENUMSTRING(EV_PRS1_EPAP_AVG);
        ENUMSTRING(EV_PRS1_RR);
        ENUMSTRING(EV_PRS1_PTB);
        ENUMSTRING(EV_PRS1_MV);
        ENUMSTRING(EV_PRS1_TV);
        ENUMSTRING(EV_PRS1_SNORE);
        ENUMSTRING(EV_PRS1_VS);
        ENUMSTRING(EV_PRS1_PP);
        ENUMSTRING(EV_PRS1_RERA);
        ENUMSTRING(EV_PRS1_FLOWRATE);
        ENUMSTRING(EV_PRS1_TEST1);
        ENUMSTRING(EV_PRS1_TEST2);
        ENUMSTRING(EV_PRS1_SETTING);
        ENUMSTRING(EV_PRS1_SLICE);
        ENUMSTRING(EV_PRS1_DISCONNECT_ALARM);
        ENUMSTRING(EV_PRS1_APNEA_ALARM);
        ENUMSTRING(EV_PRS1_LOW_MV_ALARM);
        ENUMSTRING(EV_PRS1_SNORES_AT_PRESSURE);
        ENUMSTRING(EV_PRS1_INTERVAL_BOUNDARY);
        default:
            s = hex(t);
            qDebug() << "Unknown PRS1ParsedEventType type:" << qPrintable(s);
            return s;
    }
    return s.mid(8).toLower();  // lop off initial EV_PRS1_
}

QString PRS1ParsedSettingEvent::settingName() const
{
    PRS1ParsedSettingType t = m_setting;
    QString s;
    switch (t) {
        ENUMSTRING(PRS1_SETTING_CPAP_MODE);
        ENUMSTRING(PRS1_SETTING_AUTO_TRIAL);
        ENUMSTRING(PRS1_SETTING_PRESSURE);
        ENUMSTRING(PRS1_SETTING_PRESSURE_MIN);
        ENUMSTRING(PRS1_SETTING_PRESSURE_MAX);
        ENUMSTRING(PRS1_SETTING_EPAP);
        ENUMSTRING(PRS1_SETTING_EPAP_MIN);
        ENUMSTRING(PRS1_SETTING_EPAP_MAX);
        ENUMSTRING(PRS1_SETTING_IPAP);
        ENUMSTRING(PRS1_SETTING_IPAP_MIN);
        ENUMSTRING(PRS1_SETTING_IPAP_MAX);
        ENUMSTRING(PRS1_SETTING_PS);
        ENUMSTRING(PRS1_SETTING_PS_MIN);
        ENUMSTRING(PRS1_SETTING_PS_MAX);
        ENUMSTRING(PRS1_SETTING_BACKUP_BREATH_MODE);
        ENUMSTRING(PRS1_SETTING_BACKUP_BREATH_RATE);
        ENUMSTRING(PRS1_SETTING_BACKUP_TIMED_INSPIRATION);
        ENUMSTRING(PRS1_SETTING_TIDAL_VOLUME);
        ENUMSTRING(PRS1_SETTING_EZ_START);
        ENUMSTRING(PRS1_SETTING_FLEX_LOCK);
        ENUMSTRING(PRS1_SETTING_FLEX_MODE);
        ENUMSTRING(PRS1_SETTING_FLEX_LEVEL);
        ENUMSTRING(PRS1_SETTING_RISE_TIME);
        ENUMSTRING(PRS1_SETTING_RISE_TIME_LOCK);
        ENUMSTRING(PRS1_SETTING_RAMP_TYPE);
        ENUMSTRING(PRS1_SETTING_RAMP_TIME);
        ENUMSTRING(PRS1_SETTING_RAMP_PRESSURE);
        ENUMSTRING(PRS1_SETTING_HUMID_STATUS);
        ENUMSTRING(PRS1_SETTING_HUMID_MODE);
        ENUMSTRING(PRS1_SETTING_HEATED_TUBE_TEMP);
        ENUMSTRING(PRS1_SETTING_HUMID_LEVEL);
        ENUMSTRING(PRS1_SETTING_HUMID_TARGET_TIME);
        ENUMSTRING(PRS1_SETTING_MASK_RESIST_LOCK);
        ENUMSTRING(PRS1_SETTING_MASK_RESIST_SETTING);
        ENUMSTRING(PRS1_SETTING_HOSE_DIAMETER);
        ENUMSTRING(PRS1_SETTING_TUBING_LOCK);
        ENUMSTRING(PRS1_SETTING_AUTO_ON);
        ENUMSTRING(PRS1_SETTING_AUTO_OFF);
        ENUMSTRING(PRS1_SETTING_APNEA_ALARM);
        ENUMSTRING(PRS1_SETTING_DISCONNECT_ALARM);
        ENUMSTRING(PRS1_SETTING_LOW_MV_ALARM);
        ENUMSTRING(PRS1_SETTING_LOW_TV_ALARM);
        ENUMSTRING(PRS1_SETTING_MASK_ALERT);
        ENUMSTRING(PRS1_SETTING_SHOW_AHI);
        default:
            s = hex(t);
            qDebug() << "Unknown PRS1ParsedSettingType type:" << qPrintable(s);
            return s;
    }
    return s.mid(13).toLower();  // lop off initial PRS1_SETTING_
}

QString PRS1ParsedSettingEvent::modeName() const
{
    int m = value();
    QString s;
    switch ((PRS1Mode) m) {
        ENUMSTRING(PRS1_MODE_UNKNOWN);  // TODO: Remove this when all the parsers are complete.
        ENUMSTRING(PRS1_MODE_CPAP);
        ENUMSTRING(PRS1_MODE_CPAPCHECK);
        ENUMSTRING(PRS1_MODE_AUTOTRIAL);
        ENUMSTRING(PRS1_MODE_AUTOCPAP);
        ENUMSTRING(PRS1_MODE_BILEVEL);
        ENUMSTRING(PRS1_MODE_AUTOBILEVEL);
        ENUMSTRING(PRS1_MODE_ASV);
        ENUMSTRING(PRS1_MODE_S);
        ENUMSTRING(PRS1_MODE_ST);
        ENUMSTRING(PRS1_MODE_PC);
        ENUMSTRING(PRS1_MODE_ST_AVAPS);
        ENUMSTRING(PRS1_MODE_PC_AVAPS);
        default:
            s = hex(m);
            qDebug() << "Unknown PRS1Mode:" << qPrintable(s);
            return s;
    }
    return s.mid(10).toLower();  // lop off initial PRS1_MODE_
}

QString PRS1ParsedEvent::timeStr(int t)
{
    int h = t / 3600;
    int m = (t - (h * 3600)) / 60;
    int s = t % 60;
#if 1
    // Optimized after profiling regression tests.
    return QString::asprintf("%02d:%02d:%02d", h, m, s);
#else
    // Unoptimized original, slows down regression tests.
    return QString("%1:%2:%3").arg(h, 2, 10, QChar('0')).arg(m, 2, 10, QChar('0')).arg(s, 2, 10, QChar('0'));
#endif
}

static QString byteList(QByteArray data, int limit=-1)
{
    int count = data.size();
    if (limit == -1 || limit > count) limit = count;
    QStringList l;
    for (int i = 0; i < limit; i++) {
        l.push_back(QString( "%1" ).arg((int) data[i] & 0xFF, 2, 16, QChar('0') ).toUpper());
    }
    if (limit < count) l.push_back("...");
    QString s = l.join(" ");
    return s;
}


QMap<QString,QString> PRS1IntervalBoundaryEvent::contents(void)
{
    QMap<QString,QString> out;
    out["start"] = timeStr(m_start);
    return out;
}


QMap<QString,QString> PRS1ParsedDurationEvent::contents(void)
{
    QMap<QString,QString> out;
    out["start"] = timeStr(m_start);
    out["duration"] = timeStr(m_duration);
    return out;
}


QMap<QString,QString> PRS1ParsedValueEvent::contents(void)
{
    QMap<QString,QString> out;
    out["start"] = timeStr(m_start);
    out["value"] = QString::number(value());
    return out;
}


QMap<QString,QString> PRS1UnknownDataEvent::contents(void)
{
    QMap<QString,QString> out;
    out["pos"] = QString::number(m_pos);
    out["data"] = byteList(m_data);
    return out;
}


QMap<QString,QString> PRS1ParsedSettingEvent::contents(void)
{
    QMap<QString,QString> out;
    QString v;
    if (m_setting == PRS1_SETTING_CPAP_MODE) {
        v = modeName();
    } else {
        v = QString::number(value());
    }
    out[settingName()] = v;
    return out;
}


QMap<QString,QString> PRS1ParsedSliceEvent::contents(void)
{
    QMap<QString,QString> out;
    out["start"] = timeStr(m_start);
    QString s;
    switch ((SliceStatus) m_value) {
        case MaskOn: s = "MaskOn"; break;
        case MaskOff: s = "MaskOff"; break;
        case EquipmentOff: s = "EquipmentOff"; break;
        case UnknownStatus: s = "Unknown"; break;
    }
    out["status"] = s;
    return out;
}


QMap<QString,QString> PRS1ParsedAlarmEvent::contents(void)
{
    QMap<QString,QString> out;
    out["start"] = timeStr(m_start);
    return out;
}


QMap<QString,QString> PRS1SnoresAtPressureEvent::contents(void)
{
    QString label;
    switch (m_kind) {
        case 0: label = "pressure"; break;
        case 1: label = "epap"; break;
        case 2: label = "ipap"; break;
        default: label = "unknown_pressure"; break;
    }
    
    QMap<QString,QString> out;
    out["start"] = timeStr(m_start);
    out[label] = QString::number(value());
    out["count"] = QString::number(m_count);
    return out;
}


//********************************************************************************************
// MARK: -
// MARK: Parse chunk contents

bool PRS1DataChunk::ParseCompliance(void)
{
    switch (this->family) {
    case 0:
        switch (this->familyVersion) {
        case 2:
        case 3:
            return this->ParseComplianceF0V23();
        case 4:
            return this->ParseComplianceF0V4();
        case 5:
            return this->ParseComplianceF0V5();
        case 6:
            return this->ParseComplianceF0V6();
        }
    default:
        ;
    }

    qWarning() << "unexpected compliance family" << this->family << "familyVersion" << this->familyVersion;
    return false;
}


bool PRS1DataChunk::ParseSummary()
{
    switch (this->family) {
    case 0:
        if (this->familyVersion == 6) {
            return this->ParseSummaryF0V6();
        } else if (this->familyVersion == 4) {
            return this->ParseSummaryF0V4();
        } else {
            return this->ParseSummaryF0V23();
        }
    case 3:
        switch (this->familyVersion) {
            case 0: return this->ParseSummaryF3V03();
            case 3: return this->ParseSummaryF3V03();
            case 6: return this->ParseSummaryF3V6();
        }
        break;
    case 5:
        if (this->familyVersion == 1) {
            return this->ParseSummaryF5V012();
        } else if (this->familyVersion == 0) {
            return this->ParseSummaryF5V012();
        } else if (this->familyVersion == 2) {
            return this->ParseSummaryF5V012();
        } else if (this->familyVersion == 3) {
            return this->ParseSummaryF5V3();
        }
    default:
        ;
    }

    qWarning() << "unexpected family" << this->family << "familyVersion" << this->familyVersion;
    return false;
}


// TODO: The nested switch statement below just begs for per-version subclasses.
bool PRS1DataChunk::ParseEvents()
{
    bool ok = false;
    switch (this->family) {
        case 0:
            switch (this->familyVersion) {
                case 2: ok = this->ParseEventsF0V23(); break;
                case 3: ok = this->ParseEventsF0V23(); break;
                case 4: ok = this->ParseEventsF0V4(); break;
                case 6: ok = this->ParseEventsF0V6(); break;
            }
            break;
        case 3:
            switch (this->familyVersion) {
                case 0: ok = this->ParseEventsF3V03(); break;
                case 3: ok = this->ParseEventsF3V03(); break;
                case 6: ok = this->ParseEventsF3V6(); break;
            }
            break;
        case 5:
            switch (this->familyVersion) {
                case 0: ok = this->ParseEventsF5V0(); break;
                case 1: ok = this->ParseEventsF5V1(); break;
                case 2: ok = this->ParseEventsF5V2(); break;
                case 3: ok = this->ParseEventsF5V3(); break;
            }
            break;
        default:
            qDebug() << "Unknown PRS1 family" << this->family << "familyVersion" << this->familyVersion;
    }
    return ok;
}


// TODO: This really should be in some kind of class hierarchy, once we figure out
// the right one.
const QVector<PRS1ParsedEventType> & GetSupportedEvents(const PRS1DataChunk* chunk)
{
    static const QVector<PRS1ParsedEventType> none;
    
    switch (chunk->family) {
        case 0:
            switch (chunk->familyVersion) {
                case 2: return ParsedEventsF0V23; break;
                case 3: return ParsedEventsF0V23; break;
                case 4: return ParsedEventsF0V4; break;
                case 6: return ParsedEventsF0V6; break;
            }
            break;
        case 3:
            switch (chunk->familyVersion) {
                case 0: return ParsedEventsF3V0; break;
                case 3: return ParsedEventsF3V3; break;
                case 6: return ParsedEventsF3V6; break;
            }
            break;
        case 5:
            switch (chunk->familyVersion) {
                case 0: return ParsedEventsF5V0; break;
                case 1: return ParsedEventsF5V1; break;
                case 2: return ParsedEventsF5V2; break;
                case 3: return ParsedEventsF5V3; break;
            }
            break;
    }
    qWarning() << "Missing supported event list for family" << chunk->family << "version" << chunk->familyVersion;
    return none;
}


QString PRS1DataChunk::DumpEvent(int t, int code, const unsigned char* data, int size)
{
    int s = t;
    int h = s / 3600; s -= h * 3600;
    int m = s / 60; s -= m * 60;
    QString dump = QString("%1:%2:%3 ")
        .arg(h, 2, 10, QChar('0'))
        .arg(m, 2, 10, QChar('0'))
        .arg(s, 2, 10, QChar('0'));
    dump = dump + " " + hex(code) + ":";
    for (int i = 0; i < size; i++) {
        dump = dump + QString(" %1").arg(data[i]);
    }
    return dump;
}


void PRS1DataChunk::AddEvent(PRS1ParsedEvent* const event)
{
    m_parsedData.push_back(event);
}



//********************************************************************************************
// MARK: -
// MARK: Parse settings shared by multiple families

// Humid F0V2 confirmed
// 0x00 = Off (presumably no humidifier present)
// 0x80 = Off
// 0x81 = 1
// 0x82 = 2
// 0x83 = 3
// 0x84 = 4
// 0x85 = 5

// Humid F3V0 confirmed
// 0x03 = 3 (but no humidification shown on hours of usage chart)
// 0x04 = 4 (but no humidification shown on hours of usage chart)
// 0x80 = Off
// 0x81 = 1
// 0x82 = 2
// 0x83 = 3
// 0x84 = 4
// 0x85 = 5

// Humid F5V0 confirmed
// 0x00 = Off (presumably no humidifier present)
// 0x80 = Off
// 0x81 = 1, bypass = no
// 0x82 = 2, bypass = no
// 0x83 = 3, bypass = no
// 0x84 = 4, bypass = no
// 0x85 = 5, bypass = no
// 0xA0 = Off, bypass = yes

void PRS1DataChunk::ParseHumidifierSetting50Series(int humid, bool add_setting)
{
    if (humid & (0x40 | 0x10 | 0x08)) UNEXPECTED_VALUE(humid, "known bits");
    if (humid & 0x20) {
        if (this->family == 5) {
            CHECK_VALUE(humid, 0xA0);  // only example of bypass set, unsure whether it can appear otherwise
        } else {
            CHECK_VALUE(humid & 0x20, 0);  // only ever seen on 950P, where "Bypass System One humidification" is "Yes"
        }
    }
    
    bool humidifier_present = ((humid & 0x80) != 0);  // humidifier connected
    int humidlevel = humid & 7;  // humidification level

    HumidMode humidmode = HUMID_Fixed;  // 50-Series didn't have adaptive or heated tube humidification
    if (add_setting) {
        this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HUMID_STATUS, humidifier_present));
        if (humidifier_present) {
            this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HUMID_MODE, humidmode));
            this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HUMID_LEVEL, humidlevel));
        }
    }

    // Check for truly unexpected values:
    if (humidlevel > 5) UNEXPECTED_VALUE(humidlevel, "<= 5");
    //if (!humidifier_present) CHECK_VALUES(humidlevel, 0, 1);  // Some machines appear to encode the humidlevel setting even when the humidifier is not present.
}




// F0V4 confirmed:
// B3 0A = HT=5, H=3, HT
// A3 0A = HT=5, H=2, HT
// 33 0A = HT=4, H=3, HT
// 23 4A = HT=4, H=2, HT
// B3 09 = HT=3, H=3, HT
// A4 09 = HT=3, H=2, HT
// A3 49 = HT=3, H=2, HT
// 22 09 = HT=2, H=2, HT
// 33 09 = HT=2, H=3, HT
// 21 09 = HT=2, H=2, HT
// 13 09 = HT=2, H=1, HT
// B5 08 = HT=1, H=3, HT
// 03 08 = HT=off,    HT; data=tube t=0,h=0
// 05 24 =       H=5, S1
// 95 06 =       H=5, S1
// 95 05 =       H=5, S1
// 94 05 =       H=4, S1
// 04 24 =       H=4, S1
// A3 05 =       H=3, S1
// 92 05 =       H=2, S1
// A2 05 =       H=2, S1
// 01 24 =       H=1, S1
// 90 05 =       H=off, S1
// 30 05 =       H=off, S1
// 95 41 =       H=5, Classic
// A4 61 =       H=4, Classic
// A3 61 =       H=3, Classic
// A2 61 =       H=2, Classic
// A1 61 =       H=1, Classic
// 90 41 =       H=Off, Classic; data=classic h=0
// 94 11 =       H=3, S1, no data [note that bits encode H=4, so no data falls back to H=3]
// 93 11 =       H=3, S1, no data
// 04 30 =       H=3, S1, no data

// F0V5 confirmed:
// 00 60 =       H=Off, Classic
// 02 60 =       H=2, Classic
// 05 60 =       H=5, Classic
// 00 70 =       H=Off, no data in chart

// F5V1 confirmed:
// A0 4A = HT=5, H=2, HT
// B1 09 = HT=3, H=3, HT
// 91 09 = HT=3, H=1, HT
// 32 09 = HT=2, H=3, HT
// B2 08 = HT=1, H=3, HT
// 00 48 = HT=off, data=tube t=0,h=0
// 95 05 =       H=5, S1
// 94 05 =       H=4, S1
// 93 05 =       H=3, S1
// 92 05 =       H=2, S1
// 91 05 =       H=1, S1
// 90 05 =       H=Off, S1
// 95 41 =       H=5, Classic
// 94 41 =       H=4, Classic
// 93 41 =       H=3, Classic
// 92 41 =       H=2, Classic
// 01 60 =       H=1, Classic
// 00 60 =       H=Off, Classic
// 00 70 =       H=3, S1, no data [no data ignores Classic mode, H bits, falls back to S1 H=3]

// F5V2 confirmed:
// 00 48 = HT=off, data=tube t=0,h=0
// 93 09 = HT=3, H=1, HT
// 00 10 =       H=3, S1, no data

// XX XX = 60-Series Humidifier bytes
//  7    = humidity level without tube [on tube disconnect / system one with 22mm hose / classic]  :  0 = humidifier off
//  8    = [never seen]
// 3     = humidity level with tube
// 4     = maybe part of humidity level? [never seen]
// 8   3 = tube temperature (high bit of humid 1 is low bit of temp)
//     4 = "System One" mode (valid even when humidifier is off)
//     8 = heated tube present
//    10 = no data in chart, maybe no humidifier attached? Seems to fall back on System One = 3 despite other (humidity level and S1) bits.
//    20 = unknown, something tube related since whenever it's set tubepresent is false
//    40 = "Classic" mode (valid even when humidifier is off, ignored when heated tube is present)
//    80 = [never seen]

void PRS1DataChunk::ParseHumidifierSetting60Series(unsigned char humid1, unsigned char humid2, bool add_setting)
{
    int humidlevel = humid1 & 7;  // Ignored when heated tube is present: humidifier setting on tube disconnect is always reported as 3
    if (humidlevel > 5) UNEXPECTED_VALUE(humidlevel, "<= 5");
    CHECK_VALUE(humid1 & 8, 0);  // never seen
    int tubehumidlevel = (humid1 >> 4) & 7;  // This mask is a best guess based on other masks.
    if (tubehumidlevel > 5) UNEXPECTED_VALUE(tubehumidlevel, "<= 5");
    CHECK_VALUE(tubehumidlevel & 4, 0);  // never seen, but would clarify whether above mask is correct

    int tubetemp = (humid1 >> 7) | ((humid2 & 3) << 1);
    if (tubetemp > 5) UNEXPECTED_VALUE(tubetemp, "<= 5");

    CHECK_VALUE(humid2 & 0x80, 0);  // never seen
    bool humidclassic = (humid2 & 0x40) != 0;  // Set on classic mode reports; evidently ignored (sometimes set!) when tube is present
    //bool no_tube? = (humid2 & 0x20) != 0;  // Something tube related: whenever it is set, tube is never present (inverse is not true)
    bool no_data = (humid2 & 0x10) != 0;  // As described in chart, settings still show up
    int tubepresent = (humid2 & 0x08) != 0;
    bool humidsystemone = (humid2 & 0x04) != 0;  // Set on "System One" humidification mode reports when tubepresent is false
    if (humidsystemone && tubepresent) {
        // On a 560P, we've observed a spurious tubepresent bit being set during two sessions.
        // Those sessions (and the ones that followed) used a 22mm hose.
        CHECK_VALUE(add_setting, false);  // We've only seen this appear during a session, not in the initial settings.
        tubepresent = false;
    }

    // When no_data, reports always say "System One" with humidity level 3, regardless of humidlevel and humidsystemone

    if (humidsystemone + tubepresent + no_data == 0) CHECK_VALUE(humidclassic, true);  // Always set when everything else is off
    if (humidsystemone + tubepresent + no_data > 1) UNEXPECTED_VALUE(humid2, "one bit set");  // Only one of these ever seems to be set at a time
    if (tubepresent && tubetemp == 0) CHECK_VALUE(tubehumidlevel, 0);  // When the heated tube is off, tube humidity seems to be 0
    
    if (tubepresent) humidclassic = false;  // Classic mode bit is evidently ignored when tube is present
    if (no_data) humidclassic = false;  // Classic mode bit is evidently ignored when tube is present

    //qWarning() << this->sessionid << (humidclassic ? "C" : ".") << (humid2 & 0x20 ? "?" : ".") << (tubepresent ? "T" : ".") << (no_data ? "X" : ".") << (humidsystemone ? "1" : ".");
    /*
    if (tubepresent) {
        if (tubetemp) {
            qWarning() << this->sessionid << "tube temp" << tubetemp << "tube humidity" << tubehumidlevel << (humidclassic ? "classic" : "systemone") << "humidity" << humidlevel;
        } else {
            qWarning() << this->sessionid << "heated tube off" << (humidclassic ? "classic" : "systemone") << "humidity" << humidlevel;
        }
    } else {
        qWarning() << this->sessionid << (humidclassic ? "classic" : "systemone") << "humidity" << humidlevel;
    }
    */
    HumidMode humidmode = HUMID_Fixed;
    if (tubepresent) {
        humidmode = HUMID_HeatedTube;
    } else {
        if (humidsystemone + humidclassic > 1) UNEXPECTED_VALUE(humid2, "fixed or adaptive");
        if (humidsystemone) humidmode = HUMID_Adaptive;
    }

    if (add_setting) {
        bool humidifier_present = (no_data == 0);
        this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HUMID_STATUS, humidifier_present));
        if (humidifier_present) {
            this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HUMID_MODE, humidmode));
            if (humidmode == HUMID_HeatedTube) {
                this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HEATED_TUBE_TEMP, tubetemp));
                this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HUMID_LEVEL, tubehumidlevel));
            } else {
                this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HUMID_LEVEL, humidlevel));
            }
        }
    }

    // Check for previously unseen data that we expect to be normal:
    if (this->family == 0) {
        // F0V4
        if (tubetemp && (tubehumidlevel < 1 || tubehumidlevel > 3)) UNEXPECTED_VALUE(tubehumidlevel, "1-3");
    } else if (this->familyVersion == 1) {
        // F5V1
        if (tubepresent) {
            // all tube temperatures seen
            if (tubetemp) {
                if (tubehumidlevel == 0 || tubehumidlevel > 3) UNEXPECTED_VALUE(tubehumidlevel, "1-3");
            }
        }
    } else if (this->familyVersion == 2) {
        // F5V2
        if (tubepresent) {
            CHECK_VALUES(tubetemp, 0, 3);
            if (tubetemp) {
                CHECK_VALUE(tubehumidlevel, 1);
            }
        }
        CHECK_VALUE(humidsystemone, false);
        CHECK_VALUE(humidclassic, false);
    }
}


// F0V6 confirmed
// 90 B0 = HT=3!,H=3!,data=none [no humidifier appears to ignore HT and H bits and show HT=3,H=3 in details]
// 8C 6C = HT=3, H=3, data=none
// 80 00 = nothing listed in details, data=none, only seen on 400G and 502G
// 54 B4 = HT=5, H=5, data=tube
// 50 90 = HT=4, H=4, data=tube
// 4C 6C = HT=3, H=3, data=tube
// 48 68 = HT=3, H=2, data=tube
// 40 60 = HT=3, H=Off, data=tube t=3,h=0
// 50 50 = HT=2, H=4, data=tube
// 4C 4C = HT=2, H=3, data=tube
// 50 30 = HT=1, H=4, data=tube
// 4C 0C = HT=off, H=3, data=tube t=0,h=3
// 34 74 = HT=3, H=5, data=adaptive (5)
// 50 B0 = HT=5, H=4, adaptive
// 30 B0 = HT=3, H=4, data=adaptive (4)
// 30 50 = HT=3, H=4, data=adaptive (4)
// 30 10 = HT=3!,H=4, data=adaptive (4) [adaptive mode appears to ignore HT bits and show HT=3 in details]
// 30 70 = HT=3, H=4, data=adaptive (4)
// 2C 6C = HT=3, H=3, data=adaptive (3)
// 28 08 =       H=2, data=adaptive (2), no details (400G)
// 28 48 = HT=3!,H=2, data=adaptive (2) [adaptive mode appears to ignore HT bits and show HT=3 in details]
// 28 68 = HT=3, H=2, data=adaptive (2)
// 24 64 = HT=3, H=1, data=adaptive (1)
// 20 60 = HT=3, H=off, data=adaptive (0)
// 14 74 = HT=3, H=5, data=fixed (5)
// 10 70 = HT=3, H=4, data=fixed (4)
// 0C 6C = HT=3, H=3, data=fixed (3)
// 08 48 = HT=3, H=2, data=fixed (2)
// 08 68 = HT=3, H=2, data=fixed (2)
// 04 64 = HT=3, H=1, data=fixed (1)
// 00 00 = HT=3, H=off, data=fixed (0)

// F5V3 confirmed:
// 90 70 = HT=3, H=3, adaptive, data=no data
// 54 14 = HT=Off, H=5, adaptive, data=tube t=0,h=5
// 54 34 = HT=1, H=5, adaptive, data=tube t=1,h=5
// 50 70 = HT=3, H=4, adaptive, data=tube t=3,h=4
// 4C 6C = HT=3, H=3, adaptive, data=tube t=3,h=3
// 4C 4C = HT=2, H=3, adaptive, data=tube t=2,h=3
// 4C 2C = HT=1, H=3, adaptive, data=tube t=1,h=3
// 4C 0C = HT=off, H=3, adaptive, data=tube t=0,h=3
// 48 08 = HT=off, H=2, adaptive, data=tube t=0,h=2
// 44 04 = HT=off, H=1, adaptive, data=tube t=0,h=1
// 40 00 = HT=off,H=off, adaptive, data=tube t=0,h=0
// 34 74 = HT=3, H=5, adaptive, data=s1 (5)
// 30 70 = HT=3, H=4, adaptive, data=s1 (4)
// 2C 6C = HT=3, H=3, adaptive, data=s1 (3)
// 28 68 = HT=3, H=2, adaptive, data=s1 (2)
// 24 64 = HT=3, H=1, adaptive, data=s1 (1)

// F3V6 confirmed:
// 84 24 = HT=3, H=3, disconnect=adaptive, data=no data
// 50 90 = HT=4, H=4, disconnect=adaptive, data=tube t=4,h=4
// 44 84 = HT=4, H=1, disconnect=adaptive, data=tube t=4,h=1
// 40 80 = HT=4, H=Off,disconnect=adaptive, data=tube t=4,h=0
// 4C 6C = HT=3, H=3, disconnect=adaptive, data=tube t=3,h=3
// 48 68 = HT=3, H=2, disconnect=adaptive, data=tube t=3,h=2
// 44 44 = HT=2, H=1, disconnect=adaptive, data=tube t=2,h=1
// 48 28 = HT=1, H=2, disconnect=adaptive, data=tube t=1,h=2
// 54 14 = HT=Off,H=5, disconnect=adaptive data=tube t=0,h=5
// 34 14 = HT=3, H=5, disconnect=adaptive, data=s1 (5)
// 30 70 = HT=3, H=4, disconnect=adaptive, data=s1 (4)
// 2C 6C = HT=3, H=3, disconnect=adaptive, data=s1 (3)
// 28 08 = HT=3, H=2, disconnect=adaptive, data=s1 (2)
// 20 20 = HT=3, H=Off, disconnect=adaptive, data=s1 (0)
// 14 14 = HT=3, H=3, disconnect=fixed, data=classic (5)
// 10 10 = HT=3, H=4, disconnect=fixed, data=classic (4) [fixed mode appears to ignore HT bits and show HT=3 in details]
// 0C 0C = HT=3, H=3, disconnect=fixed, data=classic (3)
// 08 08 = HT=3, H=2, disconnect=fixed, data=classic (2)
// 04 64 = HT=3, H=1, disconnect=fixed, data=classic (1)

// The data is consistent among all fileVersion 3 models: F0V6, F5V3, F3V6.
//
// NOTE: F5V3 and F3V6 charts report the "Adaptive" setting as "System One" and the "Fixed"
// setting as "Classic", despite labeling the settings "Adaptive" and "Fixed" just like F0V6.
// F0V6 is consistent and labels both settings and chart as "Adaptive" and "Fixed".
//
// 400G and 502G appear to omit the humidifier settings in their details, though they
// do support humidifiers, and will show the humidification in the charts.

void PRS1DataChunk::ParseHumidifierSettingV3(unsigned char byte1, unsigned char byte2, bool add_setting)
{
    bool humidifier_present = true;
    bool humidfixed = false;  // formerly called "Classic"
    bool humidadaptive = false;  // formerly called "System One"
    bool tubepresent = false;
    bool passover = false;
    bool error = false;

    // Byte 1: 0x90 (no humidifier data), 0x50 (15ht, tube 4/5, humid 4), 0x54 (15ht, tube 5, humid 5) 0x4c (15ht, tube temp 3, humidifier 3)
    // 0x0c (15, tube 3, humid 3, fixed)
    // 0b1001 0000 no humidifier data
    // 0b0101 0000 tube 4 and 5, humidifier 4
    // 0b0101 0100 15ht, tube 5, humidifier 5
    // 0b0100 1100 15ht, tube 3, humidifier 3
    // 0b1011 0000 15, tube 3, humidifier 3, "Error" on humidification chart with asterisk at 4
    // 0b0111 0000 15, tube 3, humidifier 3, "Passover" on humidification chart with notch at 4
    //   842       = humidifier status
    //      1 84   = humidifier setting
    //          ??
    CHECK_VALUE(byte1 & 3, 0);
    int humid = byte1 >> 5;
    switch (humid) {
    case 0: humidfixed = true; break;  // fixed, ignores tubetemp bits and reports tubetemp=3
    case 1: humidadaptive = true; break;  // adaptive, ignores tubetemp bits and reports tubetemp=3
    case 2: tubepresent = true; break;  // heated tube
    case 3: passover = true; break;  // passover mode (only visible in chart)
    case 4: humidifier_present = false; break;  // no humidifier, reports tubetemp=3 and humidlevel=3
    case 5: error = true; break;  // "Error" in humidification chart, reports tubetemp=3 and humidlevel=3 in settings
    default:
        UNEXPECTED_VALUE(humid, "known value");
        break;
    }
    int humidlevel = (byte1 >> 2) & 7;

    // Byte 2: 0xB4 (15ht, tube 5, humid 5), 0xB0 (15ht, tube 5, humid 4), 0x90 (tube 4, humid 4), 0x6C (15ht, tube temp 3, humidifier 3)
    // 0x80?
    // 0b1011 0100 15ht, tube 5, humidifier 5
    // 0b1011 0000 15ht, tube 5, humidifier 4
    // 0b1001 0000 tube 4, humidifier 4
    // 0b0110 1100 15ht, tube 3, humidifier 3
    //   842       = tube temperature
    //      1 84   = humidity level when using heated tube, thus far always identical to humidlevel
    //          ??
    CHECK_VALUE(byte2 & 3, 0);
    int tubehumidlevel = (byte2 >> 2) & 7;
    CHECK_VALUE(humidlevel, tubehumidlevel);  // thus far always the same
    int tubetemp = (byte2 >> 5) & 7;
    if (humidifier_present) {
        if (humidlevel > 5 || humidlevel < 0) UNEXPECTED_VALUE(humidlevel, "0-5");  // 0=off is valid when a humidifier is attached
        if (humid == 2) {  // heated tube
            if (tubetemp > 5 || tubetemp < 0) UNEXPECTED_VALUE(tubetemp, "0-5");  // TODO: maybe this is only if heated tube? 0=off is valid even in heated tube mode
        }
    }

    // TODO: move this up into the switch statement above, given how many modes there now are.
    HumidMode humidmode = HUMID_Fixed;
    if (tubepresent) {
        humidmode = HUMID_HeatedTube;
    } else if (humidadaptive) {
        humidmode = HUMID_Adaptive;
    } else if (passover) {
        humidmode = HUMID_Passover;
    } else if (error) {
        humidmode = HUMID_Error;
    }

    if (add_setting) {
        this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HUMID_STATUS, humidifier_present));
        if (humidifier_present) {
            this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HUMID_MODE, humidmode));
            if (humidmode == HUMID_HeatedTube) {
                this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HEATED_TUBE_TEMP, tubetemp));
                this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HUMID_LEVEL, tubehumidlevel));
            } else {
                this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HUMID_LEVEL, humidlevel));
            }
        }
    }

    // Check for previously unseen data that we expect to be normal:
    if (family == 0) {
        // All variations seen.
    } else if (family == 5) {
        if (tubepresent) {
            // All tube temperature and humidity levels seen.
        } else if (humidadaptive) {
            // All humidity levels seen.
        } else if (humidfixed) {
            if (humidlevel < 3) UNEXPECTED_VALUE(humidlevel, "3-5");
        }
    } else if (family == 3) {
        if (tubepresent) {
            // All tube temperature and humidity levels seen.
        } else if (humidadaptive) {
            // All humidity levels seen.
        } else if (humidfixed) {
            // All humidity levels seen.
        }
    }
}


void PRS1DataChunk::ParseTubingTypeV3(unsigned char type)
{
    int diam;
    switch (type) {
    case 0: diam = 22; break;
    case 1: diam = 15; break;
    case 2: diam = 15; break;  // 15HT, though the reports only say "15" for DreamStation models
    case 3: diam = 12; break;  // seen on DreamStation Go models
    default:
        UNEXPECTED_VALUE(type, "known tubing type");
        return;
    }
    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HOSE_DIAMETER, diam));
}


//********************************************************************************************
// MARK: -
// MARK: Parse and verify chunk from stream

typedef quint16 crc16_t;
typedef quint32 crc32_t;
static crc16_t CRC16(unsigned char * data, size_t data_len, crc16_t crc=0);
static crc32_t CRC32(const unsigned char *data, size_t data_len, crc32_t crc=0xffffffffU);
static crc32_t CRC32wchar(const unsigned char *data, size_t data_len, crc32_t crc=0xffffffffU);

PRS1DataChunk::PRS1DataChunk(RawDataDevice & f, PRS1Loader* in_loader) : loader(in_loader)
{
    m_path = f.name();
}

PRS1DataChunk::~PRS1DataChunk()
{
    for (int i=0; i < m_parsedData.count(); i++) {
        PRS1ParsedEvent* e = m_parsedData.at(i);
        delete e;
    }
}


PRS1DataChunk* PRS1DataChunk::ParseNext(RawDataDevice & f, PRS1Loader* loader)
{
    PRS1DataChunk* out_chunk = nullptr;
    PRS1DataChunk* chunk = new PRS1DataChunk(f, loader);

    do {
        // Parse the header and calculate its checksum.
        bool ok = chunk->ReadHeader(f);
        if (!ok) {
            break;
        }

        // Make sure the calculated checksum matches the stored checksum.
        if (chunk->calcChecksum != chunk->storedChecksum) {
            qWarning() << chunk->m_path << "header checksum calc" << chunk->calcChecksum << "!= stored" << chunk->storedChecksum;
            break;
        }

        // Read the block's data and calculate the block CRC.
        ok = chunk->ReadData(f);
        if (!ok) {
            break;
        }
        
        // Make sure the calculated CRC over the entire chunk (header and data) matches the stored CRC.
        if (chunk->calcCrc != chunk->storedCrc) {
            // Corrupt data block, warn about it.
            qWarning() << chunk->m_path << "@" << chunk->m_filepos << "block CRC calc" << hex << chunk->calcCrc << "!= stored" << hex << chunk->storedCrc;
            
            // TODO: When this happens, it's usually because the chunk was truncated and another chunk header
            // exists within the blockSize bytes. In theory it should be possible to rewing and resync by
            // looking for another chunk header with the same fileVersion, htype, family, familyVersion, and
            // ext (blockSize and other fields could vary).
            //
            // But this is quite rare, so for now we bail on the rest of the file.
            break;
        }

        // Only return the chunk if it has passed all tests above.
        out_chunk = chunk;
    } while (false);

    if (out_chunk == nullptr) delete chunk;
    return out_chunk;
}


bool PRS1DataChunk::ReadHeader(RawDataDevice & f)
{
    bool ok = false;
    do {
        // Read common header fields.
        this->m_filepos = f.pos();
        this->m_header = f.read(15);
        if (this->m_header.size() != 15) {
            if (this->m_header.size() == 0) {
                qWarning() << this->m_path << "empty, skipping";
            } else {
                qWarning() << this->m_path << "file too short?";
            }
            break;
        }
        
        unsigned char * header = (unsigned char *)this->m_header.data();
        this->fileVersion = header[0];    // Correlates to DataFileVersion in PROP[erties].TXT, only 2 or 3 has ever been observed
        this->blockSize = (header[2] << 8) | header[1];
        this->htype = header[3];      // 00 = normal, 01=waveform
        this->family = header[4];
        this->familyVersion = header[5];
        this->ext = header[6];
        this->sessionid = (header[10] << 24) | (header[9] << 16) | (header[8] << 8) | header[7];
        this->timestamp = (header[14] << 24) | (header[13] << 16) | (header[12] << 8) | header[11];

        // Do a few early sanity checks before any variable-length header data.
        if (this->blockSize == 0) {
            qWarning() << this->m_path << "@" << hex << this->m_filepos << "blocksize 0, skipping remainder of file";
            break;
        }
        if (this->fileVersion < 2 || this->fileVersion > 3) {
            if (this->m_filepos > 0) {
                qWarning() << this->m_path << "@" << hex << this->m_filepos << "corrupt PRS1 header, skipping remainder of file";
            } else {
                qWarning() << this->m_path << "unsupported PRS1 header version" << this->fileVersion;
            }
            break;
        }
        if (this->htype != PRS1_HTYPE_NORMAL && this->htype != PRS1_HTYPE_INTERVAL) {
            qWarning() << this->m_path << "unexpected htype:" << this->htype;
            break;
        }

        // Read format-specific variable-length header data.
        bool hdr_ok = false;
        if (this->htype != PRS1_HTYPE_INTERVAL) {  // Not just waveforms: the 1160P uses this for its .002 events file.
            // Not a waveform/interval chunk
            switch (this->fileVersion) {
                case 2:
                    hdr_ok = ReadNormalHeaderV2(f);
                    break;
                case 3:
                    hdr_ok = ReadNormalHeaderV3(f);
                    break;
                default:
                    //hdr_ok remains false, warning is above
                    break;
            }
        } else {
            // Waveform/interval chunk
            hdr_ok = ReadWaveformHeader(f);
        }
        if (!hdr_ok) {
            break;
        }

        // The 8bit checksum comes at the end.
        QByteArray checksum = f.read(1);
        if (checksum.size() < 1) {
            qWarning() << this->m_path << "read error header checksum";
            break;
        }
        this->storedChecksum = checksum.data()[0];

        // Calculate 8bit additive header checksum.
        header = (unsigned char *)this->m_header.data(); // important because its memory location could move
        int header_size = this->m_header.size();
        quint8 achk=0;
        for (int i=0; i < header_size; i++) {
            achk += header[i];
        }
        this->calcChecksum = achk;
        
        // Append the stored checksum to the raw data *after* calculating the checksum on the preceding data.
        this->m_header.append(checksum);

        ok = true;
    } while (false);

    return ok;
}


bool PRS1DataChunk::ReadNormalHeaderV2(RawDataDevice & /*f*/)
{
    this->m_headerblock = QByteArray();
    return true;  // always OK
}


bool PRS1DataChunk::ReadNormalHeaderV3(RawDataDevice & f)
{
    bool ok = false;
    unsigned char * header;
    QByteArray headerB2;

    // This is a new machine, byte 15 is header data block length
    // followed by variable, data byte pairs
    do {
        QByteArray extra = f.read(1);
        if (extra.size() < 1) {
            qWarning() << this->m_path << "read error extended header";
            break;
        }
        this->m_header.append(extra);
        header = (unsigned char *)this->m_header.data();

        int hdb_len = header[15];
        int hdb_size = hdb_len * 2;

        headerB2 = f.read(hdb_size);
        if (headerB2.size() != hdb_size) {
            qWarning() << this->m_path << "read error in extended header";
            break;
        }
        this->m_headerblock = headerB2;
        
        this->m_header.append(headerB2);
        header = (unsigned char *)this->m_header.data();
        const unsigned char * hd = (unsigned char *)headerB2.constData();
        int pos = 0;
        int recs = header[15];
        for (int i=0; i<recs; i++) {
            this->hblock[hd[pos]] = hd[pos+1];
            pos += 2;
        }
        
        ok = true;
    } while (false);

    return ok;
}


bool PRS1DataChunk::ReadWaveformHeader(RawDataDevice & f)
{
    bool ok = false;
    unsigned char * header;
    do {
        // Read the fixed-length waveform header.
        QByteArray extra = f.read(4);
        if (extra.size() != 4) {
            qWarning() << this->m_path << "read error in waveform header";
            break;
        }
        this->m_header.append(extra);
        header = (unsigned char *)this->m_header.data();

        // Parse the fixed-length portion.
        this->interval_count = header[0x0f] | header[0x10] << 8;
        this->interval_seconds = header[0x11];  // not always 1 after all
        this->duration = this->interval_count * this->interval_seconds;  // ??? the last entry doesn't always seem to be a full interval?
        quint8 wvfm_signals = header[0x12];

        // Read the variable-length data + trailing byte.
        int ws_size = (this->fileVersion == 3) ? 4 : 3;
        int sbsize = wvfm_signals * ws_size + 1;

        extra = f.read(sbsize);
        if (extra.size() != sbsize) {
            qWarning() << this->m_path << "read error in waveform header 2";
            break;
        }
        this->m_header.append(extra);
        header = (unsigned char *)this->m_header.data();

        // Parse the variable-length waveform information.
        // TODO: move these checks into the parser, after the header checksum has been verified
        // For now just skip them for the one known sample with a bad checksum.
        if (this->sessionid == 268962649) return true;

        int pos = 0x13;
        for (int i = 0; i < wvfm_signals; ++i) {
            quint8 kind = header[pos];
            CHECK_VALUE(kind, i);  // always seems to range from 0...wvfm_signals-1, alert if not
            quint16 interleave = header[pos + 1] | header[pos + 2] << 8;  // samples per interval
            if (this->fileVersion == 2) {
                this->waveformInfo.push_back(PRS1Waveform(interleave, kind));
                pos += 3;
            } else if (this->fileVersion == 3) {
                int always_8 = header[pos + 3];  // sample size in bits?
                CHECK_VALUE(always_8, 8);
                this->waveformInfo.push_back(PRS1Waveform(interleave, kind));
                pos += 4;
            }
        }
        
        // And the trailing byte, whatever it is.
        int always_0 = header[pos];
        CHECK_VALUE(always_0, 0);
       
        ok = true;
    } while (false);

    return ok;
}


bool PRS1DataChunk::ReadData(RawDataDevice & f)
{
    bool ok = false;
    do {
        // Read data block
        int data_size = this->blockSize - this->m_header.size();
        if (data_size < 0) {
            qWarning() << this->m_path << "chunk size smaller than header";
            break;
        }
        this->m_data = f.read(data_size);
        if (this->m_data.size() < data_size) {
            qWarning() << this->m_path << "less data in file than specified in header";
            break;
        }

        // Extract the stored CRC from the data buffer and calculate the current CRC.
        if (this->fileVersion==3) {
            // The last 4 bytes contain a CRC32 checksum of the data.
            if (!ExtractStoredCrc(4)) {
                break;
            }
            this->calcCrc = CRC32wchar((unsigned char *)this->m_data.data(), this->m_data.size());
        } else {
            // The last 2 bytes contain a CRC16 checksum of the data.
            if (!ExtractStoredCrc(2)) {
                break;
            }
            this->calcCrc = CRC16((unsigned char *)this->m_data.data(), this->m_data.size());
        }
        
        ok = true;
    } while (false);

    return ok;
}


bool PRS1DataChunk::ExtractStoredCrc(int size)
{
    // Make sure there's enough data for the CRC.
    int offset = this->m_data.size() - size;
    if (offset < 0) {
        qWarning() << this->m_path << "chunk truncated";
        return false;
    }
    
    // Read the last 16- or 32-bit little-endian integer.
    quint32 storedCrc = 0;
    unsigned char* data = (unsigned char*)this->m_data.data();
    for (int i=0; i < size; i++) {
        storedCrc |= data[offset+i] << (8*i);
    }
    this->storedCrc = storedCrc;

    // Drop the CRC from the data.
    this->m_data.chop(size);
    
    return true;
}


// CRC-16/KERMIT, polynomial: 0x11021, bit reverse algorithm
// Table generated by crcmod (crc-kermit)

typedef quint16 crc16_t;
static crc16_t CRC16(unsigned char * data, size_t data_len, crc16_t crc)
{
    static const crc16_t table[256] = {
    0x0000U, 0x1189U, 0x2312U, 0x329bU, 0x4624U, 0x57adU, 0x6536U, 0x74bfU,
    0x8c48U, 0x9dc1U, 0xaf5aU, 0xbed3U, 0xca6cU, 0xdbe5U, 0xe97eU, 0xf8f7U,
    0x1081U, 0x0108U, 0x3393U, 0x221aU, 0x56a5U, 0x472cU, 0x75b7U, 0x643eU,
    0x9cc9U, 0x8d40U, 0xbfdbU, 0xae52U, 0xdaedU, 0xcb64U, 0xf9ffU, 0xe876U,
    0x2102U, 0x308bU, 0x0210U, 0x1399U, 0x6726U, 0x76afU, 0x4434U, 0x55bdU,
    0xad4aU, 0xbcc3U, 0x8e58U, 0x9fd1U, 0xeb6eU, 0xfae7U, 0xc87cU, 0xd9f5U,
    0x3183U, 0x200aU, 0x1291U, 0x0318U, 0x77a7U, 0x662eU, 0x54b5U, 0x453cU,
    0xbdcbU, 0xac42U, 0x9ed9U, 0x8f50U, 0xfbefU, 0xea66U, 0xd8fdU, 0xc974U,
    0x4204U, 0x538dU, 0x6116U, 0x709fU, 0x0420U, 0x15a9U, 0x2732U, 0x36bbU,
    0xce4cU, 0xdfc5U, 0xed5eU, 0xfcd7U, 0x8868U, 0x99e1U, 0xab7aU, 0xbaf3U,
    0x5285U, 0x430cU, 0x7197U, 0x601eU, 0x14a1U, 0x0528U, 0x37b3U, 0x263aU,
    0xdecdU, 0xcf44U, 0xfddfU, 0xec56U, 0x98e9U, 0x8960U, 0xbbfbU, 0xaa72U,
    0x6306U, 0x728fU, 0x4014U, 0x519dU, 0x2522U, 0x34abU, 0x0630U, 0x17b9U,
    0xef4eU, 0xfec7U, 0xcc5cU, 0xddd5U, 0xa96aU, 0xb8e3U, 0x8a78U, 0x9bf1U,
    0x7387U, 0x620eU, 0x5095U, 0x411cU, 0x35a3U, 0x242aU, 0x16b1U, 0x0738U,
    0xffcfU, 0xee46U, 0xdcddU, 0xcd54U, 0xb9ebU, 0xa862U, 0x9af9U, 0x8b70U,
    0x8408U, 0x9581U, 0xa71aU, 0xb693U, 0xc22cU, 0xd3a5U, 0xe13eU, 0xf0b7U,
    0x0840U, 0x19c9U, 0x2b52U, 0x3adbU, 0x4e64U, 0x5fedU, 0x6d76U, 0x7cffU,
    0x9489U, 0x8500U, 0xb79bU, 0xa612U, 0xd2adU, 0xc324U, 0xf1bfU, 0xe036U,
    0x18c1U, 0x0948U, 0x3bd3U, 0x2a5aU, 0x5ee5U, 0x4f6cU, 0x7df7U, 0x6c7eU,
    0xa50aU, 0xb483U, 0x8618U, 0x9791U, 0xe32eU, 0xf2a7U, 0xc03cU, 0xd1b5U,
    0x2942U, 0x38cbU, 0x0a50U, 0x1bd9U, 0x6f66U, 0x7eefU, 0x4c74U, 0x5dfdU,
    0xb58bU, 0xa402U, 0x9699U, 0x8710U, 0xf3afU, 0xe226U, 0xd0bdU, 0xc134U,
    0x39c3U, 0x284aU, 0x1ad1U, 0x0b58U, 0x7fe7U, 0x6e6eU, 0x5cf5U, 0x4d7cU,
    0xc60cU, 0xd785U, 0xe51eU, 0xf497U, 0x8028U, 0x91a1U, 0xa33aU, 0xb2b3U,
    0x4a44U, 0x5bcdU, 0x6956U, 0x78dfU, 0x0c60U, 0x1de9U, 0x2f72U, 0x3efbU,
    0xd68dU, 0xc704U, 0xf59fU, 0xe416U, 0x90a9U, 0x8120U, 0xb3bbU, 0xa232U,
    0x5ac5U, 0x4b4cU, 0x79d7U, 0x685eU, 0x1ce1U, 0x0d68U, 0x3ff3U, 0x2e7aU,
    0xe70eU, 0xf687U, 0xc41cU, 0xd595U, 0xa12aU, 0xb0a3U, 0x8238U, 0x93b1U,
    0x6b46U, 0x7acfU, 0x4854U, 0x59ddU, 0x2d62U, 0x3cebU, 0x0e70U, 0x1ff9U,
    0xf78fU, 0xe606U, 0xd49dU, 0xc514U, 0xb1abU, 0xa022U, 0x92b9U, 0x8330U,
    0x7bc7U, 0x6a4eU, 0x58d5U, 0x495cU, 0x3de3U, 0x2c6aU, 0x1ef1U, 0x0f78U,
    };

    for (size_t i=0; i < data_len; i++) {
        crc = table[(*data ^ (unsigned char)crc) & 0xFF] ^ (crc >> 8);
        data++;
    }
    return crc;
}


// CRC-32/MPEG-2, polynomial: 0x104C11DB7
// Table generated by crcmod (crc-32-mpeg)

static crc32_t CRC32(const unsigned char *data, size_t data_len, crc32_t crc)
{
    static const crc32_t table[256] = {
    0x00000000U, 0x04c11db7U, 0x09823b6eU, 0x0d4326d9U,
    0x130476dcU, 0x17c56b6bU, 0x1a864db2U, 0x1e475005U,
    0x2608edb8U, 0x22c9f00fU, 0x2f8ad6d6U, 0x2b4bcb61U,
    0x350c9b64U, 0x31cd86d3U, 0x3c8ea00aU, 0x384fbdbdU,
    0x4c11db70U, 0x48d0c6c7U, 0x4593e01eU, 0x4152fda9U,
    0x5f15adacU, 0x5bd4b01bU, 0x569796c2U, 0x52568b75U,
    0x6a1936c8U, 0x6ed82b7fU, 0x639b0da6U, 0x675a1011U,
    0x791d4014U, 0x7ddc5da3U, 0x709f7b7aU, 0x745e66cdU,
    0x9823b6e0U, 0x9ce2ab57U, 0x91a18d8eU, 0x95609039U,
    0x8b27c03cU, 0x8fe6dd8bU, 0x82a5fb52U, 0x8664e6e5U,
    0xbe2b5b58U, 0xbaea46efU, 0xb7a96036U, 0xb3687d81U,
    0xad2f2d84U, 0xa9ee3033U, 0xa4ad16eaU, 0xa06c0b5dU,
    0xd4326d90U, 0xd0f37027U, 0xddb056feU, 0xd9714b49U,
    0xc7361b4cU, 0xc3f706fbU, 0xceb42022U, 0xca753d95U,
    0xf23a8028U, 0xf6fb9d9fU, 0xfbb8bb46U, 0xff79a6f1U,
    0xe13ef6f4U, 0xe5ffeb43U, 0xe8bccd9aU, 0xec7dd02dU,
    0x34867077U, 0x30476dc0U, 0x3d044b19U, 0x39c556aeU,
    0x278206abU, 0x23431b1cU, 0x2e003dc5U, 0x2ac12072U,
    0x128e9dcfU, 0x164f8078U, 0x1b0ca6a1U, 0x1fcdbb16U,
    0x018aeb13U, 0x054bf6a4U, 0x0808d07dU, 0x0cc9cdcaU,
    0x7897ab07U, 0x7c56b6b0U, 0x71159069U, 0x75d48ddeU,
    0x6b93dddbU, 0x6f52c06cU, 0x6211e6b5U, 0x66d0fb02U,
    0x5e9f46bfU, 0x5a5e5b08U, 0x571d7dd1U, 0x53dc6066U,
    0x4d9b3063U, 0x495a2dd4U, 0x44190b0dU, 0x40d816baU,
    0xaca5c697U, 0xa864db20U, 0xa527fdf9U, 0xa1e6e04eU,
    0xbfa1b04bU, 0xbb60adfcU, 0xb6238b25U, 0xb2e29692U,
    0x8aad2b2fU, 0x8e6c3698U, 0x832f1041U, 0x87ee0df6U,
    0x99a95df3U, 0x9d684044U, 0x902b669dU, 0x94ea7b2aU,
    0xe0b41de7U, 0xe4750050U, 0xe9362689U, 0xedf73b3eU,
    0xf3b06b3bU, 0xf771768cU, 0xfa325055U, 0xfef34de2U,
    0xc6bcf05fU, 0xc27dede8U, 0xcf3ecb31U, 0xcbffd686U,
    0xd5b88683U, 0xd1799b34U, 0xdc3abdedU, 0xd8fba05aU,
    0x690ce0eeU, 0x6dcdfd59U, 0x608edb80U, 0x644fc637U,
    0x7a089632U, 0x7ec98b85U, 0x738aad5cU, 0x774bb0ebU,
    0x4f040d56U, 0x4bc510e1U, 0x46863638U, 0x42472b8fU,
    0x5c007b8aU, 0x58c1663dU, 0x558240e4U, 0x51435d53U,
    0x251d3b9eU, 0x21dc2629U, 0x2c9f00f0U, 0x285e1d47U,
    0x36194d42U, 0x32d850f5U, 0x3f9b762cU, 0x3b5a6b9bU,
    0x0315d626U, 0x07d4cb91U, 0x0a97ed48U, 0x0e56f0ffU,
    0x1011a0faU, 0x14d0bd4dU, 0x19939b94U, 0x1d528623U,
    0xf12f560eU, 0xf5ee4bb9U, 0xf8ad6d60U, 0xfc6c70d7U,
    0xe22b20d2U, 0xe6ea3d65U, 0xeba91bbcU, 0xef68060bU,
    0xd727bbb6U, 0xd3e6a601U, 0xdea580d8U, 0xda649d6fU,
    0xc423cd6aU, 0xc0e2d0ddU, 0xcda1f604U, 0xc960ebb3U,
    0xbd3e8d7eU, 0xb9ff90c9U, 0xb4bcb610U, 0xb07daba7U,
    0xae3afba2U, 0xaafbe615U, 0xa7b8c0ccU, 0xa379dd7bU,
    0x9b3660c6U, 0x9ff77d71U, 0x92b45ba8U, 0x9675461fU,
    0x8832161aU, 0x8cf30badU, 0x81b02d74U, 0x857130c3U,
    0x5d8a9099U, 0x594b8d2eU, 0x5408abf7U, 0x50c9b640U,
    0x4e8ee645U, 0x4a4ffbf2U, 0x470cdd2bU, 0x43cdc09cU,
    0x7b827d21U, 0x7f436096U, 0x7200464fU, 0x76c15bf8U,
    0x68860bfdU, 0x6c47164aU, 0x61043093U, 0x65c52d24U,
    0x119b4be9U, 0x155a565eU, 0x18197087U, 0x1cd86d30U,
    0x029f3d35U, 0x065e2082U, 0x0b1d065bU, 0x0fdc1becU,
    0x3793a651U, 0x3352bbe6U, 0x3e119d3fU, 0x3ad08088U,
    0x2497d08dU, 0x2056cd3aU, 0x2d15ebe3U, 0x29d4f654U,
    0xc5a92679U, 0xc1683bceU, 0xcc2b1d17U, 0xc8ea00a0U,
    0xd6ad50a5U, 0xd26c4d12U, 0xdf2f6bcbU, 0xdbee767cU,
    0xe3a1cbc1U, 0xe760d676U, 0xea23f0afU, 0xeee2ed18U,
    0xf0a5bd1dU, 0xf464a0aaU, 0xf9278673U, 0xfde69bc4U,
    0x89b8fd09U, 0x8d79e0beU, 0x803ac667U, 0x84fbdbd0U,
    0x9abc8bd5U, 0x9e7d9662U, 0x933eb0bbU, 0x97ffad0cU,
    0xafb010b1U, 0xab710d06U, 0xa6322bdfU, 0xa2f33668U,
    0xbcb4666dU, 0xb8757bdaU, 0xb5365d03U, 0xb1f740b4U,
    };
    
    for (size_t i=0; i < data_len; i++) {
        crc = table[(*data ^ (unsigned char)(crc >> 24)) & 0xFF] ^ (crc << 8);
        data++;
    }
    return crc;
}


// Strangely, the PRS1 CRC32 appears to consider every byte a 32-bit wchar_t.
// Nothing like trying a bunch of encodings and CRC32 variants on PROP.TXT files
// until you find a winner.

static crc32_t CRC32wchar(const unsigned char *data, size_t data_len, crc32_t crc)
{
    for (size_t i=0; i < data_len; i++) {
        static unsigned char wch[4] = { 0, 0, 0, 0 };
        wch[3] = *data++;
        crc = CRC32(wch, 4, crc);
    }
    return crc;
}
