#include <unistd.h>
#include <zlib.h>
#undef Z_NULL
#define Z_NULL nullptr

// Qt headers
#include <QDir>
#include <QFileInfo>
#include <QByteArray>
#include <QNetworkReply>
#include <QAuthenticator>

// MythTV headers
#include "datadirect.h"
#include "sourceutil.h"
#include "channelutil.h"
#include "frequencytables.h"
#include "listingsources.h"
#include "mythmiscutil.h"
#include "mythcontext.h"
#include "mythdb.h"
#include "mythlogging.h"
#include "mythversion.h"
#include "mythdate.h"
#include "dbutil.h"
#include "mythsystemlegacy.h"
#include "exitcodes.h"
#include "mythdownloadmanager.h"
#include "mythtvexp.h"
#include "mythdate.h"

#define LOC QString("DataDirect: ")

static QMutex  user_agent_lock;
static QString user_agent;

static QMutex lineup_type_lock;
static QMap<QString,uint> lineupid_to_srcid;
static QMap<uint,QString> srcid_to_type;

static void    set_lineup_type(const QString &lineupid, const QString &type);
static QString get_lineup_type(uint sourceid);
static QString get_setting(QString line, QString key);
static bool    has_setting(QString line, QString key);
static QString html_escape(QString str);
static void    get_atsc_stuff(QString channum, int freqid,
                              int &major, int &minor, long long &freq);
static QString process_dd_station(uint sourceid,
                                  QString  chan_major, QString  chan_minor,
                                  QString &tvformat,   uint    &freqid);
static uint    update_channel_basic(uint    sourceid,   bool    insert,
                                    QString xmltvid,    QString callsign,
                                    QString name,       uint    freqid,
                                    QString chan_major, QString chan_minor);
void authenticationCallback(QNetworkReply *reply, QAuthenticator *auth,
                            void *arg);
QByteArray gUncompress(const QByteArray &data);

DataDirectStation::DataDirectStation(void) :
    stationid(""),              callsign(""),
    stationname(""),            affiliate(""),
    fccchannelnumber("")
{
}

DataDirectLineup::DataDirectLineup() :
    lineupid(""), name(""), displayname(""), type(""), postal(""), device("")
{
}

DataDirectLineupMap::DataDirectLineupMap() :
    lineupid(""), stationid(""), channel(""), channelMinor("")
{
}

DataDirectSchedule::DataDirectSchedule() :
    programid(""),              stationid(""),
    time(),                     duration(),
    repeat(false),              isnew(false),
    stereo(false),              dolby(false),
    subtitled(false),
    hdtv(false),                closecaptioned(false),
    tvrating(""),
    partnumber(0),              parttotal(0)
{
}

DataDirectProgram::DataDirectProgram() :
    programid(""),  seriesid(""),      title(""),
    subtitle(""),   description(""),   mpaaRating(""),
    starRating(""), duration(),        year(""),
    showtype(""),   colorcode(""),     originalAirDate(),
    syndicatedEpisodeNumber("")
{
}

DataDirectProductionCrew::DataDirectProductionCrew() :
    programid(""), role(""), givenname(""), surname(""), fullname("")
{
}

DataDirectGenre::DataDirectGenre() :
    programid(""), gclass(""), relevance("")
{
}

// XXX Program duration should be stored as seconds, not as a QTime.
//     limited to 24 hours this way.

bool DDStructureParser::startElement(const QString &pnamespaceuri,
                                     const QString &plocalname,
                                     const QString &pqname,
                                     const QXmlAttributes &pxmlatts)
{
    (void)pnamespaceuri;
    (void)plocalname;

    currtagname = pqname;
    if (currtagname == "xtvd")
    {
        QString   beg   = pxmlatts.value("from");
        QDateTime begts = MythDate::fromString(beg);
        parent.SetDDProgramsStartAt(begts);

        QString   end   = pxmlatts.value("to");
        QDateTime endts = MythDate::fromString(end);
        parent.SetDDProgramsEndAt(endts);
    }
    else if (currtagname == "station")
    {
        curr_station.Reset();
        curr_station.stationid = pxmlatts.value("id");
    }
    else if (currtagname == "lineup")
    {
        curr_lineup.Reset();
        curr_lineup.name = pxmlatts.value("name");
        curr_lineup.type = pxmlatts.value("type");
        curr_lineup.device = pxmlatts.value("device");
        curr_lineup.postal = pxmlatts.value("postalCode");
        curr_lineup.lineupid = pxmlatts.value("id");
        curr_lineup.displayname = curr_lineup.name + "-" + curr_lineup.type +
            "-" + curr_lineup.device + "-" +
            curr_lineup.postal + "-" +
            curr_lineup.lineupid;

        if (curr_lineup.lineupid.isEmpty())
        {
            curr_lineup.lineupid = curr_lineup.name + curr_lineup.postal +
                curr_lineup.device + curr_lineup.type;
        }
    }
    else if (currtagname == "map")
    {
        int tmpindex;
        curr_lineupmap.Reset();
        curr_lineupmap.lineupid = curr_lineup.lineupid;
        curr_lineupmap.stationid = pxmlatts.value("station");
        curr_lineupmap.channel = pxmlatts.value("channel");
        tmpindex = pxmlatts.index("channelMinor"); // for ATSC
        if (tmpindex != -1)
            curr_lineupmap.channelMinor = pxmlatts.value(tmpindex);
    }
    else if (currtagname == "schedule")
    {
        curr_schedule.Reset();
        curr_schedule.programid = pxmlatts.value("program");
        curr_schedule.stationid = pxmlatts.value("station");

        curr_schedule.time = MythDate::fromString(pxmlatts.value("time"));
        QString durstr = pxmlatts.value("duration");
        curr_schedule.duration = QTime(durstr.mid(2, 2).toInt(),
                                       durstr.mid(5, 2).toInt(), 0, 0);

        curr_schedule.repeat = (pxmlatts.value("repeat") == "true");
        curr_schedule.isnew = (pxmlatts.value("new") == "true");
        curr_schedule.stereo = (pxmlatts.value("stereo") == "true");
        curr_schedule.dolby = (pxmlatts.value("dolby") == "Dolby" ||
                               pxmlatts.value("dolby") == "Dolby Digital");
        curr_schedule.subtitled = (pxmlatts.value("subtitled") == "true");
        curr_schedule.hdtv = (pxmlatts.value("hdtv") == "true");
        curr_schedule.closecaptioned = (pxmlatts.value("closeCaptioned") ==
                                        "true");
        curr_schedule.tvrating = pxmlatts.value("tvRating");
    }
    else if (currtagname == "part")
    {
        curr_schedule.partnumber = pxmlatts.value("number").toInt();
        curr_schedule.parttotal = pxmlatts.value("total").toInt();
    }
    else if (currtagname == "program")
    {
        curr_program.Reset();
        curr_program.programid = pxmlatts.value("id");
    }
    else if (currtagname == "crew")
    {
        curr_program.Reset();
        lastprogramid = pxmlatts.value("program");
    }
    else if (currtagname == "programGenre")
    {
        curr_genre.Reset();
        lastprogramid = pxmlatts.value("program");
    }

    return true;
}

