/*
 * Copyright (C) 2005-2009 Patrick Ohly <patrick.ohly@gmx.de>
 * Copyright (C) 2009 Intel Corporation
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) version 3.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301  USA
 */

#include "config.h"
#include <stddef.h>
#include <iostream>
#include <memory>

#include <libgen.h>
#ifdef HAVE_GLIB
#include <glib-object.h>
#endif

#ifdef DBUS_SERVICE

// must come before other header files which pull in boost/intrusive_ptr.hpp
// because it defines some intrusive_ptr_release implementations
#include <gdbus-cxx-bridge.h>

#include <syncevo/declarations.h>

SE_BEGIN_CXX
struct SourceStatus {
    std::string m_mode;
    std::string m_status;
    uint32_t m_error;
};
SE_END_CXX

namespace GDBusCXX {
template<> struct dbus_traits<SyncEvo::SourceStatus> :
public dbus_struct_traits<SyncEvo::SourceStatus,
       dbus_member<SyncEvo::SourceStatus, std::string, &SyncEvo::SourceStatus::m_mode,
       dbus_member<SyncEvo::SourceStatus, std::string, &SyncEvo::SourceStatus::m_status,
       dbus_member_single<SyncEvo::SourceStatus, uint32_t, &SyncEvo::SourceStatus::m_error> > > >
{};
}

using namespace GDBusCXX;

#endif

#include <syncevo/Cmdline.h>
#include <syncevo/SyncContext.h>
#include <syncevo/SuspendFlags.h>
#include <syncevo/LogRedirect.h>
#include <syncevo/LocalTransportAgent.h>
#include <syncevo/CmdlineSyncClient.h>

#include <dlfcn.h>
#include <signal.h>

SE_BEGIN_CXX

#if defined(ENABLE_MAEMO) && defined (ENABLE_EBOOK)

// really override the symbol, even if redefined by EDSAbiWrapper
#undef e_contact_new_from_vcard
extern "C" EContact *e_contact_new_from_vcard(const char *vcard)
{
    static decltype(e_contact_new_from_vcard) *impl;

    if (!impl) {
        impl = (decltype(impl))dlsym(RTLD_NEXT, "e_contact_new_from_vcard");
    }

    // Old versions of EDS-DBus parse_changes_array() call
    // e_contact_new_from_vcard() with a pointer which starts
    // with a line break; Evolution is not happy with that and
    // refuses to parse it. This code forwards until it finds
    // the first non-whitespace, presumably the BEGIN:VCARD.
    while (*vcard && isspace(*vcard)) {
        vcard++;
    }

    return impl ? impl(vcard) : nullptr;
}
#endif

#ifdef DBUS_SERVICE
class RemoteSession;
typedef std::map<std::string, StringMap> Config_t;

/**
 * Act as a dbus server. All requests to dbus server
 * are passed through this class.
 */
class RemoteDBusServer : public DBusRemoteObject
{
public:
    RemoteDBusServer();

    GMainLoop *getLoop() { return m_loop; }

    /** 
     * Check whether the server is started and can be attached.
     * Printing an error message is optional, some callers might
     * prefer a different kind of error handling.
     */
    bool checkStarted(bool printError = true);

    /**
     * execute arguments from command line
     * @param args the arguments of command line
     * @param config the config name parsed from arguments if has
     * @param runSync arguments to run a sync
     * @return true if successfully
     */
    bool execute(const std::vector<std::string> &args, const std::string &config, bool runSync);

    /**
     * To implement the feature of '--monitor' option, monitor a
     * given config if there is a session running.
     * If config is empty, then peak a running session to monitor.
     * @param config the config name parsed from arguments if has
     * @return true if successfully
     */ 
    bool monitor(const std::string &config);

    /**
     * To implement the feature of '--status' without a server.
     * get and print all running sessions in the dbus server
     */
    void runningSessions();

    /** whether the dbus call(s) has/have completed */
    bool done() { return m_replyTotal == m_replyCounter; }

