/*
 * Copyright 2014 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 .
 *
 * Authors:
 *   Charles Kerr 
 */
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
namespace unity {
namespace indicator {
namespace datetime {
/***
****
***/
namespace
{
/**
 * Plays a sound, possibly looping.
 */
class Sound
{
    typedef Sound Self;
public:
    Sound(const std::shared_ptr& clock,
          const std::string& filename,
          unsigned int volume,
          unsigned int duration_minutes,
          bool loop):
        m_clock(clock),
        m_filename(filename),
        m_volume(volume),
        m_loop(loop),
        m_canberra_id(get_next_canberra_id()),
        m_loop_end_time(clock->localtime().add_full(0, 0, 0, 0, duration_minutes, 0.0))
    {
        if (m_loop)
        {
            g_debug("Looping '%s' until cutoff time %s",
                    m_filename.c_str(),
                    m_loop_end_time.format("%F %T").c_str());
        }
        else
        {
            g_debug("Playing '%s' once", m_filename.c_str());
        }
        const auto rv = ca_context_create(&m_context);
        if (rv == CA_SUCCESS)
        {
            play();
        }
        else
        {
            g_warning("Failed to create canberra context: %s", ca_strerror(rv));
            m_context = nullptr;
        }
    }
    ~Sound()
    {
        stop();
        g_clear_pointer(&m_context, ca_context_destroy);
    }
private:
    void stop()
    {
        if (m_context != nullptr)
        {
            const auto rv = ca_context_cancel(m_context, m_canberra_id);
            if (rv != CA_SUCCESS)
                g_warning("Failed to cancel alarm sound: %s", ca_strerror(rv));
        }
        if (m_loop_tag != 0)
        {
            g_source_remove(m_loop_tag);
            m_loop_tag = 0;
        }
    }
    void play()
    {
        auto context = m_context;
        g_return_if_fail(context != nullptr);
        const auto filename = m_filename.c_str();
        const float gain = get_gain_level(m_volume);
        ca_proplist* props = nullptr;
        ca_proplist_create(&props);
        ca_proplist_sets(props, CA_PROP_MEDIA_FILENAME, filename);
        ca_proplist_setf(props, CA_PROP_CANBERRA_VOLUME, "%f", gain);
        const auto rv = ca_context_play_full(context, m_canberra_id, props,
                                             on_done_playing, this);
        if (rv != CA_SUCCESS)
            g_warning("Unable to play '%s': %s", filename, ca_strerror(rv));
        g_clear_pointer(&props, ca_proplist_destroy);
    }
    static float get_gain_level(unsigned int volume)
    {
        const unsigned int clamped_volume = CLAMP(volume, 1, 100);
        /* This range isn't set in stone --
           arrived at from manual tests on Nextus 4 */
        constexpr float gain_low = -10;
        constexpr float gain_high = 10;
        constexpr float gain_range = gain_high - gain_low;
        return gain_low + (gain_range * (clamped_volume / 100.0f));
    }
    static void on_done_playing(ca_context*, uint32_t, int rv, void* gself)
    {
        // if we still need to loop, wait a second, then play it again
        if (rv == CA_SUCCESS)
        {
            auto self = static_cast(gself);
            if ((self->m_loop_tag == 0) &&
                (self->m_loop) &&
                (self->m_clock->localtime() < self->m_loop_end_time))
            {
                self->m_loop_tag = g_timeout_add_seconds(1, play_idle, self);
            }
        }
    }
    static gboolean play_idle(gpointer gself)
    {
        auto self = static_cast(gself);
        self->m_loop_tag = 0;
        self->play();
        return G_SOURCE_REMOVE;
    }
    /***
    ****
    ***/
    static int32_t get_next_canberra_id()
    {
        static int32_t next_canberra_id = 1;
        return next_canberra_id++;
    }
    const std::shared_ptr m_clock;
    const std::string m_filename;
    const unsigned int m_volume;
    const bool m_loop;
    const int32_t m_canberra_id;
    const DateTime m_loop_end_time;
    ca_context* m_context = nullptr;
    guint m_loop_tag = 0;
};
class SoundBuilder
{
public:
    void set_clock(const std::shared_ptr& c) {m_clock = c;}
    void set_filename(const std::string& s) {m_filename = s;}
    void set_volume(const unsigned int v) {m_volume = v;}
    void set_duration_minutes(int unsigned i) {m_duration_minutes=i;}
    void set_looping(bool b) {m_looping=b;}
    Sound* operator()() {
        return new Sound (m_clock,
                          m_filename,
                          m_volume,
                          m_duration_minutes,
                          m_looping);
    }
private:
    std::shared_ptr m_clock;
    std::string m_filename;
    unsigned int m_volume = 50;
    unsigned int m_duration_minutes = 30;
    bool m_looping = true;
};
/**
 * A popup notification (with optional sound)
 * that emits a Response signal when done.
 */
class Popup
{
public:
    Popup(const Appointment& appointment, const SoundBuilder& sound_builder):
        m_appointment(appointment),
        m_interactive(get_interactive()),
        m_sound_builder(sound_builder)
    {
        // ensure notify_init() is called once
        // before we start popping up dialogs
        static bool m_nn_inited = false;
        if (G_UNLIKELY(!m_nn_inited))
        {
            if(!notify_init("indicator-datetime-service"))
                g_critical("libnotify initialization failed");
            m_nn_inited = true;
        }
        show();
    }
    ~Popup()
    {
        if (m_nn != nullptr)
        {
            notify_notification_clear_actions(m_nn);
            g_signal_handlers_disconnect_by_data(m_nn, this);
            g_clear_object(&m_nn);
        }
    }
    typedef enum
    {
        RESPONSE_SHOW,
        RESPONSE_DISMISS,
        RESPONSE_CLOSE
    }
    Response;
    core::Signal& response() { return m_response; }
private:
    void show()
    {
        const Appointment& appointment = m_appointment;
        /// strftime(3) format string for an alarm's snap decision
        const auto timestr = appointment.begin.format(_("%a, %X"));
        auto title = g_strdup_printf(_("Alarm %s"), timestr.c_str());
        const auto body = appointment.summary;
        const gchar* icon_name = "alarm-clock";
        m_nn = notify_notification_new(title, body.c_str(), icon_name);
        if (m_interactive)
        {
            notify_notification_set_hint_string(m_nn,
                                                "x-canonical-snap-decisions",
                                                "true");
            notify_notification_set_hint_string(m_nn,
                                                "x-canonical-private-button-tint",
                                                "true");
            /// alarm popup dialog's button to show the active alarm
            notify_notification_add_action(m_nn, "show", _("Show"),
                                           on_snap_show, this, nullptr);
            /// alarm popup dialog's button to shut up the alarm
            notify_notification_add_action(m_nn, "dismiss", _("Dismiss"),
                                           on_snap_dismiss, this, nullptr);
            g_signal_connect(m_nn, "closed", G_CALLBACK(on_snap_closed), this);
        }
        bool shown = true;
        GError* error = nullptr;
        notify_notification_show(m_nn, &error);
        if (error != NULL)
        {
            g_critical("Unable to show snap decision for '%s': %s",
                       body.c_str(), error->message);
            g_error_free(error);
            shown = false;
        }
        // Loop the sound *only* if we're prompting the user for a response.
        // Otherwise, just play the sound once.
        m_sound_builder.set_looping (shown && m_interactive);
        m_sound.reset (m_sound_builder());
        // if showing the notification didn't work,
        // treat it as if the user clicked the 'show' button
        if (!shown)
        {
            on_snap_show(nullptr, nullptr, this);
            on_snap_dismiss(nullptr, nullptr, this);
        }
        g_free(title);
    }
    // user clicked 'show'
    static void on_snap_show(NotifyNotification*, gchar*, gpointer gself)
    {
        auto self = static_cast(gself);
        self->m_response_value = RESPONSE_SHOW;
        self->m_sound.reset();
    }
    // user clicked 'dismiss'
    static void on_snap_dismiss(NotifyNotification*, gchar*, gpointer gself)
    {
        auto self = static_cast(gself);
        self->m_response_value = RESPONSE_DISMISS;
        self->m_sound.reset();
    }
    // the popup was closed
    static void on_snap_closed(NotifyNotification*, gpointer gself)
    {
        auto self = static_cast(gself);
        self->m_sound.reset();
        self->m_response(self->m_response_value);
    }
    /***
    ****  Interactive
    ***/
    static std::set get_server_caps()
    {
        std::set caps_set;
        auto caps_gl = notify_get_server_caps();
        std::string caps_str;
        for(auto l=caps_gl; l!=nullptr; l=l->next)
        {
            caps_set.insert((const char*)l->data);
            caps_str += (const char*) l->data;;
            if (l->next != nullptr)
              caps_str += ", ";
        }
        g_debug("%s notify_get_server() returned [%s]", G_STRFUNC, caps_str.c_str());
        g_list_free_full(caps_gl, g_free);
        return caps_set;
    }
    static bool get_interactive()
    {
        static bool interactive;
        static bool inited = false;
        if (G_UNLIKELY(!inited))
        {
            interactive = get_server_caps().count("actions") != 0;
            inited = true;
        }
        return interactive;
    }
    /***
    ****
    ***/
    typedef Popup Self;
    const Appointment m_appointment;
    const bool m_interactive;
    SoundBuilder m_sound_builder;
    std::unique_ptr m_sound;
    core::Signal m_response;
    Response m_response_value = RESPONSE_CLOSE;
    NotifyNotification* m_nn = nullptr;
};
/** 
***  libnotify -- snap decisions
**/
std::string get_local_filename (const std::string& str)
{
    std::string ret;
    if (!str.empty())
    {
        GFile* files[] = { g_file_new_for_path(str.c_str()),
                           g_file_new_for_uri(str.c_str()) };
        for(auto& file : files)
        {
            if (g_file_is_native(file) && g_file_query_exists(file, nullptr))
            {
                char* tmp = g_file_get_path(file);
                if (tmp != nullptr)
                {
                    ret = tmp;
                    g_free(tmp);
                    break;
                }
            }
        }
        for(auto& file : files)
            g_object_unref(file);
    }
    return ret;
}
std::string get_alarm_sound(const Appointment& appointment,
                            const std::shared_ptr& settings)
{
    const char* FALLBACK {"/usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg"};
    const std::string candidates[] = { appointment.audio_url,
                                       settings->alarm_sound.get(),
                                       FALLBACK };
    std::string alarm_sound;
    for(const auto& candidate : candidates)
    {
        alarm_sound = get_local_filename(candidate);
        if (!alarm_sound.empty())
            break;
    }
    g_debug("%s: Appointment \"%s\" using alarm sound \"%s\"",
            G_STRFUNC, appointment.summary.c_str(), alarm_sound.c_str());
    return alarm_sound;
}
} // unnamed namespace
/***
****
***/
Snap::Snap(const std::shared_ptr& clock,
           const std::shared_ptr& settings):
    m_clock(clock),
    m_settings(settings)
{
}
Snap::~Snap()
{
}
void Snap::operator()(const Appointment& appointment,
                      appointment_func show,
                      appointment_func dismiss)
{
    if (!appointment.has_alarms)
    {
        dismiss(appointment);
        return;
    }
    // create a popup...
    SoundBuilder sound_builder;
    sound_builder.set_filename(get_alarm_sound(appointment, m_settings));
    sound_builder.set_volume(m_settings->alarm_volume.get());
    sound_builder.set_clock(m_clock);
    sound_builder.set_duration_minutes(m_settings->alarm_duration.get());
    auto popup = new Popup(appointment, sound_builder);
     
    // listen for it to finish...
    popup->response().connect([appointment,
                               show,
                               dismiss,
                               popup](Popup::Response response){
     
        // we can't delete the Popup inside its response() signal handler,
        // so push that to an idle func
        g_idle_add([](gpointer gdata){
            delete static_cast(gdata);
            return G_SOURCE_REMOVE;
        }, popup);
        // maybe notify the client code that the popup's done
        if (response == Popup::RESPONSE_SHOW)
            show(appointment);
        else if (response == Popup::RESPONSE_DISMISS)
            dismiss(appointment);
    });
}
/***
****
***/
} // namespace datetime
} // namespace indicator
} // namespace unity