bool DDStructureParser::endElement(const QString &pnamespaceuri,
                                   const QString &plocalname,
                                   const QString &pqname)
{
    (void)pnamespaceuri;
    (void)plocalname;

    MSqlQuery query(MSqlQuery::DDCon());

    if (pqname == "station")
    {
        parent.m_stations[curr_station.stationid] = curr_station;

        query.prepare(
            "INSERT INTO dd_station "
            "     ( stationid,  callsign,  stationname, "
            "       affiliate,  fccchannelnumber)       "
            "VALUES "
            "     (:STATIONID, :CALLSIGN, :STATIONNAME, "
            "      :AFFILIATE, :FCCCHANNUM)");

        query.bindValue(":STATIONID",   curr_station.stationid);
        query.bindValue(":CALLSIGN",    curr_station.callsign);
        query.bindValue(":STATIONNAME", curr_station.stationname);
        query.bindValue(":AFFILIATE",   curr_station.affiliate);
        query.bindValue(":FCCCHANNUM",  curr_station.fccchannelnumber);

        if (!query.exec())
            MythDB::DBError("Inserting into dd_station", query);
    }
    else if (pqname == "lineup")
    {
        set_lineup_type(curr_lineup.lineupid, curr_lineup.type);

        parent.m_lineups.push_back(curr_lineup);

        query.prepare(
            "INSERT INTO dd_lineup "
            "     ( lineupid,  name,  type,  device,  postal) "
            "VALUES "
            "     (:LINEUPID, :NAME, :TYPE, :DEVICE, :POSTAL)");

        query.bindValue(":LINEUPID",    curr_lineup.lineupid);
        query.bindValue(":NAME",        curr_lineup.name);
        query.bindValue(":TYPE",        curr_lineup.type);
        query.bindValue(":DEVICE",      curr_lineup.device);
        query.bindValue(":POSTAL",      curr_lineup.postal);

        if (!query.exec())
            MythDB::DBError("Inserting into dd_lineup", query);
    }
    else if (pqname == "map")
    {
        parent.m_lineupmaps[curr_lineupmap.lineupid].push_back(curr_lineupmap);

        query.prepare(
            "INSERT INTO dd_lineupmap "
            "     ( lineupid,  stationid,  channel,  channelMinor) "
            "VALUES "
            "     (:LINEUPID, :STATIONID, :CHANNEL, :CHANNELMINOR)");

        query.bindValue(":LINEUPID",    curr_lineupmap.lineupid);
        query.bindValue(":STATIONID",   curr_lineupmap.stationid);
        query.bindValue(":CHANNEL",     curr_lineupmap.channel);
        query.bindValue(":CHANNELMINOR",curr_lineupmap.channelMinor);
        if (!query.exec())
            MythDB::DBError("Inserting into dd_lineupmap", query);
    }
    else if (pqname == "schedule")
    {
        QDateTime endtime = curr_schedule.time.addSecs(
            MythDate::toSeconds( curr_schedule.duration ));

        query.prepare(
            "INSERT INTO dd_schedule "
            "     ( programid,      stationid,   scheduletime,   "
            "       duration,       isrepeat,    stereo,         "
            "       dolby,          subtitled,   hdtv,           "
            "       closecaptioned, tvrating,    partnumber,      "
            "       parttotal,      endtime,     isnew) "
            "VALUES "
            "     (:PROGRAMID, :STATIONID,  :TIME,           "
            "      :DURATION,  :ISREPEAT,   :STEREO,         "
            "      :DOLBY,     :SUBTITLED,  :HDTV,           "
            "      :CAPTIONED, :TVRATING,   :PARTNUMBER,     "
            "      :PARTTOTAL, :ENDTIME,    :ISNEW)");

        query.bindValue(":PROGRAMID",   curr_schedule.programid);
        query.bindValue(":STATIONID",   curr_schedule.stationid);
        query.bindValue(":TIME",        curr_schedule.time);
        query.bindValue(":DURATION",    curr_schedule.duration);
        query.bindValue(":ISREPEAT",    curr_schedule.repeat);
        query.bindValue(":STEREO",      curr_schedule.stereo);
        query.bindValue(":DOLBY",       curr_schedule.dolby);
        query.bindValue(":SUBTITLED",   curr_schedule.subtitled);
        query.bindValue(":HDTV",        curr_schedule.hdtv);
        query.bindValue(":CAPTIONED",   curr_schedule.closecaptioned);
        query.bindValue(":TVRATING",    curr_schedule.tvrating);
        query.bindValue(":PARTNUMBER",  curr_schedule.partnumber);
        query.bindValue(":PARTTOTAL",   curr_schedule.parttotal);
        query.bindValue(":ENDTIME",     endtime);
        query.bindValue(":ISNEW",       curr_schedule.isnew);

        if (!query.exec())
            MythDB::DBError("Inserting into dd_schedule", query);
    }
    else if (pqname == "program")
    {
        float staravg = 0.0;
        if (!curr_program.starRating.isEmpty())
        {
            int fullstarcount = curr_program.starRating.count("*");
            int halfstarcount = curr_program.starRating.count("+");
            staravg = (fullstarcount + (halfstarcount * .5)) / 4;
        }

        QString cat_type = "";
        QString prefix = curr_program.programid.left(2);

        if (prefix == "MV")
            cat_type = "movie";
        else if (prefix == "SP")
            cat_type = "sports";
        else if (prefix == "EP" ||
                 curr_program.showtype.contains("series", Qt::CaseInsensitive))
            cat_type = "series";
        else
            cat_type = "tvshow";

        query.prepare(
            "INSERT INTO dd_program "
            "     ( programid,    title,       subtitle,       "
            "       description,  showtype,    category_type,  "
            "       mpaarating,   starrating,  stars,          "
            "       runtime,      year,        seriesid,       "
            "       colorcode,    syndicatedepisodenumber, originalairdate) "
            "VALUES "
            "     (:PROGRAMID,   :TITLE,      :SUBTITLE,       "
            "      :DESCRIPTION, :SHOWTYPE,   :CATTYPE,        "
            "      :MPAARATING,  :STARRATING, :STARS,          "
            "      :RUNTIME,     :YEAR,       :SERIESID,       "
            "      :COLORCODE,   :SYNDNUM,    :ORIGAIRDATE)    ");

        query.bindValue(":PROGRAMID",   curr_program.programid);
        query.bindValue(":TITLE",       curr_program.title);
        query.bindValue(":SUBTITLE",    curr_program.subtitle);
        query.bindValue(":DESCRIPTION", curr_program.description);
        query.bindValue(":SHOWTYPE",    curr_program.showtype);
        query.bindValue(":CATTYPE",     cat_type);
        query.bindValue(":MPAARATING",  curr_program.mpaaRating);
        query.bindValue(":STARRATING",  curr_program.starRating);
        query.bindValue(":STARS",       staravg);
        query.bindValue(":RUNTIME",     curr_program.duration);
        query.bindValue(":YEAR",        curr_program.year);
        query.bindValue(":SERIESID",    curr_program.seriesid);
        query.bindValue(":COLORCODE",   curr_program.colorcode);
        query.bindValue(":SYNDNUM",     curr_program.syndicatedEpisodeNumber);
        query.bindValue(":ORIGAIRDATE", curr_program.originalAirDate);

        if (!query.exec())
            MythDB::DBError("Inserting into dd_program", query);
    }
    else if (pqname == "member")
    {
        QString roleunderlines = curr_productioncrew.role.replace(" ", "_");

        QString fullname = curr_productioncrew.givenname;
        if (!fullname.isEmpty())
            fullname += " ";
        fullname += curr_productioncrew.surname;

        query.prepare(
            "INSERT INTO dd_productioncrew "
            "       ( programid,  role,  givenname,  surname,  fullname) "
            "VALUES (:PROGRAMID, :ROLE, :GIVENNAME, :SURNAME, :FULLNAME)");

        query.bindValue(":PROGRAMID",   lastprogramid);
        query.bindValue(":ROLE",        roleunderlines);
        query.bindValue(":GIVENNAME",   curr_productioncrew.givenname);
        query.bindValue(":SURNAME",     curr_productioncrew.surname);
        query.bindValue(":FULLNAME",    fullname);

        if (!query.exec())
            MythDB::DBError("Inserting into dd_productioncrew", query);

        curr_productioncrew.givenname = "";
        curr_productioncrew.surname = "";
    }
    else if (pqname == "genre")
    {
        query.prepare(
            "INSERT INTO dd_genre "
            "       ( programid,  class,  relevance) "
            "VALUES (:PROGRAMID, :CLASS, :RELEVANCE)");

        query.bindValue(":PROGRAMID",   lastprogramid);
        query.bindValue(":CLASS",       curr_genre.gclass);
        query.bindValue(":RELEVANCE",   curr_genre.relevance);

        if (!query.exec())
            MythDB::DBError("Inserting into dd_genre", query);
    }

    return true;
}

bool DDStructureParser::startDocument()
{
    parent.CreateTempTables();
    return true;
}

bool DDStructureParser::endDocument()
{
    return true;
}

bool DDStructureParser::characters(const QString& pchars)
{
#if 0
    LOG(VB_GENERAL, LOG_DEBUG, LOC + "Characters : " + pchars);
#endif
    if (pchars.trimmed().isEmpty())
        return true;

    if (currtagname == "message")
    {
        if (pchars.contains("expire"))
        {
            QString ExtractDateFromMessage = pchars.right(20);
            QDateTime EDFM = MythDate::fromString(ExtractDateFromMessage);
            QString SDDateFormat = GetMythDB()->GetSetting("DateFormat",
                                                           "ddd d MMMM");
            // Ensure we show the year when it's important, regardless of
            // specified DateFormat
            if ((!SDDateFormat.contains('y')) &&
                (EDFM.date().year() != MythDate::current().date().year()))
            {
                SDDateFormat.append(" (yyyy)");
            }
            QString dateFormat = QString("%1 %2")
                    .arg(SDDateFormat)
                    .arg(GetMythDB()->GetSetting("TimeFormat", "hh:mm"));
            QString ExpirationDate = EDFM.toString(dateFormat);

            QString ExpirationDateMessage = "Your subscription expires on " +
                ExpirationDate;

            QDateTime curTime = MythDate::current();
            if (curTime.daysTo(EDFM) <= 5)
            {
                LOG(VB_GENERAL, LOG_WARNING, LOC + QString("WARNING: ") +
                        ExpirationDateMessage);
            }
            else
            {
                LOG(VB_GENERAL, LOG_INFO, LOC + ExpirationDateMessage);
            }

            gCoreContext->SaveSettingOnHost("DataDirectMessage",
                                            ExpirationDateMessage,
                                            nullptr);
        }
    }
    if (currtagname == "callSign")
        curr_station.callsign = pchars;
    else if (currtagname == "name")
        curr_station.stationname = pchars;
    else if (currtagname == "affiliate")
        curr_station.affiliate = pchars;
    else if (currtagname == "fccChannelNumber")
        curr_station.fccchannelnumber = pchars;
    else if (currtagname == "title")
        curr_program.title = pchars;
    else if (currtagname == "subtitle")
        curr_program.subtitle = pchars;
    else if (currtagname == "description")
        curr_program.description = pchars;
    else if (currtagname == "showType")
        curr_program.showtype = pchars;
    else if (currtagname == "series")
        curr_program.seriesid = pchars;
    else if (currtagname == "colorCode")
        curr_program.colorcode = pchars;
    else if (currtagname == "mpaaRating")
        curr_program.mpaaRating = pchars;
    else if (currtagname == "starRating")
        curr_program.starRating = pchars;
    else if (currtagname == "year")
        curr_program.year = pchars;
    else if (currtagname == "syndicatedEpisodeNumber")
        curr_program.syndicatedEpisodeNumber = pchars;
    else if (currtagname == "runTime")
    {
        QString runtimestr = pchars;
        QTime runtime = QTime(runtimestr.mid(2,2).toInt(),
                              runtimestr.mid(5,2).toInt(), 0, 0);
        curr_program.duration = runtime;
    }
    else if (currtagname == "originalAirDate")
    {
        QDate airdate = QDate::fromString(pchars, Qt::ISODate);
        curr_program.originalAirDate = airdate;
    }
    else if (currtagname == "role")
        curr_productioncrew.role = pchars;
    else if (currtagname == "givenname")
        curr_productioncrew.givenname = pchars;
    else if (currtagname == "surname")
        curr_productioncrew.surname = pchars;
    else if (currtagname == "class")
        curr_genre.gclass = pchars;
    else if (currtagname == "relevance")
        curr_genre.relevance = pchars;

    return true;
}

DataDirectProcessor::DataDirectProcessor(uint lp, QString user, QString pass) :
    m_listingsProvider(lp % DD_PROVIDER_COUNT),
    m_userid(user),                   m_password(pass),
    m_tmpDir("/tmp"),                 m_cacheData(false),
    m_inputFilename(""),              m_cookieFileDT()
{
    {
        QMutexLocker locker(&user_agent_lock);
        QString mythVersion = MYTH_SOURCE_VERSION;
        if (mythVersion.startsWith("v"))
            mythVersion = mythVersion.right(mythVersion.length() - 1); // Trim off the leading 'v'
        user_agent = QString("MythTV/%1")
            .arg(mythVersion);
    }

    DataDirectURLs urls0(
        "Tribune Media Zap2It",
        "http://datadirect.webservices.zap2it.com/tvlistings/xtvdService",
        "http://labs.zap2it.com",
        "/ztvws/ztvws_login/1,1059,TMS01-1,00.html");
    DataDirectURLs urls1(
        "Schedules Direct",
        "http://dd.schedulesdirect.org"
        "/schedulesdirect/tvlistings/xtvdService",
        "http://schedulesdirect.org",
        "/login/index.php");
    m_providers.push_back(urls0);
    m_providers.push_back(urls1);
}

