#!/usr/bin/python3

# Copyright (C) 2019-2020
#
# * Felix Delattre <felix@delattre.de>
# * W. Martin Borgert <debacle@debian.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import dbussy


MM_OBJECT = "org.freedesktop.ModemManager1"
MODEM_INTERFACE = MM_OBJECT + ".Modem"
MESSAGING_INTERFACE = MODEM_INTERFACE + ".Messaging"
SIM_INTERFACE = MM_OBJECT + ".Sim"
SMS_INTERFACE = MM_OBJECT + ".Sms"
OBJECTMANAGER_INTERFACE = "org.freedesktop.DBus.ObjectManager"


class GatewaySms:
    def __init__(self, config, loop, logger):
        self.config = config
        self.loop = loop
        self.logger = logger
        self.gateway = None
        self.modem = None

        self.loop.run_until_complete(self._dbus_setup())
        self.loop.run_until_complete(self._modem_setup())

    def setup(self, gateway):
        self.gateway = gateway
        self.loop.create_task(self._prepare())

    async def _prepare(self):
        """setup the DBus signal receiver and wait for SMS, forever in a loop"""
        self.connection.bus_add_match(
            "type='signal',interface='%s',member='Added'" % MESSAGING_INTERFACE
        )
        self.connection.bus_add_match(
            "type='signal',interface='%s',member='InterfacesAdded'" % OBJECTMANAGER_INTERFACE
        )
        self.connection.bus_add_match(
            "type='signal',interface='%s',member='InterfacesRemoved'" % OBJECTMANAGER_INTERFACE
        )
        self.connection.enable_receive_message({dbussy.DBUS.MESSAGE_TYPE_SIGNAL})
        while True:
            message = await self.connection.receive_message_async()
            if (
                message.type == dbussy.DBUS.MESSAGE_TYPE_SIGNAL
                and message.interface == MESSAGING_INTERFACE
                and message.member == "Added"
                and message.path == self.modem
            ):
                await self.message(message)
            elif (
                message.type == dbussy.DBUS.MESSAGE_TYPE_SIGNAL
                and message.interface == OBJECTMANAGER_INTERFACE
                and message.member in ["InterfacesAdded", "InterfacesRemoved"]
            ):
                await self._reinit_modem(message)

    async def message(self, message):
        """check whether this is an SMS for us,
        get its properties and pass them to the other gateway"""
        path, received = [*message.objects]
        if not received:
            self.logger.log.debug("SMS added locally, ignored")
            return
        number, smsc, text, timestamp = await self._get_sms_properties(path)
        self.logger.log.debug("SMS received from %s: %s", number, path)
        self.logger.log.debug("SMS passed on to gateway")
        await self.gateway.dispatch(number, smsc, text, timestamp)
        await self._delete_sms(message.path, path)
        self.logger.log.debug("SMS deleted: %s", path)

    async def dispatch(self, number, text):
        message = dbussy.Message.new_method_call(
            destination=dbussy.valid_bus_name(MM_OBJECT),
            path=dbussy.unsplit_path(self.modem),
            iface=MESSAGING_INTERFACE,
            method="Create",
        )
        message.append_objects("a{sv}", {"number": ("s", number), "text": ("s", text)})
        self.logger.log.debug("SMS prepared")
        reply = await self._send_await_reply(message)
        if reply is not None:
            created = reply.expect_return_objects("o")[0]
        else:
            raise Exception("object did not reply in time")

        self.logger.log.debug("SMS created: %s", created)
        message = dbussy.Message.new_method_call(
            destination=dbussy.valid_bus_name(MM_OBJECT),
            path=dbussy.unsplit_path(created),
            iface=SMS_INTERFACE,
            method="Send",
        )
        reply = await self._send_await_reply(message)
        self.logger.log.debug("SMS sent: %s", created)
        await self._delete_sms(self.modem, created)
        self.logger.log.debug("SMS deleted: %s", created)

    async def _get_sms_properties(self, path):
        number = await self._get_property(path, SMS_INTERFACE, "Number")
        smsc = await self._get_property(path, SMS_INTERFACE, "SMSC")
        text = await self._get_property(path, SMS_INTERFACE, "Text")
        timestamp = await self._get_property(path, SMS_INTERFACE, "Timestamp")
        # old format (MM < 1.12.0): 110228115050-05
        # new format (MM >= 1.12.0): 2011-02-28T11:50:50-05:00
        if len(timestamp) == 15:
            timestamp = "".join(
                ["20"]
                + [
                    item
                    for pair in zip(
                        map("".join, zip(*[iter(timestamp[:-3])] * 2)),
                        ["-", "-", "T", ":", ":", ""],
                    )
                    for item in pair
                ]
                + [timestamp[-3:], ":00"]
            )
        return number, smsc, text, timestamp

    async def _delete_sms(self, path, sms):
        message = dbussy.Message.new_method_call(
            destination=dbussy.valid_bus_name(MM_OBJECT),
            path=dbussy.unsplit_path(path),
            iface=MESSAGING_INTERFACE,
            method="Delete",
        )
        message.append_objects("o", sms)
        await self._send_await_reply(message)

    async def _get_property(self, path, interface, name):
        message = dbussy.Message.new_method_call(
            destination=dbussy.valid_bus_name(MM_OBJECT),
            path=dbussy.unsplit_path(path),
            iface=dbussy.DBUS.INTERFACE_PROPERTIES,
            method="Get",
        )
        message.append_objects("ss", interface, name)
        reply = await self._send_await_reply(message)
        if reply is not None:
            proptype, propvalue = reply.expect_return_objects("v")[0]
            return propvalue
        else:
            raise Exception("object did not reply in time")

    async def _dbus_setup(self):
        self.connection = await dbussy.Connection.bus_get_async(
            type=dbussy.DBUS.BUS_SYSTEM, private=False, loop=self.loop
        )

    async def _modem_setup(self):
        self.modem = None
        modems = await self._modem_get_all()
        modem_filter = self.config.modem_filter

        if modem_filter:
            if ":" in modem_filter:
                filter_type, filter_value = modem_filter.split(":", 1)
                for k, v in modems.items():
                    if MODEM_INTERFACE in v:
                        if (
                            filter_type in v[MODEM_INTERFACE]
                            and v[MODEM_INTERFACE][filter_type][1] == filter_value
                        ):
                            self.modem = k
                            break
            else:
                self.logger.log.warning("invalid modem filter")
        elif len(modems) == 1:
            self.modem = list(modems.keys())[0]
        elif len(modems) > 1:
            self.logger.log.warning("more than one modem found, but no modem filter set")
        else:
            self.logger.log.warning("no modem found")

        if self.modem:
            await self.modem_enable(True)
            if len(self.config.modem_sim_pin):
                await self._send_pin()
        self.logger.log.debug("Using modem: %s", self.modem)

    async def _modem_get_all(self):
        MM_PATH = "/org/freedesktop/ModemManager1"

        message = dbussy.Message.new_method_call(
            destination=dbussy.valid_bus_name(MM_OBJECT),
            path=dbussy.unsplit_path(MM_PATH),
            iface=OBJECTMANAGER_INTERFACE,
            method="GetManagedObjects",
        )
        reply = await self._send_await_reply(message)
        if reply is not None:
            return reply.expect_return_objects("a{oa{sa{sv}}}")[0]
        else:
            raise Exception("object did not reply in time")

    async def _reinit_modem(self, message):
        self.logger.log.debug("Noted a change of state in the modem")
        path, interfaces = [*message.objects]  # noqa: F841
        if MODEM_INTERFACE in interfaces:
            await self._modem_setup()

    async def modem_enable(self, enable):
        message = dbussy.Message.new_method_call(
            destination=dbussy.valid_bus_name(MM_OBJECT),
            path=dbussy.unsplit_path(self.modem),
            iface=MODEM_INTERFACE,
            method="Enable",
        )
        message.append_objects("b", enable)
        await self._send_await_reply(message)

    async def _send_pin(self):
        sim = await self._get_property(self.modem, MODEM_INTERFACE, "Sim")
        message = dbussy.Message.new_method_call(
            destination=dbussy.valid_bus_name(MM_OBJECT),
            path=dbussy.unsplit_path(sim),
            iface=SIM_INTERFACE,
            method="SendPin",
        )
        message.append_objects("s", self.config.modem_sim_pin)
        await self._send_await_reply(message)

    async def get_modem_properties(self):
        if not self.modem:
            raise Exception("no modem found")

        manufacturer = await self._get_property(self.modem, MODEM_INTERFACE, "Manufacturer")
        model = await self._get_property(self.modem, MODEM_INTERFACE, "Model")
        signalquality = await self._get_property(self.modem, MODEM_INTERFACE, "SignalQuality")
        ownnumbers = await self._get_property(self.modem, MODEM_INTERFACE, "OwnNumbers")
        return manufacturer, model, signalquality, ownnumbers

    async def _send_await_reply(self, message):
        return await self.connection.send_await_reply(message, dbussy.DBUS.TIMEOUT_USE_DEFAULT)
