// -*- Mode: c++ -*-

// Simplified implementation that does only the following:
// - update existing program if new program has the same starttime and endtime
// - delete all programs that otherwise overlap with the new program
// - insert new program if there is no match for update
// Tested with DVB-C from Ziggo.

#include <limits.h>

// C++ includes
#include <algorithm>
using namespace std;

// MythTV headers
#include "programdata.h"
#include "channelutil.h"
#include "mythdb.h"
#include "mythlogging.h"
#include "dvbdescriptors.h"

#define LOC      QString("ProgramData: ")

static const char *roles[] =
{
    "",
    "actor",     "director",    "producer", "executive_producer",
    "writer",    "guest_star",  "host",     "adapter",
    "presenter", "commentator", "guest",
};

static QString denullify(const QString &str)
{
    return str.isNull() ? "" : str;
}

DBPerson::DBPerson(const DBPerson &other) :
    role(other.role), name(other.name)
{
    name.squeeze();
}

DBPerson::DBPerson(Role _role, const QString &_name) :
    role(_role), name(_name)
{
    name.squeeze();
}

DBPerson::DBPerson(const QString &_role, const QString &_name) :
    role(kUnknown), name(_name)
{
    if (!_role.isEmpty())
    {
        for (uint i = 0; i < sizeof(roles) / sizeof(char *); i++)
        {
            if (_role == QString(roles[i]))
                role = (Role) i;
        }
    }
    name.squeeze();
}

QString DBPerson::GetRole(void) const
{
    if ((role < kActor) || (role > kGuest))
        return "guest";
    return roles[role];
}

uint DBPerson::InsertDB(MSqlQuery &query, uint chanid,
                        const QDateTime &starttime) const
{
    uint personid = GetPersonDB(query);
    if (!personid && InsertPersonDB(query))
        personid = GetPersonDB(query);

    return InsertCreditsDB(query, personid, chanid, starttime);
}

uint DBPerson::GetPersonDB(MSqlQuery &query) const
{
    query.prepare(
        "SELECT person "
        "FROM people "
        "WHERE name = :NAME");
    query.bindValue(":NAME", name);

    if (!query.exec())
        MythDB::DBError("get_person", query);
    else if (query.next())
        return query.value(0).toUInt();

    return 0;
}

uint DBPerson::InsertPersonDB(MSqlQuery &query) const
{
    query.prepare(
        "INSERT IGNORE INTO people (name) "
        "VALUES (:NAME);");
    query.bindValue(":NAME", name);

    if (query.exec())
        return 1;

    MythDB::DBError("insert_person", query);
    return 0;
}

uint DBPerson::InsertCreditsDB(MSqlQuery &query, uint personid, uint chanid,
                               const QDateTime &starttime) const
{
    if (!personid)
        return 0;

    query.prepare(
        "REPLACE INTO credits "
        "       ( person,  chanid,  starttime,  role) "
        "VALUES (:PERSON, :CHANID, :STARTTIME, :ROLE) ");
    query.bindValue(":PERSON",    personid);
    query.bindValue(":CHANID",    chanid);
    query.bindValue(":STARTTIME", starttime);
    query.bindValue(":ROLE",      GetRole());

    if (query.exec())
        return 1;

    MythDB::DBError("insert_credits", query);
    return 0;
}

DBEvent &DBEvent::operator=(const DBEvent &other)
{
    if (this == &other)
        return *this;

    title           = other.title;
    subtitle        = other.subtitle;
    description     = other.description;
    category        = other.category;
    starttime       = other.starttime;
    endtime         = other.endtime;
    airdate         = other.airdate;
    originalairdate = other.originalairdate;

    if (credits != other.credits)
    {
        if (credits)
        {
            delete credits;
            credits = NULL;
        }

        if (other.credits)
        {
            credits = new DBCredits;
            credits->insert(credits->end(),
                            other.credits->begin(),
                            other.credits->end());
        }
    }

    partnumber      = other.partnumber;
    parttotal       = other.parttotal;
    syndicatedepisodenumber = other.syndicatedepisodenumber;
    subtitleType    = other.subtitleType;
    audioProps      = other.audioProps;
    videoProps      = other.videoProps;
    stars           = other.stars;
    categoryType    = other.categoryType;
    seriesId        = other.seriesId;
    programId       = other.programId;
    previouslyshown = other.previouslyshown;
    ratings         = other.ratings;
    listingsource   = other.listingsource;

    Squeeze();

    return *this;
}