DataDirectProcessor::~DataDirectProcessor()
{
    LOG(VB_GENERAL, LOG_INFO, LOC + "Deleting temporary files");

    if (!m_tmpPostFile.isEmpty())
    {
        QByteArray tmp = m_tmpPostFile.toLatin1();
        unlink(tmp.constData());
    }

    if (!m_tmpResultFile.isEmpty())
    {
        QByteArray tmp = m_tmpResultFile.toLatin1();
        unlink(tmp.constData());
    }

    if (!m_tmpDDPFile.isEmpty())
    {
        QByteArray tmp = m_tmpDDPFile.toLatin1();
        unlink(tmp.constData());
    }

    if (!m_cookieFile.isEmpty())
    {
        QByteArray tmp = m_cookieFile.toLatin1();
        unlink(tmp.constData());
    }

    QDir d(m_tmpDir, "mythtv_dd_cache_*", QDir::Name,
           QDir::Files | QDir::NoSymLinks);

    for (uint i = 0; i < d.count(); i++)
    {
        QString    tmps = m_tmpDir + "/" + d[i];
        QByteArray tmpa = tmps.toLatin1();
        unlink(tmpa.constData());
    }

    if (m_tmpDir != "/tmp")
    {
        QByteArray tmp = m_tmpDir.toLatin1();
        rmdir(tmp.constData());
    }
}

void DataDirectProcessor::UpdateStationViewTable(QString lineupid)
{
    MSqlQuery query(MSqlQuery::DDCon());

    if (!query.exec("TRUNCATE TABLE dd_v_station;"))
        MythDB::DBError("Truncating temporary table dd_v_station", query);

    query.prepare(
        "INSERT INTO dd_v_station "
        "     ( stationid,            callsign,         stationname, "
        "       affiliate,            fccchannelnumber, channel,     "
        "       channelMinor) "
        "SELECT dd_station.stationid, callsign,         stationname, "
        "       affiliate,            fccchannelnumber, channel,     "
        "       channelMinor "
        "FROM dd_station, dd_lineupmap "
        "WHERE ((dd_station.stationid  = dd_lineupmap.stationid) AND "
        "       (dd_lineupmap.lineupid = :LINEUP))");

    query.bindValue(":LINEUP", lineupid);

    if (!query.exec())
        MythDB::DBError("Populating temporary table dd_v_station", query);
}

void DataDirectProcessor::UpdateProgramViewTable(uint sourceid)
{
    MSqlQuery query(MSqlQuery::DDCon());

    if (!query.exec("TRUNCATE TABLE dd_v_program;"))
        MythDB::DBError("Truncating temporary table dd_v_program", query);

    QString qstr =
        "INSERT INTO dd_v_program "
        "     ( chanid,                  starttime,       endtime,         "
        "       title,                   subtitle,        description,     "
        "       airdate,                 stars,           previouslyshown, "
        "       stereo,                  dolby,           subtitled,       "
        "       hdtv,                    closecaptioned,  partnumber,      "
        "       parttotal,               seriesid,        originalairdate, "
        "       showtype,                category_type,   colorcode,       "
        "       syndicatedepisodenumber, tvrating,        mpaarating,      "
        "       programid )      "
        "SELECT chanid,                  scheduletime,    endtime,         "
        "       title,                   subtitle,        description,     "
        "       year,                    stars,           isrepeat,        "
        "       stereo,                  dolby,           subtitled,       "
        "       hdtv,                    closecaptioned,  partnumber,      "
        "       parttotal,               seriesid,        originalairdate, "
        "       showtype,                category_type,   colorcode,       "
        "       syndicatedepisodenumber, tvrating,        mpaarating,      "
        "       dd_program.programid "
        "FROM channel, dd_schedule, dd_program "
        "WHERE ((dd_schedule.programid = dd_program.programid)  AND "
        "       (channel.xmltvid       = dd_schedule.stationid) AND "
        "       (channel.sourceid      = :SOURCEID))";

    query.prepare(qstr);

    query.bindValue(":SOURCEID", sourceid);

    if (!query.exec())
        MythDB::DBError("Populating temporary table dd_v_program", query);

    if (!query.exec("ANALYZE TABLE dd_v_program;"))
        MythDB::DBError("Analyzing table dd_v_program", query);

    if (!query.exec("ANALYZE TABLE dd_productioncrew;"))
        MythDB::DBError("Analyzing table dd_productioncrew", query);
}

int DataDirectProcessor::UpdateChannelsSafe(
    uint sourceid,
    bool insert_channels,
    bool filter_new_channels)
{
    int new_channels = 0;

    if (!SourceUtil::GetConnectionCount(sourceid))
    {
        LOG(VB_GENERAL, LOG_WARNING, LOC +
            QString("Not inserting channels into disconnected source %1.")
                .arg(sourceid));
        return -1;
    }

    if (!SourceUtil::IsProperlyConnected(sourceid, true))
        return -1;

    // Find all the channels in the dd_v_station temp table
    // where there is no channel with the same xmltvid in the
    // DB using the same source.
    MSqlQuery query(MSqlQuery::DDCon());
    query.prepare(
        "SELECT dd_v_station.stationid,   dd_v_station.callsign,         "
        "       dd_v_station.stationname, dd_v_station.fccchannelnumber, "
        "       dd_v_station.channel,     dd_v_station.channelMinor      "
        "FROM dd_v_station LEFT JOIN channel ON "
        "     dd_v_station.stationid = channel.xmltvid AND "
        "     channel.sourceid = :SOURCEID "
        "WHERE channel.chanid IS NULL");
    query.bindValue(":SOURCEID", sourceid);

    if (!query.exec())
    {
        MythDB::DBError("Selecting new channels", query);
        return -1;
    }

    bool is_encoder = (SourceUtil::IsCableCardPresent(sourceid) ||
                       SourceUtil::IsEncoder(sourceid, true) ||
                       SourceUtil::IsUnscanable(sourceid));

    while (query.next())
    {
        QString xmltvid    = query.value(0).toString();
        QString callsign   = query.value(1).toString();
        QString name       = query.value(2).toString();
        uint    freqid     = query.value(3).toUInt();
        QString chan_major = query.value(4).toString();
        QString chan_minor = query.value(5).toString();

        if (filter_new_channels && is_encoder &&
            (query.value(5).toUInt() > 0))
        {
#if 0
            LOG(VB_GENERAL, LOG_INFO, LOC +
                QString("Not adding channel %1-%2 '%3' (%4),\n\t\t\t"
                        "looks like a digital channel on an analog source.")
                    .arg(chan_major).arg(chan_minor).arg(name).arg(callsign));
#endif
            continue;
        }

        uint mods =
            update_channel_basic(sourceid, insert_channels && is_encoder,
                                 xmltvid, callsign, name, freqid,
                                 chan_major, chan_minor);

        (void) mods;
#if 0
        if (!insert_channels && !mods)
        {
            LOG(VB_GENERAL, LOG_INFO, LOC +
                QString("Not adding channel '%1' (%2).")
                    .arg(name).arg(callsign));
        }
#endif
        new_channels++;
    }

    teardown_frequency_tables();

    return new_channels;
}

bool DataDirectProcessor::UpdateChannelsUnsafe(
    uint sourceid, bool filter_new_channels)
{
    if (filter_new_channels &&
        !SourceUtil::IsProperlyConnected(sourceid, false))
    {
        return false;
    }

    MSqlQuery dd_station_info(MSqlQuery::DDCon());
    dd_station_info.prepare(
        "SELECT callsign,         stationname, stationid,"
        "       fccchannelnumber, channel,     channelMinor "
        "FROM dd_v_station");
    if (!dd_station_info.exec())
        return false;

    if (dd_station_info.size() == 0)
        return true;

    MSqlQuery chan_update_q(MSqlQuery::DDCon());
    chan_update_q.prepare(
        "UPDATE channel "
        "SET callsign  = :CALLSIGN,  name   = :NAME, "
        "    channum   = :CHANNUM,   freqid = :FREQID, "
        "    atsc_major_chan = :MAJORCHAN, "
        "    atsc_minor_chan = :MINORCHAN "
        "WHERE xmltvid = :STATIONID AND sourceid = :SOURCEID");

    bool is_encoder = (SourceUtil::IsCableCardPresent(sourceid) ||
                       SourceUtil::IsEncoder(sourceid, true) ||
                       SourceUtil::IsUnscanable(sourceid));

    while (dd_station_info.next())
    {
        uint    freqid     = dd_station_info.value(3).toUInt();
        QString chan_major = dd_station_info.value(4).toString();
        QString chan_minor = dd_station_info.value(5).toString();
        QString tvformat;
        QString channum    = process_dd_station(
            sourceid, chan_major, chan_minor, tvformat, freqid);

        if (filter_new_channels && is_encoder &&
            (dd_station_info.value(5).toUInt() > 0))
        {
#if 0
            LOG(VB_GENERAL, LOG_INFO, LOC +
                QString("Not adding channel %1-%2 '%3' (%4),\n\t\t\t"
                        "looks like a digital channel on an analog source.")
                    .arg(chan_major).arg(chan_minor)
                    .arg(dd_station_info.value(1).toString())
                    .arg(dd_station_info.value(0).toString()));
#endif
            continue;
        }

        chan_update_q.bindValue(":CALLSIGN",  dd_station_info.value(0));
        chan_update_q.bindValue(":NAME",      dd_station_info.value(1));
        chan_update_q.bindValue(":STATIONID", dd_station_info.value(2));
        chan_update_q.bindValue(":CHANNUM",   channum);
        chan_update_q.bindValue(":SOURCEID",  sourceid);
        chan_update_q.bindValue(":FREQID",    freqid);
        chan_update_q.bindValue(":MAJORCHAN", chan_major.toUInt());
        chan_update_q.bindValue(":MINORCHAN", chan_minor.toUInt());

        if (!chan_update_q.exec())
        {
            MythDB::DBError("Updating channel table", chan_update_q);
        }
    }

    return true;
}

