/*
 * Copyright (C) 2016 Canonical Ltd.
 *
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 3, as published
 * by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranties of
 * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
 * PURPOSE.  See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Authors:
 *     Antti Kaijanmäki <antti.kaijanmaki@canonical.com>
 */

#include <nmofono/wwan/sim.h>

#include <ofono/dbus.h>

#define slots
#include <qofono-qt5/qofonoconnectionmanager.h>
#undef slots

#include <NetworkManagerInterface.h>
#include <NetworkManagerDeviceInterface.h>

using namespace std;

namespace nmofono
{
namespace wwan
{
namespace
{

}

Sim::Ptr Sim::fromQOfonoSimWrapper(const QOfonoSimWrapper *wrapper,
                                   connection::ActiveConnectionManager::SPtr activeConnectionManager)
{
    auto sim = Sim::Ptr(new Sim(wrapper->iccid(),
                                "",
                                "",
                                "",
                                "",
                                {},
                                false,
                                activeConnectionManager));
    sim->setOfonoSimManager(wrapper->ofonoSimManager());
    return sim;
}

class Sim::Private : public QObject, public std::enable_shared_from_this<Private>
{
    Q_OBJECT

public:

    Sim& p;

    int m_index = -1;

    QString m_simIdentifier;

    connection::ActiveConnectionManager::SPtr m_activeConnectionManager;
    unique_ptr<OrgFreedesktopNetworkManagerInterface> m_nmInterface;
    unique_ptr<OrgFreedesktopNetworkManagerDeviceInterface> m_nmDeviceInterface;

    QString m_nmModemPath;
    connection::ActiveConnection::SPtr m_activeConnection;

    shared_ptr<QOfonoSimManager> m_simManager;
    shared_ptr<QOfonoConnectionManager> m_connManager;

    QSet<QString> m_interfaces;

    QString m_iccid;
    QString m_imsi;
    QStringList m_phoneNumbers;
    QString m_primaryPhoneNumber;
    QString m_mcc;
    QString m_mnc;
    QStringList m_preferredLanguages;
    bool m_dataRoamingEnabled = false;
    bool m_mobileDataEnabled = false;

    bool m_locked = false;

    Private(Sim &parent)
      : p(parent)
      , m_nmInterface(make_unique<OrgFreedesktopNetworkManagerInterface>(
                      NM_DBUS_SERVICE, NM_DBUS_PATH, QDBusConnection::systemBus()))
    {
    }

    Private(Sim& parent, shared_ptr<QOfonoSimManager> simmgr)
      : p(parent)
      , m_nmInterface(make_unique<OrgFreedesktopNetworkManagerInterface>(
                      NM_DBUS_SERVICE, NM_DBUS_PATH, QDBusConnection::systemBus()))
    {
        simManagerChanged(simmgr);


        /// @todo hook up with system-settings to allow changing the identifier.
        if (m_simIdentifier.isEmpty())
        {
            setSimIdentifier(m_primaryPhoneNumber);
        }
    }

    void nmInterfaceChanged(const QString& path)
    {
        if (m_nmModemPath == path)
        {
            return;
        }

        m_nmModemPath = path;

        if (!m_nmModemPath.isEmpty()) {
            m_nmDeviceInterface = make_unique<OrgFreedesktopNetworkManagerDeviceInterface>(NM_DBUS_SERVICE, m_nmModemPath, QDBusConnection::systemBus());
            connect(m_nmDeviceInterface.get(), &OrgFreedesktopNetworkManagerDeviceInterface::StateChanged,
                    this, &Private::stateChanged);

            /* Disable the `Autoconnect` property, as we want to control mobile data ourselves */
            m_nmDeviceInterface->setAutoconnect(false);

            /* Check if the device has an active connection already */
            auto connection = m_nmDeviceInterface->activeConnection();
            if (!connection.path().isEmpty())
            {
                m_activeConnection = m_activeConnectionManager->connection(connection);
            }
        } else {
            m_nmDeviceInterface.reset();
        }

        update();
    }

public Q_SLOTS:

