#!/usr/bin/env seiscomp-python
# -*- coding: utf-8 -*-
############################################################################
# Copyright (C) GFZ Potsdam                                                #
# All rights reserved.                                                     #
#                                                                          #
# GNU Affero General Public License Usage                                  #
# This file may be used under the terms of the GNU Affero                  #
# Public License version 3.0 as published by the Free Software Foundation  #
# and appearing in the file LICENSE included in the packaging of this      #
# file. Please review the following information to ensure the GNU Affero   #
# Public License version 3.0 requirements will be met:                     #
# https://www.gnu.org/licenses/agpl-3.0.html.                              #
############################################################################

import sys
import subprocess
import traceback

from seiscomp import client, core, datamodel, logging, seismology, system, math


class VoiceAlert(client.Application):

    def __init__(self, argc, argv):
        client.Application.__init__(self, argc, argv)

        self.setMessagingEnabled(True)
        self.setDatabaseEnabled(True, True)
        self.setLoadRegionsEnabled(True)
        self.setMessagingUsername("")
        self.setPrimaryMessagingGroup(client.Protocol.LISTENER_GROUP)
        self.addMessagingSubscription("EVENT")
        self.addMessagingSubscription("LOCATION")
        self.addMessagingSubscription("MAGNITUDE")

        self.setAutoApplyNotifierEnabled(True)
        self.setInterpretNotifierEnabled(True)

        self.setLoadCitiesEnabled(True)
        self.setLoadRegionsEnabled(True)

        self._ampType = "snr"
        self._citiesMaxDist = 20
        self._citiesMinPopulation = 50000

        self._cache = None
        self._eventDescriptionPattern = None
        self._ampScript = None
        self._alertScript = None
        self._eventScript = None

        self._ampProc = None
        self._alertProc = None
        self._eventProc = None

        self._newWhenFirstSeen = False
        self._prevMessage = {}
        self._agencyIDs = []

    def createCommandLineDescription(self):
        self.commandline().addOption(
            "Generic",
            "first-new",
            "Calls an event a new event when it is seen the first time.",
        )
        self.commandline().addGroup("Alert")
        self.commandline().addStringOption(
            "Alert",
            "amp-type",
            "Specify the amplitude type to listen to.",
            self._ampType,
        )
        self.commandline().addStringOption(
            "Alert",
            "amp-script",
            "Specify the script to be called when a "
            "stationamplitude arrived, network-, stationcode and amplitude are "
            "passed as parameters $1, $2 and $3.",
        )
        self.commandline().addStringOption(
            "Alert",
            "alert-script",
            "Specify the script to be called when a "
            "preliminary origin arrived, latitude and longitude are passed as "
            "parameters $1 and $2.",
        )
        self.commandline().addStringOption(
            "Alert",
            "event-script",
            "Specify the script to be called when an "
            "event has been declared; the message string, a flag (1=new event, "
            "0=update event), the EventID, the arrival count and the magnitude "
            "(optional when set) are passed as parameter $1, $2, $3, $4 and $5.",
        )
        self.commandline().addGroup("Cities")
        self.commandline().addStringOption(
            "Cities",
            "max-dist",
            "Maximum distance for using the distance from a city to the earthquake.",
            str(self._citiesMaxDist),
        )
        self.commandline().addStringOption(
            "Cities",
            "min-population",
            "Minimum population for a city to become a point of interest.",
            str(self._citiesMinPopulation),
        )
        self.commandline().addGroup("Debug")
        self.commandline().addStringOption("Debug", "eventid,E", "Specify event ID.")
        return True

    def init(self):
        if not client.Application.init(self):
            return False

        try:
            self._newWhenFirstSeen = self.configGetBool("firstNew")
        except BaseException:
            pass

        try:
            agencyIDs = self.configGetStrings("agencyIDs")
            for item in agencyIDs:
                item = item.strip()
                if item not in self._agencyIDs:
                    self._agencyIDs.append(item)
        except BaseException:
            pass

        try:
            if self.commandline().hasOption("first-new"):
                self._newWhenFirstSeen = True
        except BaseException:
            pass

        try:
            self._eventDescriptionPattern = self.configGetString("poi.message")
        except BaseException:
            pass

        try:
            self._citiesMaxDist = self.configGetDouble("poi.maxDist")
        except BaseException:
            pass

        try:
            self._citiesMaxDist = self.commandline().optionDouble("max-dist")
        except BaseException:
            pass

        try:
            self._citiesMinPopulation = self.configGetInt("poi.minPopulation")
        except BaseException:
            pass

        try:
            self._citiesMinPopulation = self.commandline().optionInt("min-population")
        except BaseException:
            pass

        try:
            self._ampType = self.commandline().optionString("amp-type")
        except BaseException:
            pass

        try:
            self._ampScript = self.commandline().optionString("amp-script")
        except BaseException:
            try:
                self._ampScript = self.configGetString("scripts.amplitude")
            except BaseException:
                logging.warning("No amplitude script defined")

        if self._ampScript:
            self._ampScript = system.Environment.Instance().absolutePath(
                self._ampScript
            )

        try:
            self._alertScript = self.commandline().optionString("alert-script")
        except BaseException:
            try:
                self._alertScript = self.configGetString("scripts.alert")
            except BaseException:
                logging.warning("No alert script defined")

        if self._alertScript:
            self._alertScript = system.Environment.Instance().absolutePath(
                self._alertScript
            )

        try:
            self._eventScript = self.commandline().optionString("event-script")
        except BaseException:
            try:
                self._eventScript = self.configGetString("scripts.event")
                logging.info(f"Using event script: {self._eventScript}")
            except BaseException:
                logging.warning("No event script defined")

        if self._eventScript:
            self._eventScript = system.Environment.Instance().absolutePath(
                self._eventScript
            )

        logging.info("Creating ringbuffer for 100 objects")
        if not self.query():
            logging.warning("No valid database interface to read from")
        self._cache = datamodel.PublicObjectRingBuffer(self.query(), 100)

        if self._ampScript and self.connection():
            self.connection().subscribe("AMPLITUDE")

        if self._newWhenFirstSeen:
            logging.info("A new event is declared when I see it the first time")

        if not self._agencyIDs:
            logging.info("agencyIDs: []")
        else:
            logging.info(f"agencyIDs: {' '.join(self._agencyIDs)}")

        return True

    def printUsage(self):

        print(
            """Usage:
  scvoice [options]

Alert the user acoustically in real time.
"""
        )

        client.Application.printUsage(self)

        print(
            """Examples:
Execute scvoice on command line with debug output
  scvoice --debug
"""
        )

    def run(self):
        try:
            try:
                eventID = self.commandline().optionString("eventid")
                event = self._cache.get(datamodel.Event, eventID)
                if event:
                    self.notifyEvent(event)
            except BaseException:
                pass

            return client.Application.run(self)
        except BaseException:
            info = traceback.format_exception(*sys.exc_info())
            for i in info:
                sys.stderr.write(i)
            return False

    def runAmpScript(self, net, sta, amp):
        if not self._ampScript:
            return

        if self._ampProc is not None:
            if self._ampProc.poll() is None:
                logging.warning("AmplitudeScript still in progress -> skipping message")
                return
        try:
            self._ampProc = subprocess.Popen([self._ampScript, net, sta, f"{amp:.2f}"])
            logging.info("Started amplitude script with pid %d" % self._ampProc.pid)
        except BaseException:
            logging.error(f"Failed to start amplitude script '{self._ampScript}'")

    def runAlert(self, lat, lon):
        if not self._alertScript:
            return

        if self._alertProc is not None:
            if self._alertProc.poll() is None:
                logging.warning("AlertScript still in progress -> skipping message")
                return
        try:
            self._alertProc = subprocess.Popen(
                [self._alertScript, f"{lat:.1f}", f"{lon:.1f}"]
            )
            logging.info("Started alert script with pid %d" % self._alertProc.pid)
        except BaseException:
            logging.error(f"Failed to start alert script '{self._alertScript}'")

    def done(self):
        self._cache = None
        client.Application.done(self)

    def handleMessage(self, msg):
        try:
            dm = core.DataMessage.Cast(msg)
            if dm:
                for att in dm:
                    org = datamodel.Origin.Cast(att)
                    if not org:
                        continue

                    try:
                        if org.evaluationStatus() == datamodel.PRELIMINARY:
                            self.runAlert(
                                org.latitude().value(), org.longitude().value()
                            )
                    except BaseException:
                        pass

            # ao = datamodel.ArtificialOriginMessage.Cast(msg)
            # if ao:
            #  org = ao.origin()
            #  if org:
            #    self.runAlert(org.latitude().value(), org.longitude().value())
            #  return

            client.Application.handleMessage(self, msg)
        except BaseException:
            info = traceback.format_exception(*sys.exc_info())
            for i in info:
                sys.stderr.write(i)

    def addObject(self, parentID, arg0):
        # pylint: disable=W0622
        try:
            obj = datamodel.Amplitude.Cast(arg0)
            if obj:
                if obj.type() == self._ampType:
                    logging.debug(
                        f"got new {self._ampType} amplitude '{obj.publicID()}'"
                    )
                    self.notifyAmplitude(obj)

            obj = datamodel.Origin.Cast(arg0)
            if obj:
                self._cache.feed(obj)
                logging.debug(f"got new origin '{obj.publicID()}'")

                try:
                    if obj.evaluationStatus() == datamodel.PRELIMINARY:
                        self.runAlert(obj.latitude().value(), obj.longitude().value())
                except BaseException:
                    pass

                return

            obj = datamodel.Magnitude.Cast(arg0)
            if obj:
                self._cache.feed(obj)
                logging.debug(f"got new magnitude '{obj.publicID()}'")
                return

            obj = datamodel.Event.Cast(arg0)
            if obj:
                org = self._cache.get(datamodel.Origin, obj.preferredOriginID())
                agencyID = org.creationInfo().agencyID()
                logging.debug(f"got new event '{obj.publicID()}'")
                if not self._agencyIDs or agencyID in self._agencyIDs:
                    self.notifyEvent(obj, True)
        except BaseException:
            info = traceback.format_exception(*sys.exc_info())
            for i in info:
                sys.stderr.write(i)

    def updateObject(self, parentID, arg0):
        try:
            obj = datamodel.Event.Cast(arg0)
            if obj:
                org = self._cache.get(datamodel.Origin, obj.preferredOriginID())
                agencyID = org.creationInfo().agencyID()
                logging.debug(f"update event '{obj.publicID()}'")
                if not self._agencyIDs or agencyID in self._agencyIDs:
                    self.notifyEvent(obj, False)
        except BaseException:
            info = traceback.format_exception(*sys.exc_info())
            for i in info:
                sys.stderr.write(i)

    def notifyAmplitude(self, amp):
        self.runAmpScript(
            amp.waveformID().networkCode(),
            amp.waveformID().stationCode(),
            amp.amplitude().value(),
        )

    def notifyEvent(self, evt, newEvent=True):
        try:
            org = self._cache.get(datamodel.Origin, evt.preferredOriginID())
            if not org:
                logging.warning(
                    "unable to get origin %s, ignoring event "
                    "message" % evt.preferredOriginID()
                )
                return

            preliminary = False
            try:
                if org.evaluationStatus() == datamodel.PRELIMINARY:
                    preliminary = True
            except BaseException:
                pass

            if not preliminary:
                nmag = self._cache.get(datamodel.Magnitude, evt.preferredMagnitudeID())
                if nmag:
                    mag = nmag.magnitude().value()
                    mag = f"magnitude {mag:.1f}"
                else:
                    if len(evt.preferredMagnitudeID()) > 0:
                        logging.warning(
                            "unable to get magnitude %s, ignoring event "
                            "message" % evt.preferredMagnitudeID()
                        )
                    else:
                        logging.warning(
                            "no preferred magnitude yet, ignoring event message"
                        )
                    return

            # keep track of old events
            if self._newWhenFirstSeen:
                if evt.publicID() in self._prevMessage:
                    newEvent = False
                else:
                    newEvent = True

            dsc = seismology.Regions.getRegionName(
                org.latitude().value(), org.longitude().value()
            )

            if self._eventDescriptionPattern:
                try:
                    city, dist, _ = self.nearestCity(
                        org.latitude().value(),
                        org.longitude().value(),
                        self._citiesMaxDist,
                        self._citiesMinPopulation,
                    )
                    if city:
                        dsc = self._eventDescriptionPattern
                        region = seismology.Regions.getRegionName(
                            org.latitude().value(), org.longitude().value()
                        )
                        distStr = str(int(math.deg2km(dist)))
                        dsc = (
                            dsc.replace("@region@", region)
                            .replace("@dist@", distStr)
                            .replace("@poi@", city.name())
                        )
                except BaseException:
                    pass

            logging.debug(f"desc: {dsc}")

            dep = org.depth().value()
            now = core.Time.GMT()
            otm = org.time().value()

            dt = (now - otm).seconds()

            #   if dt > dtmax:
            #       return

            if dt > 3600:
                dt = "%d hours %d minutes ago" % (int(dt / 3600), int((dt % 3600) / 60))
            elif dt > 120:
                dt = "%d minutes ago" % int(dt / 60)
            else:
                dt = "%d seconds ago" % int(dt)

            if preliminary:
                message = "earthquake, preliminary, %%s, %s" % dsc
            else:
                message = "earthquake, %%s, %s, %s, depth %d kilometers" % (
                    dsc,
                    mag,
                    int(dep + 0.5),
                )
            # at this point the message lacks the "ago" part

            if (
                evt.publicID() in self._prevMessage
                and self._prevMessage[evt.publicID()] == message
            ):
                logging.info(f"Suppressing repeated message '{message}'")
                return

            self._prevMessage[evt.publicID()] = message
            message = message % dt  # fill the "ago" part
            logging.info(message)

            if not self._eventScript:
                return

            if self._eventProc is not None:
                if self._eventProc.poll() is None:
                    logging.warning("EventScript still in progress -> skipping message")
                    return

            try:
                param2 = 0
                param3 = 0
                param4 = ""
                if newEvent:
                    param2 = 1

                org = self._cache.get(datamodel.Origin, evt.preferredOriginID())
                if org:
                    try:
                        param3 = org.quality().associatedPhaseCount()
                    except BaseException:
                        pass

                nmag = self._cache.get(datamodel.Magnitude, evt.preferredMagnitudeID())
                if nmag:
                    param4 = f"{nmag.magnitude().value():.1f}"

                self._eventProc = subprocess.Popen(
                    [
                        self._eventScript,
                        message,
                        "%d" % param2,
                        evt.publicID(),
                        "%d" % param3,
                        param4,
                    ]
                )
                logging.info("Started event script with pid %d" % self._eventProc.pid)
            except BaseException:
                logging.error(
                    "Failed to start event script '%s %s %d %d %s'"
                    % (self._eventScript, message, param2, param3, param4)
                )
        except BaseException:
            info = traceback.format_exception(*sys.exc_info())
            for i in info:
                sys.stderr.write(i)


app = VoiceAlert(len(sys.argv), sys.argv)
sys.exit(app())