void DataDirectProcessor::DataDirectProgramUpdate(void)
{
    MSqlQuery query(MSqlQuery::DDCon());

#if 0
    LOG(VB_GENERAL, LOG_DEBUG, LOC +
        "Adding rows to main program table from view table");
#endif
    query.prepare(
        "INSERT IGNORE INTO program "
        "  ( chanid,        starttime,   endtime,         title,           "
        "    subtitle,      description, showtype,        category,        "
        "    category_type, airdate,     stars,           previouslyshown, "
        "    stereo,        subtitled,   subtitletypes,   videoprop,       "
        "    audioprop,     hdtv,        closecaptioned,  partnumber,      "
        "    parttotal,     seriesid,    originalairdate, colorcode,       "
        "    syndicatedepisodenumber,                                      "
        "                   programid,   listingsource)                    "
        "  SELECT                                                          "
        "    dd_v_program.chanid,                                          "
        "    DATE_ADD(starttime, INTERVAL channel.tmoffset MINUTE),        "
        "    DATE_ADD(endtime, INTERVAL channel.tmoffset MINUTE),          "
        "                                                 title,           "
        "    subtitle,      description, showtype,        dd_genre.class,  "
        "    category_type, airdate,     stars,           previouslyshown, "
        "    stereo,        subtitled,                                     "
        "    (subtitled << 1 ) | closecaptioned, hdtv,                     "
        "    (dolby << 3) | stereo,                                        "
        "                   hdtv,        closecaptioned,  partnumber,      "
        "    parttotal,     seriesid,    originalairdate, colorcode,       "
        "    syndicatedepisodenumber,                                      "
        "                   dd_v_program.programid,                        "
        "                               :LSOURCE                           "
        "FROM (dd_v_program, channel) "
        "LEFT JOIN dd_genre ON "
        "  ( dd_v_program.programid = dd_genre.programid AND  "
        "    dd_genre.relevance     = '0' ) "
        "WHERE dd_v_program.chanid = channel.chanid");

    query.bindValue(":LSOURCE", kListingSourceDDSchedulesDirect);

    if (!query.exec())
        MythDB::DBError("Inserting into program table", query);

#if 0
    LOG(VB_GENERAL, LOG_DEBUG, LOC +
        "Finished adding rows to main program table");
    LOG(VB_GENERAL, LOG_DEBUG, LOC + "Adding program ratings");
#endif

    if (!query.exec("INSERT IGNORE INTO programrating (chanid, starttime, "
                    "`system`, rating) SELECT dd_v_program.chanid, "
                    "DATE_ADD(starttime, INTERVAL channel.tmoffset MINUTE), "
                    " 'MPAA', "
                    "mpaarating FROM dd_v_program, channel WHERE "
                    "mpaarating != '' AND dd_v_program.chanid = "
                    "channel.chanid"))
        MythDB::DBError("Inserting into programrating table", query);

    if (!query.exec("INSERT IGNORE INTO programrating (chanid, starttime, "
                    "`system`, rating) SELECT dd_v_program.chanid, "
                    "DATE_ADD(starttime, INTERVAL channel.tmoffset MINUTE), "
                    "'VCHIP', "
                    "tvrating FROM dd_v_program, channel WHERE tvrating != ''"
                    " AND dd_v_program.chanid = channel.chanid"))
        MythDB::DBError("Inserting into programrating table", query);

#if 0
    LOG(VB_GENERAL, LOG_DEBUG, LOC + "Finished adding program ratings");
    LOG(VB_GENERAL, LOG_DEBUG, LOC +
        "Populating people table from production crew list");
#endif

    if (!query.exec("INSERT IGNORE INTO people (name) "
                    "SELECT fullname "
                    "FROM dd_productioncrew "
                    "LEFT OUTER JOIN people "
                    "ON people.name = dd_productioncrew.fullname "
                    "WHERE people.name IS NULL;"))
        MythDB::DBError("Inserting into people table", query);

#if 0
    LOG(VB_GENERAL, LOG_INFO, LOC + "Finished adding people");
    LOG(VB_GENERAL, LOG_INFO, LOC +
        "Adding credits entries from production crew list");
#endif

    if (!query.exec("INSERT IGNORE INTO credits (chanid, starttime, person, role)"
                    "SELECT dd_v_program.chanid, "
                    "DATE_ADD(dd_v_program.starttime, INTERVAL channel.tmoffset MINUTE), "
                    "people.person, "
                    "dd_productioncrew.role "
                    "FROM dd_v_program "
                    "JOIN channel "
                    "ON dd_v_program.chanid = channel.chanid "
                    "JOIN dd_productioncrew "
                    "ON dd_productioncrew.programid = dd_v_program.programid "
                    "JOIN people "
                    "ON people.name = dd_productioncrew.fullname "
                    "LEFT OUTER JOIN credits "
                    "ON credits.chanid = dd_v_program.chanid "
                    "AND credits.starttime = DATE_ADD(dd_v_program.starttime, INTERVAL channel.tmoffset MINUTE) "
                    "AND credits.person = people.person "
                    "AND credits.role = dd_productioncrew.role "
                    "WHERE credits.role IS NULL;"))
        MythDB::DBError("Inserting into credits table", query);

#if 0
    LOG(VB_GENERAL, LOG_DEBUG, LOC + "Finished inserting credits");
    LOG(VB_GENERAL, LOG_DEBUG, LOC + "Adding genres");
#endif

    if (!query.exec("INSERT IGNORE INTO programgenres (chanid, starttime, "
                    "relevance, genre) SELECT dd_v_program.chanid, "
                    "DATE_ADD(starttime, INTERVAL channel.tmoffset MINUTE), "
                    "relevance, class FROM dd_v_program, dd_genre, channel "
                    "WHERE (dd_v_program.programid = dd_genre.programid) "
                    "AND dd_v_program.chanid = channel.chanid"))
        MythDB::DBError("Inserting into programgenres table",query);

#if 0
    LOG(VB_GENERAL, LOG_DEBUG, LOC + "Done");
#endif
}

void authenticationCallback(QNetworkReply *reply, QAuthenticator *auth,
                            void *arg)
{
    if (!arg)
        return;

    DataDirectProcessor *dd = reinterpret_cast<DataDirectProcessor *>(arg);
    dd->authenticationCallback(reply, auth);
}

void DataDirectProcessor::authenticationCallback(QNetworkReply *reply,
                                                 QAuthenticator *auth)
{
    LOG(VB_FILE, LOG_DEBUG, "DataDirect auth callback");
    (void)reply;
    auth->setUser(GetUserID());
    auth->setPassword(GetPassword());
}

bool DataDirectProcessor::DDPost(QString    ddurl,        QString   &inputFile,
                                 QDateTime  pstartDate,   QDateTime  pendDate,
                                 QString   &err_txt)
{
    if (!inputFile.isEmpty() && QFile(inputFile).exists())
    {
        return true;
    }

    QString startdatestr = pstartDate.toString(Qt::ISODate) + "Z";
    QString enddatestr = pendDate.toString(Qt::ISODate) + "Z";
    QByteArray postdata;
    postdata  = "<?xml version='1.0' encoding='utf-8'?>\n";
    postdata += "<SOAP-ENV:Envelope\n";
    postdata += "xmlns:SOAP-ENV='http://schemas.xmlsoap.org/soap/envelope/'\n";
    postdata += "xmlns:xsd='http://www.w3.org/2001/XMLSchema'\n";
    postdata += "xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'\n";
    postdata += "xmlns:SOAP-ENC='http://schemas.xmlsoap.org/soap/encoding/'>\n";
    postdata += "<SOAP-ENV:Body>\n";
    postdata += "<ns1:download  xmlns:ns1='urn:TMSWebServices'>\n";
    postdata += "<startTime xsi:type='xsd:dateTime'>";
    postdata += startdatestr;
    postdata += "</startTime>\n";
    postdata += "<endTime xsi:type='xsd:dateTime'>";
    postdata += enddatestr;
    postdata += "</endTime>\n";
    postdata += "</ns1:download>\n";
    postdata += "</SOAP-ENV:Body>\n";
    postdata += "</SOAP-ENV:Envelope>\n";

    if (inputFile.isEmpty())
    {
        bool ok;
        inputFile = GetDDPFilename(ok);
        if (!ok)
        {
            LOG(VB_GENERAL, LOG_ERR, LOC +
            "Failure creating temp ddp file");
            return false;
        }
    }

    QHash<QByteArray, QByteArray> headers;
    headers.insert("Accept-Encoding", "gzip");
    headers.insert("Content-Type", "application/soap+xml; charset=utf-8");

    LOG(VB_GENERAL, LOG_INFO, "Downloading DataDirect feed");

    MythDownloadManager *manager = GetMythDownloadManager();

    if (!manager->postAuth(ddurl, &postdata, &::authenticationCallback, this,
                           &headers))
    {
        err_txt = QString("Download error");
        return false;
    }

    LOG(VB_GENERAL, LOG_INFO, QString("Downloaded %1 bytes")
        .arg(postdata.size()));

    LOG(VB_GENERAL, LOG_INFO, "Uncompressing DataDirect feed");

    QByteArray uncompressed = gUncompress(postdata);

    LOG(VB_GENERAL, LOG_INFO, QString("Uncompressed to %1 bytes")
        .arg(uncompressed.size()));

    if (uncompressed.size() == 0)
        uncompressed = postdata;

    LOG(VB_GENERAL, LOG_INFO, QString("Writing to temporary file: [%1]")
        .arg( inputFile ));

    QFile file(inputFile);
    if (!file.open(QIODevice::WriteOnly))
    {
        LOG(VB_GENERAL, LOG_ERR, LOC + QString("Failed to open temporary file: %1").arg(inputFile));
        return false;
    }
    file.write(uncompressed);
    file.close();

    if (uncompressed.size() == 0)
    {
        err_txt = QString("Error uncompressing data");
        return false;
    }

    return true;
}