void DBEvent::Squeeze(void)
{
    title.squeeze();
    subtitle.squeeze();
    description.squeeze();
    category.squeeze();
    syndicatedepisodenumber.squeeze();
    seriesId.squeeze();
    programId.squeeze();
}

void DBEvent::AddPerson(DBPerson::Role role, const QString &name)
{
    if (!credits)
        credits = new DBCredits;

    credits->push_back(DBPerson(role, name));
}

void DBEvent::AddPerson(const QString &role, const QString &name)
{
    if (!credits)
        credits = new DBCredits;

    credits->push_back(DBPerson(role, name));
}

bool DBEvent::HasTimeConflict(const DBEvent &o) const
{
    return ((starttime <= o.starttime && o.starttime < endtime) ||
            (o.endtime <= endtime     && starttime   < o.endtime));
}

uint DBEvent::GetOverlappingPrograms(
    MSqlQuery &query, uint chanid, vector<DBEvent> &programs) const
{
    uint count = 0;
    query.prepare(
        "SELECT title,          subtitle,      description, "
        "       category,       category_type, "
        "       starttime,      endtime, "
        "       subtitletypes+0,audioprop+0,   videoprop+0, "
        "       seriesid,       programid, "
        "       partnumber,     parttotal, "
        "       syndicatedepisodenumber, "
        "       airdate,        originalairdate, "
        "       previouslyshown,listingsource, "
        "       stars+0 "
        "FROM program "
        "WHERE chanid   = :CHANID AND "
        "      manualid = 0       AND "
        "      ( ( starttime >= :STIME1 AND starttime <  :ETIME1 ) OR "
        "        ( endtime   >  :STIME2 AND endtime   <= :ETIME2 ) OR "
        "        ( starttime <  :STIME3 AND endtime   >  :ETIME3 ) )");
    query.bindValue(":CHANID", chanid);
    query.bindValue(":STIME1", starttime);
    query.bindValue(":ETIME1", endtime);
    query.bindValue(":STIME2", starttime);
    query.bindValue(":ETIME2", endtime);
    query.bindValue(":STIME3", starttime);
    query.bindValue(":ETIME3", endtime);
    // Test STIME3/ETIME3 for new program inside old program

    if (!query.exec())
    {
        MythDB::DBError("GetOverlappingPrograms 1", query);
        return 0;
    }

    while (query.next())
    {
        ProgramInfo::CategoryType category_type =
            string_to_myth_category_type(query.value(4).toString());

        DBEvent prog(
            query.value(0).toString(),
            query.value(1).toString(),
            query.value(2).toString(),
            query.value(3).toString(),
            category_type,
            MythDate::as_utc(query.value(5).toDateTime()),
            MythDate::as_utc(query.value(6).toDateTime()),
            query.value(7).toUInt(),
            query.value(8).toUInt(),
            query.value(9).toUInt(),
            query.value(19).toDouble(),
            query.value(10).toString(),
            query.value(11).toString(),
            query.value(18).toUInt());

        prog.partnumber = query.value(12).toUInt();
        prog.parttotal  = query.value(13).toUInt();
        prog.syndicatedepisodenumber = query.value(14).toString();
        prog.airdate    = query.value(15).toUInt();
        prog.originalairdate  = query.value(16).toDate();
        prog.previouslyshown  = query.value(17).toBool();
        ;

        programs.push_back(prog);
        count++;
    }

    return count;
}

static bool delete_program(MSqlQuery &query, uint chanid, const QDateTime &st)
{
    query.prepare(
        "DELETE from program "
        "WHERE chanid    = :CHANID AND "
        "      starttime = :STARTTIME");

    query.bindValue(":CHANID",    chanid);
    query.bindValue(":STARTTIME", st);

    if (!query.exec())
    {
        MythDB::DBError("delete_program", query);
        return false;
    }

    query.prepare(
        "DELETE from credits "
        "WHERE chanid    = :CHANID AND "
        "      starttime = :STARTTIME");

    query.bindValue(":CHANID",    chanid);
    query.bindValue(":STARTTIME", st);

    if (!query.exec())
    {
        MythDB::DBError("delete_credits", query);
        return false;
    }

    return true;
}

