//-*-c++-*-
/***************************************************************************
 *   Copyright (C) 2003 by Fred Schaettgen                                 *
 *   kbluetoothd@schaettgen.de                                             *
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 ***************************************************************************/

#include "devicescanner.h"
#include <kglobal.h>
#include <kconfig.h>
#include <kstandarddirs.h>
#include <qtimer.h>
#include <qdir.h>
#include <qregexp.h>
#include <algorithm>
#include <klocale.h>
#include "procinheritsock.h"
#include <kmessagebox.h>
#include <libkbluetooth/hcisocket.h>
#include <libkbluetooth/inquiry.h>
#include <libkbluetooth/deviceaddress.h>
#include <sys/socket.h>
#include <bluetooth/hci.h>
#include <kdebug.h>
#include "neighbourmonitor.h"
#include <libkbluetooth/configinfo.h>

using namespace std;
using namespace KBluetooth;

DeviceScanner::DeviceScanner(QObject *parent, HciListener* hciListener) :
    DCOPObject("DeviceScanner"), QObject(parent)
{
    nextUpdateTime = QDateTime::currentDateTime();
    updateTimer = new QTimer(this);
    //lastInquiryTime = QDateTime::currentDateTime();
    connect(updateTimer, SIGNAL(timeout()), this, SLOT(slotUpdate()));
    
    jobExecuteTimer = new QTimer(this);
    connect(jobExecuteTimer, SIGNAL(timeout()), this, SLOT(findAndExecuteJob()));
    
    load();
    neighbourMonitor = new NeighbourMonitor(this, hciListener);
    connect(neighbourMonitor, SIGNAL(neighboursChanged()),
        this, SLOT(slotNeighboursChanged()));
}

void DeviceScanner::start()
{
   jobExecuteTimer->start(10000);
   slotUpdate(); 
}

DeviceScanner::~DeviceScanner()
{
    delete (KProcessInheritSocket*)currentProcess;
}

void DeviceScanner::load()
{
    KConfig* cfg = KGlobal::config();
    cfg->setGroup("DeviceScanner");
    QStringList jobNames = findJobs();
    jobs.clear();
    for (uint n=0; n<jobNames.count(); ++n) {
        QString label = jobNames[n];
        ScanJob props;
        props.name = label;
        props.exe = QDir(getJobDir()).filePath(label);
        props.enabled = cfg->readBoolEntry(
            QString("enabled_%1").arg(label), false);
        QStringList deviceList = cfg->readListEntry(
            QString("devicelist_%1").arg(label));
        for (size_t n=0; n<deviceList.size(); ++n) {
            props.deviceList.insert(DeviceAddress(deviceList[n]));
        }
        props.useJobList = cfg->readBoolEntry(
            QString("useJobList_%1").arg(label), false);
        props.isWhitelist = cfg->readBoolEntry(
            QString("isWhitelist_%1").arg(label), false);
        props.minExecInterval = cfg->readNumEntry(
            QString("minExecInterval_%1").arg(label), 0);
        props.intervalNotificationTimeout = cfg->readNumEntry(
            QString("intervalNotification_%1").arg(label), 0);
        jobs[label] = props;
    }

    inquiryInterval = cfg->readNumEntry("inquiryInterval", 300);
    QStringList pagedDevices = cfg->readListEntry(QString("pagedDevices"));
    for (size_t n=0; n<pagedDevices.size(); ++n) {
        pageInterval[DeviceAddress(pagedDevices[n])] = 
            cfg->readNumEntry(QString("pageInterval_%1").arg(pagedDevices[n]), 600);
        // We set the last page time to "now", so paged devices
        // won't be paged immediately.
        lastPageTime[DeviceAddress(pagedDevices[n])] = QDateTime::currentDateTime();
    }
}