bool DataDirectProcessor::GrabNextSuggestedTime(void)
{
    LOG(VB_GENERAL, LOG_INFO, LOC + "Grabbing next suggested grabbing time");

    QString ddurl = m_providers[m_listingsProvider].webServiceURL;

    bool ok;
    QString resultFilename = GetResultFilename(ok);
    if (!ok)
    {
        LOG(VB_GENERAL, LOG_ERR, LOC +
            "GrabNextSuggestedTime: Creating temp result file");
        return false;
    }

    QByteArray postdata;
    postdata  = "<?xml version='1.0' encoding='utf-8'?>\n";
    postdata += "<SOAP-ENV:Envelope\n";
    postdata += "xmlns:SOAP-ENV='http://schemas.xmlsoap.org/soap/envelope/'\n";
    postdata += "xmlns:xsd='http://www.w3.org/2001/XMLSchema'\n";
    postdata += "xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'\n";
    postdata += "xmlns:SOAP-ENC='http://schemas.xmlsoap.org/soap/encoding/'>\n";
    postdata += "<SOAP-ENV:Body>\n";
    postdata += "<tms:acknowledge xmlns:tms='urn:TMSWebServices'>\n";
    postdata += "</SOAP-ENV:Body>\n";
    postdata += "</SOAP-ENV:Envelope>\n";

    QHash<QByteArray, QByteArray> headers;
    headers.insert("Content-Type", "application/soap+xml; charset=utf-8");

    MythDownloadManager *manager = GetMythDownloadManager();

    if (!manager->postAuth(ddurl, &postdata, &::authenticationCallback, this,
                           &headers))
    {
        LOG(VB_GENERAL, LOG_ERR, LOC +
            "GrabNextSuggestedTime: Could not download");
        return false;
    }

    QDateTime nextSuggestedTime;
    QDateTime blockedTime;

    LOG(VB_GENERAL, LOG_INFO, QString("Suggested Time data: %1 bytes")
        .arg(postdata.size()));

    QFile file(resultFilename);
    if (!file.open(QIODevice::WriteOnly))
    {
        LOG(VB_GENERAL, LOG_ERR, LOC + QString("Failed to open result file: %1").arg(resultFilename));
        return false;
    }
    file.write(postdata);
    file.close();

    if (file.open(QIODevice::ReadOnly))
    {
        QTextStream stream(&file);
        QString line;
        while (!stream.atEnd())
        {
            line = stream.readLine();
            if (line.contains("<suggestedTime>", Qt::CaseInsensitive))
            {
                QString tmpStr = line;
                tmpStr.replace(
                    QRegExp(".*<suggestedTime>([^<]*)</suggestedTime>.*"),
                    "\\1");

                nextSuggestedTime = MythDate::fromString(tmpStr);

                LOG(VB_GENERAL, LOG_INFO, LOC +
                    QString("nextSuggestedTime is: ") +
                    nextSuggestedTime.toString(Qt::ISODate));
            }

            if (line.contains("<blockedTime>", Qt::CaseInsensitive))
            {
                QString tmpStr = line;
                tmpStr.replace(
                    QRegExp(".*<blockedTime>([^<]*)</blockedTime>.*"), "\\1");

                blockedTime = MythDate::fromString(tmpStr);
                LOG(VB_GENERAL, LOG_INFO, LOC + QString("BlockedTime is: ") +
                    blockedTime.toString(Qt::ISODate));
            }
        }
        file.close();
    }

    if (nextSuggestedTime.isValid())
    {
        gCoreContext->SaveSettingOnHost(
            "MythFillSuggestedRunTime",
            nextSuggestedTime.toString(Qt::ISODate), nullptr);
    }

    return nextSuggestedTime.isValid();
}

bool DataDirectProcessor::GrabData(const QDateTime &pstartDate,
                                   const QDateTime &pendDate)
{
    QString msg = (pstartDate.addSecs(1) == pendDate) ? "channel" : "listing";
    LOG(VB_GENERAL, LOG_INFO, LOC + "Grabbing " + msg + " data");

    QString err = "";
    QString ddurl = m_providers[m_listingsProvider].webServiceURL;
    QString inputfile = m_inputFilename;
    QString cache_dd_data;

    if (m_cacheData)
    {
        cache_dd_data = m_tmpDir +
            QString("/mythtv_dd_cache_%1_UTC_%2_to_%3")
            .arg(GetListingsProvider())
            .arg(MythDate::toString(pstartDate, MythDate::kFilename))
            .arg(MythDate::toString(pendDate, MythDate::kFilename));

        if (QFile(cache_dd_data).exists() && m_inputFilename.isEmpty())
        {
            LOG(VB_GENERAL, LOG_INFO, LOC + "Using DD cache");
        }

        if (m_inputFilename.isEmpty())
            inputfile = cache_dd_data;
    }

    if (!DDPost(ddurl, inputfile, pstartDate, pendDate, err))
    {
        LOG(VB_GENERAL, LOG_ERR, LOC + QString("Failed to get data: %1")
                .arg(err));
        return false;
    }

    QFile file(inputfile);

    if (!file.open(QIODevice::ReadOnly))
    {
        LOG(VB_GENERAL, LOG_ERR, LOC + QString("Failed to open file: %1").arg(inputfile));
        return false;
    }

    QByteArray data = file.readAll();
    file.close();

    if (data.isEmpty())
    {
        LOG(VB_GENERAL, LOG_ERR, LOC + "Data is empty");
        return false;
    }

    bool ok = true;

    DDStructureParser ddhandler(*this);
    QXmlInputSource  xmlsource;
    QXmlSimpleReader xmlsimplereader;

    xmlsource.setData(data);
    xmlsimplereader.setContentHandler(&ddhandler);
    if (!xmlsimplereader.parse(xmlsource))
    {
        LOG(VB_GENERAL, LOG_ERR, LOC +
            "DataDirect XML failed to properly parse, downloaded listings "
            "were probably corrupt.");
        ok = false;
    }

    return ok;
}

bool DataDirectProcessor::GrabLineupsOnly(void)
{
    const QDateTime start = QDateTime(MythDate::current().date().addDays(2),
                                      QTime(23, 59, 0), Qt::UTC);
    const QDateTime end   = start.addSecs(1);

    return GrabData(start, end);
}

bool DataDirectProcessor::GrabAllData(void)
{
    return GrabData(MythDate::current().addDays(-2),
                    MythDate::current().addDays(15));
}

void DataDirectProcessor::CreateATempTable(const QString &ptablename,
                                           const QString &ptablestruct)
{
    MSqlQuery query(MSqlQuery::DDCon());
    QString querystr;
    querystr = "CREATE TEMPORARY TABLE IF NOT EXISTS " + ptablename + " " +
        ptablestruct + " ENGINE=MyISAM;";

    if (!query.exec(querystr))
        MythDB::DBError("Creating temporary table", query);

    querystr = "TRUNCATE TABLE " + ptablename + ";";

    if (!query.exec(querystr))
        MythDB::DBError("Truncating temporary table", query);
}

void DataDirectProcessor::CreateTempTables()
{
    QMap<QString,QString> dd_tables;

    dd_tables["dd_station"] =
        "( stationid char(12),           callsign char(10),     "
        "  stationname varchar(40),      affiliate varchar(25), "
        "  fccchannelnumber char(15) )";

    dd_tables["dd_lineup"] =
        "( lineupid char(100),           name char(42),  "
        "  type char(20),                postal char(6), "
        "  device char(30) )";

    dd_tables["dd_lineupmap"] =
        "( lineupid char(100),           stationid char(12),   "
        "  channel char(5),              channelMinor char(3) )";


    dd_tables["dd_v_station"] =
        "( stationid char(12),           callsign char(10),     "
        "  stationname varchar(40),      affiliate varchar(25), "
        "  fccchannelnumber char(15),    channel char(5),       "
        "  channelMinor char(3) )";

    dd_tables["dd_schedule"] =
        "( programid char(40),           stationid char(12), "
        "  scheduletime datetime,        duration time,      "
        "  isrepeat bool,                stereo bool,        "
        "  dolby bool, "
        "  subtitled bool,               hdtv bool,          "
        "  closecaptioned bool,          tvrating char(5),   "
        "  partnumber int,               parttotal int,      "
        "  endtime datetime,             isnew bool,         "
        "INDEX progidx (programid) )";

    dd_tables["dd_program"] =
        "( programid char(40) NOT NULL,  seriesid char(12),     "
        "  title varchar(120),           subtitle varchar(150), "
        "  description text,             mpaarating char(5),    "
        "  starrating char(5),           runtime time,          "
        "  year char(4),                 showtype char(30),     "
        "  category_type char(64),       colorcode char(20),    "
        "  originalairdate date,         syndicatedepisodenumber char(20), "
        "  stars float unsigned, "
        "PRIMARY KEY (programid))";

    dd_tables["dd_v_program"] =
        "( chanid int unsigned NOT NULL, starttime datetime NOT NULL, "
        "  endtime datetime,             title varchar(128),          "
        "  subtitle varchar(128),        description text,            "
        "  category varchar(64),         category_type varchar(64),   "
        "  airdate year,                 stars float unsigned,        "
        "  previouslyshown tinyint,      isrepeat bool,               "
        "  stereo bool,                  dolby bool,                  "
        "  subtitled bool,              "
        "  hdtv bool,                    closecaptioned bool,         "
        "  partnumber int,               parttotal int,               "
        "  seriesid char(12),            originalairdate date,        "
        "  showtype varchar(30),         colorcode varchar(20),       "
        "  syndicatedepisodenumber varchar(20), programid char(40),   "
        "  tvrating char(5),             mpaarating char(5),          "
        "INDEX progidx (programid))";

    dd_tables["dd_productioncrew"] =
        "( programid char(40),           role char(30),    "
        "  givenname char(20),           surname char(20), "
        "  fullname char(41), "
        "INDEX progidx (programid), "
        "INDEX nameidx (fullname))";

    dd_tables["dd_genre"] =
        "( programid char(40) NOT NULL,  class char(30), "
        "  relevance char(1), "
        "INDEX progidx (programid))";

    QMap<QString,QString>::const_iterator it;
    for (it = dd_tables.begin(); it != dd_tables.end(); ++it)
        CreateATempTable(it.key(), *it);
}

bool DataDirectProcessor::GrabLoginCookiesAndLineups(bool parse_lineups)
{
    LOG(VB_GENERAL, LOG_INFO, LOC + "Grabbing login cookies and lineups");

    PostList list;
    list.push_back(PostItem("username", GetUserID()));
    list.push_back(PostItem("password", GetPassword()));
    list.push_back(PostItem("action",   "Login"));

    QString labsURL   = m_providers[m_listingsProvider].webURL;
    QString loginPage = m_providers[m_listingsProvider].loginPage;

    bool ok;
    QString resultFilename = GetResultFilename(ok);
    if (!ok)
    {
        LOG(VB_GENERAL, LOG_ERR, LOC + "GrabLoginCookiesAndLineups: "
                "Creating temp result file");
        return false;
    }
    QString cookieFilename = GetCookieFilename(ok);
    if (!ok)
    {
        LOG(VB_GENERAL, LOG_ERR, LOC + "GrabLoginCookiesAndLineups: "
                "Creating temp cookie file");
        return false;
    }

    ok = Post(labsURL + loginPage, list, resultFilename, "",
              cookieFilename);

    bool got_cookie = QFileInfo(cookieFilename).size() > 100;

    ok &= got_cookie && (!parse_lineups || ParseLineups(resultFilename));
    if (ok)
        m_cookieFileDT = MythDate::current();

    return ok;
}