    /** one reply returns. Increase reply counter. */
    void replyInc();

    /** set whether there is an error */
    void setResult(bool result) { m_result = result; }

    /** call 'Server.InfoResponse' */
    void infoResponse(const std::string &id, const std::string &state, const StringMap &resp);

private:
    /** call 'Attach' until it returns */
    void attachSync();

    /** update active session vector according to 'SessionChanged' signal */
    void updateSessions(const std::string &session, bool active);

    /** check m_session is active */
    bool isActive();

    /** get all running sessions. Used internally. */
    void getRunningSessions();

    /** set the total number of replies we must wait */
    void resetReplies(int total = 1) 
    { 
        m_replyTotal = total;
        m_replyCounter = 0; 
    }

    /** signal handler for 'CTRL-C' */
    static void handleSignal(int sig);

    // session used for signal handler, 
    // used to call 'suspend' and 'abort'
    static std::weak_ptr<RemoteSession> g_session;

    // the main loop
    GMainLoop *m_loop;
    // whether client can attach to the daemon. 
    // It is also used to indicate whether daemon is ready to use.
    bool m_attached;
    // error flag
    bool m_result;
    // config name
    std::string m_configName;
    // active session object path
    std::shared_ptr<std::string> m_activeSession;
    // session created or monitored
    std::shared_ptr<RemoteSession> m_session;
    // active sessions after listening to 'SessionChanged' signals
    std::vector<std::string> m_activeSessions;
    // the number of total dbus calls  
    unsigned int m_replyTotal;
    // the number of returned dbus calls 
    unsigned int m_replyCounter;
    // listen to dbus server signal 'SessionChanged'
    SignalWatch<DBusObject_t, bool> m_sessionChanged;
    // listen to dbus server signal 'LogOutput'
    SignalWatch<DBusObject_t, std::string, std::string, std::string> m_logOutput;
    // listen to dbus server signal 'InfoRequest'
    SignalWatch<std::string,
                 DBusObject_t,
                 std::string,
                 std::string,
                 std::string,
                 StringMap > m_infoReq; 

    /** watch daemon whether it is gone */
    std::shared_ptr<Watch> m_daemonWatch;
};

/**
 * Act as a session. All requests to a session are passed
 * through this class.
 */
class RemoteSession : public DBusRemoteObject
{
public:
    RemoteSession(RemoteDBusServer &server, const std::string &path);
    RemoteDBusServer &getServer() { return m_server; }

    /**
     * call 'Execute' method of 'Session' in dbus server
     * without waiting for return
     */
    void executeAsync(const std::vector<std::string> &args);

    /**
     * call 'Suspend' or 'Abort' method of 'Session' in dbus server
     * without waiting for return
     */
    void interruptAsync(const char *operation);

    /** copy config name from server's config */
    void setConfigName(const Config_t &config);

    /** get config name of this session */
    std::string configName() { return m_configName; }

    /** status 'done' is sent by session */
    bool statusDone() { return boost::iequals(m_status, "done"); }

    /** get current status */
    std::string status() { return m_status; }

    /** set the flag to indicate the session is running sync */
    void setRunSync(bool runSync) { m_runSync = runSync; }

    /** pass through logoutput and print them if m_output is true */
    void logOutput(Logger::Level level, const std::string &log, const std::string &procname);

    /** set whether to print output */
    void setOutput(bool output) { m_output = output; }

    /** process signals from daemon */
    void infoReq(const std::string &id,
                 const DBusObject_t &session,
                 const std::string &state,
                 const std::string &handler,
                 const std::string &type,
                 const StringMap &params);

    /** remove InfoReq objects from map */
    void removeInfoReq(const std::string &id);

    typedef std::map<std::string, SourceStatus> SourceStatuses_t;

private:
    /**
     * InfoReq to handle info requests from daemon and
     * call 'Server.InfoResponse' to send its response
     */
    class InfoReq
    {
        /** the session reference */
        RemoteSession &m_session;
        /** the id of InfoRequest */
        std::string m_id;
        /** the type of InfoRequest */
        std::string m_type;
        /** the response map sent to the daemon*/
        StringMap m_resp;