uint DBEvent::UpdateDB(
    MSqlQuery &query, uint chanid, int match_threshold) const
{
    vector<DBEvent> programs;
    uint count = GetOverlappingPrograms(query, chanid, programs);
    bool update = false;
    uint status = 0;

    // Update if we have an overlapping program with
    // exactly the same start- and endtime.
    // Delete all other overlapping programs.
    for (uint i=0; i<count; i++)
    {
	if (starttime == programs[i].starttime &&
	    endtime == programs[i].endtime)
	{
	    LOG(VB_EIT, LOG_DEBUG,
		QString("EIT: update chanid %1 %2 '%3'")
		.arg(chanid)
		.arg(starttime.toString(Qt::ISODate))
		.arg(title));
	    status = InsertDB(query, chanid);
	    update = true;
	}
	else
	{
	    LOG(VB_EIT, LOG_DEBUG,
		QString("EIT: delete chanid %1 %2 '%3'")
		.arg(chanid)
		.arg(programs[i].starttime.toString(Qt::ISODate))
		.arg(programs[i].title));
	    delete_program(query, chanid, programs[i].starttime);
	}
    }

    // If we have not done an update then we will insert a new program.
    if (!update)
    {
	LOG(VB_EIT, LOG_DEBUG,
	    QString("EIT: insert chanid %1 %2 '%3'")
	    .arg(chanid)
	    .arg(starttime.toString(Qt::ISODate))
	    .arg(title));
        status = InsertDB(query, chanid);
    }

    return status;
}

uint DBEvent::InsertDB(MSqlQuery &query, uint chanid) const
{
    query.prepare(
        "REPLACE INTO program ("
        "  chanid,         title,          subtitle,        description, "
        "  category,       category_type, "
        "  starttime,      endtime, "
        "  closecaptioned, stereo,         hdtv,            subtitled, "
        "  subtitletypes,  audioprop,      videoprop, "
        "  stars,          partnumber,     parttotal, "
        "  syndicatedepisodenumber, "
        "  airdate,        originalairdate,listingsource, "
        "  seriesid,       programid,      previouslyshown ) "
        "VALUES ("
        " :CHANID,        :TITLE,         :SUBTITLE,       :DESCRIPTION, "
        " :CATEGORY,      :CATTYPE, "
        " :STARTTIME,     :ENDTIME, "
        " :CC,            :STEREO,        :HDTV,           :HASSUBTITLES, "
        " :SUBTYPES,      :AUDIOPROP,     :VIDEOPROP, "
        " :STARS,         :PARTNUMBER,    :PARTTOTAL, "
        " :SYNDICATENO, "
        " :AIRDATE,       :ORIGAIRDATE,   :LSOURCE, "
        " :SERIESID,      :PROGRAMID,     :PREVSHOWN) ");

    QString cattype = myth_category_type_to_string(categoryType);
    QString empty("");
    query.bindValue(":CHANID",      chanid);
    query.bindValue(":TITLE",       denullify(title));
    query.bindValue(":SUBTITLE",    denullify(subtitle));
    query.bindValue(":DESCRIPTION", denullify(description));
    query.bindValue(":CATEGORY",    denullify(category));
    query.bindValue(":CATTYPE",     cattype);
    query.bindValue(":STARTTIME",   starttime);
    query.bindValue(":ENDTIME",     endtime);
    query.bindValue(":CC",          subtitleType & SUB_HARDHEAR ? true : false);
    query.bindValue(":STEREO",      audioProps   & AUD_STEREO   ? true : false);
    query.bindValue(":HDTV",        videoProps   & VID_HDTV     ? true : false);
    query.bindValue(":HASSUBTITLES",subtitleType & SUB_NORMAL   ? true : false);
    query.bindValue(":SUBTYPES",    subtitleType);
    query.bindValue(":AUDIOPROP",   audioProps);
    query.bindValue(":VIDEOPROP",   videoProps);
    query.bindValue(":STARS",       stars);
    query.bindValue(":PARTNUMBER",  partnumber);
    query.bindValue(":PARTTOTAL",   parttotal);
    query.bindValue(":SYNDICATENO", denullify(syndicatedepisodenumber));
    query.bindValue(":AIRDATE",     airdate ? QString::number(airdate):"0000");
    query.bindValue(":ORIGAIRDATE", originalairdate);
    query.bindValue(":LSOURCE",     listingsource);
    query.bindValue(":SERIESID",    denullify(seriesId));
    query.bindValue(":PROGRAMID",   denullify(programId));
    query.bindValue(":PREVSHOWN",   previouslyshown);

    if (!query.exec())
    {
        MythDB::DBError("InsertDB", query);
        return 0;
    }

    if (credits)
    {
        for (uint i = 0; i < credits->size(); i++)
            (*credits)[i].InsertDB(query, chanid, starttime);
    }

    return 1;
}

