// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

const GLib = imports.gi.GLib;
const Gio = imports.gi.Gio;
const GnomeDesktop = imports.gi.GnomeDesktop;
const GObject = imports.gi.GObject;
const Gtk = imports.gi.Gtk;
const GWeather = imports.gi.GWeather;
const Lang = imports.lang;
const Mainloop = imports.mainloop;
const Pango = imports.gi.Pango;
const Cairo = imports.cairo;
const Clutter = imports.gi.Clutter;
const Shell = imports.gi.Shell;
const St = imports.gi.St;
const Atk = imports.gi.Atk;

const Params = imports.misc.params;
const Util = imports.misc.util;
const Main = imports.ui.main;
const PanelMenu = imports.ui.panelMenu;
const PopupMenu = imports.ui.popupMenu;
const Calendar = imports.ui.calendar;
const Weather = imports.misc.weather;
const System = imports.system;

function _isToday(date) {
    let now = new Date();
    return now.getYear() == date.getYear() &&
           now.getMonth() == date.getMonth() &&
           now.getDate() == date.getDate();
}

var TodayButton = new Lang.Class({
    Name: 'TodayButton',

    _init(calendar) {
        // Having the ability to go to the current date if the user is already
        // on the current date can be confusing. So don't make the button reactive
        // until the selected date changes.
        this.actor = new St.Button({ style_class: 'datemenu-today-button',
                                     x_expand: true, x_align: St.Align.START,
                                     can_focus: true,
                                     reactive: false
                                   });
        this.actor.connect('clicked', () => {
            this._calendar.setDate(new Date(), false);
        });

        let hbox = new St.BoxLayout({ vertical: true });
        this.actor.add_actor(hbox);

        this._dayLabel = new St.Label({ style_class: 'day-label',
                                        x_align: Clutter.ActorAlign.START });
        hbox.add_actor(this._dayLabel);

        this._dateLabel = new St.Label({ style_class: 'date-label' });
        hbox.add_actor(this._dateLabel);

        this._calendar = calendar;
        this._calendar.connect('selected-date-changed', (calendar, date) => {
            // Make the button reactive only if the selected date is not the
            // current date.
            this.actor.reactive = !_isToday(date)
        });
    },

    setDate(date) {
        this._dayLabel.set_text(date.toLocaleFormat('%A'));

        /* Translators: This is the date format to use when the calendar popup is
         * shown - it is shown just below the time in the shell (e.g. "Tue 9:29 AM").
         */
        let dateFormat = Shell.util_translate_time_string (N_("%B %e %Y"));
        this._dateLabel.set_text(date.toLocaleFormat(dateFormat));

        /* Translators: This is the accessible name of the date button shown
         * below the time in the shell; it should combine the weekday and the
         * date, e.g. "Tuesday February 17 2015".
         */
        dateFormat = Shell.util_translate_time_string (N_("%A %B %e %Y"));
        this.actor.accessible_name = date.toLocaleFormat(dateFormat);
    }
});