    void simManagerChanged(shared_ptr<QOfonoSimManager> simmgr)
    {
        if (m_simManager == simmgr)
        {
            return;
        }

        m_simManager = simmgr;

        if (simmgr)
        {
            connect(simmgr.get(), &QOfonoSimManager::subscriberIdentityChanged, this, &Private::imsiChanged);
            connect(simmgr.get(), &QOfonoSimManager::subscriberNumbersChanged, this, &Private::phoneNumbersChanged);
            connect(simmgr.get(), &QOfonoSimManager::mobileCountryCodeChanged, this, &Private::mccChanged);
            connect(simmgr.get(), &QOfonoSimManager::mobileNetworkCodeChanged, this, &Private::mncChanged);
            connect(simmgr.get(), &QOfonoSimManager::preferredLanguagesChanged, this, &Private::preferredLanguagesChanged);
            imsiChanged(simmgr->subscriberIdentity());
            phoneNumbersChanged(simmgr->subscriberNumbers());
            mccChanged(simmgr->mobileCountryCode());
            mncChanged(simmgr->mobileNetworkCode());

            /* Locate the modem device from NetworkManager */
            const auto devices = m_nmInterface->GetDevices().value();
            for (const auto& devicePath : devices) {
                auto device = OrgFreedesktopNetworkManagerDeviceInterface(m_nmInterface->service(), devicePath.path(), m_nmInterface->connection());
                /* The UDI is defined as a device hardware identifier. For ModemManager and oFono,
                * it is the modem path on DBus, which we can use to associate the SIM with the modem device in NetworkManager.
                */
                if (device.deviceType() == NM_DEVICE_TYPE_MODEM &&
                    device.udi() == m_simManager->modemPath())
                {
                    nmInterfaceChanged(devicePath.path());
                    break;
                }

                /* We didn't find a matching modem device, so set the modem interface to nullptr just in case */
                nmInterfaceChanged(QString());
            }
        }
        else
        {
            /* If we had a SIM manager before, we need to clear the modem interface */
            nmInterfaceChanged(QString());
        }

        update();

        Q_EMIT p.presentChanged(m_simManager.get() != nullptr);
    }

    void phoneNumbersChanged(const QStringList &value)
    {
        if (value.isEmpty())
        {
            return;
        }

        m_phoneNumbers = value;
        m_primaryPhoneNumber = value[0];
        Q_EMIT p.primaryPhoneNumberChanged(m_primaryPhoneNumber);
    }

    void imsiChanged(const QString &value)
    {
        if (value.isEmpty())
        {
            return;
        }

        m_imsi = value;
        Q_EMIT p.imsiChanged(m_imsi);
    }

    void mccChanged(const QString &value)
    {
        if (value.isEmpty())
        {
            return;
        }

        m_mcc = value;
        Q_EMIT p.mccChanged(m_mcc);
    }

    void mncChanged(const QString &value)
    {
        if (value.isEmpty())
        {
            return;
        }

        m_mnc = value;
        Q_EMIT p.mncChanged(m_mnc);
    }

    void preferredLanguagesChanged(const QStringList &value)
    {
        if (value.isEmpty())
        {
            return;
        }

        m_preferredLanguages = value;
        Q_EMIT p.preferredLanguagesChanged();
    }

    void stateChanged(uint newState, uint oldState, uint reason)
    {
        update();
    }

    void setConnManager(shared_ptr<QOfonoConnectionManager> connmgr)
    {
        if (m_connManager == connmgr)
        {
            return;
        }

        m_connManager = connmgr;
        if (m_connManager)
        {
            connect(m_connManager.get(),
                    &QOfonoConnectionManager::roamingAllowedChanged, this,
                    &Private::update);

            m_connManager->setRoamingAllowed(m_dataRoamingEnabled);
        }

        update();
    }

    void update()
    {
        if (m_connManager)
        {
            bool powered = m_connManager->powered();
            bool roamingAllowed = m_connManager->roamingAllowed();

            /*
             * Connectivity Service is the policy manager for the system.
             * If ofono has different settings force them back to stored values.
             */
            if (m_dataRoamingEnabled != roamingAllowed)
            {
                m_connManager->setRoamingAllowed(m_dataRoamingEnabled);
            }

            /* Enable ConnMan if not enabled already */
            if (m_mobileDataEnabled && !powered)
            {
                m_connManager->setPowered(true);
            }
        }
        else
        {
            /* empty */
        }

        if (m_nmDeviceInterface) {
            /* We do a wide comparison here to grab a modem device in any activation state, rather than
             * only being activated or deactivated.
             */
            bool enabled = m_nmDeviceInterface->state() > 30 && m_nmDeviceInterface->state() < 110;

            if (m_mobileDataEnabled != enabled)
            {
                if (m_mobileDataEnabled) {
                    m_activeConnection = m_activeConnectionManager->activate(QDBusObjectPath("/"),
                                                                             QDBusObjectPath(m_nmModemPath),
                                                                             QDBusObjectPath("/"));
                } else {
                    m_activeConnectionManager->deactivate(m_activeConnection);
                }
            }
        }
    }