ProgInfo::ProgInfo(const ProgInfo &other) :
    DBEvent(other.listingsource)
{
    *this = other;
}

ProgInfo &ProgInfo::operator=(const ProgInfo &other)
{
    if (this == &other)
        return *this;

    DBEvent::operator=(other);

    channel         = other.channel;
    startts         = other.startts;
    endts           = other.endts;
    stars           = other.stars;
    title_pronounce = other.title_pronounce;
    showtype        = other.showtype;
    colorcode       = other.colorcode;
    clumpidx        = other.clumpidx;
    clumpmax        = other.clumpmax;

    channel.squeeze();
    startts.squeeze();
    endts.squeeze();
    stars.squeeze();
    title_pronounce.squeeze();
    showtype.squeeze();
    colorcode.squeeze();
    clumpidx.squeeze();
    clumpmax.squeeze();

    return *this;
}

void ProgInfo::Squeeze(void)
{
    DBEvent::Squeeze();
    channel.squeeze();
    startts.squeeze();
    endts.squeeze();
    stars.squeeze();
    title_pronounce.squeeze();
    showtype.squeeze();
    colorcode.squeeze();
    clumpidx.squeeze();
    clumpmax.squeeze();
}

uint ProgInfo::InsertDB(MSqlQuery &query, uint chanid) const
{
    LOG(VB_XMLTV, LOG_INFO,
        QString("Inserting new program    : %1 - %2 %3 %4")
            .arg(starttime.toString(Qt::ISODate))
            .arg(endtime.toString(Qt::ISODate))
            .arg(channel)
            .arg(title));

    query.prepare(
        "REPLACE INTO program ("
        "  chanid,         title,          subtitle,        description, "
        "  category,       category_type,  "
        "  starttime,      endtime, "
        "  closecaptioned, stereo,         hdtv,            subtitled, "
        "  subtitletypes,  audioprop,      videoprop, "
        "  partnumber,     parttotal, "
        "  syndicatedepisodenumber, "
        "  airdate,        originalairdate,listingsource, "
        "  seriesid,       programid,      previouslyshown, "
        "  stars,          showtype,       title_pronounce, colorcode ) "

        "VALUES("
        " :CHANID,        :TITLE,         :SUBTITLE,       :DESCRIPTION, "
        " :CATEGORY,      :CATTYPE,       "
        " :STARTTIME,     :ENDTIME, "
        " :CC,            :STEREO,        :HDTV,           :HASSUBTITLES, "
        " :SUBTYPES,      :AUDIOPROP,     :VIDEOPROP, "
        " :PARTNUMBER,    :PARTTOTAL, "
        " :SYNDICATENO, "
        " :AIRDATE,       :ORIGAIRDATE,   :LSOURCE, "
        " :SERIESID,      :PROGRAMID,     :PREVSHOWN, "
        " :STARS,         :SHOWTYPE,      :TITLEPRON,      :COLORCODE)");

    QString cattype = myth_category_type_to_string(categoryType);

    query.bindValue(":CHANID",      chanid);
    query.bindValue(":TITLE",       denullify(title));
    query.bindValue(":SUBTITLE",    denullify(subtitle));
    query.bindValue(":DESCRIPTION", denullify(description));
    query.bindValue(":CATEGORY",    denullify(category));
    query.bindValue(":CATTYPE",     cattype);
    query.bindValue(":STARTTIME",   starttime);
    query.bindValue(":ENDTIME",     endtime);
    query.bindValue(":CC",
                    subtitleType & SUB_HARDHEAR ? true : false);
    query.bindValue(":STEREO",
                    audioProps   & AUD_STEREO   ? true : false);
    query.bindValue(":HDTV",
                    videoProps   & VID_HDTV     ? true : false);
    query.bindValue(":HASSUBTITLES",
                    subtitleType & SUB_NORMAL   ? true : false);
    query.bindValue(":SUBTYPES",    subtitleType);
    query.bindValue(":AUDIOPROP",   audioProps);
    query.bindValue(":VIDEOPROP",   videoProps);
    query.bindValue(":PARTNUMBER",  partnumber);
    query.bindValue(":PARTTOTAL",   parttotal);
    query.bindValue(":SYNDICATENO", denullify(syndicatedepisodenumber));
    query.bindValue(":AIRDATE",     airdate ? QString::number(airdate):"0000");
    query.bindValue(":ORIGAIRDATE", originalairdate);
    query.bindValue(":LSOURCE",     listingsource);
    query.bindValue(":SERIESID",    denullify(seriesId));
    query.bindValue(":PROGRAMID",   denullify(programId));
    query.bindValue(":PREVSHOWN",   previouslyshown);
    query.bindValue(":STARS",       stars);
    query.bindValue(":SHOWTYPE",    showtype);
    query.bindValue(":TITLEPRON",   title_pronounce);
    query.bindValue(":COLORCODE",   colorcode);

    if (!query.exec())
    {
        MythDB::DBError("program insert", query);
        return 0;
    }

    QList<EventRating>::const_iterator j = ratings.begin();
    for (; j != ratings.end(); ++j)
    {
        query.prepare(
            "INSERT INTO programrating "
            "       ( chanid, starttime, system, rating) "
            "VALUES (:CHANID, :START,    :SYS,  :RATING)");
        query.bindValue(":CHANID", chanid);
        query.bindValue(":START",  starttime);
        query.bindValue(":SYS",    (*j).system);
        query.bindValue(":RATING", (*j).rating);

        if (!query.exec())
            MythDB::DBError("programrating insert", query);
    }

    if (credits)
    {
        for (uint i = 0; i < credits->size(); ++i)
            (*credits)[i].InsertDB(query, chanid, starttime);
    }

    return 1;
}