bool DataDirectProcessor::GrabLineupForModify(const QString &lineupid)
{
    LOG(VB_GENERAL, LOG_INFO, LOC +
        QString("Grabbing lineup %1 for modification").arg(lineupid));

    RawLineupMap::const_iterator it = m_rawLineups.find(lineupid);
    if (it == m_rawLineups.end())
        return false;

    PostList list;
    list.push_back(PostItem("udl_id",    GetRawUDLID(lineupid)));
    list.push_back(PostItem("zipcode",   GetRawZipCode(lineupid)));
    list.push_back(PostItem("lineup_id", lineupid));
    list.push_back(PostItem("submit",    "Modify"));

    bool ok;
    QString resultFilename = GetResultFilename(ok);
    if (!ok)
    {
        LOG(VB_GENERAL, LOG_ERR, LOC + "GrabLoginCookiesAndLineups: "
                "Creating temp result file");
        return false;
    }
    QString cookieFilename = GetCookieFilename(ok);
    if (!ok)
    {
        LOG(VB_GENERAL, LOG_ERR, LOC + "GrabLoginCookiesAndLineups: "
                "Creating temp cookie file");
        return false;
    }

    QString labsURL = m_providers[m_listingsProvider].webURL;
    ok = Post(labsURL + (*it).get_action, list, resultFilename,
                   cookieFilename, "");

    return ok && ParseLineup(lineupid, resultFilename);
}

void DataDirectProcessor::SetAll(const QString &lineupid, bool val)
{
    LOG(VB_GENERAL, LOG_INFO, LOC + QString("%1 all channels in lineup %2")
            .arg((val) ? "Selecting" : "Deselecting").arg(lineupid));

    RawLineupMap::iterator lit = m_rawLineups.find(lineupid);
    if (lit == m_rawLineups.end())
        return;

    RawLineupChannels &ch = (*lit).channels;
    for (RawLineupChannels::iterator it = ch.begin(); it != ch.end(); ++it)
        (*it).chk_checked = val;
}

static QString get_cache_filename(const QString &lineupid)
{
    return QString("/tmp/.mythtv_cached_lineup_") + lineupid;
}

QDateTime DataDirectProcessor::GetLineupCacheAge(const QString &lineupid) const
{
    QDateTime cache_dt(QDate(1971, 1, 1), QTime(0,0,0), Qt::UTC);
    QFile lfile(get_cache_filename(lineupid));
    if (!lfile.exists())
    {
        LOG(VB_GENERAL, LOG_ERR, LOC + "GrabLineupCacheAge("+lineupid+
            ") failed -- " +
            QString("file '%1' doesn't exist")
                .arg(get_cache_filename(lineupid)));
        return cache_dt;
    }
    if (lfile.size() < 8)
    {
        LOG(VB_GENERAL, LOG_ERR, LOC + "GrabLineupCacheAge("+lineupid+
            ") failed -- " +
            QString("file '%1' size %2 too small")
                .arg(get_cache_filename(lineupid)).arg(lfile.size()));
        return cache_dt;
    }
    if (!lfile.open(QIODevice::ReadOnly))
    {
        LOG(VB_GENERAL, LOG_ERR, LOC + "GrabLineupCacheAge("+lineupid+
            ") failed -- " +
            QString("cannot open file '%1'")
                .arg(get_cache_filename(lineupid)));
        return cache_dt;
    }

    QString tmp;
    QTextStream io(&lfile);
    io >> tmp;
    cache_dt = MythDate::fromString(tmp);

    LOG(VB_GENERAL, LOG_INFO, LOC + "GrabLineupCacheAge("+lineupid+") -> " +
            cache_dt.toString(Qt::ISODate));

    return cache_dt;
}

bool DataDirectProcessor::GrabLineupsFromCache(const QString &lineupid)
{
    QFile lfile(get_cache_filename(lineupid));
    if (!lfile.exists() || (lfile.size() < 8) ||
        !lfile.open(QIODevice::ReadOnly))
    {
        LOG(VB_GENERAL, LOG_ERR, LOC + "GrabLineupFromCache("+lineupid+
            ") -- failed");
        return false;
    }

    QString tmp;
    uint size;
    QTextStream io(&lfile);
    io >> tmp; // read in date
    io >> size; // read in number of channels mapped

    for (uint i = 0; i < 14; i++)
        io.readLine(); // read extra lines

    DDLineupChannels &channels = m_lineupmaps[lineupid];
    channels.clear();

    for (uint i = 0; i < size; i++)
    {
        io.readLine(); // read "start record" string

        DataDirectLineupMap chan;
        chan.lineupid     = lineupid;
        chan.stationid    = io.readLine();
        chan.channel      = io.readLine();
        chan.channelMinor = io.readLine();

        chan.mapFrom = QDate();
        tmp = io.readLine();
        if (!tmp.isEmpty())
            chan.mapFrom.fromString(tmp, Qt::ISODate);

        chan.mapTo = QDate();
        tmp = io.readLine();
        if (!tmp.isEmpty())
            chan.mapTo.fromString(tmp, Qt::ISODate);

        channels.push_back(chan);

        DDStation station;
        station.stationid   = chan.stationid;
        station.callsign    = io.readLine();
        station.stationname = io.readLine();
        station.affiliate   = io.readLine();
        station.fccchannelnumber = io.readLine();
        tmp = io.readLine(); // read "end record" string

        m_stations[station.stationid] = station;
    }

    LOG(VB_GENERAL, LOG_INFO, LOC + "GrabLineupFromCache("+lineupid+
        ") -- success");

    return true;
}

bool DataDirectProcessor::SaveLineupToCache(const QString &lineupid) const
{
    QString fn = get_cache_filename(lineupid);
    QByteArray fna = fn.toLatin1();
    QFile lfile(fna.constData());
    if (!lfile.open(QIODevice::WriteOnly))
    {
        LOG(VB_GENERAL, LOG_ERR, LOC + "SaveLineupToCache("+lineupid+
            ") -- failed");
        return false;
    }

    QTextStream io(&lfile);
    io << MythDate::current_iso_string() << endl;

    const DDLineupChannels channels = GetDDLineup(lineupid);
    io << channels.size() << endl;

    io << endl;
    io << "# start record"       << endl;
    io << "#   stationid"        << endl;
    io << "#   channel"          << endl;
    io << "#   channelMinor"     << endl;
    io << "#   mapped from date" << endl;
    io << "#   mapped to date"   << endl;
    io << "#   callsign"         << endl;
    io << "#   stationname"      << endl;
    io << "#   affiliate"        << endl;
    io << "#   fccchannelnumber" << endl;
    io << "# end record"         << endl;
    io << endl;

    DDLineupChannels::const_iterator it;
    for (it = channels.begin(); it != channels.end(); ++it)
    {
        io << "# start record"    << endl;
        io << (*it).stationid     << endl;
        io << (*it).channel       << endl;
        io << (*it).channelMinor  << endl;
        io << (*it).mapFrom.toString(Qt::ISODate) << endl;
        io << (*it).mapTo.toString(Qt::ISODate)   << endl;

        DDStation station = GetDDStation((*it).stationid);
        io << station.callsign    << endl;
        io << station.stationname << endl;
        io << station.affiliate   << endl;
        io << station.fccchannelnumber << endl;
        io << "# end record"      << endl;
    }
    io << flush;

    LOG(VB_GENERAL, LOG_INFO, LOC + "SaveLineupToCache("+lineupid+
        ") -- success");

    bool ret = makeFileAccessible(fna.constData()); // Let anybody update it
    if (!ret)
    {
        // Nothing, makeFileAccessible will print an error
    }

    return true;
}

bool DataDirectProcessor::GrabFullLineup(const QString &lineupid,
                                         bool restore, bool onlyGrabSelected,
                                         uint cache_age_allowed_in_seconds)
{
    if (cache_age_allowed_in_seconds)
    {
        QDateTime exp_time = GetLineupCacheAge(lineupid)
            .addSecs(cache_age_allowed_in_seconds);
        bool valid = exp_time > MythDate::current();
        if (valid && GrabLineupsFromCache(lineupid))
            return true;
    }

    bool ok = GrabLoginCookiesAndLineups();
    if (!ok)
        return false;

    ok = GrabLineupForModify(lineupid);
    if (!ok)
        return false;

    RawLineupMap::iterator lit = m_rawLineups.find(lineupid);
    if (lit == m_rawLineups.end())
        return false;

    const RawLineupChannels orig_channels = (*lit).channels;

    if (!onlyGrabSelected)
    {
        SetAll(lineupid, true);
        if (!SaveLineupChanges(lineupid))
            return false;
    }

    ok = GrabLineupsOnly();

    if (ok)
        SaveLineupToCache(lineupid);

    (*lit).channels = orig_channels;
    if (restore && !onlyGrabSelected)
        ok &= SaveLineupChanges(lineupid);

    return ok;
}

bool DataDirectProcessor::SaveLineup(const QString &lineupid,
                                     const QMap<QString,bool> &xmltvids)
{
    QMap<QString,bool> callsigns;
    RawLineupMap::iterator lit = m_rawLineups.find(lineupid);
    if (lit == m_rawLineups.end())
        return false;

    // Grab login cookies if they are more than 5 minutes old
    if ((!m_cookieFileDT.isValid() ||
         m_cookieFileDT.addSecs(5*60) < MythDate::current()) &&
        !GrabLoginCookiesAndLineups(false))
    {
        return false;
    }

    // Get callsigns based on xmltv ids (aka stationid)
    DDLineupMap::const_iterator ddit = m_lineupmaps.find(lineupid);
    DDLineupChannels::const_iterator it;
    for (it = (*ddit).begin(); it != (*ddit).end(); ++it)
    {
        if (xmltvids.find((*it).stationid) != xmltvids.end())
            callsigns[GetDDStation((*it).stationid).callsign] = true;
    }

    // Set checked mark based on whether the channel is mapped
    RawLineupChannels &ch = (*lit).channels;
    RawLineupChannels::iterator cit;
    for (cit = ch.begin(); cit != ch.end(); ++cit)
    {
        bool chk = callsigns.find((*cit).lbl_callsign) != callsigns.end();
        (*cit).chk_checked = chk;
    }

    // Save these changes
    return SaveLineupChanges(lineupid);
}

bool DataDirectProcessor::SaveLineupChanges(const QString &lineupid)
{
    RawLineupMap::const_iterator lit = m_rawLineups.find(lineupid);
    if (lit == m_rawLineups.end())
        return false;

    const RawLineup &lineup = *lit;
    const RawLineupChannels &ch = lineup.channels;
    RawLineupChannels::const_iterator it;

    PostList list;
    for (it = ch.begin(); it != ch.end(); ++it)
    {
        if ((*it).chk_checked)
            list.push_back(PostItem((*it).chk_name, (*it).chk_value));
    }
    list.push_back(PostItem("action", "Update"));

    LOG(VB_GENERAL, LOG_INFO, LOC + QString("Saving lineup %1 with %2 channels")
            .arg(lineupid).arg(list.size() - 1));

    bool ok;
    QString cookieFilename = GetCookieFilename(ok);
    if (!ok)
    {
        LOG(VB_GENERAL, LOG_ERR, LOC + "GrabLoginCookiesAndLineups: "
                "Creating temp cookie file");
        return false;
    }

    QString labsURL = m_providers[m_listingsProvider].webURL;
    return Post(labsURL + lineup.set_action, list, "",
                cookieFilename, "");
}