        /** InfoRequest state */
        enum State {
            INIT, // init
            WORKING, //'working'
            RESPONSE, // 'response'
            DONE // 'done'
        };
        /** the current state of InfoRequest */
        State m_state;
    public:
        InfoReq(RemoteSession &session, const std::string &id, const std::string &type);

        /**
         * process the info request dispatched by session
         */
        void process(const std::string &id,
                     const DBusObject_t &session,
                     const std::string &state,
                     const std::string &handler,
                     const std::string &type,
                     const StringMap &params);
    };

    /**
     * implement requirements from info req. Called by InfoReq.
     */
    void handleInfoReq(const std::string &type, const StringMap &params, StringMap &resp);

    /** dbus server */
    RemoteDBusServer &m_server;

    /* whether to log output */
    bool m_output;

    /** config name of the session */
    std::string m_configName;

    /** current status */
    std::string m_status;
    
    /** session is running sync */
    bool m_runSync;

    /** signal watch 'StatusChanged' */
    SignalWatch<std::string, uint32_t, SourceStatuses_t> m_statusChanged;

    /** InfoReq map. store all infoReq belongs to this session */
    std::map<std::string, std::shared_ptr<InfoReq> > m_infoReqs;
};

/**
 * Get current known environment variables, which might be used
 * in executing command line arguments. This is only necessary
 * when using dbus daemon.
 * @param vars the returned environment variables
 */
static void getEnvVars(std::map<std::string, std::string> &vars);

#endif

extern "C"
int main( int argc, char **argv )
{
    if (boost::ends_with(argv[0], "syncevo-local-sync")) {
        return LocalTransportMain(argc, argv);
    }

    // Intercept stderr and route it through our logging.
    // stdout is printed normally. Deconstructing it when
    // leaving main() does one final processing of pending
    // output.
    PushLogger<LogRedirect> redirect(new LogRedirect(LogRedirect::STDERR));
    setvbuf(stderr, nullptr, _IONBF, 0);
    setvbuf(stdout, nullptr, _IONBF, 0);

    SyncContext::initMain("syncevolution");

    // Expand PATH to cover the directory we were started from?
    // This might be needed to find normalize_vcard.
    char *exe = strdup(argv[0]);
    if (!exe) {
        SE_THROW("out of memory");
    }
    if (strchr(exe, '/') ) {
        char *dir = dirname(exe);
        std::string path;
        char *oldpath = getenv("PATH");
        if (oldpath) {
            path += oldpath;
            path += ":";
        }
        path += dir;
        setenv("PATH", path.c_str(), 1);
    }
    free(exe);

    try {
        if (getenv("SYNCEVOLUTION_DEBUG")) {
            Logger::instance().setLevel(Logger::DEBUG);
        }

        SyncEvo::KeyringSyncCmdline cmdline(argc, argv);
        std::vector<std::string> parsedArgs;
        if(!cmdline.parse(parsedArgs)) {
            return 1;
        }

        if (cmdline.dontRun()) {
            return 0;
        }

        Bool useDaemon = cmdline.useDaemon();

        if(cmdline.monitor()) {

#ifdef DBUS_SERVICE
            // monitor a session
            RemoteDBusServer server;
            if(server.checkStarted() && server.monitor(cmdline.getConfigName())) {
                return 0;
            }
            return 1;
#else
            SE_LOG_ERROR(NULL, "this syncevolution binary was compiled without support for monitoring a background sync");
            return 1;
#endif
        } else if(cmdline.status() && 
                  cmdline.getConfigName().empty()) {

#ifdef DBUS_SERVICE
            // '--status' and no server name, try to get running sessions 
            RemoteDBusServer server;
            if(server.checkStarted()) {
                server.runningSessions();
                return 0;
            }
            return 1;
#else
            SE_LOG_SHOW(NULL, "this syncevolution binary was compiled without support for monitoring a background sync");
            return 1;
#endif
        } else if (useDaemon ||
                   !useDaemon.wasSet()) {
#ifdef DBUS_SERVICE
            RemoteDBusServer server;

            // Running execute() without the server available will print errors.
            // Avoid that unless the user explicitly asked for the daemon.
            bool result = server.checkStarted(false);
            if (useDaemon.wasSet() || result) {
                return !server.execute(parsedArgs, cmdline.getConfigName(), cmdline.isSync());
            } else {
                // User didn't select --use-daemon and thus doesn't need to know about it
                // not being available.
                // SE_LOG_SHOW(NULL, "WARNING: cannot run syncevolution as daemon. "
                //             "Trying to run it without daemon.");
            }
#else
            if (useDaemon.wasSet()) {
                SE_LOG_SHOW(NULL, "ERROR: this syncevolution binary was compiled without support of daemon. "
                            "Either run syncevolution with '--use-daemon=no' or without that option."); 
                return 1;
            }
#endif
        } 

        // if forcing not using daemon or trying to use daemon with failures,
        // run arguments in the process
        if (!useDaemon.wasSet() ||
            !useDaemon) {
            EDSAbiWrapperInit();

            /*
             * don't log errors to cerr: LogRedirect cannot distinguish
             * between our valid error messages and noise from other
             * libs, therefore it would get suppressed (logged at
             * level DEVELOPER, while output is at most INFO)
             */
            if (cmdline.run()) {
                return 0;
            } else {
                return 1;
            }
        }
    } catch ( const std::exception &ex ) {
        SE_LOG_ERROR(NULL, "%s", ex.what());
    } catch (...) {
        SE_LOG_ERROR(NULL, "unknown error");
    }

    return 1;
}