var WorldClocksSection = new Lang.Class({
    Name: 'WorldClocksSection',

    _init() {
        this._clock = new GnomeDesktop.WallClock();
        this._clockNotifyId = 0;

        this._locations = [];

        this.actor = new St.Button({ style_class: 'world-clocks-button',
                                     x_fill: true,
                                     can_focus: true });
        this.actor.connect('clicked', () => {
            this._clockAppMon.activateApp();

            Main.overview.hide();
            Main.panel.closeCalendar();
        });

        let layout = new Clutter.GridLayout({ orientation: Clutter.Orientation.VERTICAL });
        this._grid = new St.Widget({ style_class: 'world-clocks-grid',
                                     layout_manager: layout });
        layout.hookup_style(this._grid);

        this.actor.child = this._grid;

        this._clockAppMon = new Util.AppSettingsMonitor('org.gnome.clocks.desktop',
                                                        'org.gnome.clocks');
        this._clockAppMon.connect('available-changed',
                                  this._sync.bind(this));
        this._clockAppMon.watchSetting('world-clocks',
                                       this._clocksChanged.bind(this));
        this._sync();
    },

    _sync() {
        this.actor.visible = this._clockAppMon.available;
    },

    _clocksChanged(settings) {
        this._grid.destroy_all_children();
        this._locations = [];

        let world = GWeather.Location.get_world();
        let clocks = settings.get_value('world-clocks').deep_unpack();
        for (let i = 0; i < clocks.length; i++) {
            if (!clocks[i].location)
                continue;
            let l = world.deserialize(clocks[i].location);
            if (l)
                this._locations.push({ location: l });
        }

        this._locations.sort((a, b) => {
            return a.location.get_timezone().get_offset() -
                   b.location.get_timezone().get_offset();
        });

        let layout = this._grid.layout_manager;
        let title = (this._locations.length == 0) ? _("Add world clocks…")
                                                  : _("World Clocks");
        let header = new St.Label({ style_class: 'world-clocks-header',
                                    x_align: Clutter.ActorAlign.START,
                                    text: title });
        layout.attach(header, 0, 0, 2, 1);
        this.actor.label_actor = header;

        for (let i = 0; i < this._locations.length; i++) {
            let l = this._locations[i].location;

            let name = l.get_level() == GWeather.LocationLevel.NAMED_TIMEZONE ? l.get_name()
                                                                              : l.get_city_name();
            let label = new St.Label({ style_class: 'world-clocks-city',
                                       text: name,
                                       x_align: Clutter.ActorAlign.START,
                                       x_expand: true });

            let time = new St.Label({ style_class: 'world-clocks-time',
                                      x_align: Clutter.ActorAlign.END,
                                      x_expand: true });

            if (this._grid.text_direction == Clutter.TextDirection.RTL) {
                layout.attach(time, 0, i + 1, 1, 1);
                layout.attach(label, 1, i + 1, 1, 1);
            } else {
                layout.attach(label, 0, i + 1, 1, 1);
                layout.attach(time, 1, i + 1, 1, 1);
            }

            this._locations[i].actor = time;
        }

        if (this._grid.get_n_children() > 1) {
            if (!this._clockNotifyId)
                this._clockNotifyId =
                    this._clock.connect('notify::clock', this._updateLabels.bind(this));
            this._updateLabels();
        } else {
            if (this._clockNotifyId)
                this._clock.disconnect(this._clockNotifyId);
            this._clockNotifyId = 0;
        }
    },

    _updateLabels() {
        for (let i = 0; i < this._locations.length; i++) {
            let l = this._locations[i];
            let tz = GLib.TimeZone.new(l.location.get_timezone().get_tzid());
            let now = GLib.DateTime.new_now(tz);
            l.actor.text = Util.formatTime(now, { timeOnly: true });
        }
    }
});