void DeviceScanner::store()
{
    KConfig* cfg = KGlobal::config();
    cfg->deleteGroup("DeviceScanner");
    cfg->setGroup("DeviceScanner");
    map<QString, ScanJob>::iterator it;
    for (it = jobs.begin(); it != jobs.end(); ++it) {
        QString label = it->first;
        ScanJob props = it->second;
        cfg->writeEntry(QString("enabled_%1").arg(label), props.enabled);
        QStringList deviceList;
        set<DeviceAddress>::const_iterator devIt;
        for (devIt = props.deviceList.begin(); devIt != props.deviceList.end(); ++devIt) {
            deviceList.append(QString(*devIt));
        }
        cfg->writeEntry(QString("devicelist_%1").arg(label), deviceList);
        cfg->writeEntry(QString("useJobList_%1").arg(label), props.useJobList);
        cfg->writeEntry(QString("isWhitelist_%1").arg(label), props.isWhitelist);
        cfg->writeEntry(QString("minExecInterval_%1").arg(label), props.minExecInterval);
        cfg->writeEntry(QString("intervalNotification_%1").arg(label), 
            props.intervalNotificationTimeout);
    }
    
    cfg->writeEntry("inquiryInterval", inquiryInterval);
    map<DeviceAddress, int>::const_iterator pageIt;
    QStringList pagedDevices;
    for (pageIt = pageInterval.begin(); pageIt != pageInterval.end(); ++pageIt) {
        cfg->writeEntry(QString("pageInterval_%1").arg(QString(pageIt->first)),
            pageIt->second);
        pagedDevices.append(QString(pageIt->first));
    }
    cfg->writeEntry("pagedDevices", pagedDevices); 

    cfg->sync();
}

bool DeviceScanner::getProperties(QString jobname, ScanJob &props)
{
    if (jobs.find(jobname) != jobs.end()) {
        props = jobs[jobname];
        return true;
    }
    else return false;
}

QStringList DeviceScanner::findJobs()
{
    QDir jobDir(getJobDir());
    QStringList ret;
    QStringList fileList = jobDir.entryList(QDir::Files, QDir::Name);
    for (size_t n=0; n<fileList.size(); ++n) {
        if (QRegExp("^\\w+$").search(fileList[n]) >= 0) {
            ret.append(fileList[n]);
        }
    }
    return ret;
}

// DCOP methods: ----------------------------------------------

void DeviceScanner::reloadJobs() {
    load();
}

QStringList DeviceScanner::getJobs()
{
    map<QString, ScanJob>::iterator it;
    QStringList ret;
    for (it = jobs.begin(); it != jobs.end(); ++it) {
        ret.append(it->first);
    }
    return ret;
}

QString DeviceScanner::getJobDir()
{
    return KApplication::kApplication()->dirs()->saveLocation("data", 
         QString(KApplication::kApplication()->name())+"/discovery_jobs/");
}

QString DeviceScanner::getJobTemplateDir() 
{
    return KDEBluetoothConfig::scannerJobTemplateDir;
}

QStringList DeviceScanner::getPagedDevices()
{
    QStringList ret;
    map<DeviceAddress,int>::const_iterator it = pageInterval.begin();
    for (;it != pageInterval.end(); ++it) {
        ret.append(QString(it->first));
    }
    return ret;
}

bool DeviceScanner::setPageInterval(QString device, int interval)
{
    kdDebug() << QString("DeviceScanner::setPageInterval(%1, %2)")
        .arg(device).arg(interval) << endl; 
    pageInterval[DeviceAddress(device)] = interval;
    store();
    slotUpdate();
    return true;
}

int DeviceScanner::getPageInterval(QString device)
{
    kdDebug() << QString("DeviceScanner::getPageInterval(%1)").arg(device) << endl; 
    if (pageInterval.find(DeviceAddress(device)) != pageInterval.end()) {
        return pageInterval[DeviceAddress(device)];
    }
    else return -1;    
}

bool DeviceScanner::removePagedDevice(QString device)
{
    kdDebug() << QString("DeviceScanner::removePagedDevice(%1)").arg(device) << endl; 
    if (pageInterval.find(DeviceAddress(device)) != pageInterval.end()) {
        pageInterval.erase(pageInterval.find(DeviceAddress(device)));
        store();
        slotUpdate();
        return true;
    }
    else return false;    
}

