define('bitbucket/internal/mirroring/fragment/mirror-config', [
    'aui',
    'jquery',
    'lodash',
    'bitbucket/util/events',
    'bitbucket/util/navbuilder',
    'bitbucket/internal/mirroring/project-selector',
    'bitbucket/internal/mirroring/project-table',
    'bitbucket/internal/util/ajax',
    'bitbucket/internal/util/connect'
], function (
    AJS,
    $,
    _,
    events,
    nav,
    ProjectSelector,
    ProjectTable,
    ajax,
    AP
) {
    'use strict';

    /**
     * Represents a mirror's current sync state.
     *
     * @type {{UNKNOWN: string, SYNCING: string, NOT_SYNCING: string}}
     */
    var SYNC_STATES = {
        UNKNOWN: 'unknown',
        SYNCING: 'syncing',
        NOT_SYNCING: 'not_syncing'
    };

    /**
     * @type {{ALL: string, SELECTED: string}}
     */
    var MIRROR_MODE = {
        ALL: 'all_projects',
        SELECTED: 'selected_projects'
    };

    /**
     * Render a message on the host upstream.
     *
     * @param title the title of the message.
     * @param msg the message to display.
     * @param type the type of message: 'error', 'hint', 'success', 'warning'
     * @param options configuration to pass to the message.
     */
    function upstreamMessage(title, msg, type, options) {
        AP.require("messages", function (messages) {
            messages[type](title, msg, options || {});
        });
    }

    /**
     * Render an error message on the upstream.
     *
     * @param title the title of the message.
     * @param msg the message to display.
     */
    function upstreamError(title, msg) {
        upstreamMessage(title, msg, 'error');
    }

    /**
     * Show an error to indicate that all the projects could not be loaded.
     */
    function projectLoadingError() {
        upstreamError(AJS.I18n.getText('bitbucket.web.mirroring.config.projects.load.failed.title'),
            AJS.I18n.getText('bitbucket.web.mirroring.config.projects.load.failed.body'));
    }

    /**
     * Widget that renders the mirror configuration page.
     *
     * @param el the container of the panel.
     * @param options configuration for the widget.
     * @param [options.upstreamServer] the base URL for the upstream server.
     * @param [options.selectedProjects] the projects already configured.
     * @constructor
     */
    function MirrorConfig(el, options) {
        _.bindAll(this);
        this.$el = $(el);

        this.upstreamServer = options.upstreamServer;
        this.selectedProjects = options.selectedProjects;
        this.mode = options.mirrorMode;
        this.destroyed = false;

        if (options.mirrorMode === MIRROR_MODE.ALL) {
            this.initAllProjects();
        } else if (options.mirrorMode === MIRROR_MODE.SELECTED) {
            this.initSelectedProjects();
        }

        this.initProgress(options.mirrorMode);
    }

    MirrorConfig.prototype.destroy = function () {
        this.destroyed = true;

        if (this.progressAjax) {
            this.progressAjax.abort();
            delete this.progressAjax;
        }

        if (this.projectTable) {
            this.projectTable.destroy();
        }
        if (this.projectSelector) {
            this.projectSelector.destroy();
        }

        clearTimeout(this.progressRequestTimeout);
        clearTimeout(this.hideProgressTimeout);

        delete this.upstreamServer;
        delete this.selectedProjects;
        delete this.progressUrl;
    };

    MirrorConfig.prototype.initAllProjects = function () {
        this.$el.html(bitbucket.internal.mirror.fragment.allProjects());
    };

    MirrorConfig.prototype.initSelectedProjects = function () {
        var self = this;
        this.$el.html(bitbucket.internal.mirror.fragment.selectedProjects());

        this.projectTable = new ProjectTable({
            pageSize: 250,
            selectedProjects: this.selectedProjects,
            target: '#projects-table',
            upstream: this.upstreamServer
        });

        this.projectTable.init();
        this.projectTable.on('projectRemoved', this.removeProject);
        this.projectTable.on('loadError', function (data) {
            self._renderFatalError(data);
        });

        this.projectSelector = new ProjectSelector({
            target: '#projects-selector',
            id: 'projects-selector-form',
            upstream: this.upstreamServer
        }).init();

        this.projectSelector.addIgnoredProjectIds(this.selectedProjects);
        this.projectSelector.on('projectsSelected', this.addProjects);
        this.projectSelector.on('loadError', projectLoadingError);

        $('#confirm-switch-mode').on('click', '#confirm-switch-mode-button', function () {
            $('#confirm-switch-mode').attr('open', false);
            self.switchMode(MIRROR_MODE.ALL);
        });
    };

    MirrorConfig.prototype.initProgress = function (mode) {
        this.syncState = SYNC_STATES.UNKNOWN;
        this.progressUrl = nav.rest('mirroring', 'latest')
            .addPathComponents('upstreamServers', this.upstreamServer.id, 'progress')
            .build();
        if (mode === MIRROR_MODE.ALL) {
            $(bitbucket.internal.mirror.fragment.syncProgress())
                .appendTo(this.$el)
                .hide();
        } else if (mode === MIRROR_MODE.SELECTED) {
            $(bitbucket.internal.mirror.fragment.selectedSyncProgress())
                .appendTo($('#projects-table').find('thead'))
                .hide();
        }

        this._requestProgress();
        events.on('bitbucket.internal.mirroring.config.updated', this._requestProgress);
    };

    /**
     * Requests the sync progress and updates the UI.
     * @private
     */
    MirrorConfig.prototype._requestProgress = function () {
        var self = this;
        this.progressAjax = ajax.rest({
            url: self.progressUrl,
            statusCode: {
                '*': false
            }
        }).always(function () {
            delete self.progressAjax;
        }).done(this._handleProgress).fail(function () {
            if (this.destroyed) {
                return;
            }
            self.syncState = SYNC_STATES.UNKNOWN;
            self.progressRequestTimeout = setTimeout(self._requestProgress, self._mapStateToTime(self.syncState));
        });
    };

    /**
     * @param {Object} response - progress response
     * @param {boolean} response.discovering
     * @param {number}  response.syncedRepos
     * @param {number}  response.totalRepos
     * @private
     */
    MirrorConfig.prototype._handleProgress = function (response) {
        if (this.destroyed) {
            return;
        }

        var synced = response.syncedRepos;
        var total = response.totalRepos;
        if (response.discovering) {
            this.syncState = SYNC_STATES.SYNCING;
            this.updateProgress(AJS.I18n.getText('bitbucket.web.mirroring.config.sync.unknown'));
        } else {
            if (synced === total) {
                this._setNotSyncing();
            } else {
                this.syncState = SYNC_STATES.SYNCING;
                this.updateProgress(AJS.I18n.getText('bitbucket.web.mirroring.config.sync.progress', synced, total), synced, total);
            }
        }

        this.progressRequestTimeout = setTimeout(this._requestProgress, this._mapStateToTime(this.syncState));
    };

    /**
     * Sets the syncState to not syncing and hides the progress UI after 3s if it is showing.
     * @private
     */
    MirrorConfig.prototype._setNotSyncing = function () {
        if (this.syncState === SYNC_STATES.SYNCING) {
            this.updateProgress(AJS.I18n.getText('bitbucket.web.mirroring.config.sync.not.syncing'), 1, 1);
            this.hideProgressTimeout = setTimeout(this.hideProgress, 3 * 1000);
        }
        this.$el.find('.mirror-all .title').text(AJS.I18n.getText('bitbucket.mirroring.config.all.projects'));
        this.$el.find('.mirror-all .sync-text').text('');
        this.syncState = SYNC_STATES.NOT_SYNCING;
    };

    /**
     * Maps the sync state to the poll time that should be used for that state.
     * @param {String} syncState
     * @returns {number} time in ms
     * @private
     */
    MirrorConfig.prototype._mapStateToTime = function (syncState) {
        switch(syncState) {
            case SYNC_STATES.NOT_SYNCING:
                return 60 * 1000;
            case SYNC_STATES.UNKNOWN:
                return 30 * 1000;
            case SYNC_STATES.SYNCING:
                return 1000;
        }
    };

    /**
     * Update the progress bar.
     * @param {String}  text         - the text to show above the progress bar
     * @param {number}  [synced]     - the number of synced repos
     * @param {number}  [total]      - the total number of repos
     */
    MirrorConfig.prototype.updateProgress = function (text, synced, total) {
        $('#mirror-sync-progress-description').text(text);
        this.showProgress();
        var progressBarSelector = '#mirror-sync-progress-bar';
        if (synced === undefined && total === undefined) {
            AJS.progressBars.setIndeterminate(progressBarSelector);
        } else {
            AJS.progressBars.update(progressBarSelector, synced / total);
        }
    };

    /**
     * Animate the progress container.
     * @param {Function} func - the function to use to animate the element
     * @private
     */
    MirrorConfig.prototype._animateProgress = function(func) {
        var $progress = this.$el.find('.mirror-sync-progress');
        if ($progress.length) {
            func.call($progress);
        }
    };

    MirrorConfig.prototype.hideProgress = function () {
        this._animateProgress($.prototype.fadeOut);
    };

    MirrorConfig.prototype.showProgress = function () {
        clearTimeout(this.hideProgressTimeout);
        this._animateProgress($.prototype.fadeIn);
    };

    /**
     * Add the passed projects to the list the mirror will mirror.
     *
     * @param projects the projects to mirror.
     */
    MirrorConfig.prototype.addProjects = function (projects) {
        projects = _.sortBy(projects, 'name');
        var data = _.pluck(projects, 'id');
        var self = this;
        var url = nav.rest('mirroring', 'latest')
            .addPathComponents('upstreamServers', self.upstreamServer.id, 'settings', 'projects')
            .build();

        this.projectSelector.disable().showSpinner();

        ajax.rest({
            url: url,
            data: data,
            dataType: 'json',
            type: 'POST',
            statusCode: {
                '*': false
            }
        }).done(function () {
            self.projectTable.add(projects);
            self.projectSelector.addIgnoredProjects(projects).clear();
            events.trigger('bitbucket.internal.mirroring.config.updated');
        }).fail(function () {
            upstreamError(AJS.I18n.getText('bitbucket.web.mirroring.config.projects.add.failed.title'),
                AJS.I18n.getText('bitbucket.web.mirroring.config.projects.add.failed.body'));
        }).always(function () {
            self.projectSelector.enable().hideSpinner();
        });
    };

    /**
     * Remove the passed projects from the list the mirror will mirror.
     *
     * @param project the projects to mirror.
     */
    MirrorConfig.prototype.removeProject = function (project) {
        var self = this;
        var url = nav.rest('mirroring', 'latest')
            .addPathComponents('upstreamServers', this.upstreamServer.id, 'settings', 'projects', project.id)
            .build();

        self.projectTable.showSpinner(project.id);

        ajax.rest({
            url: url,
            type: 'DELETE',
            statusCode: {
                '*': false
            }
        }).done(function () {
            self.projectTable.removeProject(project.id);
            self.projectSelector.removeIgnoredProjectIds([project.id]);
            events.trigger('bitbucket.internal.mirroring.config.updated');
        }).fail(function () {
            self.projectTable.hideSpinner(project.id);

            upstreamError(AJS.I18n.getText('bitbucket.web.mirroring.config.projects.remove.failed.title'),
                AJS.I18n.getText('bitbucket.web.mirroring.config.projects.remove.failed.body', project.name));
        });
    };

    /**
     * Switch the mirror mode.
     * @param {String} mode - the mirror mode to switch to. ('all_projects' | 'selected_projects')
     */
    MirrorConfig.prototype.switchMode = function (mode) {
        var self = this;
        var url = nav.rest('mirroring', 'latest')
            .addPathComponents('upstreamServers', this.upstreamServer.id, 'settings', 'mode')
            .build();

        ajax.rest({
            url: url,
            type: 'PUT',
            statusCode: {
                '*': false
            },
            data: mode
        }).done(function () {
            events.trigger('bitbucket.internal.mirroring.config.updated');
            self.rerender(mode);
        }).fail(function () {
            upstreamError(AJS.I18n.getText('bitbucket.web.mirroring.config.mode.switch.failed.title'),
                AJS.I18n.getText('bitbucket.web.mirroring.config.mode.switch.failed.body'));
        });
    };

    /**
     * Re-renders the entire config content in the specified mode.
     * @param mode
     */
    MirrorConfig.prototype.rerender = function (mode) {
        var options = {
            mirrorMode: mode,
            upstreamServer: this.upstreamServer,
            selectedProjects: this.selectedProjects
        };
        this.destroy();
        new MirrorConfig(this.$el, options);
    };

    /**
     * @param data the data from the error handler.
     * @private
     */
    MirrorConfig.prototype._renderFatalError = function (data) {
        var $el = this.$el;
        var oauth = data && (data.code === 'user_already_denied_access' || data.code === 'user_denied_access');

        this.destroy();
        $el.html(bitbucket.internal.mirror.fragment.mirrorError({
            oauth: oauth
        }));
    };

    return MirrorConfig;
});