var WeatherSection = new Lang.Class({
    Name: 'WeatherSection',

    _init() {
        this._weatherClient = new Weather.WeatherClient();

        this.actor = new St.Button({ style_class: 'weather-button',
                                     x_fill: true,
                                     can_focus: true });
        this.actor.connect('clicked', () => {
            this._weatherClient.activateApp();

            Main.overview.hide();
            Main.panel.closeCalendar();
        });
        this.actor.connect('notify::mapped', () => {
            if (this.actor.mapped)
                this._weatherClient.update();
        });

        let box = new St.BoxLayout({ style_class: 'weather-box',
                                      vertical: true });

        this.actor.child = box;

        box.add_child(new St.Label({ style_class: 'weather-header',
                                     x_align: Clutter.ActorAlign.START,
                                     text: _("Weather") }));

        this._conditionsLabel = new St.Label({ style_class: 'weather-conditions',
                                               x_align: Clutter.ActorAlign.START });
        this._conditionsLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
        this._conditionsLabel.clutter_text.line_wrap = true;
        box.add_child(this._conditionsLabel);

        this._weatherClient.connect('changed', this._sync.bind(this));
        this._sync();
    },

    _getSummary(info, capitalize=false) {
        let options = capitalize ? GWeather.FormatOptions.SENTENCE_CAPITALIZATION
                                 : GWeather.FormatOptions.NO_CAPITALIZATION;

        let [ok, phenomenon, qualifier] = info.get_value_conditions();
        if (ok)
            return new GWeather.Conditions({ significant: true,
                                             phenomenon,
                                             qualifier }).to_string_full(options);

        let [, sky] = info.get_value_sky();
        return GWeather.Sky.to_string_full(sky, options);
    },

    _sameSummary(info1, info2) {
        let [ok1, phenom1, qualifier1] = info1.get_value_conditions();
        let [ok2, phenom2, qualifier2] = info2.get_value_conditions();
        if (ok1 || ok2)
            return ok1 == ok2 && phenom1 == phenom2 && qualifier1 == qualifier2;

        let [, sky1] = info1.get_value_sky();
        let [, sky2] = info2.get_value_sky();
        return sky1 == sky2;
    },

    _getSummaryText() {
        let info = this._weatherClient.info;
        let forecasts = info.get_forecast_list();
        if (forecasts.length == 0) // No forecasts, just current conditions
            return '%s.'.format(this._getSummary(info, true));

        let current = info;
        let infos = [info];
        for (let i = 0; i < forecasts.length; i++) {
            let [ok, timestamp] = forecasts[i].get_value_update();
            if (!_isToday(new Date(timestamp * 1000)))
                continue; // Ignore forecasts from other days

            if (this._sameSummary(current, forecasts[i]))
                continue; // Ignore consecutive runs of equal summaries

            current = forecasts[i];
            if (infos.push(current) == 3)
                break; // Use a maximum of three summaries
        }

        let fmt;
        switch(infos.length) {
            /* Translators: %s is a weather condition like "Clear sky"; see
               libgweather for the possible condition strings. If at all
               possible, the sentence should match the grammatical case etc. of
               the inserted conditions. */
            case 1: fmt = _("%s all day."); break;

            /* Translators: %s is a weather condition like "Clear sky"; see
               libgweather for the possible condition strings. If at all
               possible, the sentence should match the grammatical case etc. of
               the inserted conditions. */
            case 2: fmt = _("%s, then %s later."); break;

            /* Translators: %s is a weather condition like "Clear sky"; see
               libgweather for the possible condition strings. If at all
               possible, the sentence should match the grammatical case etc. of
               the inserted conditions. */
            case 3: fmt = _("%s, then %s, followed by %s later."); break;
        }
        let summaries = infos.map((info, i) => {
            let capitalize = i == 0 && fmt.startsWith('%s');
            return this._getSummary(info, capitalize);
        });
        return String.prototype.format.apply(fmt, summaries);
    },

    _getLabelText() {
        if (!this._weatherClient.hasLocation)
            return _("Select a location…");

        if (this._weatherClient.loading)
            return _("Loading…");

        let info = this._weatherClient.info;
        if (info.is_valid())
            return this._getSummaryText() + ' ' +
                   /* Translators: %s is a temperature with unit, e.g. "23℃" */
                   _("Feels like %s.").format(info.get_apparent());

        if (info.network_error())
            return _("Go online for weather information");

        return _("Weather information is currently unavailable");
    },

    _sync() {
        this.actor.visible = this._weatherClient.available;

        if (!this.actor.visible)
            return;

        this._conditionsLabel.text = this._getLabelText();
    }
});