void DeviceScanner::setInquiryInterval(int interval)
{
    inquiryInterval = interval;
    store();
    slotUpdate();
}

int DeviceScanner::getInquiryInterval()
{
    return inquiryInterval;
}

QStringList DeviceScanner::getJobDeviceList(QString job)
{
    ScanJob props;
    QStringList ret;
    if (getProperties(job, props)) {
        set<DeviceAddress>::const_iterator it;
        for (it = props.deviceList.begin(); it != props.deviceList.end(); ++it) {
            ret.append(QString(*it));
        }    
    }
    return ret;
}

bool DeviceScanner::addJobDevice(QString job, QString device)
{
    kdDebug() << QString("DeviceScanner::addJobDevice(%1,%2)")
        .arg(job).arg(device) << endl; 
    ScanJob props;
    if (getProperties(job, props)) {
        jobs[job].deviceList.insert(DeviceAddress(device));
        store();
        slotUpdate();
        return true;
    }
    else return false;    
}

bool DeviceScanner::removeJobDevice(QString job, QString device)
{
    kdDebug() << QString("DeviceScanner::removeJobDevice(%1,%2)")
        .arg(job).arg(device) << endl; 
    ScanJob props;
    if (getProperties(job, props)) {
        set<DeviceAddress> &deviceList = jobs[job].deviceList;
        if (deviceList.find(DeviceAddress(device)) != deviceList.end()) {
            deviceList.erase(deviceList.find(DeviceAddress(device)));
            store();
            slotUpdate();
            return true;
        }
        else return false;
    }
    else return false;    
}

bool DeviceScanner::getJobEnabled(QString job)
{
    ScanJob props;
    if (getProperties(job, props)) {
        return props.enabled;
    }
    else return false;
}

bool DeviceScanner::setJobEnabled(QString job, bool enabled)
{
    ScanJob props;
    if (getProperties(job, props)) {
        jobs[job].enabled = enabled;
        store();
        slotUpdate();
        return true;
    }
    else return false;
}

bool DeviceScanner::getUseJobList(QString job)
{
    ScanJob props;
    if (getProperties(job, props)) {
        return props.useJobList;
    }
    else return false;
}

bool DeviceScanner::setUseJobList(QString job, bool bJobList)
{
    ScanJob props;
    if (getProperties(job, props)) {
        jobs[job].useJobList = bJobList;
        store();
        slotUpdate();
        return true;
    }
    else return false;
}


bool DeviceScanner::getIsWhitelist(QString job)
{
    ScanJob props;
    if (getProperties(job, props)) {
        return props.isWhitelist;
    }
    else return false;
}

bool DeviceScanner::setIsWhitelist(QString job, bool bWhitelist)
{
    ScanJob props;
    if (getProperties(job, props)) {
        jobs[job].isWhitelist = bWhitelist;
        store();
        slotUpdate();
        return true;
    }
    else return false;
}

int DeviceScanner::getJobMinExecInterval(QString job)
{
    ScanJob props;
    if (getProperties(job, props)) {
        return props.minExecInterval;
    }
    else return 0;
}

bool DeviceScanner::setJobMinExecInterval(QString job, int timeout)
{
    ScanJob props;
    if (getProperties(job, props)) {
        jobs[job].minExecInterval = timeout;
        store();
        slotUpdate();
        return true;
    }
    else return false;
}

bool DeviceScanner::setIntervalNotification(QString job, int interval)
{
    ScanJob props;
    if (getProperties(job, props)) {
        jobs[job].intervalNotificationTimeout = interval;
        store();
        slotUpdate();
        return true;
    }
    else return false;
}

int DeviceScanner::getIntervalNotification(QString job)
{
    ScanJob props;
    if (getProperties(job, props)) {
        return props.intervalNotificationTimeout;
    }
    else return 0;
}