bool ProgramData::ClearDataByChannel(
    uint chanid, const QDateTime &from, const QDateTime &to,
    bool use_channel_time_offset)
{
    int secs = 0;
    if (use_channel_time_offset)
        secs = ChannelUtil::GetTimeOffset(chanid) * 60;

    QDateTime newFrom = from.addSecs(secs);
    QDateTime newTo   = to.addSecs(secs);

    MSqlQuery query(MSqlQuery::InitCon());
    query.prepare("DELETE FROM program "
                  "WHERE starttime >= :FROM AND starttime < :TO "
                  "AND chanid = :CHANID ;");
    query.bindValue(":FROM", newFrom);
    query.bindValue(":TO", newTo);
    query.bindValue(":CHANID", chanid);
    bool ok = query.exec();

    query.prepare("DELETE FROM programrating "
                  "WHERE starttime >= :FROM AND starttime < :TO "
                  "AND chanid = :CHANID ;");
    query.bindValue(":FROM", newFrom);
    query.bindValue(":TO", newTo);
    query.bindValue(":CHANID", chanid);
    ok &= query.exec();

    query.prepare("DELETE FROM credits "
                  "WHERE starttime >= :FROM AND starttime < :TO "
                  "AND chanid = :CHANID ;");
    query.bindValue(":FROM", newFrom);
    query.bindValue(":TO", newTo);
    query.bindValue(":CHANID", chanid);
    ok &= query.exec();

    query.prepare("DELETE FROM programgenres "
                  "WHERE starttime >= :FROM AND starttime < :TO "
                  "AND chanid = :CHANID ;");
    query.bindValue(":FROM", newFrom);
    query.bindValue(":TO", newTo);
    query.bindValue(":CHANID", chanid);
    ok &= query.exec();

    return ok;
}