bool DataDirectProcessor::UpdateListings(uint sourceid)
{
    MSqlQuery query(MSqlQuery::DDCon());
    query.prepare(
        "SELECT xmltvid "
        "FROM channel "
        "WHERE sourceid = :SOURCEID");
    query.bindValue(":SOURCEID", sourceid);

    if (!query.exec() || !query.isActive())
    {
        MythDB::DBError("Selecting existing channels", query);
        return false;
    }

    QString a, b, c, lineupid;
    if (!SourceUtil::GetListingsLoginData(sourceid, a, b, c, lineupid))
        return false;

    QMap<QString,bool> xmltvids;
    while (query.next())
    {
        if (!query.value(0).toString().isEmpty())
            xmltvids[query.value(0).toString()] = true;
    }

    LOG(VB_GENERAL, LOG_INFO, LOC + "Saving updated DataDirect listing");
    bool ok = SaveLineup(lineupid, xmltvids);

    if (!ok)
        LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to update DataDirect listings.");

    return ok;
}

QString DataDirectProcessor::GetRawUDLID(const QString &lineupid) const
{
    RawLineupMap::const_iterator it = m_rawLineups.find(lineupid);
    if (it == m_rawLineups.end())
        return QString();
    return (*it).udl_id;
}

QString DataDirectProcessor::GetRawZipCode(const QString &lineupid) const
{
    RawLineupMap::const_iterator it = m_rawLineups.find(lineupid);
    if (it == m_rawLineups.end())
        return QString();
    return (*it).zipcode;
}

RawLineup DataDirectProcessor::GetRawLineup(const QString &lineupid) const
{
    RawLineup tmp;
    RawLineupMap::const_iterator it = m_rawLineups.find(lineupid);
    if (it == m_rawLineups.end())
        return tmp;
    return (*it);
}

void DataDirectProcessor::CreateTemp(
    const QString &templatefilename,
    const QString &errmsg,
    bool           directory,
    QString       &filename,
    bool          &ok) const
{
    QString tmp = createTempFile(templatefilename, directory);
    if (templatefilename == tmp)
    {
        m_fatalErrors.push_back(errmsg);
        ok = false;
    }
    else
    {
        filename = tmp;
        ok = true;
    }
}

QString DataDirectProcessor::CreateTempDirectory(bool *pok) const
{
    bool ok;
    pok = (pok) ? pok : &ok;
    if (m_tmpDir == "/tmp")
    {
        CreateTemp("/tmp/mythtv_ddp_XXXXXX",
                   "Failed to create temp directory",
                   true, m_tmpDir, *pok);
    }
    return m_tmpDir;
}


QString DataDirectProcessor::GetResultFilename(bool &ok) const
{
    ok = true;
    if (m_tmpResultFile.isEmpty())
    {
        CreateTemp(m_tmpDir + "/mythtv_result_XXXXXX",
                   "Failed to create temp result file",
                   false, m_tmpResultFile, ok);
    }
    return m_tmpResultFile;
}

QString DataDirectProcessor::GetCookieFilename(bool &ok) const
{
    ok = true;
    if (m_cookieFile.isEmpty())
    {
        CreateTemp(m_tmpDir + "/mythtv_cookies_XXXXXX",
                   "Failed to create temp cookie file",
                   false, m_cookieFile, ok);
    }
    return m_cookieFile;
}

QString DataDirectProcessor::GetDDPFilename(bool &ok) const
{
    ok = true;
    if (m_tmpDDPFile.isEmpty())
    {
        CreateTemp(m_tmpDir + "/mythtv_ddp_XXXXXX",
                   "Failed to create temp ddp file",
                   false, m_tmpDDPFile, ok);
    }
    return m_tmpDDPFile;
}

bool DataDirectProcessor::Post(QString url, const PostList &list,
                               QString documentFile,
                               QString inCookieFile, QString outCookieFile)
{
    MythDownloadManager *manager = GetMythDownloadManager();

    if (!inCookieFile.isEmpty())
        manager->loadCookieJar(inCookieFile);

    QByteArray postdata;
    for (uint i = 0; i < list.size(); i++)
    {
        postdata += ((i) ? "&" : "") + list[i].key + "=";
        postdata += html_escape(list[i].value);
    }

    if (!manager->post(url, &postdata))
        return false;

    if (!outCookieFile.isEmpty())
        manager->saveCookieJar(outCookieFile);

    if (documentFile.isEmpty())
        return true;

    QFile file(documentFile);
    if (!file.open(QIODevice::WriteOnly))
    {
        LOG(VB_GENERAL, LOG_ERR, LOC + QString("Failed to open document file: %1").arg(documentFile));
        return false;
    }
    file.write(postdata);
    file.close();

    QFileInfo fi(documentFile);
    return fi.size();
}

bool DataDirectProcessor::ParseLineups(const QString &documentFile)
{
    QFile file(documentFile);
    if (!file.open(QIODevice::ReadOnly))
    {
        LOG(VB_GENERAL, LOG_ERR, LOC + QString("Failed to open '%1'")
                .arg(documentFile));
        return false;
    }

    QTextStream stream(&file);
    bool in_form = false;
    QString get_action;
    QMap<QString,QString> name_value;

    m_rawLineups.clear();

    while (!stream.atEnd())
    {
        QString line = stream.readLine();
        QString llow = line.toLower();
        int frm = llow.indexOf("<form");
        if (frm >= 0)
        {
            in_form = true;
            get_action = get_setting(line.mid(frm + 5), "action");
            name_value.clear();
#if 0
            LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("action: %1").arg(action));
#endif
        }

        if (!in_form)
            continue;

        int inp = llow.indexOf("<input");
        if (inp >= 0)
        {
            QString input_line = line.mid(inp + 6);
#if 0
            LOG(VB_GENERAL, LOG_DEBUG, LOC +
                QString("input: %1").arg(input_line));
#endif
            QString name  = get_setting(input_line, "name");
            QString value = get_setting(input_line, "value");
#if 0
            LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("name: %1").arg(name));
            LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("value: %1").arg(value));
#endif
            if (!name.isEmpty() && !value.isEmpty())
                name_value[name] = value;
        }

        if (llow.contains("</form>"))
        {
            in_form = false;
            if (!get_action.isEmpty() &&
                !name_value["udl_id"].isEmpty() &&
                !name_value["zipcode"].isEmpty() &&
                !name_value["lineup_id"].isEmpty())
            {
                RawLineup item(get_action, name_value["udl_id"],
                               name_value["zipcode"]);

                m_rawLineups[name_value["lineup_id"]] = item;
#if 0
                LOG(VB_GENERAL, LOG_DEBUG, LOC +
                    QString("<%1>  \t--> <%2,%3,%4>")
                        .arg(name_value["lineup_id"])
                        .arg(item.udl_id).arg(item.zipcode)
                        .arg(item.get_action));
#endif
            }
        }
    }
    return true;
}

bool DataDirectProcessor::ParseLineup(const QString &lineupid,
                                      const QString &documentFile)
{
    QFile file(documentFile);
    if (!file.open(QIODevice::ReadOnly))
    {
        LOG(VB_GENERAL, LOG_ERR, LOC + QString("Failed to open '%1'")
                .arg(documentFile));

        return false;
    }

    QTextStream stream(&file);
    bool in_form = false;
    int in_label = 0;
    QMap<QString,QString> settings;

    RawLineup &lineup = m_rawLineups[lineupid];
    RawLineupChannels &ch = lineup.channels;

    while (!stream.atEnd())
    {
        QString line = stream.readLine();
        QString llow = line.toLower();
        int frm = llow.indexOf("<form");
        if (frm >= 0)
        {
            in_form = true;
            lineup.set_action = get_setting(line.mid(frm + 5), "action");
#if 0
            LOG(VB_GENERAL, LOG_DEBUG, LOC + "set_action: " +
                lineup.set_action);
#endif
        }

        if (!in_form)
            continue;

        int inp = llow.indexOf("<input");
        if (inp >= 0)
        {
            QString in_line = line.mid(inp + 6);
            settings.clear();
            settings["chk_name"]    = get_setting(in_line, "name");
            settings["chk_id"]      = get_setting(in_line, "id");
            settings["chk_value"]   = get_setting(in_line, "value");
            settings["chk_checked"] = has_setting(in_line, "checked")?"1":"0";
        }

        int lbl = llow.indexOf("<label");
        if (lbl >= 0)
        {
            QString lbl_line = line.mid(inp + 6);
            QString name = get_setting(lbl_line, "for");
            in_label = (name == settings["chk_name"]) ? 1 : 0;
        }

        if (in_label)
        {
            int start = (lbl >= 0) ? lbl + 6 : 0;
            int beg = llow.indexOf("<td>", start), end = -1;
            if (beg)
                end = llow.indexOf("</td>", beg + 4);

            if (end >= 0)
            {
                QString key = (in_label == 1) ? "lbl_ch" : "lbl_callsign";
                QString val = line.mid(beg + 4, end - beg - 4);
                settings[key] = val.replace("&nbsp;", "", Qt::CaseInsensitive);
                in_label++;
            }
        }

        in_label = (llow.indexOf("</label") >= 0) ? 0 : in_label;

        if (!in_label &&
            !settings["chk_name"].isEmpty() &&
            !settings["chk_id"].isEmpty() &&
            !settings["chk_value"].isEmpty() &&
            !settings["chk_checked"].isEmpty() &&
            !settings["lbl_ch"].isEmpty() &&
            !settings["lbl_callsign"].isEmpty())
        {
            RawLineupChannel chan(
                settings["chk_name"],  settings["chk_id"],
                settings["chk_value"], settings["chk_checked"] == "1",
                settings["lbl_ch"],    settings["lbl_callsign"]);

#if 0
            LOG(VB_GENERAL, LOG_DEBUG, LOC +
                QString("name: %1  id: %2  value: %3  "
                        "checked: %4  ch: %5  call: %6")
                    .arg(settings["chk_name"]).arg(settings["chk_id"])
                    .arg(settings["chk_value"]).arg(settings["chk_checked"])
                    .arg(settings["lbl_ch"],4).arg(settings["lbl_callsign"]));
#endif

            ch.push_back(chan);
            settings.clear();
        }

        if (llow.contains("</form>"))
        {
            in_form = false;
        }
    }
    return true;
}