var MessagesIndicator = new Lang.Class({
    Name: 'MessagesIndicator',

    _init() {
        this.actor = new St.Icon({ icon_name: 'message-indicator-symbolic',
                                   icon_size: 16,
                                   visible: false, y_expand: true,
                                   y_align: Clutter.ActorAlign.CENTER });

        this._sources = [];

        Main.messageTray.connect('source-added', this._onSourceAdded.bind(this));
        Main.messageTray.connect('source-removed', this._onSourceRemoved.bind(this));
        Main.messageTray.connect('queue-changed', this._updateCount.bind(this));

        let sources = Main.messageTray.getSources();
        sources.forEach(source => { this._onSourceAdded(null, source); });
    },

    _onSourceAdded(tray, source) {
        source.connect('count-updated', this._updateCount.bind(this));
        this._sources.push(source);
        this._updateCount();
    },

    _onSourceRemoved(tray, source) {
        this._sources.splice(this._sources.indexOf(source), 1);
        this._updateCount();
    },

    _updateCount() {
        let count = 0;
        this._sources.forEach(source => { count += source.unseenCount; });
        count -= Main.messageTray.queueCount;

        this.actor.visible = (count > 0);
    }
});

var IndicatorPad = new Lang.Class({
    Name: 'IndicatorPad',
    Extends: St.Widget,

    _init(actor) {
        this._source = actor;
        this._source.connect('notify::visible', () => { this.queue_relayout(); });
        this.parent();
    },

    vfunc_get_preferred_width(container, forHeight) {
        if (this._source.visible)
            return this._source.get_preferred_width(forHeight);
        return [0, 0];
    },

    vfunc_get_preferred_height(container, forWidth) {
        if (this._source.visible)
            return this._source.get_preferred_height(forWidth);
        return [0, 0];
    }
});

var FreezableBinLayout = new Lang.Class({
    Name: 'FreezableBinLayout',
    Extends: Clutter.BinLayout,

    _init() {
        this.parent();

        this._frozen = false;
        this._savedWidth = [NaN, NaN];
        this._savedHeight = [NaN, NaN];
    },

    set frozen(v) {
        if (this._frozen == v)
            return;

        this._frozen = v;
        if (!this._frozen)
            this.layout_changed();
    },

    vfunc_get_preferred_width(container, forHeight) {
        if (!this._frozen || this._savedWidth.some(isNaN))
            return this.parent(container, forHeight);
        return this._savedWidth;
    },

    vfunc_get_preferred_height(container, forWidth) {
        if (!this._frozen || this._savedHeight.some(isNaN))
            return this.parent(container, forWidth);
        return this._savedHeight;
    },

    vfunc_allocate(container, allocation, flags) {
        this.parent(container, allocation, flags);

        let [width, height] = allocation.get_size();
        this._savedWidth = [width, width];
        this._savedHeight = [height, height];
    }
});

var CalendarColumnLayout = new Lang.Class({
    Name: 'CalendarColumnLayout',
    Extends: Clutter.BoxLayout,

    _init(actor) {
        this.parent({ orientation: Clutter.Orientation.VERTICAL });
        this._calActor = actor;
    },

    vfunc_get_preferred_width(container, forHeight) {
        if (!this._calActor || this._calActor.get_parent() != container)
            return this.parent(container, forHeight);
        return this._calActor.get_preferred_width(forHeight);
    }
});