#ifdef DBUS_SERVICE
/********************** RemoteDBusServer implementation **************************/
RemoteDBusServer::RemoteDBusServer() :
    DBusRemoteObject(dbus_get_bus_connection("SESSION", nullptr, true, nullptr),
                     "/org/syncevolution/Server",
                     "org.syncevolution.Server",
                     "org.syncevolution",
                     true),
    m_attached(false), m_result(true),
    m_replyTotal(0), m_replyCounter(0),
    m_sessionChanged(*this,"SessionChanged"),
    m_logOutput(*this, "LogOutput"),
    m_infoReq(*this, "InfoRequest")
{
    m_loop = g_main_loop_new (nullptr, FALSE);

    if (getConnection()) {
        //check whether we can attach to the daemon
        //also set up the daemon watch when attaching to server
        attachSync();
        if(m_attached) {
            auto sessionChangedCb = [this] (const DBusObject_t &object, bool active) {
                // update active sessions if needed
                updateSessions(object, active);
                g_main_loop_quit(m_loop);
            };
            m_sessionChanged.activate(sessionChangedCb);
            auto logOutputCb = [this] (const DBusObject_t &object,
                                       const std::string &level,
                                       const std::string &log,
                                       const std::string &procname) {
                if (m_session &&
                    (boost::equals(object, getPath()) ||
                     boost::equals(object, m_session->getPath()))) {
                    m_session->logOutput(Logger::strToLevel(level.c_str()), log, procname);
                }
            };
            m_logOutput.activate(logOutputCb);
            auto infoReqCb = [this] (const std::string &id,
                                     const DBusObject_t &session,
                                     const std::string &state,
                                     const std::string &handler,
                                     const std::string &type,
                                     const StringMap &params) {
                // if m_session is null, just ignore
                if (m_session) {
                    m_session->infoReq(id, session, state, handler, type, params);
                }
            };
            m_infoReq.activate(infoReqCb);
        }
    }
}

bool RemoteDBusServer::checkStarted(bool printError)
{
    if(!m_attached) {
        if (printError) {
            SE_LOG_ERROR(NULL, "SyncEvolution D-Bus server not available.");
        }
        return false;
    }
    return true;
}

