/* Channel / Schema 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 <QFile>
#include <QDebug>
#include <QDomDocument>
#include <QDomElement>
#include <QDomNode>
#include <QMessageBox>
#include <QApplication>

#include "common.h"
#include "schema.h"
#include "common_gui.h"

#include "SleepLib/profiles.h"

QColor adjustcolor(QColor color, float ar=1.0, float ag=1.0, float ab=1.0)
{
    int r = color.red();
    int g = color.green();
    int b = color.blue();

    r += rand() & 64;
    g += rand() & 64;
    b += rand() & 64;

    r = qMin(int(r * ar), 255);
    g = qMin(int(g * ag), 255);
    b = qMin(int(b * ab), 255);

    return QColor(r,g,b, color.alpha());
}


QColor darken(QColor color, float p);

namespace schema {

void resetChannels();

ChannelList channel;
Channel EmptyChannel;
Channel *SessionEnabledChannel;

QHash<QString, ChanType> ChanTypes;
QHash<QString, DataType> DataTypes;
QHash<QString, ScopeType> Scopes;

bool schema_initialized = false;

// Order in which indices appear on Daily page
void setOrders() {
    int order = 1;
    schema::channel[CPAP_PB].setOrder(order++);
    schema::channel[CPAP_CSR].setOrder(order++);
    schema::channel[CPAP_Ramp].setOrder(order++);
    schema::channel[CPAP_LargeLeak].setOrder(order++);

    schema::channel[CPAP_ClearAirway].setOrder(order++);
    schema::channel[CPAP_NRI].setOrder(order++);
    schema::channel[CPAP_AllApnea].setOrder(order++);
    schema::channel[CPAP_Obstructive].setOrder(order++);
    schema::channel[CPAP_Apnea].setOrder(order++);
    schema::channel[CPAP_Hypopnea].setOrder(order++);
    schema::channel[CPAP_FlowLimit].setOrder(order++);

    schema::channel[CPAP_RERA].setOrder(order++);
    schema::channel[CPAP_ExP].setOrder(order++);
    schema::channel[CPAP_VSnore].setOrder(order++);
    schema::channel[CPAP_VSnore2].setOrder(order++);

    // Any channels not set above appear here, as default value for order is 255.

    // Finally, include user flags
    order = 256;
    schema::channel[CPAP_UserFlag1].setOrder(order++);
    schema::channel[CPAP_UserFlag2].setOrder(order++);
}

void init()
{
    if (schema_initialized) { return; }

    schema_initialized = true;

    EmptyChannel = Channel(0, DATA, MT_UNKNOWN, DAY, "Empty", "Empty", "Empty Channel", "", "");
    SessionEnabledChannel = new Channel(1, DATA, MT_UNKNOWN, DAY, "Enabled", "Enabled", "Session Enabled", "", "");

    channel.channels[1] = SessionEnabledChannel;
    channel.names["Enabled"] = SessionEnabledChannel;
    SESSION_ENABLED = 1;
    ChanTypes["data"] = DATA;
    //Types["waveform"]=WAVEFORM;
    ChanTypes["setting"] = SETTING;

    Scopes["session"] = SESSION;
    Scopes["day"] = DAY;
    Scopes["machine"] = MACHINE;
    Scopes["global"] = GLOBAL;

    DataTypes[""] = DEFAULT;
    DataTypes["bool"] = BOOL;
    DataTypes["double"] = DOUBLE;
    DataTypes["integer"] = INTEGER;
    DataTypes["string"] = STRING;
    DataTypes["richtext"] = RICHTEXT;
    DataTypes["date"] = DATE;
    DataTypes["datetime"] = DATETIME;
    DataTypes["time"] = TIME;

    // Note: Old channel names stored in channels.xml are not translatable.. they need to be moved to be defined AFTER here instead
    if (!schema::channel.Load(":/docs/channels.xml")) {
        QMessageBox::critical(0, STR_MessageBox_Error,
                              QObject::tr("Couldn't parse Channels.xml, OSCAR cannot continue and is exiting."),
                              QMessageBox::Ok);
        QApplication::exit(-1);
    }


    // Lookup Code strings are used internally and not meant to be translated
    // Lookup Code strings are also used as a key to a graph of this channel

    //                  Group                ChannelID            Code    Type                   Scope    Lookup Code      Translable Name                  Description                                    Shortened Name              Units String            FieldType   Default Color

    // Pressure Related Settings
    schema::channel.add(GRP_CPAP, new Channel(CPAP_Pressure      = 0x110C, WAVEFORM,    MT_CPAP, SESSION, STR_GRAPH_Pressure, STR_TR_Pressure,              QObject::tr("Therapy Pressure"),               STR_TR_Pressure,             STR_UNIT_CMH2O,        DEFAULT,    QColor("red")));
    schema::channel.add(GRP_CPAP, new Channel(CPAP_IPAP          = 0x110D, WAVEFORM,    MT_CPAP, SESSION, "IPAP",           STR_TR_IPAP,                    QObject::tr("Inspiratory Pressure"),           STR_TR_IPAP,                 STR_UNIT_CMH2O,        DEFAULT,    QColor("red")));
    schema::channel.add(GRP_CPAP, new Channel(CPAP_IPAPLo        = 0x1110, WAVEFORM,    MT_CPAP, SESSION, "IPAPLo",         STR_TR_IPAPLo,                  QObject::tr("Lower Inspiratory Pressure"),     STR_TR_IPAPLo,               STR_UNIT_CMH2O,        DEFAULT,    QColor("orange")));
    schema::channel.add(GRP_CPAP, new Channel(CPAP_IPAPHi        = 0x1111, WAVEFORM,    MT_CPAP, SESSION, "IPAPHi",         STR_TR_IPAPHi,                  QObject::tr("Higher Inspiratory Pressure"),    STR_TR_IPAPHi,               STR_UNIT_CMH2O,        DEFAULT,    QColor("orange")));
    schema::channel.add(GRP_CPAP, new Channel(CPAP_EPAP          = 0x110E, WAVEFORM,    MT_CPAP, SESSION, "EPAP",           STR_TR_EPAP,                    QObject::tr("Expiratory Pressure"),            STR_TR_EPAP,                 STR_UNIT_CMH2O,        DEFAULT,    QColor("green")));
    schema::channel.add(GRP_CPAP, new Channel(CPAP_EPAPLo        = 0x111C, WAVEFORM,    MT_CPAP, SESSION, "EPAPLo",         STR_TR_EPAPLo,                  QObject::tr("Lower Expiratory Pressure"),      STR_TR_EPAPLo,               STR_UNIT_CMH2O,        DEFAULT,    QColor("light blue")));
    schema::channel.add(GRP_CPAP, new Channel(CPAP_EPAPHi        = 0x111D, WAVEFORM,    MT_CPAP, SESSION, "EPAPHi",         STR_TR_EPAPHi,                  QObject::tr("Higher Expiratory Pressure"),     STR_TR_EPAPHi,               STR_UNIT_CMH2O,        DEFAULT,    QColor("aqua")));
    schema::channel.add(GRP_CPAP, new Channel(CPAP_PS            = 0x110F, WAVEFORM,    MT_CPAP, SESSION, "PS",             STR_TR_PS,                      QObject::tr("Pressure Support"),               STR_TR_PS,                   STR_UNIT_CMH2O,        DEFAULT,    QColor("grey")));
    schema::channel.add(GRP_CPAP, new Channel(CPAP_PSMin         = 0x111A, SETTING,     MT_CPAP, SESSION, "PSMin",          QObject::tr("PS Min") ,         QObject::tr("Pressure Support Minimum"),       QObject::tr("PS Min"),       STR_UNIT_CMH2O,        DEFAULT,    QColor("dark cyan")));
    schema::channel.add(GRP_CPAP, new Channel(CPAP_PSMax         = 0x111B, SETTING,     MT_CPAP, SESSION, "PSMax",          QObject::tr("PS Max"),          QObject::tr("Pressure Support Maximum"),       QObject::tr("PS Max"),       STR_UNIT_CMH2O,        DEFAULT,    QColor("dark magenta")));
    schema::channel.add(GRP_CPAP, new Channel(CPAP_PressureMin   = 0x1020, SETTING,     MT_CPAP, SESSION, "PressureMin",    QObject::tr("Min Pressure"),    QObject::tr("Minimum Therapy Pressure"),       QObject::tr("Pressure Min"), STR_UNIT_CMH2O,        DEFAULT,    QColor("orange")));
    schema::channel.add(GRP_CPAP, new Channel(CPAP_PressureMax   = 0x1021, SETTING,     MT_CPAP, SESSION, "PressureMax",    QObject::tr("Max Pressure"),    QObject::tr("Maximum Therapy Pressure"),       QObject::tr("Pressure Max"), STR_UNIT_CMH2O,        DEFAULT,    QColor("light blue")));
    schema::channel.add(GRP_CPAP, new Channel(CPAP_RampTime      = 0x1022, SETTING,     MT_CPAP, SESSION, "RampTime",       QObject::tr("Ramp Time") ,      QObject::tr("Ramp Delay Period"),              QObject::tr("Ramp Time"),    STR_UNIT_Minutes,      DEFAULT,    QColor("black")));
    schema::channel.add(GRP_CPAP, new Channel(CPAP_RampPressure  = 0x1023, SETTING,     MT_CPAP, SESSION, "RampPressure",   QObject::tr("Ramp Pressure"),   QObject::tr("Starting Ramp Pressure"),         QObject::tr("Ramp Pressure"),STR_UNIT_CMH2O,        DEFAULT,    QColor("black")));
    schema::channel.add(GRP_CPAP, new Channel(CPAP_Ramp          = 0x1027, SPAN,        MT_CPAP, SESSION, "Ramp",           QObject::tr("Ramp Event") ,     QObject::tr("Ramp Event"),                     QObject::tr("Ramp"),         STR_UNIT_EventsPerHour,DEFAULT,    QColor("light blue")));
    schema::channel.add(GRP_CPAP, new Channel(CPAP_PressureSet   = 0x11A4, WAVEFORM,    MT_CPAP, SESSION, "PressureSet",    QObject::tr("Pressure Set"),    QObject::tr("Pressure Setting"),               QObject::tr("Pressure Set"), STR_UNIT_CMH2O,        DEFAULT,    QColor("dark red")));
    schema::channel.add(GRP_CPAP, new Channel(CPAP_IPAPSet       = 0x11A5, WAVEFORM,    MT_CPAP, SESSION, "IPAPSet",        QObject::tr("IPAP Set"),        QObject::tr("IPAP Setting"),                   QObject::tr("IPAP Set"),     STR_UNIT_CMH2O,        DEFAULT,    QColor("dark red")));
    schema::channel.add(GRP_CPAP, new Channel(CPAP_EPAPSet       = 0x11A6, WAVEFORM,    MT_CPAP, SESSION, "EPAPSet",        QObject::tr("EPAP Set"),        QObject::tr("EPAP Setting"),                   QObject::tr("EPAP Set"),     STR_UNIT_CMH2O,        DEFAULT,    QColor("dark green")));
    // Flags
    schema::channel.add(GRP_CPAP, new Channel(CPAP_CSR           = 0x1000, SPAN,        MT_CPAP, SESSION, "CSR",
            QObject::tr("Cheyne Stokes Respiration"), QObject::tr("An abnormal period of Cheyne Stokes Respiration"), QObject::tr("CSR"), STR_UNIT_Percentage,DEFAULT,    COLOR_CSR));
    schema::channel.add(GRP_CPAP, new Channel(CPAP_PB            = 0x1028, SPAN,        MT_CPAP, SESSION, "PB",
            QObject::tr("Periodic Breathing"),QObject::tr("An abnormal period of Periodic Breathing"), QObject::tr("PB"),STR_UNIT_Percentage,   DEFAULT,    COLOR_CSR));
    schema::channel.add(GRP_CPAP, new Channel(CPAP_ClearAirway   = 0x1001, FLAG,        MT_CPAP, SESSION, "ClearAirway",
            QObject::tr("Clear Airway"), QObject::tr("An apnea where the airway is open"), QObject::tr("CA"),       STR_UNIT_EventsPerHour,    DEFAULT,    QColor("purple")));
    schema::channel.add(GRP_CPAP, new Channel(CPAP_Obstructive   = 0x1002, FLAG,        MT_CPAP, SESSION, "Obstructive",
            QObject::tr("Obstructive"), QObject::tr("An apnea caused by airway obstruction"), QObject::tr("OA"), STR_UNIT_EventsPerHour,    DEFAULT,    QColor("#40c0ff")));
    schema::channel.add(GRP_CPAP, new Channel(CPAP_Hypopnea      = 0x1003, FLAG,        MT_CPAP, SESSION, "Hypopnea",
            QObject::tr("Hypopnea"), QObject::tr("A partially obstructed airway"), QObject::tr("H"),        STR_UNIT_EventsPerHour,    DEFAULT,    QColor("blue")));
    schema::channel.add(GRP_CPAP, new Channel(CPAP_Apnea         = 0x1004, FLAG,        MT_CPAP, SESSION, "Apnea",
            QObject::tr("Unclassified Apnea"), QObject::tr("An apnea that couldn't be determined as Central or Obstructive."),QObject::tr("UA"),       STR_UNIT_EventsPerHour,    DEFAULT,    QColor("dark green")));
    schema::channel.add(GRP_CPAP, new Channel(CPAP_AllApnea      = 0x1010, FLAG,        MT_CPAP, SESSION, "AllApnea",
            QObject::tr("Apnea"), QObject::tr("An apnea reportred by your CPAP machine."),QObject::tr("A"),       STR_UNIT_EventsPerHour,    DEFAULT,    QColor("#40c0ff")));
    schema::channel.add(GRP_CPAP, new Channel(CPAP_FlowLimit     = 0x1005, FLAG,        MT_CPAP, SESSION, "FlowLimit",
            QObject::tr("Flow Limitation"), QObject::tr("A restriction in breathing from normal, causing a flattening of the flow waveform."), QObject::tr("FL"), STR_UNIT_EventsPerHour,    DEFAULT,    QColor("#404040")));
    schema::channel.add(GRP_CPAP, new Channel(CPAP_RERA          = 0x1006, FLAG,        MT_CPAP, SESSION, "RERA",
            QObject::tr("RERA"),QObject::tr("Respiratory Effort Related Arousal: An restriction in breathing that causes an either an awakening or sleep disturbance."), QObject::tr("RE"),       STR_UNIT_EventsPerHour,    DEFAULT,    COLOR_Gold));
    schema::channel.add(GRP_CPAP, new Channel(CPAP_VSnore        = 0x1007, FLAG,        MT_CPAP, SESSION, "VSnore",
            QObject::tr("Vibratory Snore"), QObject::tr("A vibratory snore"), QObject::tr("VS"),       STR_UNIT_EventsPerHour,    DEFAULT,    QColor("red")));
    schema::channel.add(GRP_CPAP, new Channel(CPAP_VSnore2       = 0x1008, FLAG,        MT_CPAP, SESSION, "VSnore2",
            QObject::tr("Vibratory Snore (VS2) "),QObject::tr("A vibratory snore as detcted by a System One machine"),QObject::tr("VS2"),      STR_UNIT_EventsPerHour,    DEFAULT,    QColor("red")));
    // This Large Leak record is just a flag marker, used by Intellipap for one
    schema::channel.add(GRP_CPAP, new Channel(CPAP_LeakFlag      = 0x100a, FLAG,        MT_CPAP, SESSION, "LeakFlag",
            QObject::tr("Leak Flag"), QObject::tr("A large mask leak affecting machine performance."), QObject::tr("LF"),       STR_UNIT_EventsPerHour,    DEFAULT,    QColor("light gray")));

    // The following is a Large Leak record that references a waveform span
    schema::channel.add(GRP_CPAP, new Channel(CPAP_LargeLeak     = 0x1158, SPAN,        MT_CPAP, SESSION, "LeakSpan",
            QObject::tr("Large Leak"),QObject::tr("A large mask leak affecting machine performance."), QObject::tr("LL"),       STR_UNIT_EventsPerHour,    DEFAULT,    QColor("light gray")));

    schema::channel.add(GRP_CPAP, new Channel(CPAP_NRI           = 0x100b, FLAG,        MT_CPAP, SESSION, "NRI",
            QObject::tr("Non Responding Event"), QObject::tr("A type of respiratory event that won't respond to a pressure increase."), QObject::tr("NR"),       STR_UNIT_EventsPerHour,    DEFAULT,    QColor("orange")));

    schema::channel.add(GRP_CPAP, new Channel(CPAP_ExP           = 0x100c, FLAG,        MT_CPAP, SESSION, "ExP",
            QObject::tr("Expiratory Puff"), QObject::tr("Intellipap event where you breathe out your mouth."), QObject::tr("EP"),       STR_UNIT_EventsPerHour,    DEFAULT,    QColor("dark magenta")));

    schema::channel.add(GRP_CPAP, new Channel(CPAP_SensAwake     = 0x100d, FLAG,        MT_CPAP, SESSION, "SensAwake",
            QObject::tr("SensAwake"),QObject::tr("SensAwake feature will reduce pressure when waking is detected."),QObject::tr("SA"),       STR_UNIT_EventsPerHour,    DEFAULT,    COLOR_Gold));

    schema::channel.add(GRP_CPAP, new Channel(CPAP_UserFlag1     = 0x101e, FLAG,        MT_CPAP, SESSION, "UserFlag1",
            QObject::tr("User Flag #1"), QObject::tr("A user definable event detected by OSCAR's flow waveform processor."), QObject::tr("UF1"),      STR_UNIT_EventsPerHour,    DEFAULT,    QColor(0xc0,0xc0,0xe0)));

    schema::channel.add(GRP_CPAP, new Channel(CPAP_UserFlag2     = 0x101f, FLAG,        MT_CPAP, SESSION, "UserFlag2",
            QObject::tr("User Flag #2"),QObject::tr("A user definable event detected by OSCAR's flow waveform processor."), QObject::tr("UF2"),      STR_UNIT_EventsPerHour,    DEFAULT,    QColor(0xa0,0xa0,0xc0)));

    schema::channel.add(GRP_CPAP, new Channel(CPAP_UserFlag3     = 0x1024, FLAG,        MT_CPAP, SESSION, "UserFlag3",
            QObject::tr("User Flag #3"),QObject::tr("A user definable event detected by OSCAR's flow waveform processor."), QObject::tr("UF3"),      STR_UNIT_EventsPerHour,    DEFAULT,    QColor("dark grey")));

    // Oximetry
    schema::channel.add(GRP_OXI, new Channel(OXI_Pulse           = 0x1800, WAVEFORM,    MT_OXIMETER, SESSION, STR_GRAPH_Oxi_Pulse,
            QObject::tr("Pulse Rate"),                    QObject::tr("Heart rate in beats per minute"), QObject::tr("Pulse Rate"), STR_UNIT_BPM,     DEFAULT,    QColor("red")));
    schema::channel[OXI_Pulse].setLowerThreshold(40);
    schema::channel[OXI_Pulse].setUpperThreshold(130);

    schema::channel.add(GRP_OXI, new Channel(OXI_SPO2            = 0x1801, WAVEFORM,    MT_OXIMETER, SESSION, STR_GRAPH_Oxi_SPO2,
            QObject::tr("SpO2 %"), QObject::tr("Blood-oxygen saturation percentage"), QObject::tr("SpO2"),       STR_UNIT_Percentage,          DEFAULT,    QColor("blue")));
    schema::channel[OXI_SPO2].setLowerThreshold(88);

    schema::channel.add(GRP_OXI, new Channel(OXI_Plethy          = 0x1802, WAVEFORM,    MT_OXIMETER, SESSION, STR_GRAPH_Oxi_Plethy,
            QObject::tr("Plethysomogram"), QObject::tr("An optical Photo-plethysomogram showing heart rhythm"), QObject::tr("Plethy"),     STR_UNIT_Hz,           DEFAULT,    QColor("#404040")));

    schema::channel.add(GRP_OXI, new Channel(OXI_Perf             = 0x1805, WAVEFORM,   MT_OXIMETER, SESSION, STR_GRAPH_Oxi_Perf,
            QObject::tr("Perfusion Index"), QObject::tr("A relative assessment of the pulse strength at the monitoring site"), QObject::tr("Perf. Index %"), STR_UNIT_Percentage,     DEFAULT,   QColor("magenta")));

    schema::channel.add(GRP_OXI, new Channel(OXI_PulseChange     = 0x1803, FLAG,        MT_OXIMETER, SESSION, STR_GRAPH_Oxi_PulseChange,
            QObject::tr("Pulse Change"), QObject::tr("A sudden (user definable) change in heart rate"), QObject::tr("PC"),         STR_UNIT_EventsPerHour,    DEFAULT,   QColor("light grey")));

    schema::channel.add(GRP_OXI, new Channel(OXI_SPO2Drop        = 0x1804, FLAG,        MT_OXIMETER, SESSION, STR_GRAPH_Oxi_SPO2Drop,
            QObject::tr("SpO2 Drop"), QObject::tr("A sudden (user definable) drop in blood oxygen saturation"), QObject::tr("SD"),         STR_UNIT_EventsPerHour,    DEFAULT,    QColor("light blue")));


    schema::channel.add(GRP_CPAP, new Channel(CPAP_FlowRate          = 0x1100, WAVEFORM,    MT_CPAP, SESSION, STR_GRAPH_FlowRate,
            QObject::tr("Flow Rate"), QObject::tr("Breathing flow rate waveform"), QObject::tr("Flow Rate"), STR_UNIT_LPM,    DEFAULT,    QColor("black")));

    schema::channel.add(GRP_CPAP, new Channel(CPAP_MaskPressure      = 0x1101, WAVEFORM,    MT_CPAP, SESSION, STR_GRAPH_MaskPressure,
            QObject::tr("Mask Pressure"), QObject::tr("Mask Pressure"), QObject::tr("Mask Pressure"), STR_UNIT_CMH2O,    DEFAULT,    QColor("blue")));

    schema::channel.add(GRP_CPAP, new Channel(CPAP_MaskPressureHi    = 0x1102, WAVEFORM,    MT_CPAP, SESSION, "MaskPressureHi",
            QObject::tr("Mask Pressure"), QObject::tr("Mask Pressure (High frequency)"), QObject::tr("Mask Pressure"), STR_UNIT_CMH2O,    DEFAULT,    QColor("blue"), 0x1101)); // linked to CPAP_MaskPressure

    schema::channel.add(GRP_CPAP, new Channel(CPAP_TidalVolume       = 0x1103, WAVEFORM,    MT_CPAP, SESSION, STR_GRAPH_TidalVolume,
            QObject::tr("Tidal Volume"), QObject::tr("Amount of air displaced per breath"), QObject::tr("Tidal Volume"), STR_UNIT_ml,    DEFAULT,    QColor("magenta")));

    schema::channel.add(GRP_CPAP, new Channel(CPAP_Snore             = 0x1104, WAVEFORM,    MT_CPAP,  SESSION, STR_GRAPH_Snore,
            QObject::tr("Snore"), QObject::tr("Graph displaying snore volume"), QObject::tr("Snore"),   STR_UNIT_Unknown,       DEFAULT,    QColor("grey")));

    schema::channel.add(GRP_CPAP, new Channel(CPAP_MinuteVent        = 0x1105, WAVEFORM,    MT_CPAP, SESSION, STR_GRAPH_MinuteVent,
            QObject::tr("Minute Ventilation"), QObject::tr("Amount of air displaced per minute"), QObject::tr("Minute Vent."), STR_UNIT_LPM,    DEFAULT,    QColor("dark cyan")));

    schema::channel.add(GRP_CPAP, new Channel(CPAP_RespRate          = 0x1106, WAVEFORM,    MT_CPAP, SESSION, STR_GRAPH_RespRate,
            QObject::tr("Respiratory Rate"), QObject::tr("Rate of breaths per minute"), QObject::tr("Resp. Rate"), STR_UNIT_BreathsPerMinute,      DEFAULT,    QColor("dark magenta")));

    schema::channel.add(GRP_CPAP, new Channel(CPAP_PTB               = 0x1107, WAVEFORM,    MT_CPAP, SESSION, STR_GRAPH_PTB,
            QObject::tr("Patient Triggered Breaths"), QObject::tr("Percentage of breaths triggered by patient"),   QObject::tr("Pat. Trig. Breaths"), STR_UNIT_Percentage,   DEFAULT,    QColor("dark grey")));

    schema::channel.add(GRP_CPAP, new Channel(CPAP_Leak              = 0x1108, WAVEFORM,    MT_CPAP, SESSION, STR_GRAPH_LeakRate,
            QObject::tr("Leak Rate"), QObject::tr("Rate of detected mask leakage"), QObject::tr("Leak Rate"), STR_UNIT_LPM,    DEFAULT,    QColor("dark green")));
    schema::channel[CPAP_Leak].setLowerThreshold(24.0);

    schema::channel.add(GRP_CPAP, new Channel(CPAP_IE                = 0x1109, WAVEFORM,    MT_CPAP,  SESSION, "IE",
            QObject::tr("I:E Ratio"), QObject::tr("Ratio between Inspiratory and Expiratory time"), QObject::tr("I:E Ratio"), STR_UNIT_Ratio,    DEFAULT,    QColor("dark red")));

    schema::channel.add(GRP_CPAP, new Channel(CPAP_Te                = 0x110A, WAVEFORM,    MT_CPAP,  SESSION, STR_GRAPH_Te,
            QObject::tr("Expiratory Time"), QObject::tr("Time taken to breathe out"), QObject::tr("Exp. Time"),          STR_UNIT_Seconds,  DEFAULT,    QColor("dark green")));

    schema::channel.add(GRP_CPAP, new Channel(CPAP_Ti                = 0x110B, WAVEFORM,    MT_CPAP, SESSION, STR_GRAPH_Ti,
            QObject::tr("Inspiratory Time"), QObject::tr("Time taken to breathe in"), QObject::tr("Insp. Time"),         STR_UNIT_Seconds,  DEFAULT,    QColor("dark blue")));

    schema::channel.add(GRP_CPAP, new Channel(CPAP_RespEvent         = 0x1112, WAVEFORM,   MT_CPAP,  SESSION, STR_GRAPH_RespEvent,
            QObject::tr("Respiratory Event"), QObject::tr("A ResMed data item: Trigger Cycle Event"),  QObject::tr("Resp. Event"), STR_UNIT_CMH2O,   DEFAULT,    QColor("black")));

    schema::channel.add(GRP_CPAP, new Channel(CPAP_FLG               = 0x1113, WAVEFORM,   MT_CPAP,  SESSION, STR_GRAPH_FlowLimitation,
            QObject::tr("Flow Limitation"), QObject::tr("Graph showing severity of flow limitations"),   QObject::tr("Flow Limit."), STR_UNIT_Severity,      DEFAULT,    QColor("#585858")));

    schema::channel.add(GRP_CPAP, new Channel(CPAP_TgMV              = 0x1114, WAVEFORM,  MT_CPAP,   SESSION, "TgMV",
            QObject::tr("Target Minute Ventilation"), QObject::tr("Target Minute Ventilation"), QObject::tr("Target Vent."), STR_UNIT_LPM,       DEFAULT,    QColor("dark red")));

    schema::channel.add(GRP_CPAP, new Channel(CPAP_MaxLeak           = 0x1115, WAVEFORM,   MT_CPAP,  SESSION, "MaxLeak",
            QObject::tr("Maximum Leak"), QObject::tr("The maximum rate of mask leakage"), QObject::tr("Max Leaks"), STR_UNIT_LPM,    DEFAULT,    QColor("dark red")));

    schema::channel.add(GRP_CPAP, new Channel(CPAP_AHI               = 0x1116, WAVEFORM,   MT_CPAP,  SESSION, "AHI",
            QObject::tr("Apnea Hypopnea Index"), QObject::tr("Graph showing running AHI for the past hour"),  QObject::tr("AHI"), STR_UNIT_EventsPerHour, DEFAULT,  QColor("dark red")));

    schema::channel.add(GRP_CPAP, new Channel(CPAP_LeakTotal         = 0x1117, WAVEFORM,   MT_CPAP,  SESSION, "LeakTotal",
            QObject::tr("Total Leak Rate"), QObject::tr("Detected mask leakage including natural Mask leakages"),  QObject::tr("Total Leaks"), STR_UNIT_LPM,    DEFAULT,    QColor("dark green")));

    schema::channel.add(GRP_CPAP, new Channel(CPAP_LeakMedian        = 0x1118, WAVEFORM,   MT_CPAP,  SESSION, "LeakMedian",
            QObject::tr("Median Leak Rate"), QObject::tr("Median rate of detected mask leakage"), QObject::tr("Median Leaks"), STR_UNIT_LPM,    DEFAULT,    QColor("dark green")));

    schema::channel.add(GRP_CPAP, new Channel(CPAP_RDI               = 0x1119, WAVEFORM,   MT_CPAP,  SESSION, "RDI",
            QObject::tr("Respiratory Disturbance Index"), QObject::tr("Graph showing running RDI for the past hour"),  QObject::tr("RDI"), STR_UNIT_EventsPerHour, DEFAULT,  QColor("dark red")));

    // Positional sensors
    schema::channel.add(GRP_POS, new Channel(POS_Orientation         = 0x2990, WAVEFORM,   MT_POSITION,  SESSION, STR_GRAPH_Orientation,
            QObject::tr("Orientation"), QObject::tr("Sleep position in degrees"),  QObject::tr("Orientation"),  STR_UNIT_Degrees, DEFAULT,  QColor("dark blue")));

    schema::channel.add(GRP_POS, new Channel(POS_Inclination         = 0x2991, WAVEFORM,   MT_POSITION, SESSION, STR_GRAPH_Inclination,
            QObject::tr("Inclination"), QObject::tr("Upright angle in degrees"),  QObject::tr("Inclination"),  STR_UNIT_Degrees, DEFAULT,  QColor("dark magenta")));

    schema::channel.add(GRP_POS, new Channel(POS_Movement            = 0x2992, WAVEFORM,   MT_POSITION, SESSION, STR_GRAPH_Motion,
            QObject::tr("Movement"), QObject::tr("Movement detector"),  QObject::tr("Movement"),  STR_UNIT_Unknown, DEFAULT,  QColor("dark green")));

    schema::channel.add(GRP_CPAP, new Channel(RMS9_MaskOnTime        = 0x1025, DATA,   MT_CPAP,  SESSION, "MaskOnTime",
            QObject::tr("Mask On Time"), QObject::tr("Time started according to str.edf"),  QObject::tr("Mask On Time"),  STR_UNIT_Unknown, DEFAULT,  Qt::black));

    schema::channel.add(GRP_CPAP, new Channel(CPAP_SummaryOnly       = 0x1026, DATA,   MT_CPAP,  SESSION, "SummaryOnly",
            QObject::tr("Summary Only"), QObject::tr("CPAP Session contains summary data only"),  QObject::tr("Summary Only"),  STR_UNIT_Unknown, DEFAULT,  Qt::black));

    Channel *ch;
    schema::channel.add(GRP_CPAP, ch = new Channel(CPAP_Mode         = 0x1200, SETTING,   MT_CPAP,  SESSION, "PAPMode",
                        QObject::tr("PAP Mode"), QObject::tr("PAP Device Mode"), QObject::tr("PAP Mode"),  QString(), LOOKUP,  Qt::black));

    ch->addOption(0, STR_TR_Unknown);
    ch->addOption(1, STR_TR_CPAP);
    ch->addOption(2, QObject::tr("APAP (Variable)"));
    ch->addOption(3, QObject::tr("Fixed Bi-Level"));
    ch->addOption(4, QObject::tr("Auto Bi-Level (Fixed PS)"));
    ch->addOption(5, QObject::tr("Auto Bi-Level (Variable PS)"));
    ch->addOption(6, QObject::tr("ASV (Fixed EPAP)"));
    ch->addOption(7, QObject::tr("ASV (Variable EPAP)"));
    ch->addOption(8, QObject::tr("AVAPS"));

    /////////////////////////////////////////////////////////////////
    // Old Journal system crap
    /////////////////////////////////////////////////////////////////

    schema::channel.add(GRP_JOURNAL, ch = new Channel(Journal_Weight = 0x0803, DATA,   MT_JOURNAL,  DAY, "Weight",      QObject::tr("Weight"), QObject::tr("Weight"), QObject::tr("Weight"),  STR_UNIT_KG, DOUBLE,  Qt::black));
    schema::channel.add(GRP_JOURNAL, ch = new Channel(0x0804, DATA,   MT_JOURNAL,  DAY, "Height", QObject::tr("Height"), QObject::tr("Physical Height"), QObject::tr("Height"),  STR_UNIT_CM, DOUBLE,  Qt::black));
    schema::channel.add(GRP_JOURNAL, ch = new Channel(Bookmark_Notes=0x0805, DATA,   MT_JOURNAL,  DAY, "BookmarkNotes",      QObject::tr("Notes"), QObject::tr("Bookmark Notes"), QObject::tr("Notes"),  QString(), STRING,  Qt::black));
    // This may as well be calculated
    schema::channel.add(GRP_JOURNAL, ch = new Channel(Journal_BMI = 0x0806, DATA,   MT_JOURNAL,  DAY, "BMI",      QObject::tr("BMI"), QObject::tr("Body Mass Index"), QObject::tr("BMI"),  QString(), DOUBLE,  Qt::black));
    schema::channel.add(GRP_JOURNAL, ch = new Channel(Journal_ZombieMeter = 0x0807, DATA,   MT_JOURNAL,  DAY, "ZombieMeter", QObject::tr("Zombie"), QObject::tr("How you feel (0 = like crap, 10 = unstoppable)"), QObject::tr("Zombie"),  QString(), DOUBLE,  Qt::black));
    schema::channel.add(GRP_JOURNAL, ch = new Channel(Bookmark_Start=0x0808, DATA,   MT_JOURNAL,  DAY, "BookmarkStart",      QObject::tr("Start"), QObject::tr("Bookmark Start"), QObject::tr("Start"),  QString(), INTEGER,  Qt::black));
    schema::channel.add(GRP_JOURNAL, ch = new Channel(Bookmark_End=0x0809, DATA,   MT_JOURNAL,  DAY, "BookmarkEnd",      QObject::tr("End"), QObject::tr("Bookmark End"), QObject::tr("End"),  QString(), DOUBLE,  Qt::black));
    schema::channel.add(GRP_JOURNAL, ch = new Channel(LastUpdated=0x080a, DATA,   MT_JOURNAL,  DAY, "LastUpdated", QObject::tr("Last Updated"), QObject::tr("Last Updated"), QObject::tr("Last Updated"),  QString(), DATETIME,  Qt::black));
    schema::channel.add(GRP_JOURNAL, ch = new Channel(Journal_Notes = 0xd000, DATA,   MT_JOURNAL,  DAY, "Journal",      QObject::tr("Journal Notes"), QObject::tr("Journal Notes"), QObject::tr("Journal"),  QString(), RICHTEXT,  Qt::black));


    //////////////////////////////////////////////////////////////////////
    // Sleep Stage Channels
    //////////////////////////////////////////////////////////////////////
    schema::channel.add(GRP_SLEEP, ch = new Channel(ZEO_SleepStage = 0x2000, WAVEFORM,   MT_SLEEPSTAGE,  SESSION, "SleepStage",
            QObject::tr("Sleep Stage"), QObject::tr("1=Awake 2=REM 3=Light Sleep 4=Deep Sleep"), QObject::tr("Sleep Stage"),  QString(), INTEGER,  Qt::darkGray));
    schema::channel.add(GRP_SLEEP, ch = new Channel(0x2001, WAVEFORM,   MT_SLEEPSTAGE,  SESSION, "ZeoBW",
            QObject::tr("Brain Wave"), QObject::tr("Brain Wave"), QObject::tr("BrainWave"),  QString(), INTEGER,  Qt::black));
    schema::channel.add(GRP_SLEEP, ch = new Channel(ZEO_Awakenings = 0x2002, DATA,   MT_SLEEPSTAGE,  SESSION, "Awakenings",  QObject::tr("Awakenings"), QObject::tr("Number of Awakenings"), QObject::tr("Awakenings"),  QString(), INTEGER,  Qt::black));
    schema::channel.add(GRP_SLEEP, ch = new Channel(ZEO_MorningFeel= 0x2003, DATA,   MT_SLEEPSTAGE,  SESSION, "MorningFeel", QObject::tr("Morning Feel"), QObject::tr("How you felt in the morning"), QObject::tr("Morning Feel"),  QString(), INTEGER,  Qt::black));
    schema::channel.add(GRP_SLEEP, ch = new Channel(ZEO_TimeInWake = 0x2004, DATA,   MT_SLEEPSTAGE,  SESSION, "TimeInWake",  QObject::tr("Time Awake"), QObject::tr("Time spent awake"), QObject::tr("Time Awake"),  STR_UNIT_Minutes, INTEGER,  Qt::black));
    schema::channel.add(GRP_SLEEP, ch = new Channel(ZEO_TimeInREM  = 0x2005, DATA,   MT_SLEEPSTAGE,  SESSION, "TimeInREM",    QObject::tr("Time In REM Sleep"), QObject::tr("Time spent in REM Sleep"), QObject::tr("Time in REM Sleep"),  STR_UNIT_Minutes, INTEGER,  Qt::black));
    schema::channel.add(GRP_SLEEP, ch = new Channel(ZEO_TimeInLight= 0x2006, DATA,   MT_SLEEPSTAGE,  SESSION, "TimeInLight",QObject::tr("Time In Light Sleep"), QObject::tr("Time spent in light sleep"), QObject::tr("Time in Light Sleep"),  STR_UNIT_Minutes, INTEGER,  Qt::black));
    schema::channel.add(GRP_SLEEP, ch = new Channel(ZEO_TimeInDeep = 0x2007, DATA,   MT_SLEEPSTAGE,  SESSION, "TimeInDeep",   QObject::tr("Time In Deep Sleep"), QObject::tr("Time spent in deep sleep"), QObject::tr("Time in Deep Sleep"),  STR_UNIT_Minutes, INTEGER,  Qt::black));
    schema::channel.add(GRP_SLEEP, ch = new Channel(ZEO_TimeToZ    = 0x2008, DATA,   MT_SLEEPSTAGE,  SESSION, "TimeToZ",      QObject::tr("Time to Sleep"), QObject::tr("Time taken to get to sleep"), QObject::tr("Time to Sleep"),  STR_UNIT_Minutes, INTEGER,  Qt::black));
    schema::channel.add(GRP_SLEEP, ch = new Channel(ZEO_ZQ         = 0x2009, DATA,   MT_SLEEPSTAGE,  SESSION, "ZeoZQ", QObject::tr("Zeo ZQ"), QObject::tr("Zeo sleep quality measurement"), QObject::tr("ZEO ZQ"),  QString(), INTEGER,  Qt::black));

    NoChannel = 0;
    /*
    CPAP_BrokenSummary = schema::channel["BrokenSummary"].id();
    CPAP_BrokenWaveform = schema::channel["BrokenWaveform"].id();
    */

