import $ from 'jquery';
import _ from 'lodash';

// Required by the soy templates
import urls from 'bitbucket/internal/impl/urls'; //eslint-disable-line no-unused-vars, module-checks/no-unused-deps

import Widget from '../widget/widget';

const ELLIPSIS_PADDING = 15;

export default class MirroringCloneSelector extends Widget {

    /**
     * @typedef {Object} MinimalMirror
     * @param {String} id       - the mirror's ID
     * @param {String} name     - the mirror's name
     * @param {String} url      - the path to the mirror repo info REST resource
     */

    /**
     * @typedef {Object} MirroredRepository
     * @param {String}              mirrorName  - the name of the mirror
     * @param {String}              id          - the ID of the mirror
     * @param {boolean}             available   - true if the repository is available on the mirror, false otherwise
     * @param {Object}              links       - a map of link type to link
     * @param {Array<NamedLink>}    links.clone - an array of clone urls available on the primary
     */

    /**
     * @typedef {Object} MinimalUpstreamServer
     * @param {String}              mirrorName  - the name
     * @param {Object}              links       - a map of link type to link
     * @param {Array<NamedLink>}    links.clone - an array of clone urls available on the primary
     */

    /**
     * @typedef {Object} NamedLink
     * @param {String} name - the type of link. Usually the protocol, for example: 'HTTP' or 'SSH'
     * @param {String} href - the URL - for this component usually a clone URL.
     */

    /**
     * Turns el into a mirroring clone URL selector.
     * @param {HTMLElement|jQuery}      el                                  - The container element
     * @param {Object}                  options                             - Options for this MirroringCloneSelector
     * @param {MinimalUpstreamServer}   options.primary                     - an object describing the upstream server's configuration
     * @param {String}                  options.cloneProtocol               - the current cloneProtocol.
     * @param {Function}                [options.updateCloneUrl=_.noop] - a function that is called after the clone url has been updated
     * @param {Function}                [options.updatePreferredMirror=_.noop] - a function that is called to update the preferred mirror
     * @param {Function<Promise<MinimalMirror>>} [options.getMirrors] - a function that returns a promise to an array of mirror servers to query.
     * @param {String}                  [options.preferredMirrorId=null]  - the ID of the preferred mirror server, or null if none is set
     */
    constructor(el, options) {
        super(options);
        this.$el = $(el);
        this.availableMirrors = [];
        this.clicked = false;
        this.loaded = false;
        this.preferredMirrorId = this.options.preferredMirrorId;
        this.cloneProtocol = this.options.cloneProtocol;
        this.init();
    }

    /**
     * Adds an available mirror to the list
     * @param {MirroredRepository} mirroredRepository - A mirrored repository
     */
    addMirror(mirroredRepository) {
        if (mirroredRepository.available) {
            this.availableMirrors.push(mirroredRepository);
            const $availableMirrorsList = $('#available-mirror-list');
            const $availableMirrors = $availableMirrorsList.find('li.mirror');
            const $newMirror = $(bitbucket.internal.component.mirroringCloneSelector.mirrorItem({
                item: mirroredRepository,
                extraClasses: 'mirror',
            }));
            const $contextEl = _.find($availableMirrors, el => $(el).text() > mirroredRepository.mirrorName);
            if ($contextEl) {
                $newMirror.insertBefore($contextEl);
            } else {
                $newMirror.appendTo($availableMirrorsList);
            }

            this.$trigger.removeAttr('disabled');

            if (this.preferredMirrorId === mirroredRepository.id && !this.clicked) {
                this.selectMirror(mirroredRepository.id);
            }
        }
    }

    /**
     * Extracts the clone URL of the current protocol for the specified mirror
     * @param {MirroredRepository|MinimalUpstreamServer} value - The Server or Repository to get a clone URL for
     * @returns {String} the clone URL or an empty string
     */
    getCloneUrl(value) {
        return _.get(_.find(value.links.clone, 'name', this.getProtocol()), 'href', '');
    }

    /**
     * @returns {String} the currently selected protocol
     */
    getProtocol() {
        return this.cloneProtocol;
    }


    /**
     * Hides the "clone from" dropdown, if it is showing.
     */
    hideDropdown() {
        if (this.$dropdown.is(':visible')) {
            this.$trigger.trigger('aui-button-invoke');
        }
    }