void RemoteDBusServer::attachSync()
{
    resetReplies();
    DBusClientCall<std::shared_ptr<Watch> > attach(*this, "Attach");

    auto attachCb = [this] (const std::shared_ptr<Watch> &watch, const std::string &error) {
        if(error.empty()) {
            //if attach is successful, watch server whether it is gone
            m_daemonWatch = watch;
            auto daemonGone = [] () {
                //print error info and exit
                SE_LOG_ERROR(NULL, "Background sync daemon has gone.");
                exit(1);
            };
            m_daemonWatch->setCallback(daemonGone);

            // don't print error information, leave it to caller
            m_attached = true;

            // do a version check now before calling replyInc()
            DBusClientCall< StringMap > getVersions(*this, "GetVersions");
            auto versionCb = [this] (const StringMap &versions,
                                     const std::string &error) {
                replyInc();
                if (!error.empty()) {
                    SE_LOG_DEBUG(NULL, "Server.GetVersions(): %s", error.c_str());
                } else {
                    auto it = versions.find("version");
                    if (it != versions.end() &&
                        it->second != VERSION) {
                        SE_LOG_INFO(NULL,
                                    "proceeding despite version mismatch between command line client 'syncevolution' and 'syncevo-dbus-server' (%s != %s)",
                                    it->second.c_str(),
                                    VERSION);
                    }
                }
            };
            getVersions.start(versionCb);
        } else {
            // done with attach phase, skip version check
            replyInc();
        }
    };

    attach.start(attachCb);
    while(!done()) {
        g_main_loop_run(m_loop);
    }
}

void RemoteDBusServer::infoResponse(const std::string &id,
                                    const std::string &state,
                                    const StringMap &resp)
{
    //call Server.InfoResponse
    DBusClientCall<> call(*this, "InfoResponse");

    auto infoResponseCb = [this] (const std::string &error) {
        replyInc();
        if (!error.empty()) {
            SE_LOG_ERROR(NULL, "information response failed.");
            m_result = false;
        }
        g_main_loop_quit(m_loop);
    };
    call.start(infoResponseCb, id, state, resp);
}

static void SuspendFlagsChanged(RemoteSession *session,
                                SuspendFlags &flags)
{
    if (flags.getState() == SuspendFlags::SUSPEND) {
        session->interruptAsync("Suspend");
    } else if(flags.getState() == SuspendFlags::ABORT) {
        session->interruptAsync("Abort");
    }
}

bool RemoteDBusServer::execute(const std::vector<std::string> &args, const std::string &peer, bool runSync)
{
    //the basic workflow is:
    //1) start a session 
    //2) waiting for the session becomes active
    //3) execute 'arguments' once it is active

    // start a new session
    DBusClientCall<DBusObject_t> startSession(*this, "StartSessionWithFlags");
    std::vector<std::string> flags;
    if (!runSync) {
        flags.push_back("no-sync");
    }
    auto startSessionCb = [this] (const DBusObject_t &sessionPath, const std::string &error) {
        replyInc();
        if (!error.empty()) {
            SE_LOG_ERROR(NULL, "starting D-Bus session failed: %s", error.c_str());
            if (error.find("org.freedesktop.DBus.Error.UnknownMethod") != error.npos) {
                SE_LOG_INFO(NULL, "syncevo-dbus-server is most likely too old");
            }
            m_result = false;
            g_main_loop_quit(m_loop);
            return;
        }
        m_session.reset(new RemoteSession(*this, sessionPath));
        g_main_loop_quit(m_loop);
    };
    startSession.start(startSessionCb, peer, flags);

    // wait until 'StartSession' returns
    resetReplies();
    while(!done()) {
        g_main_loop_run(m_loop);
    }

    if(m_session) {
        m_session->setRunSync(true);

        //if session is not active, just wait
        while(!isActive()) {
            g_main_loop_run(m_loop);
        }
        // Logger::Level level = Logger::instance().getLevel();
        // Logger::instance().setLevel(Logger::DEBUG);
        resetReplies();
        m_session->executeAsync(args);


        while(m_result && !done()) {
            g_main_loop_run(m_loop);
        }

        //if encoutering errors, return
        if(!m_result) {
            return m_result;
        }

        // Acticate signal handling in all cases.
        // We let SuspendFlags catch them and then
        // react in the normal event loop.
        SuspendFlags &flags(SuspendFlags::getSuspendFlags());
        std::shared_ptr<SuspendFlags::Guard> signalGuard = flags.activate();
        flags.m_stateChanged.connect(SuspendFlags::StateChanged_t::slot_type(SuspendFlagsChanged, m_session.get(), boost::placeholders::_1).track_foreign(m_session));

        //wait until status is 'done'
        while(m_result && !m_session->statusDone()) {
            g_main_loop_run(m_loop);
        }

        //restore logging level
        // Logger::instance().setLevel(level);
        m_session->setRunSync(false);
    }
    return m_result;
}

