var WaitingTrainingsPoller = {
    /* An object to poll data about waiting trainings at some interval

    This is meant to be used on a trainings page that already hold the "operations in progress"
    part, and will update it with fresh data from the server.

    Attributes
    ----------
    url: str
        The url used to fetch data from the server
    interval: int
        Number of milliseconds to wait between two polls (default to 30000, ie 30 seconds)
    data: dict
        Will hold current state of waiting trainings. Must be set at the beginning with same data
        as the ones used to render the html
    $lines: dict
        Will hold all lines in the html used to display waiting trainings. The keys are the
        trainings ids, and the values are the jquery object for the matching line
    $lines_holder: jquery
        A jquery object for the parent node of all lines for waiting trainings
    line_template: str
        The html template used to create a new line for a waiting training received via poll that
        does not yet exist on the page
    colspan: int
        The value for the ``colspan`` attribute of the cell holding the training name in
        ``line_template``
    spinner_template: str
        The html template to use to display a spinner in a cell that hold the count of waiting
        trainings
    task_types: array
        All the training task types handled by the poller
    $poll: function
        The ``pool`` method, bound to the current object (to avoid calling bind/$.proxy each time)
    $on_poll_success: function
        The ``on_poll_success`` method, bound to the current object
    $on_poll_failure: function
        The ``on_poll_failure`` method, bound to the current object

    */

    url: null,
    interval: 30000,
    timeout_id: null,
    data: null,
    $lines: {},
    $lines_holder: null,
    line_template: '<tr class="waiting-training"><td></td><td></td><td></td></tr>',
    colspan: 1,
    spinner_template: '<span class="spinner"></span>',
    task_types: ['CREATE_DRAFT', 'DUPLICATE_TO_MEMBER'],
    $recent_holder: null,

    poll: function() {
        /* Get the polling url and call ``on_poll_success`` or ``on_poll_failure`` */

        $.get(this.url)
            .done(this.$on_poll_success)
            .fail(this.$on_poll_failure);
    },

    on_poll_success: function(data) {
        /* Manage new data received from the server

        Parameters
        ----------
        data: dict
            A dict with a ``grouped_tasks`` key, that have training pks as keys, and as values,
            a dict with a ``training_name`` and a ``tasks`` keys, this last one holding a dict
            with an entry for each ``task_type`` for which the training has waiting keys, each one
            being a dict with a ``remaining`` keys holding a list of the remaining tasks ids for
            this training+task_type
            There may also be a ``recent_trainings`` entry, that will be the html of all the recent
            entries to display to the user.

            {"grouped_tasks": {
                "123": {
                    "training_name": "foo",
                    "tasks: {
                        "CREATE_DRAFT": {"remaining": [1, 2]},
                        "DUPLICATE_MEMBERS: {"remaining": [3]}
                    }
                },
                "234": {
                    "training_name": "bar",
                    "tasks: {
                        "DUPLICATE_MEMBERS: {"remaining": [5, 6]}
                    }
                }
            }

        Notes
        -----
        This method will merge received data with existing ones, will add new training lines if
        needed, and will update the counters on each line to match the last state of the waiting
        trainings on the server.

        */

        this.timeout_id = null; // stop holding a non existing value, the timeout being done

        // update existing data with new data
        $.each(data.grouped_tasks, $.proxy(function(training_pk, training_info) {
            if (!this.data[training_pk]) {
                this.add_line(training_pk, training_info)
            } else {
                this.update_data(training_pk, training_info);
            }
        }, this));

        // consider finished the existing data without matching new data
        $.each(this.data, $.proxy(function(training_pk, training_info) {
            if (!data.grouped_tasks[training_pk]) {
                // we simulate new data, but without remaining tasks
                this.update_data(training_pk, {
                    training: training_info.training,
                    tasks: {}
                });
            }
        }, this));

        // we can now update all lines
        $.each(this.$lines, $.proxy(this.update_line, this));

        // and display the recent trainings if any
        this.display_recent_trainings(data.recent_trainings);

        // and continue polling if still needed
        if (this.$lines_holder.find('.spinner').length) {
            this.timeout_id = setTimeout(this.$poll, this.interval);
        }
    },

    on_poll_failure: function() {
        /* Wait longer to retry if there was a problem on the server */

        this.timeout_id = setTimeout(this.$poll, this.interval * 2);
    },

    add_line: function(training_pk, training_info) {
        /* Create a line in the html for a new training having waiting tasks

        This also save the data from ``training_info`` in ``this.data``.

        Parameters
        ----------
        training_pk: str
            The primary key of the training to add
        training_info: dict
            The data got from the server for this very training, with ``training_name`` and
            ``training_info`` keys, as detailed in ``on_poll_success``

        */

        // Create a new line in the table
        this.$lines[training_pk] = $(this.line_template);
        this.$lines[training_pk].children('td').first()
                .attr('colspan', this.colspan)
                .text(training_info.training_name)
                .data('training-id', training_pk);
        this.$lines_holder.append(this.$lines[training_pk]);

        // And save the data
        this.data[training_pk] = training_info;
    },

    update_data: function(training_pk, training_info) {
        /* Merge existing data with new data for a specific training

        Parameters
        ----------
        training_pk: str
            The primary key of the training to add
        training_info: dict
            The data got from the server for this very training, with ``training_name`` and
            ``training_info`` keys, as detailed in ``on_poll_success``

        Notes
        -----
        This will add a ``all`` key in the data for the training, that will hold all tasks ids, ie
        the one already present at the beginning, and the potential new ones in the new data
        It will be used to update the last number in "x / y", ``x`` being the current number of
        remaining tasks, ``y`` the total tasks that had to be done for this training+task type

        */

        var data = this.data[training_pk].tasks,  // shortcut to update existing data
            new_data = training_info.tasks,  // shortcut to access new data
            i, task_type, tasks, new_tasks;

        // we iterate on each type managed by the poller
        for (i = 0; i < this.task_types.length; i++) {
            task_type = this.task_types[i];

            if (!new_data[task_type]) {
                // if not data for this type, simulate an empty list
                new_data[task_type] = {'remaining': []};
            }

            new_tasks = new_data[task_type]; // shortcut to access new tasks

            if (!data[task_type]) {
                // we have data for this type for the first time, we just keep them
                data[task_type] = new_tasks; // shortcut to update existing tasks

            } else {
                tasks = data[task_type];
                if (!tasks.all) {
                    // it's the first time, we initialize the ``all`` key
                    tasks.all = tasks.remaining;
                }

                // we join previous tasks and new ones, to know the total length
                tasks.all = this.combine_tasks(tasks.all, new_tasks.remaining);

                // and we save the remaining from the data we just got
                tasks.remaining = new_tasks.remaining
            }
        }
    },

    combine_tasks: function(array1, array2) {
        /* Combine both arrays in a single one, avoiding duplicate values

        Parameters
        ----------
        array1: array
            The first array to be combined
        array2: array
            The second array to be combined

        Returns
        -------
        array
            An array with all unique values from both parameters

        */

        var result = array1;
        for (var i = 0; i < array2.length; i++) {
            var value = array2[i];
            if ($.inArray(value, result) == -1) {
                // new value not in first array, we can add it
                result.push(value);
            }
        }
        return result;
    },

    update_line: function(training_pk, $line) {
        /* Update the line in the html for the given training pk

        Parameters
        ----------
        training_pk: str
            The primary key of the training to add
        $line: jquery
            The jquery object holding the node of the html line to update

        */

        var data = this.data[training_pk],
            $cells = $line.children('td'),
            i, tasks, remaining, total, done, content;

        // we iterate on each type managed by the poller
        for (i = 0; i < this.task_types.length; i++) {
            task_type = this.task_types[i];
            content = '';  // by default no content

            if (data.tasks[task_type]) {
                // we may have content to set only if there is something for this task type
                tasks = data.tasks[task_type];

                //  get counts and compute
                remaining = (tasks.remaining || []).length;
                all = (tasks.all || tasks.remaining || []).length;

                if (all) {
                    // something to display in the cell only if there were waiting tasks
                    done = all - remaining;
                    content = done + ' / ' + all;
                    if (remaining) {
                        content += ' ' + this.spinner_template;
                    }
                }
            }

            $($cells[i+1]).html(content);  // +1 because the first cell, 0, holds the training name
        }
    },

    display_recent_trainings: function(trainings_html) {
        /* Add or update the html with the given one

        Parameters
        ----------
        trainings_html: str
            The html of all the recent trainings

        */

        if (!this.$recent_holder) {
            // nothing to do if no html and no holder
            if (!trainings_html) { return;}

            // but if html, we need a holder
            this.$recent_holder = $('tbody.recent-trainings');
            if (!this.$recent_holder.length) {
                this.$recent_holder = $('<tbody class="recent-trainings"></tbody>');
                this.$recent_holder.insertBefore('tbody.normal-trainings');
            }
        }

        // Now we just need to set the html in the holder, because we have all the recent trainings,
        // not only the ones we didn't have until now
        this.$recent_holder.html(trainings_html);
    },

    init: function(config) {
        /* Init some attributes and start polling

        Parameters
        ----------
        config: dict
            Each key/value in this dict will be set on the current object.
            Needed for example to pass ``data``, ``url`` and ``colspan``, or other values to
            override

        */

        // init some values now that dom is ready
        this.$lines_holder = $('tbody.waiting-trainings');

        // save config values
        $.each(config, $.proxy(function(key, value) {
            this[key] = value;
        }, this));

        // register bound methods
        this.$poll = $.proxy(this.poll, this);
        this.$on_poll_success = $.proxy(this.on_poll_success, this);

        // associate lines with their training id
        var $lines = this.$lines_holder.children('tr');
        for (var i = 0; i < $lines.length; i++) {
            var $line = $($lines[i]);
            this.$lines[$line.data('training-id')] = $line;
        }

        // launch the polling
        this.timeout_id = setTimeout(this.$poll, this.interval);
    }
};