    init() {
        this.$el.html(bitbucket.internal.component.mirroringCloneSelector.main({ primary: this.options.primary }));
        this.$trigger = this.$el.find('#available-mirrors-trigger');
        this.$trigger.on('click', e => e.preventDefault());
        this.initDropdown();
    }

    /**
     * Initialize any dropdown handlers.
     */
    initDropdown() {
        this.$dropdown = $('#available-mirrors');

        /**
         * @this {HTMLElement} the clicked element
         */
        this.$dropdown.on('click', '.mirror-item', e => {
            e.preventDefault();
            this.selectMirror($(e.currentTarget).data('id'));
            this._updatePreferredMirror();
            this.clicked = true;
        });
    }

    /**
     *
     */
    load() {
        if (!this.loaded) {
            this.loaded = true;
            this.options.getMirrors().done(mirrors => {

                let preferred = [];

                if (this.preferredMirrorId) {
                    preferred = _.remove(mirrors, item => item.id === this.preferredMirrorId);
                }

                preferred.forEach(mirror => {
                    this.loadMirror(mirror);
                });

                mirrors.forEach(mirror => {
                    this.loadMirror(mirror);
                });
            });
        }
    }

    /**
     * Calls the mirror's REST API to check if it is available and retrieve clone URLs
     * @param {MinimalMirror} mirror - The Mirror to load clone URLs for if it is available
     */
    loadMirror(mirror) {
        $.ajax({
            type: 'GET',
            contentType: 'application/json',
            url: mirror.url,
            dataType: 'json',
        }).done(availableMirror => {
            availableMirror.id = mirror.id;
            this.addMirror(availableMirror);
        });
    }

    /**
     * Selects the mirror with the specified ID
     * @param {string} mirrorId - The mirror's ID
     */
    selectMirror(mirrorId) {
        this.currentMirrorId = mirrorId;
        this.updateCloneUrl();
    }

    /**
     * Show or hide the set push url instructions
     * @param {boolean} visible - Should the instructions be visible
     */
    showSetPushUpstream(visible) {
        const $container = $('#update-push-url-container');
        if (visible) {
            $('#update-push-url-input').val('git remote set-url --push origin ' + this.getCloneUrl(this.options.primary));
            $container.removeClass('hidden');
        } else {
            $container.addClass('hidden');
        }
    }

    /**
     * Updates the current clone protocol to the value supplied.
     * This should be called by the component's host whenever the selected protocol changes.
     * @param {String} protocol - the protocol to update the state to, for example 'ssh' or 'http'
     * @returns {String} protocol
     */
    updateCloneProtocol(protocol) {
        this.cloneProtocol = protocol;
        this.updateCloneUrl();
        return protocol;
    }

    /**
     * Sets the clone URL.
     */
    updateCloneUrl() {
        let currentMirror;
        if (this.currentMirrorId) {
            currentMirror = _.find(this.availableMirrors, 'id', this.currentMirrorId);
            this.showSetPushUpstream(true);
        } else {
            currentMirror = this.options.primary;
            this.showSetPushUpstream(false);
        }
        this.currentMirrorId = currentMirror.id;
        this.$trigger.text(currentMirror.mirrorName);
        // We need to set the maxWidth because: the label is i18n and can vary in width and the $trigger's width
        // is controlled by AUI.
        this.$trigger.css('maxWidth', this.$el.width() - this.$el.find('#clone-from-label').width() - ELLIPSIS_PADDING);

        this.options.updateCloneUrl(this.getProtocol(), this.getCloneUrl(currentMirror));
    }

    show() {
        this.$el.show();
    }

    hide() {
        this.hideDropdown();
        this.$el.hide();
    }

    _updatePreferredMirror() {
        this.preferredMirrorId = this.currentMirrorId;
        this.options.updatePreferredMirror(this.preferredMirrorId);
    }
}

MirroringCloneSelector.defaults = {
    cloneProtocol: 'http',
    preferredMirrorId: null,
    updateCloneUrl: _.noop,
    updatePreferredMirror: _.noop,
    getMirrors: function() {
        return $.when([]);
    },
};