bool RemoteDBusServer::isActive()
{
    /** if current session is active and then start to call 'Execute' method */
    if(m_session) {
        for (const std::string &session: m_activeSessions) {
            if(boost::equals(m_session->getPath(), session.c_str())) {
                return true;
            }
        }
    }
    return false;
}

void RemoteDBusServer::runningSessions()
{
    //the basic working flow is:
    //1) get all sessions
    //2) check each session and collect running sessions
    //3) get config name of running sessions and print them
    std::vector<DBusObject_t> sessions = DBusClientCall< std::vector<DBusObject_t> >(*this, "GetSessions")();

    if (sessions.empty()) {
        SE_LOG_SHOW(NULL, "Background sync daemon is idle.");
    } else {
        SE_LOG_SHOW(NULL, "Running session(s): ");

        // create local objects for sessions
        for (const DBusObject_t &path: sessions) {
            RemoteSession session(*this, path);

            // Get status. Slight race condition here, session might
            // disappear before we can ask. In that case we fail by
            // showing the exception string instead of showing some
            // more comprehensible error message. Unlikely, so don't
            // bother...
            std::tuple<std::string, uint32_t, RemoteSession::SourceStatuses_t> status =
                DBusClientCall<std::string, uint32_t, RemoteSession::SourceStatuses_t>(session, "GetStatus")();
            std::string syncStatus = std::get<0>(status);
            if (boost::istarts_with(syncStatus, "running")) {
                Config_t config = DBusClientCall<Config_t>(session, "GetConfig")(false);
                session.setConfigName(config);

                if (!session.configName().empty()) {
                    SE_LOG_SHOW(NULL, "   %s (%s)", session.configName().c_str(), session.getPath());
                }
            }
        }
    }
}

void RemoteDBusServer::updateSessions(const std::string &session, bool active)
{
    if(active) {
        //add it into active list
        m_activeSessions.push_back(session);
    } else {
        //if inactive, remove it from active list
        for(auto it = m_activeSessions.begin();
                it != m_activeSessions.end(); ++it) {
            if(boost::equals(session, *it)) {
                m_activeSessions.erase(it);
                break;
            }
        }
    }
}

void RemoteDBusServer::replyInc()
{
    // increase counter and check whether all replies are returned
    m_replyCounter++;
    if(done()) {
        g_main_loop_quit(m_loop);
    }
}