var DateMenuButton = new Lang.Class({
    Name: 'DateMenuButton',
    Extends: PanelMenu.Button,

    _init() {
        let item;
        let hbox;
        let vbox;

        let menuAlignment = 0.5;
        if (Clutter.get_default_text_direction() == Clutter.TextDirection.RTL)
            menuAlignment = 1.0 - menuAlignment;
        this.parent(menuAlignment);

        this._clockDisplay = new St.Label({ y_align: Clutter.ActorAlign.CENTER });
        this._indicator = new MessagesIndicator();

        let box = new St.BoxLayout();
        box.add_actor(new IndicatorPad(this._indicator.actor));
        box.add_actor(this._clockDisplay);
        box.add_actor(this._indicator.actor);

        this.actor.label_actor = this._clockDisplay;
        this.actor.add_actor(box);
        this.actor.add_style_class_name ('clock-display');


        let layout = new FreezableBinLayout();
        let bin = new St.Widget({ layout_manager: layout });
        // For some minimal compatibility with PopupMenuItem
        bin._delegate = this;
        this.menu.box.add_child(bin);

        hbox = new St.BoxLayout({ name: 'calendarArea' });
        bin.add_actor(hbox);

        this._calendar = new Calendar.Calendar();
        this._calendar.connect('selected-date-changed',
                               (calendar, date) => {
                                   layout.frozen = !_isToday(date);
                                   this._messageList.setDate(date);
                               });

        this.menu.connect('open-state-changed', (menu, isOpen) => {
            // Whenever the menu is opened, select today
            if (isOpen) {
                let now = new Date();
                this._calendar.setDate(now);
                this._date.setDate(now);
                this._messageList.setDate(now);
            }
        });

        // Fill up the first column
        this._messageList = new Calendar.CalendarMessageList();
        hbox.add(this._messageList.actor, { expand: true, y_fill: false, y_align: St.Align.START });

        // Fill up the second column
        let boxLayout = new CalendarColumnLayout(this._calendar.actor);
        vbox = new St.Widget({ style_class: 'datemenu-calendar-column',
                               layout_manager: boxLayout });
        boxLayout.hookup_style(vbox);
        hbox.add(vbox);

        this._date = new TodayButton(this._calendar);
        vbox.add_actor(this._date.actor);

        vbox.add_actor(this._calendar.actor);

        this._displaysSection = new St.ScrollView({ style_class: 'datemenu-displays-section vfade',
                                                    x_expand: true, x_fill: true,
                                                    overlay_scrollbars: true });
        this._displaysSection.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC);
        vbox.add_actor(this._displaysSection);

        let displaysBox = new St.BoxLayout({ vertical: true,
                                             style_class: 'datemenu-displays-box' });
        this._displaysSection.add_actor(displaysBox);

        this._clocksItem = new WorldClocksSection();
        displaysBox.add(this._clocksItem.actor, { x_fill: true });

        this._weatherItem = new WeatherSection();
        displaysBox.add(this._weatherItem.actor, { x_fill: true });

        // Done with hbox for calendar and event list

        this._clock = new GnomeDesktop.WallClock();
        this._clock.bind_property('clock', this._clockDisplay, 'text', GObject.BindingFlags.SYNC_CREATE);
        this._clock.connect('notify::timezone', this._updateTimeZone.bind(this));

        Main.sessionMode.connect('updated', this._sessionUpdated.bind(this));
        this._sessionUpdated();
    },

    _getEventSource() {
        return new Calendar.DBusEventSource();
    },

    _setEventSource(eventSource) {
        if (this._eventSource)
            this._eventSource.destroy();

        this._calendar.setEventSource(eventSource);
        this._messageList.setEventSource(eventSource);

        this._eventSource = eventSource;
    },

    _updateTimeZone() {
        // SpiderMonkey caches the time zone so we must explicitly clear it
        // before we can update the calendar, see
        // https://bugzilla.gnome.org/show_bug.cgi?id=678507
        System.clearDateCaches();

        this._calendar.updateTimeZone();
    },

    _sessionUpdated() {
        let eventSource;
        let showEvents = Main.sessionMode.showCalendarEvents;
        if (showEvents) {
            eventSource = this._getEventSource();
        } else {
            eventSource = new Calendar.EmptyEventSource();
        }
        this._setEventSource(eventSource);

        // Displays are not actually expected to launch Settings when activated
        // but the corresponding app (clocks, weather); however we can consider
        // that display-specific settings, so re-use "allowSettings" here ...
        this._displaysSection.visible = Main.sessionMode.allowSettings;
    }
});