//  <channel id="0x111e" class="data" name="TestChan1" details="Debugging Channel #1" label="Test #1" unit="" color="pink"/>
//  <channel id="0x111f" class="data" name="TestChan2" details="Debugging Channel #2" label="Test #2" unit="" color="blue"/>

    schema::channel.add(GRP_CPAP, ch=new Channel(CPAP_Test1 = 0x111e, DATA, MT_CPAP, SESSION, STR_GRAPH_TestChan1, QObject::tr("Debugging channel #1"), QObject::tr("For internal use only"), QObject::tr("Test #1"),  QString(), INTEGER,  QColor("pink")));
    schema::channel.add(GRP_CPAP, ch=new Channel(CPAP_Test2 = 0x111f, DATA, MT_CPAP, SESSION, STR_GRAPH_TestChan2, QObject::tr("Debugging channel #2"), QObject::tr("For internal use only"), QObject::tr("Test #2"),  QString(), INTEGER,  Qt::blue));

    RMS9_E01 = schema::channel["RMS9_E01"].id();
    RMS9_E02 = schema::channel["RMS9_E02"].id();
    RMS9_SetPressure = schema::channel["SetPressure"].id(); // TODO: this isn't needed anymore
    CPAP_HumidSetting = schema::channel["HumidSet"].id();
    INTELLIPAP_Unknown1 = schema::channel["IntUnk1"].id();
    INTELLIPAP_Unknown2 = schema::channel["IntUnk2"].id();

    schema::channel[CPAP_Leak].setShowInOverview(true);
    schema::channel[CPAP_RespRate].setShowInOverview(true);
    schema::channel[CPAP_MinuteVent].setShowInOverview(true);
    schema::channel[CPAP_TidalVolume].setShowInOverview(true);
    schema::channel[CPAP_CSR].setShowInOverview(true);
    schema::channel[CPAP_PB].setShowInOverview(true);
    schema::channel[CPAP_LargeLeak].setShowInOverview(true);
    schema::channel[CPAP_FLG].setShowInOverview(true);

    // Identify the channels that contribute to AHI calculation
    // When adding more AHI-contributing channels,
    // 1) update this list
    // 2) Update setOrders() above
    // 3) Search source for CPAP_Obstructive to look for possible other places to add new channel
    // 4) Search for AllAhiChannels to find all uses of the AHI-contributing channel list
    ahiChannels.append(CPAP_ClearAirway);
    ahiChannels.append(CPAP_AllApnea);
    ahiChannels.append(CPAP_Obstructive);
    ahiChannels.append(CPAP_Hypopnea);
    ahiChannels.append(CPAP_Apnea);
}