bool ProgramData::ClearDataBySource(
    uint sourceid, const QDateTime &from, const QDateTime &to,
    bool use_channel_time_offset)
{
    vector<uint> chanids = ChannelUtil::GetChanIDs(sourceid);

    bool ok = true;
    for (uint i = 0; i < chanids.size(); i++)
        ok &= ClearDataByChannel(chanids[i], from, to, use_channel_time_offset);

    return ok;
}

static bool start_time_less_than(const DBEvent *a, const DBEvent *b)
{
    return (a->starttime < b->starttime);
}

void ProgramData::FixProgramList(QList<ProgInfo*> &fixlist)
{
    qStableSort(fixlist.begin(), fixlist.end(), start_time_less_than);

    QList<ProgInfo*>::iterator it = fixlist.begin();
    while (1)
    {
        QList<ProgInfo*>::iterator cur = it;
        ++it;

        // fill in miss stop times
        if ((*cur)->endts.isEmpty() || (*cur)->startts > (*cur)->endts)
        {
            if (it != fixlist.end())
            {
                (*cur)->endts   = (*it)->startts;
                (*cur)->endtime = (*it)->starttime;
            }
            else
            {
                (*cur)->endtime = (*cur)->starttime;
                if ((*cur)->endtime < QDateTime(
                        (*cur)->endtime.date(), QTime(6, 0), Qt::UTC))
                {
                    (*cur)->endtime = QDateTime(
                        (*cur)->endtime.date(), QTime(6, 0), Qt::UTC);
                }
                else
                {
                    (*cur)->endtime = QDateTime(
                        (*cur)->endtime.date().addDays(1),
                        QTime(0, 0), Qt::UTC);
                }

                (*cur)->endts =
                    MythDate::toString((*cur)->endtime, MythDate::kFilename);
            }
        }

        if (it == fixlist.end())
            break;

        // remove overlapping programs
        if ((*cur)->HasTimeConflict(**it))
        {
            QList<ProgInfo*>::iterator tokeep, todelete;

            if ((*cur)->endtime <= (*cur)->starttime)
                tokeep = it, todelete = cur;
            else if ((*it)->endtime <= (*it)->starttime)
                tokeep = cur, todelete = it;
            else if (!(*cur)->subtitle.isEmpty() &&
                     (*it)->subtitle.isEmpty())
                tokeep = cur, todelete = it;
            else if (!(*it)->subtitle.isEmpty()  &&
                     (*cur)->subtitle.isEmpty())
                tokeep = it, todelete = cur;
            else if (!(*cur)->description.isEmpty() &&
                     (*it)->description.isEmpty())
                tokeep = cur, todelete = it;
            else
                tokeep = it, todelete = cur;


            LOG(VB_XMLTV, LOG_INFO,
                QString("Removing conflicting program: %1 - %2 %3 %4")
                    .arg((*todelete)->starttime.toString(Qt::ISODate))
                    .arg((*todelete)->endtime.toString(Qt::ISODate))
                    .arg((*todelete)->channel)
                    .arg((*todelete)->title));

            LOG(VB_XMLTV, LOG_INFO,
                QString("Conflicted with            : %1 - %2 %3 %4")
                    .arg((*tokeep)->starttime.toString(Qt::ISODate))
                    .arg((*tokeep)->endtime.toString(Qt::ISODate))
                    .arg((*tokeep)->channel)
                    .arg((*tokeep)->title));

            bool step_back = todelete == it;
            it = fixlist.erase(todelete);
            if (step_back)
                --it;
        }
    }
}