static QString html_escape(QString str)
{
    QString new_str = "";
    for (int i = 0; i < str.length(); i++)
    {
        if (str[i].isLetterOrNumber())
            new_str += str[i];
        else
        {
            new_str += QString("%%1").arg((int)str[1].toLatin1(), 0, 16);
        }
    }

    return new_str;
}

static QString get_setting(QString line, QString key)
{
    QString llow = line.toLower();
    QString kfind = key + "=\"";
    int beg = llow.indexOf(kfind);

    if (beg >= 0)
    {
        int end = llow.indexOf("\"", beg + kfind.length());
        return line.mid(beg + kfind.length(), end - beg - kfind.length());
    }

    kfind = key + "=";
    beg = llow.indexOf(kfind);
    if (beg < 0)
        return QString();

    int i = beg + kfind.length();
    while (i < line.length() && !line[i].isSpace() && line[i] != '>')
        i++;

    if (i < line.length() && (line[i].isSpace() || line[i] == '>'))
        return line.mid(beg + kfind.length(), i - beg - kfind.length());

    return QString();
}

static bool has_setting(QString line, QString key)
{
    return (line.toLower().indexOf(key) >= 0);
}

static void get_atsc_stuff(QString channum, int freqid,
                           int &major, int &minor, long long &freq)
{
    major = freqid;
    minor = 0;

    int chansep = channum.indexOf(QRegExp("\\D"));
    if (chansep < 0)
        return;

    major = channum.left(chansep).toInt();
    minor = channum.right(channum.length() - (chansep + 1)).toInt();

    freq = get_center_frequency("atsc", "vsb8", "us", freqid);
}

static QString process_dd_station(
    uint sourceid, QString chan_major, QString chan_minor,
    QString &tvformat, uint &freqid)
{
    QString channum = chan_major;
    bool ok;
    uint minor = chan_minor.toUInt(&ok);

    tvformat = "Default";

    if (minor && ok)
    {
        tvformat = "atsc";
        channum += SourceUtil::GetChannelSeparator(sourceid) + chan_minor;
    }
    else if (!freqid && (get_lineup_type(sourceid) == "LocalBroadcast"))
        freqid = chan_major.toInt();
    else
        freqid = channum.toInt();

    return channum;
}

static uint update_channel_basic(uint    sourceid,   bool    insert,
                                 QString xmltvid,    QString callsign,
                                 QString name,       uint    freqid,
                                 QString chan_major, QString chan_minor)
{
    callsign = (callsign.isEmpty()) ? name : callsign;

    QString tvformat;
    QString channum = process_dd_station(
        sourceid, chan_major, chan_minor, tvformat, freqid);

    // First check if channel already in DB, but without xmltvid
    MSqlQuery query(MSqlQuery::DDCon());
    query.prepare("SELECT chanid, callsign, name "
                  "FROM channel "
                  "WHERE sourceid = :SOURCEID AND "
                  "      ( xmltvid = '0' OR xmltvid = '') AND "
                  "      ( channum = :CHANNUM OR "
                  "        ( freqid  = :FREQID AND "
                  "          freqid != '0'     AND "
                  "          freqid != ''      AND "
                  "          atsc_minor_chan = '0') OR "
                  "        ( atsc_major_chan = :MAJORCHAN AND "
                  "          atsc_minor_chan = :MINORCHAN ) )");
    query.bindValue(":SOURCEID",  sourceid);
    query.bindValue(":CHANNUM",   channum);
    query.bindValue(":FREQID",    freqid);
    query.bindValue(":MAJORCHAN", chan_major.toUInt());
    query.bindValue(":MINORCHAN", chan_minor.toUInt());

    if (!query.exec() || !query.isActive())
    {
        MythDB::DBError("Getting chanid of existing channel", query);
        return 0; // go on to next channel without xmltv
    }

    if (query.next())
    {
        // The channel already exists in DB, at least once,
        // so set the xmltvid..
        MSqlQuery chan_update_q(MSqlQuery::DDCon());
        chan_update_q.prepare(
            "UPDATE channel "
            "SET xmltvid = :XMLTVID, name = :NAME, callsign = :CALLSIGN "
            "WHERE chanid = :CHANID AND sourceid = :SOURCEID");

        uint i = 0;
        do
        {
            uint chanid = query.value(0).toInt();

            QString new_callsign = query.value(1).toString();
            new_callsign =
                (new_callsign.indexOf(ChannelUtil::GetUnknownCallsign()) == 0) ?
                callsign : new_callsign;

            QString new_name = query.value(2).toString();
            new_name = (new_name.isEmpty()) ? name         : new_name;
            new_name = (new_name.isEmpty()) ? new_callsign : new_name;

            chan_update_q.bindValue(":CHANID",   chanid);
            chan_update_q.bindValue(":NAME",     new_name);
            chan_update_q.bindValue(":CALLSIGN", new_callsign);
            chan_update_q.bindValue(":XMLTVID",  xmltvid);
            chan_update_q.bindValue(":SOURCEID", sourceid);

#if 0
            LOG(VB_GENERAL, LOG_INFO, LOC +
                QString("Updating channel %1: '%2' (%3).")
                    .arg(chanid).arg(name).arg(callsign));
#endif

            if (!chan_update_q.exec() || !chan_update_q.isActive())
            {
                MythDB::DBError(
                    "Updating XMLTVID of existing channel", chan_update_q);
                continue; // go on to next instance of this channel
            }
            i++;
        }
        while (query.next());

        return i; // go on to next channel without xmltv
    }

    if (!insert)
        return 0; // go on to next channel without xmltv

    // The channel doesn't exist in the DB, insert it...
    int mplexid = -1, majorC, minorC, chanid = 0;
    long long freq = -1;
    get_atsc_stuff(channum, freqid, majorC, minorC, freq);

    if (minorC > 0 && freq >= 0)
        mplexid = ChannelUtil::CreateMultiplex(sourceid, "atsc", freq, "8vsb");

    if ((mplexid > 0) || (minorC == 0))
        chanid = ChannelUtil::CreateChanID(sourceid, channum);

    LOG(VB_GENERAL, LOG_INFO, LOC + QString("Adding channel %1 '%2' (%3).")
            .arg(channum).arg(name).arg(callsign));

    if (chanid > 0)
    {
        QString icon   = "";
        int  serviceid = 0;
        bool oag       = false; // use on air guide
        bool hidden    = false;
        bool hidden_in_guide = false;
        QString freq_id= QString::number(freqid);

        ChannelUtil::CreateChannel(
            mplexid,   sourceid,  chanid,
            callsign,  name,      channum,
            serviceid, majorC,    minorC,
            oag,       hidden,    hidden_in_guide,
            freq_id,   icon,      tvformat,
            xmltvid);
    }

    return 1;
}

static void set_lineup_type(const QString &lineupid, const QString &type)
{
    QMutexLocker locker(&lineup_type_lock);
    if (lineupid_to_srcid[lineupid])
        return;

    // get lineup to source mapping
    uint srcid = 0;
    MSqlQuery query(MSqlQuery::InitCon());
    query.prepare(
        "SELECT sourceid "
        "FROM videosource "
        "WHERE lineupid = :LINEUPID");
    query.bindValue(":LINEUPID", lineupid);

    if (!query.exec() || !query.isActive())
        MythDB::DBError("end_element", query);
    else if (query.next())
        srcid = query.value(0).toUInt();

    if (srcid)
    {
        lineupid_to_srcid[lineupid] = srcid;

        // set type for source
        srcid_to_type[srcid] = type;

        LOG(VB_GENERAL, LOG_INFO, LOC +
            QString("sourceid %1 has lineup type: %2").arg(srcid).arg(type));
    }
}

static QString get_lineup_type(uint sourceid)
{
    QMutexLocker locker(&lineup_type_lock);
    return srcid_to_type[sourceid];
}

/*
 * This function taken from:
 * http://stackoverflow.com/questions/2690328/qt-quncompress-gzip-data
 *
 * Based on zlib example code.
 *
 * Copyright (c) 2011 Ralf Engels <ralf-engels@gmx.de>
 * Copyright (C) 1995-2012 Jean-loup Gailly and Mark Adler
 *
 * Licensed under the terms of the ZLib license which is found at
 * http://zlib.net/zlib_license.html and is as follows:
 *
 *  This software is provided 'as-is', without any express or implied
 *  warranty.  In no event will the authors be held liable for any damages
 *  arising from the use of this software.
 *
 *  Permission is granted to anyone to use this software for any purpose,
 *  including commercial applications, and to alter it and redistribute it
 *  freely, subject to the following restrictions:
 *
 *  1. The origin of this software must not be misrepresented; you must not
 *     claim that you wrote the original software. If you use this software
 *     in a product, an acknowledgment in the product documentation would be
 *     appreciated but is not required.
 *  2. Altered source versions must be plainly marked as such, and must not be
 *     misrepresented as being the original software.
 *  3. This notice may not be removed or altered from any source distribution.
 *
 *  NOTE: The Zlib license is listed as being GPL-compatible
 *    http://www.gnu.org/licenses/license-list.html#ZLib
 */

QByteArray gUncompress(const QByteArray &data)
{
    if (data.size() <= 4) {
        LOG(VB_GENERAL, LOG_WARNING, "gUncompress: Input data is truncated");
        return QByteArray();
    }

    QByteArray result;

    int ret;
    z_stream strm;
    static const int CHUNK_SIZE = 1024;
    char out[CHUNK_SIZE];

    /* allocate inflate state */
    strm.zalloc = Z_NULL;
    strm.zfree = Z_NULL;
    strm.opaque = Z_NULL;
    strm.avail_in = data.size();
    strm.next_in = (Bytef*)(data.data());

    ret = inflateInit2(&strm, 15 +  32); // gzip decoding
    if (ret != Z_OK)
        return QByteArray();

    // run inflate()
    do {
        strm.avail_out = CHUNK_SIZE;
        strm.next_out = (Bytef*)(out);

        ret = inflate(&strm, Z_NO_FLUSH);
        Q_ASSERT(ret != Z_STREAM_ERROR);  // state not clobbered

        switch (ret) {
        case Z_NEED_DICT:
            ret = Z_DATA_ERROR;
            [[clang::fallthrough]];
        case Z_DATA_ERROR:
        case Z_MEM_ERROR:
            (void)inflateEnd(&strm);
            return QByteArray();
        }

        result.append(out, CHUNK_SIZE - strm.avail_out);
    } while (strm.avail_out == 0);

    // clean up and return
    inflateEnd(&strm);
    return result;
}

/* vim: set expandtab tabstop=4 shiftwidth=4: */