void done()
{
    schema::channel.names.clear();
    for (auto & c : schema::channel.channels.values()) {
        delete c;
    }
    schema::channel.channels.clear();
    schema::channel.groups.clear();

    schema_initialized = false;
}


void resetChannels()
{
    done();
    init();

    QList<MachineLoader *> list = GetLoaders();
    for (int i=0; i< list.size(); ++i) {
        MachineLoader * loader = list.at(i);
        loader->initChannels();
    }
    setOrders();
}

Channel::Channel(ChannelID id, ChanType type, MachineType machtype, ScopeType scope, QString code, QString fullname,
                 QString description, QString label, QString unit, DataType datatype, QColor color, int link):
    m_id(id),
    m_type(type),
    m_machtype(machtype),
    m_scope(scope),
    m_code(code),
    m_fullname(fullname),
    m_description(description),
    m_label(label),
    m_unit(unit),
    m_datatype(datatype),
    m_defaultcolor(color),
    m_link(link),
    m_upperThreshold(0),
    m_lowerThreshold(0),
    m_upperThresholdColor(Qt::red),
    m_lowerThresholdColor(Qt::green),
    m_enabled(true),
    m_order(255)
{
    if (type == WAVEFORM) {
        calc[Calc_Min] = ChannelCalc(id, Calc_Min, adjustcolor(color, 0.25f, 1.0f, 1.3f), false);
        calc[Calc_Middle] = ChannelCalc(id, Calc_Middle, adjustcolor(color, 1.3f, 1.0f, 1.0f), false);
        calc[Calc_Perc] = ChannelCalc(id, Calc_Perc, adjustcolor(color, 1.1f, 1.2f, 1.0f), false);
        calc[Calc_Max] = ChannelCalc(id, Calc_Max,  adjustcolor(color, 0.5f, 1.2f, 1.0f), false);
        calc[Calc_Zero] = ChannelCalc(id, Calc_Zero, Qt::red, false);
        calc[Calc_LowerThresh] = ChannelCalc(id, Calc_LowerThresh, Qt::blue, false);
        calc[Calc_UpperThresh] = ChannelCalc(id, Calc_UpperThresh, Qt::red, false);
    }
    m_showInOverview = false;

    default_fullname = fullname;
    default_label = label;
    default_description = description;

}
bool Channel::isNull()
{
    return (this == &EmptyChannel);
}