void ProgramData::HandlePrograms(
    uint sourceid, QMap<QString, QList<ProgInfo> > &proglist)
{
    uint unchanged = 0, updated = 0;

    MSqlQuery query(MSqlQuery::InitCon());

    QMap<QString, QList<ProgInfo> >::const_iterator mapiter;
    for (mapiter = proglist.begin(); mapiter != proglist.end(); ++mapiter)
    {
        if (mapiter.key().isEmpty())
            continue;

        query.prepare(
            "SELECT chanid "
            "FROM channel "
            "WHERE sourceid = :ID AND "
            "      xmltvid  = :XMLTVID");
        query.bindValue(":ID",      sourceid);
        query.bindValue(":XMLTVID", mapiter.key());

        if (!query.exec())
        {
            MythDB::DBError("ProgramData::HandlePrograms", query);
            continue;
        }

        vector<uint> chanids;
        while (query.next())
            chanids.push_back(query.value(0).toUInt());

        if (chanids.empty())
        {
            LOG(VB_GENERAL, LOG_NOTICE,
                QString("Unknown xmltv channel identifier: %1"
                        " - Skipping channel.").arg(mapiter.key()));
            continue;
        }

        QList<ProgInfo> &list = proglist[mapiter.key()];
        QList<ProgInfo*> sortlist;
        QList<ProgInfo>::iterator it = list.begin();
        for (; it != list.end(); ++it)
            sortlist.push_back(&(*it));

        FixProgramList(sortlist);

        for (uint i = 0; i < chanids.size(); ++i)
        {
            HandlePrograms(query, chanids[i], sortlist, unchanged, updated);
        }
    }

    LOG(VB_GENERAL, LOG_INFO,
        QString("Updated programs: %1 Unchanged programs: %2")
                .arg(updated) .arg(unchanged));
}

void ProgramData::HandlePrograms(MSqlQuery             &query,
                                 uint                   chanid,
                                 const QList<ProgInfo*> &sortlist,
                                 uint &unchanged,
                                 uint &updated)
{
    QList<ProgInfo*>::const_iterator it = sortlist.begin();
    for (; it != sortlist.end(); ++it)
    {
        if (IsUnchanged(query, chanid, **it))
        {
            unchanged++;
            continue;
        }

        if (!DeleteOverlaps(query, chanid, **it))
            continue;

        updated += (*it)->InsertDB(query, chanid);
    }
}

int ProgramData::fix_end_times(void)
{
    int count = 0;
    QString chanid, starttime, endtime, querystr;
    MSqlQuery query1(MSqlQuery::InitCon()), query2(MSqlQuery::InitCon());

    querystr = "SELECT chanid, starttime, endtime FROM program "
               "WHERE (DATE_FORMAT(endtime,'%H%i') = '0000') "
               "ORDER BY chanid, starttime;";

    if (!query1.exec(querystr))
    {
        LOG(VB_GENERAL, LOG_ERR,
            QString("fix_end_times query failed: %1").arg(querystr));
        return -1;
    }

    while (query1.next())
    {
        starttime = query1.value(1).toString();
        chanid = query1.value(0).toString();
        endtime = query1.value(2).toString();

        querystr = QString("SELECT chanid, starttime, endtime FROM program "
                           "WHERE starttime BETWEEN '%1 00:00:00'"
                           "AND '%2 23:59:59' AND chanid = '%3' "
                           "ORDER BY starttime LIMIT 1;")
                           .arg(endtime.left(10))
                           .arg(endtime.left(10))
                           .arg(chanid);

        if (!query2.exec(querystr))
        {
            LOG(VB_GENERAL, LOG_ERR,
                QString("fix_end_times query failed: %1").arg(querystr));
            return -1;
        }

        if (query2.next() && (endtime != query2.value(1).toString()))
        {
            count++;
            endtime = query2.value(1).toString();
            querystr = QString("UPDATE program SET starttime = '%1', "
                               "endtime = '%2' WHERE (chanid = '%3' AND "
                               "starttime = '%4');")
                               .arg(starttime)
                               .arg(endtime)
                               .arg(chanid)
                               .arg(starttime);

            if (!query2.exec(querystr))
            {
                LOG(VB_GENERAL, LOG_ERR,
                    QString("fix_end_times query failed: %1").arg(querystr));
                return -1;
            }
        }
    }

    return count;
}