bool RemoteDBusServer::monitor(const std::string &peer)
{
    //the basic working flow is:
    //1) get all sessions
    //2) check each session and collect running sessions or
    //3) peak one session with the given peer and monitor it
    std::vector<DBusObject_t> sessions = DBusClientCall< std::vector<DBusObject_t> >(*this, "GetSessions")();

    if (sessions.empty()) {
        SE_LOG_SHOW(NULL, "Background sync daemon is idle, no session available to be be monitored.");
    } else {
        // cheating: client and server might normalize the peer name differently...
        std::string peerNorm = SyncConfig::normalizeConfigString(peer);

        // create local objects for sessions
        for (const DBusObject_t &path: sessions) {
            auto session = std::make_shared<RemoteSession>(*this, path);

            std::tuple<std::string, uint32_t, RemoteSession::SourceStatuses_t> status =
                DBusClientCall<std::string, uint32_t, RemoteSession::SourceStatuses_t>(*session, "GetStatus")();
            std::string syncStatus = std::get<0>(status);
            if (boost::istarts_with(syncStatus, "running")) {
                Config_t config = DBusClientCall<Config_t>(*session, "GetConfig")(false);
                session->setConfigName(config);

                if (peer.empty() ||
                    peerNorm == session->configName()) {
                    SE_LOG_SHOW(NULL, "Monitoring '%s' (%s)\n",
                                session->configName().c_str(),
                                session->getPath());
                    // set DBusServer::m_session so that RemoteSession::logOutput gets called
                    // and enable printing that output
                    m_session = session;
                    session->setOutput(true);

                    // now wait for session to complete
                    while (!session->statusDone()) {
                        g_main_loop_run(getLoop());
                    }

                    SE_LOG_SHOW(NULL, "Monitoring done");
                    return true;
                }
            }
        }
        SE_LOG_SHOW(NULL, "'%s' is not running.", peer.c_str());
    }
    return false;
}


/********************** RemoteSession implementation **************************/
RemoteSession::RemoteSession(RemoteDBusServer &server,
                             const std::string &path) :
    DBusRemoteObject(server.getConnection(),
                     path,
                     "org.syncevolution.Session",
                     "org.syncevolution"),
    m_server(server), m_output(false), m_runSync(false),
    m_statusChanged(*this, "StatusChanged")
{
    auto statusChangedCb = [this] (const std::string &status,
                                   uint32_t errorCode,
                                   const SourceStatuses_t &sourceStatus) {
        // This is not code that runs inside the constructor, although
        // to cppcheck it looks like it does...
        // cppcheck-suppress useInitializationList
        m_status = status;

        if (errorCode) {
            m_server.setResult(false);
            g_main_loop_quit(m_server.getLoop());
        }

        if(status == "done") {
            //if session is done, quit the loop
            g_main_loop_quit(m_server.getLoop());
            m_output = false;
        }
    };
    m_statusChanged.activate(statusChangedCb);
}

void RemoteSession::executeAsync(const std::vector<std::string> &args)
{
    //start to print outputs
    m_output = true;
    std::map<std::string, std::string> vars;
    getEnvVars(vars);
    DBusClientCall<> call(*this, "Execute");
    auto executeCb = [this] (const std::string &error) {
        m_server.replyInc();
        if(!error.empty()) {
            SE_LOG_ERROR(NULL, "running the command line inside the D-Bus server failed");
            m_server.setResult(false);
            //end to print outputs
            m_output = false;
            return;
        }
    };
    call.start(executeCb, args, vars);
}

void RemoteSession::setConfigName(const Config_t &config)
{
    auto it = config.find("");
    if(it != config.end()) {
        StringMap global = it->second;
        auto git = global.find("configName");
        if(git != global.end()) {
            m_configName = git->second;
        }
    }
}

static void interruptCb(const std::string &error)
{
    if (!error.empty()) {
        SE_LOG_DEBUG(NULL, "interruptAsync() error from remote: %s", error.c_str());
    }
}

void RemoteSession::interruptAsync(const char *operation)
{
    // call Suspend() without checking result
    DBusClientCall<> suspend(*this, operation);
    suspend.start(interruptCb);
}

void RemoteSession::logOutput(Logger::Level level, const std::string &log, const std::string &procname)
{
    if(m_output) {
        Logger::MessageOptions options(level);
        options.m_processName = &procname;
        SyncEvo::Logger::instance().messageWithOptions(options, "%s", log.c_str());
    }
}