QStringList DeviceScanner::getCurrentNeighbours()
{
    QStringList ret;
    set<DeviceAddress> neighbors = neighbourMonitor->getNeighbourSet();
    for (set<DeviceAddress>::iterator it = neighbors.begin(); it != neighbors.end(); ++it) {
        ret.append(QString(*it));
    }
    return ret;
}

bool DeviceScanner::executeJob(QString name)
{
    if (jobs.find(name) != jobs.end()) {
        executeJob(name, jobs[name], "run", true);
        return true;
    }
    else return false;
}

bool DeviceScanner::configureJob(QString name)
{
    if (jobs.find(name) != jobs.end()) {
        executeJob(name, jobs[name], "configure", true);
        return true;
    }
    else return false;
}

// End of DCOP methods. ------------------------------------------

/**
 * This function iterates through all jobs, start inquries/paging
 * when needed and sets a Timer when the next action is due.
 */
void DeviceScanner::slotUpdate()
{
    QDateTime curTime = QDateTime::currentDateTime();
    if (curTime >= nextUpdateTime) {
        nextUpdateTime = curTime.addDays(1);
    }
    
    if (lastInquiryTime.addSecs(inquiryInterval) < curTime && inquiryInterval > 0) {
        kdDebug() << "DeviceScanner::slotUpdate: scheduled inquiry." << endl; 
        neighbourMonitor->addInquiry();     
        lastInquiryTime = curTime;   
    }
    nextUpdateTime = min(nextUpdateTime, 
        lastInquiryTime.addSecs(inquiryInterval));
        
    // Look at the devices that we have to page
    map<DeviceAddress,int>::const_iterator pageIt;
    for (pageIt=pageInterval.begin(); pageIt!=pageInterval.end(); ++pageIt) {
        if (lastPageTime[pageIt->first].isNull() || 
            lastPageTime[pageIt->first].secsTo(curTime) >= pageIt->second) {
            // Add the current address to page to the list
            // of pending page jobs
            kdDebug() << "DeviceScanner::slotUpdate: scheduled paging of " 
                << QString(pageIt->first) << endl;
            neighbourMonitor->addPage(pageIt->first);
            
            lastPageTime[pageIt->first] = curTime;
        }
        nextUpdateTime = min(nextUpdateTime, 
            lastPageTime[pageIt->first].addSecs(pageIt->second));
    }
    
    updateTimer->start(max(1000, 1000*curTime.secsTo(nextUpdateTime)), TRUE); 
}

void DeviceScanner::slotNeighboursChanged()
{
    emit neighboursChanged();
    findAndExecuteJob();
}

// Called by jobExecuteTimer and when new devices are detected
void DeviceScanner::findAndExecuteJob()
{
    // If there is still a process running we will wait for
    // it to end. findAndExecuteJob will be called again then.
    if (currentProcess && currentProcess->isRunning()) {
        return;
    }

    // Walk through all scan jobs and see if the remembered sets
    // of nearby devices have changed since we checked the last time
    QDateTime curTime = QDateTime::currentDateTime();
    map<QString, ScanJob>::iterator jobIt;        
    for (jobIt = jobs.begin(); jobIt != jobs.end(); ++jobIt) {
        if (jobIt->second.enabled == false) continue;
        ScanJob &job = jobIt->second;
        if (job.lastExecTime.isValid() == false  || 
            job.lastExecTime.secsTo(curTime) >= job.minExecInterval) {
            executeJob(jobIt->first, job, "run");
        }                             
    }        
}