ChannelList::ChannelList()
    : m_doctype("channels")
{
}
ChannelList::~ChannelList()
{
    for (auto & chan : channels) {
        delete chan;
    }
}
bool ChannelList::Load(QString filename)
{
    QDomDocument doc(m_doctype);
    QFile file(filename);
    qDebug() << "Opening " << filename;

    if (!file.open(QIODevice::ReadOnly)) {
//        qWarning() << "Could not open" << filename;
        qWarning() << "Could not open" << filename << "for reading, error code" << file.error() << file.errorString();
        return false;
    }

    QString errorMsg;
    int errorLine;

    if (!doc.setContent(&file, false, &errorMsg, &errorLine)) {
        qWarning() << "Invalid XML Content in" << filename;
        qWarning() << "Error line" << errorLine << ":" << errorMsg;
        return false;
    }

    file.close();


    QDomElement root = doc.documentElement();

    if (root.tagName().toLower() != "channels") {
        return false;
    }

    QString language = root.attribute("language", "en");
    QString version = root.attribute("version", "");

    if (version.isEmpty()) {
        qWarning() << "No Version Field in" << m_doctype << "Schema, assuming 1.0" << filename;
        version = "1.0";
    }

    qDebug() << "Processing xml file:" << m_doctype << language << version;
    QDomNodeList grp = root.elementsByTagName("group");
    QDomNode node, n, ch;
    QDomElement e;

    bool ok;
    int id, linkid;
    QString chantype, scopestr, typestr, name, group, idtxt, details, label, unit, datatypestr, defcolor, link;
    ChanType type;
    DataType datatype;
    Channel *chan;
    QColor color;
    //bool multi;
    ScopeType scope;
    int line;

    for (int i = 0; i < grp.size(); i++) {
        node = grp.at(i);
        group = node.toElement().attribute("name");
        //qDebug() << "Group Name" << group;
        // Why do I have to skip the first node here? (shows up empty)
        n = node.firstChildElement();

        while (!n.isNull()) {
            line = n.lineNumber();
            e = n.toElement();

            if (e.nodeName().toLower() != "channel") {
                qWarning() << "Ignoring unrecognized schema type " << e.nodeName() << "in" << filename << "line" <<
                           line;
                continue;
            }

            ch = n.firstChild();
            n = n.nextSibling();

            idtxt = e.attribute("id");
            id = idtxt.toInt(&ok, 16);

            if (!ok) {
                qWarning() << "Dodgy ID number " << e.nodeName() << "in" << filename << "line" << line;
                continue;
            }

            chantype = e.attribute("class", "data").toLower();

            if (!ChanTypes.contains(chantype)) {
                qWarning() << "Dodgy class " << chantype << "in" << filename << "line" << line;
                continue;
            }

            type = ChanTypes[chantype];

            scopestr = e.attribute("scope", "session");

            if (scopestr.at(0) == QChar('!')) {
                scopestr = scopestr.mid(1);
                //multi=true;
            } //multi=false;

            if (!Scopes.contains(scopestr)) {
                qWarning() << "Dodgy Scope " << scopestr << "in" << filename << "line" << line;
                continue;
            }

            scope = Scopes[scopestr];

            name = e.attribute("name", "");
            details = e.attribute("details", "");
            label = e.attribute("label", "");

            if (name.isEmpty() || details.isEmpty() || label.isEmpty()) {
                qWarning() << "Missing name,details or label attribute in" << filename << "line" << line;
                continue;
            }

            unit = e.attribute("unit");

            defcolor = e.attribute("color", "black");
            color = QColor(defcolor);

            if (!color.isValid()) {
                qWarning() << "Invalid Color " << defcolor << "in" << filename << "line" << line;
                color = Qt::black;
            }

            datatypestr = e.attribute("type", "").toLower();

            link = e.attribute("link", "");

            if (!link.isEmpty()) {
                linkid = link.toInt(&ok, 16);

                if (!ok) {
                    qWarning() << "Dodgy Link ID number " << e.nodeName() << "in" << filename << " line" << line;
                }
            } else { linkid = 0; }

            if (DataTypes.contains(datatypestr)) {
                datatype = DataTypes[typestr];
            } else {
                qWarning() << "Ignoring unrecognized schema datatype in" << filename << "line" << line;
                continue;
            }

            if (channels.contains(id)) {
                qWarning() << "Schema already contains id" << id << "in" << filename << "line" << line;
                continue;
            }

            if (names.contains(name)) {
                qWarning() << "Schema already contains name" << name << "in" << filename << "line" << line;
                continue;
            }

            chan = new Channel(id, type, MT_UNKNOWN, scope, name, name, details, label, unit, datatype, color, linkid);
            channels[id] = chan;
            names[name] = chan;
            //qDebug() << "Channel" << id << name << label;
            groups[group][name] = chan;

            if (linkid > 0) {
                if (channels.contains(linkid)) {
                    Channel *it = channels[linkid];
                    it->m_links.push_back(chan);
                    //int i=0;
                } else {
                    qWarning() << "Linked channel" << name << ":" << linkid << "should be defined first in" << filename << "line" << line;
                }
            }

            // process children
            while (!ch.isNull()) {
                e = ch.toElement();
                QString sub = ch.nodeName().toLower();
                QString id2str, name2str;
                int id2;

                if (sub == "option") {
                    id2str = e.attribute("id");
                    id2 = id2str.toInt(&ok, 10);
                    name2str = e.attribute("value");
                    //qDebug() << sub << id2 << name2str;
                    chan->addOption(id2, name2str);
                } else if (sub == "color") {
                }

                ch = ch.nextSibling();
            }
        }
    }

    return true;
}