    void setSimIdentifier(const QString& simIdentifier)
    {
        if (m_simIdentifier == simIdentifier)
        {
            return;
        }

        m_simIdentifier = simIdentifier;
        Q_EMIT p.simIdentifierUpdated(m_simIdentifier);
    }

    void setOfono(shared_ptr<QOfonoSimManager> simmgr)
    {
        simManagerChanged(simmgr);
        if (simmgr)
        {
            auto connManager = make_shared<QOfonoConnectionManager>(this);
            connManager->setModemPath(simmgr->modemPath());
            setConnManager(connManager);
        }
        else
        {
            setConnManager(shared_ptr<QOfonoConnectionManager>());
        }
    }


};

Sim::Sim(const QString &iccid,
         const QString &imsi,
         const QString &primaryPhoneNumber,
         const QString &mcc,
         const QString &mnc,
         const QStringList &preferredLanguages,
         bool dataRoamingEnabled,
         connection::ActiveConnectionManager::SPtr activeConnectionManager)
    : d{new Private(*this)}
{
    d->m_iccid = iccid;
    d->m_imsi = imsi;
    d->m_primaryPhoneNumber = primaryPhoneNumber;
    d->m_mcc = mcc;
    d->m_mnc = mnc;
    d->m_preferredLanguages = preferredLanguages;
    d->m_dataRoamingEnabled = dataRoamingEnabled;

    d->m_activeConnectionManager = activeConnectionManager;
}

Sim::~Sim()
{}

const QString&
Sim::simIdentifier() const
{
    return d->m_simIdentifier;
}

QString Sim::iccid() const
{
    return d->m_iccid;
}

QString Sim::imsi() const
{
    return d->m_imsi;
}

QString Sim::primaryPhoneNumber() const
{
    return d->m_primaryPhoneNumber;
}

bool Sim::locked() const
{
    return d->m_locked;
}

bool Sim::present() const
{
    return d->m_simManager.get() != nullptr;
}

QString Sim::mcc() const
{
    return d->m_mcc;
}

QString Sim::mnc() const
{
    return d->m_mnc;
}

QList<QString> Sim::preferredLanguages() const
{
    return d->m_preferredLanguages;
}

bool Sim::dataRoamingEnabled() const
{
    return d->m_dataRoamingEnabled;
}

void Sim::setDataRoamingEnabled(bool value)
{
    if (d->m_dataRoamingEnabled == value)
    {
        return;
    }
    d->m_dataRoamingEnabled = value;
    if (d->m_connManager)
    {
        d->m_connManager->setRoamingAllowed(d->m_dataRoamingEnabled);
    }
    Q_EMIT dataRoamingEnabledChanged(value);
}

bool Sim::mobileDataEnabled() const
{
    return d->m_mobileDataEnabled;
}

void Sim::setMobileDataEnabled(bool value)
{
    if (d->m_mobileDataEnabled == value)
    {
        return;
    }
    d->m_mobileDataEnabled = value;

    if (d->m_connManager)
    {
        if (value && !d->m_connManager->powered())
        {
            d->m_connManager->setPowered(true);
        }
    }

    if (!d->m_nmModemPath.isEmpty())
    {
        if (value)
            d->m_activeConnection = d->m_activeConnectionManager->activate(QDBusObjectPath("/"),
                                                                           QDBusObjectPath(d->m_nmModemPath),
                                                                           QDBusObjectPath("/"));
        else
            d->m_activeConnectionManager->deactivate(d->m_activeConnection);
    }
    Q_EMIT mobileDataEnabledChanged(value);
}

void Sim::unlock()
{

}

QString Sim::ofonoPath() const
{
    if (d->m_simManager)
    {
        return d->m_simManager->objectPath();
    }
    return QString();
}

void Sim::setOfonoSimManager(std::shared_ptr<QOfonoSimManager> simmgr)
{
    d->setOfono(simmgr);
}

}
}

#include "sim.moc"