void DeviceScanner::executeJob(QString jobName, ScanJob& job, QString jobCmd, bool bForceExec) 
{
    set<DeviceAddress> newNeighbourSet = neighbourMonitor->getNeighbourSet();
    
    set<DeviceAddress> oldNeighbourSet = job.lastNeighbourSet;
    
    // Figure out which addresses are new in the neighbor list
    // and which are gone.
    set<DeviceAddress> newAddresses;
    set_difference(
        newNeighbourSet.begin(), newNeighbourSet.end(),
        oldNeighbourSet.begin(), oldNeighbourSet.end(),
        insert_iterator<set<DeviceAddress> >(newAddresses, newAddresses.end()));
    set<DeviceAddress> lostAddresses;
    set_difference(
        oldNeighbourSet.begin(), oldNeighbourSet.end(),
        newNeighbourSet.begin(), newNeighbourSet.end(),
        insert_iterator<set<DeviceAddress> >(lostAddresses, lostAddresses.end()));
    
    // now we know which devices appeared and which were lost.
    // If the current job wishes to know about any of these found
    // or lost devices, we will call the job executable now
    set<DeviceAddress> notifyLostAddresses, notifyNewAddresses, notifyCurrentAddresses;
    if (job.useJobList) {
        if (job.isWhitelist) {
            set_intersection(
                lostAddresses.begin(), lostAddresses.end(),
                job.deviceList.begin(), job.deviceList.end(),
                insert_iterator<set<DeviceAddress> >(
                    notifyLostAddresses, notifyLostAddresses.end()));
            set_intersection(
                newAddresses.begin(), newAddresses.end(),
                job.deviceList.begin(), job.deviceList.end(),
                insert_iterator<set<DeviceAddress> >(
                    notifyNewAddresses, notifyNewAddresses.end()));
            set_intersection(
                newNeighbourSet.begin(), newNeighbourSet.end(),
                job.deviceList.begin(), job.deviceList.end(),
                insert_iterator<set<DeviceAddress> >(
                    notifyCurrentAddresses, notifyCurrentAddresses.end()));
        }                    
        else {
            set_difference(
                lostAddresses.begin(), lostAddresses.end(),
                job.deviceList.begin(), job.deviceList.end(),
                insert_iterator<set<DeviceAddress> >(
                    notifyLostAddresses, notifyLostAddresses.end()));
            set_difference(
                newAddresses.begin(), newAddresses.end(),
                job.deviceList.begin(), job.deviceList.end(),
                insert_iterator<set<DeviceAddress> >(
                    notifyNewAddresses, notifyNewAddresses.end()));
            set_difference(
                newNeighbourSet.begin(), newNeighbourSet.end(),
                job.deviceList.begin(), job.deviceList.end(),
                insert_iterator<set<DeviceAddress> >(
                    notifyCurrentAddresses, notifyCurrentAddresses.end()));
        }
    }
    else {
        notifyLostAddresses = lostAddresses;
        notifyNewAddresses = newAddresses;
        notifyCurrentAddresses = newNeighbourSet;
    }
        
    if ((notifyNewAddresses.size() > 0) ||
        (notifyLostAddresses.size() >0) || bForceExec) {
        kdDebug() << "Trying to start job:" << jobName <<  
            " (" << job.exe << ")" << endl;
    
        // We run only one job at a time, but when one job exits,
        // we immediately check for new jobs to be started
        if (!currentProcess || !currentProcess->isRunning()) {
            delete (KProcessInheritSocket*)currentProcess;
            currentProcess = new KProcessInheritSocket(0);
            connect(currentProcess, SIGNAL(processExited(KProcess*)),
                this, SLOT(slotProcessExited(KProcess*)));
            connect(currentProcess, SIGNAL(receivedStdout(KProcess*,char*,int)),
                this, SLOT(slotProcessStdout(KProcess*,char*,int)));
            connect(currentProcess, SIGNAL(receivedStderr(KProcess*,char*,int)),
                this, SLOT(slotProcessStderr(KProcess*,char*,int)));
            *currentProcess << job.exe.local8Bit() << jobCmd.local8Bit();
            set<DeviceAddress>::iterator it;
            
            QStringList lostAddresses;
            for (it = notifyLostAddresses.begin(); 
                it != notifyLostAddresses.end(); ++it) 
            {
                kdDebug() << "Adding lost address: " << QString(*it) << endl;
                lostAddresses.append(QString(*it));
            }
            currentProcess->setEnvironment("LOST_DEVICES", lostAddresses.join(" "));
            
            QStringList foundAddresses;
            for (it = notifyNewAddresses.begin(); 
                it != notifyNewAddresses.end(); ++it) 
            {
                kdDebug() << "Adding found address: " << QString(*it) << endl;
                foundAddresses.append(QString(*it));
            }
            currentProcess->setEnvironment("FOUND_DEVICES", foundAddresses.join(" "));
            
            QStringList curAddresses;
            for (it = notifyCurrentAddresses.begin(); 
                it != notifyCurrentAddresses.end(); ++it) 
            {
                kdDebug() << "Adding current address: " << QString(*it) << endl;
                curAddresses.append(QString(*it));
            }
            currentProcess->setEnvironment("CURRENT_DEVICES", curAddresses.join(" "));
            currentProcess->setEnvironment("JOB_PATH", job.exe.local8Bit());
            currentProcess->setEnvironment("JOB_TEMPLATE_DIR", getJobTemplateDir());
            currentProcess->setEnvironment("JOB_DIR", getJobDir());
            
            currentlyRunningJob = job;
            kdDebug() << "Starting job " << jobName <<  
                " (" << job.exe << ")" << endl;
            if (!currentProcess->start(KProcess::NotifyOnExit, 
                KProcess::AllOutput)) 
            {
                KNotifyClient::event(
#if (QT_VERSION >= 0x030200)
KApplication::kApplication()->mainWidget()->winId(),
#endif
"ProcessFailed",
i18n("<p>Executable <b>%1</b> for device search job <b>%2</b> \
could not be started.</p>").arg(job.exe).arg(jobName));
            }
            // Update the remembered set of neighbors
            //job.lastDiscoverableSet = discoverableNeighbourSet;
            //job.lastPageableSet = nondiscoverableNeighbourSet;
            job.lastNeighbourSet = newNeighbourSet;
            
            job.lastExecTime = QDateTime::currentDateTime();
        }
        else {
            kdDebug() << "Couldn't start job " << jobName << ", other job running." << endl;
        }
    }
}