void ChannelList::add(QString group, Channel *chan)
{
    if (chan == nullptr) {
        qCritical() << "ChannelList::add called with null chan object";
        return;
    }

    if (channels.contains(chan->id())) {
        qCritical() << "Channels already contains id" << chan->id() << chan->code();
        return;
    }

    if (names.contains(chan->code())) {
        qCritical() << "Channels already contains name" << chan->id() << chan->code();
        return;
    }

    channels[chan->id()] = chan;
    names[chan->code()] = chan;
    groups[group][chan->code()] = chan;

    if (channels.contains(chan->linkid())) {
        Channel *it = channels[chan->linkid()];
        it->m_links.push_back(chan);
        //int i=0;
    } else {
        if (chan->linkid()>0) {
            qWarning() << "Linked channel must be defined first for" << chan->code();
        }
    }
}

bool ChannelList::Save(QString filename)
{
    if (filename.isEmpty())
        return false;

    qDebug() << "In ChannelList::Save() saving " << filename;;

    QDomDocument doc("channels");

    QDomElement droot = doc.createElement(STR_AppName);
    doc.appendChild(droot);

    QDomElement root = doc.createElement("channels");
    droot.appendChild(root);


    for (auto git=groups.begin(), end=groups.end(); git != end; ++git) {
        auto & chanlist = git.value();

        QDomElement grp = doc.createElement("group");
        grp.setAttribute("name", git.key());
        root.appendChild(grp);
        for (auto it = chanlist.begin(), cend=chanlist.end(); it!=cend; ++it) {
            Channel * chan = it.value();
            QDomElement cn = doc.createElement("channel");
            cn.setAttribute("id", chan->id());
            cn.setAttribute("code", it.key());
            cn.setAttribute("label", chan->label());
            cn.setAttribute("name", chan->fullname());
            cn.setAttribute("description", chan->description());
            cn.setAttribute("color", chan->defaultColor().name());
            cn.setAttribute("upper", chan->upperThreshold());
            cn.setAttribute("lower", chan->lowerThreshold());
            cn.setAttribute("order", chan->order());
            cn.setAttribute("type", chan->type());
            cn.setAttribute("datatype", chan->datatype());
            cn.setAttribute("overview", chan->showInOverview());
            for (auto op=chan->m_options.begin(), opend=chan->m_options.end(); op!=opend; ++op) {
                QDomElement c2 = doc.createElement("option");
                c2.setAttribute("key", op.key());
                c2.setAttribute("value", op.value());
                cn.appendChild(c2);
            }

            //cn.appendChild(doc.createTextNode(i.value().toDateTime().toString("yyyy-MM-dd HH:mm:ss")));
            grp.appendChild(cn);
        }
    }

    QFile file(filename);

    if (!file.open(QIODevice::WriteOnly)) {
        qWarning() << "Could not open" << filename << "for writing, error code" << file.error() << file.errorString();
        return false;
    }

    QTextStream ts(&file);
    ts << doc.toString();
    file.close();

    return true;
}

} // namespace

QString ChannelCalc::label()
{
    QString lab = schema::channel[code].label();
    QString m_label;
    switch(type) {
    case Calc_Min:
        m_label = QString("%1 %2").arg(STR_TR_Min).arg(lab);
        break;
    case Calc_Middle:
        m_label = Day::calcMiddleLabel(code);
        break;
    case Calc_Perc:
        m_label = Day::calcPercentileLabel(code);
        break;
    case Calc_Max:
        m_label = Day::calcMaxLabel(code);
        break;
    case Calc_Zero:
        m_label = QObject::tr("Zero");
        break;
    case Calc_UpperThresh:
        m_label = QString("%1 %2").arg(lab).arg(QObject::tr("Upper Threshold"));
        break;
    case Calc_LowerThresh:
        m_label = QString("%1 %2").arg(lab).arg(QObject::tr("Lower Threshold"));
        break;
    }
    return m_label;

}

//typedef schema::Channel * ChannelID;