bool ProgramData::IsUnchanged(
    MSqlQuery &query, uint chanid, const ProgInfo &pi)
{
    query.prepare(
        "SELECT count(*) "
        "FROM program "
        "WHERE chanid          = :CHANID     AND "
        "      starttime       = :START      AND "
        "      endtime         = :END        AND "
        "      title           = :TITLE      AND "
        "      subtitle        = :SUBTITLE   AND "
        "      description     = :DESC       AND "
        "      category        = :CATEGORY   AND "
        "      category_type   = :CATEGORY_TYPE AND "
        "      airdate         = :AIRDATE    AND "
        "      stars >= (:STARS1 - 0.001)    AND "
        "      stars <= (:STARS2 + 0.001)    AND "
        "      previouslyshown = :PREVIOUSLYSHOWN AND "
        "      title_pronounce = :TITLE_PRONOUNCE AND "
        "      audioprop       = :AUDIOPROP  AND "
        "      videoprop       = :VIDEOPROP  AND "
        "      subtitletypes   = :SUBTYPES   AND "
        "      partnumber      = :PARTNUMBER AND "
        "      parttotal       = :PARTTOTAL  AND "
        "      seriesid        = :SERIESID   AND "
        "      showtype        = :SHOWTYPE   AND "
        "      colorcode       = :COLORCODE  AND "
        "      syndicatedepisodenumber = :SYNDICATEDEPISODENUMBER AND "
        "      programid       = :PROGRAMID");

    QString cattype = myth_category_type_to_string(pi.categoryType);

    query.bindValue(":CHANID",     chanid);
    query.bindValue(":START",      pi.starttime);
    query.bindValue(":END",        pi.endtime);
    query.bindValue(":TITLE",      denullify(pi.title));
    query.bindValue(":SUBTITLE",   denullify(pi.subtitle));
    query.bindValue(":DESC",       denullify(pi.description));
    query.bindValue(":CATEGORY",   denullify(pi.category));
    query.bindValue(":CATEGORY_TYPE", cattype);
    query.bindValue(":AIRDATE",    pi.airdate);
    query.bindValue(":STARS1",     pi.stars);
    query.bindValue(":STARS2",     pi.stars);
    query.bindValue(":PREVIOUSLYSHOWN", pi.previouslyshown);
    query.bindValue(":TITLE_PRONOUNCE", pi.title_pronounce);
    query.bindValue(":AUDIOPROP",  pi.audioProps);
    query.bindValue(":VIDEOPROP",  pi.videoProps);
    query.bindValue(":SUBTYPES",   pi.subtitleType);
    query.bindValue(":PARTNUMBER", pi.partnumber);
    query.bindValue(":PARTTOTAL",  pi.parttotal);
    query.bindValue(":SERIESID",   denullify(pi.seriesId));
    query.bindValue(":SHOWTYPE",   pi.showtype);
    query.bindValue(":COLORCODE",  pi.colorcode);
    query.bindValue(":SYNDICATEDEPISODENUMBER",
                    denullify(pi.syndicatedepisodenumber));
    query.bindValue(":PROGRAMID",  denullify(pi.programId));

    if (query.exec() && query.next())
        return query.value(0).toUInt() > 0;

    return false;
}

bool ProgramData::DeleteOverlaps(
    MSqlQuery &query, uint chanid, const ProgInfo &pi)
{
    if (VERBOSE_LEVEL_CHECK(VB_XMLTV, LOG_INFO))
    {
        // Get overlaps..
        query.prepare(
            "SELECT title,starttime,endtime "
            "FROM program "
            "WHERE chanid     = :CHANID AND "
            "      starttime >= :START AND "
            "      starttime <  :END;");
        query.bindValue(":CHANID", chanid);
        query.bindValue(":START",  pi.starttime);
        query.bindValue(":END",    pi.endtime);

        if (!query.exec())
            return false;

        if (!query.next())
            return true;

        do
        {
            LOG(VB_XMLTV, LOG_INFO,
                QString("Removing existing program: %1 - %2 %3 %4")
                .arg(MythDate::as_utc(query.value(1).toDateTime()).toString(Qt::ISODate))
                .arg(MythDate::as_utc(query.value(2).toDateTime()).toString(Qt::ISODate))
                .arg(pi.channel)
                .arg(query.value(0).toString()));
        } while (query.next());
    }

    if (!ClearDataByChannel(chanid, pi.starttime, pi.endtime, false))
    {
        LOG(VB_XMLTV, LOG_ERR,
            QString("Program delete failed    : %1 - %2 %3 %4")
                .arg(pi.starttime.toString(Qt::ISODate))
                .arg(pi.endtime.toString(Qt::ISODate))
                .arg(pi.channel)
                .arg(pi.title));
        return false;
    }

    return true;
}