void DeviceScanner::slotProcessStderr(KProcess*, char* msg, int)
{
    kdDebug() << "Job error msg: " << msg << endl; 
    QString message = QString(msg).replace("\n","<br/>\n");
    KNotifyClient::event(
#if (QT_VERSION >= 0x030200)
KApplication::kApplication()->mainWidget()->winId(),
#endif
"ProcessStderr",
i18n("<p>Job <b>%1</b>: <i>%2</i></p>")
.arg(currentlyRunningJob.name).arg(message));  
}

void DeviceScanner::slotProcessStdout(KProcess*, char* msg, int)
{
    kdDebug() << "Job msg: " << msg << endl; 
    KNotifyClient::event(
#if (QT_VERSION >= 0x030200)
KApplication::kApplication()->mainWidget()->winId(),
#endif
"ProcessStdout",
i18n("<p>Job <b>%1</b>: <i>%2</i></p>")
.arg(currentlyRunningJob.name).arg(msg));  
}

void DeviceScanner::slotProcessExited(KProcess *process) 
{
    kdDebug() << "Execution of last job finished." << endl;
    if (process->normalExit()) {
        if (process->exitStatus() > 0) {
            KNotifyClient::event(
#if (QT_VERSION >= 0x030200)
KApplication::kApplication()->mainWidget()->winId(),
#endif
"ProcessFailed",
i18n("<p>Device search job <b>%1</b> returned with error code <b>%2</b>.</p>")
.arg(currentlyRunningJob.exe).arg(process->exitStatus()));
        }
    }
    else {
        KNotifyClient::event(
#if (QT_VERSION >= 0x030200)
KApplication::kApplication()->mainWidget()->winId(),
#endif
"ProcessFailed",
i18n("<p>Device search job <b>%1</b> was killed.</p>").arg(currentlyRunningJob.exe));
    } 
       
    findAndExecuteJob();
}


#include "devicescanner.moc"