void RemoteSession::infoReq(const std::string &id,
                            const DBusObject_t &session,
                            const std::string &state,
                            const std::string &handler,
                            const std::string &type,
                            const StringMap &params)
{
    //if command line runs a sync, then try to handle req
    if (m_runSync && boost::iequals(session, getPath())) {
        //only handle password now
        if (boost::iequals("password", type)) {
            auto it = m_infoReqs.find(id);
            if (it != m_infoReqs.end()) {
                it->second->process(id, session, state, handler, type, params);
            } else {
                auto passwd = std::make_shared<InfoReq>(*this, id, type);
                m_infoReqs[id] = passwd;
                passwd->process(id, session, state, handler, type, params);
            }
        } 
    }
}

void RemoteSession::handleInfoReq(const std::string &type, const StringMap &params, StringMap &resp)
{
    if (boost::iequals(type, "password")) {
        char buffer[256];

        std::string descr;
        auto it = params.find("description");
        if (it != params.end()) {
            descr = it->second;
        }
        printf("Enter password for %s: ", descr.c_str());
        fflush(stdout);
        if (fgets(buffer, sizeof(buffer), stdin) &&
                strcmp(buffer, "\n")) {
            size_t len = strlen(buffer);
            if (len && buffer[len - 1] == '\n') {
                buffer[len - 1] = 0;
            }
            resp["password"] = std::string(buffer);
        } else {
            SE_LOG_ERROR(NULL, "could not read password for %s", descr.c_str());
        }
    }
}

void RemoteSession::removeInfoReq(const std::string &id) 
{
    auto it = m_infoReqs.find(id);
    if (it != m_infoReqs.end()) {
        m_infoReqs.erase(it);
    }
}

/********************** InfoReq implementation **************************/
RemoteSession::InfoReq::InfoReq(RemoteSession &session,
                                const std::string &id,
                                const std::string &type)
    :m_session(session), m_id(id), m_type(type), m_state(INIT)
{
}

void RemoteSession::InfoReq::process(const std::string &id,
                                     const DBusObject_t &session,
                                     const std::string &state,
                                     const std::string &handler,
                                     const std::string &type,
                                     const StringMap &params)
{
    //only handle info belongs to this InfoReq
    if (boost::equals(m_id, id)) {
        //check the state and response if necessary
        if (m_state == INIT && boost::iequals("request", state)) {
            m_session.getServer().infoResponse(m_id, "working", StringMap());
            m_state = WORKING;
            m_session.handleInfoReq(type, params, m_resp);
        } else if ((m_state == WORKING) && boost::iequals("waiting", state)) {
            m_session.getServer().infoResponse(m_id, "response", m_resp);
            m_state = RESPONSE;
        } else if (boost::iequals("done", state)) {
            //if request is 'done', remove it
            m_session.removeInfoReq(m_id);
        }
    }
}

void getEnvVars(std::map<std::string, std::string> &vars)
{
    //environment variables used to run command line
    static const char *varNames[] = {
        "http_proxy",
        "HOME",
        "PATH",
        "SYNCEVOLUTION_BACKEND_DIR",
        "SYNCEVOLUTION_DEBUG",
        "SYNCEVOLUTION_GNUTLS_DEBUG",
        "SYNCEVOLUTION_TEMPLATE_DIR",
        "SYNCEVOLUTION_XML_CONFIG_DIR",
        "SYNC_EVOLUTION_EVO_CALENDAR_DELAY",
        "XDG_CACHE_HOME",
        "XDG_CONFIG_HOME",
        "XDG_DATA_HOME"
    };

    for (unsigned int i = 0; i < sizeof(varNames) / sizeof(const char*); i++) {
        const char *value;
        //get values of environment variables if they are set
        if ((value = getenv(varNames[i])) != nullptr) {
            vars.insert(make_pair(varNames[i], value));
        }
    }
}

#endif

SE_END_CXX
