// @flow

import * as DOM from '../../util/dom.js';
import {bindAll} from '../../util/util.js';
import config from '../../util/config.js';
import {getHashString} from '../hash.js';

import type Map, {ControlPosition} from '../map.js';

type Options = {
    compact?: boolean,
    customAttribution?: ?string | ?Array<string>
};

/**
 * An `AttributionControl` control presents the map's [attribution information](https://docs.mapbox.com/help/how-mapbox-works/attribution/).
 * Add this control to a map using {@link Map#addControl}.
 *
 * @implements {IControl}
 * @param {Object} [options]
 * @param {boolean} [options.compact] If `true`, force a compact attribution that shows the full attribution on mouse hover. If `false`, force the full attribution control. The default is a responsive attribution that collapses when the map is less than 640 pixels wide. **Attribution should not be collapsed if it can comfortably fit on the map. `compact` should only be used to modify default attribution when map size makes it impossible to fit [default attribution](https://docs.mapbox.com/help/how-mapbox-works/attribution/) and when the automatic compact resizing for default settings are not sufficient**.
 * @param {string | Array<string>} [options.customAttribution] String or strings to show in addition to any other attributions. You can also set a custom attribution when initializing your map with {@link https://docs.mapbox.com/mapbox-gl-js/api/map/#map-parameters the customAttribution option}.
 * @example
 * const map = new mapboxgl.Map({attributionControl: false})
 *     .addControl(new mapboxgl.AttributionControl({
 *         customAttribution: 'Map design by me'
 *     }));
 */
class AttributionControl {
    options: Options;
    _map: Map;
    _container: HTMLElement;
    _innerContainer: HTMLElement;
    _compactButton: HTMLButtonElement;
    _editLink: ?HTMLAnchorElement;
    _attribHTML: string;
    styleId: string;
    styleOwner: string;

    constructor(options: Options = {}) {
        this.options = options;

        bindAll([
            '_toggleAttribution',
            '_updateEditLink',
            '_updateData',
            '_updateCompact'
        ], this);
    }

    getDefaultPosition(): ControlPosition {
        return 'bottom-right';
    }

    onAdd(map: Map): HTMLElement {
        const compact = this.options && this.options.compact;

        this._map = map;
        this._container = DOM.create('div', 'mapboxgl-ctrl mapboxgl-ctrl-attrib');
        this._compactButton = DOM.create('button', 'mapboxgl-ctrl-attrib-button', this._container);
        DOM.create('span', `mapboxgl-ctrl-icon`, this._compactButton).setAttribute('aria-hidden', 'true');
        this._compactButton.type = 'button';
        // $FlowFixMe[method-unbinding]
        this._compactButton.addEventListener('click', this._toggleAttribution);
        this._setElementTitle(this._compactButton, 'ToggleAttribution');
        this._innerContainer = DOM.create('div', 'mapboxgl-ctrl-attrib-inner', this._container);

        if (compact) {
            this._container.classList.add('mapboxgl-compact');
        }

        this._updateAttributions();
        this._updateEditLink();

        // $FlowFixMe[method-unbinding]
        this._map.on('styledata', this._updateData);
        // $FlowFixMe[method-unbinding]
        this._map.on('sourcedata', this._updateData);
        // $FlowFixMe[method-unbinding]
        this._map.on('moveend', this._updateEditLink);

        if (compact === undefined) {
            // $FlowFixMe[method-unbinding]
            this._map.on('resize', this._updateCompact);
            this._updateCompact();
        }

        return this._container;
    }

    onRemove() {
        this._container.remove();

        // $FlowFixMe[method-unbinding]
        this._map.off('styledata', this._updateData);
        // $FlowFixMe[method-unbinding]
        this._map.off('sourcedata', this._updateData);
        // $FlowFixMe[method-unbinding]
        this._map.off('moveend', this._updateEditLink);
        // $FlowFixMe[method-unbinding]
        this._map.off('resize', this._updateCompact);

        this._map = (undefined: any);
        this._attribHTML = (undefined: any);
    }

    _setElementTitle(element: HTMLElement, title: string) {
        const str = this._map._getUIString(`AttributionControl.${title}`);
        element.setAttribute('aria-label', str);
        element.removeAttribute('title');
        if (element.firstElementChild) element.firstElementChild.setAttribute('title', str);
    }

    _toggleAttribution() {
        if (this._container.classList.contains('mapboxgl-compact-show')) {
            this._container.classList.remove('mapboxgl-compact-show');
            this._compactButton.setAttribute('aria-expanded', 'false');
        } else {
            this._container.classList.add('mapboxgl-compact-show');
            this._compactButton.setAttribute('aria-expanded', 'true');
        }
    }

    _updateEditLink() {
        let editLink = this._editLink;
        if (!editLink) {
            editLink = this._editLink = (this._container.querySelector('.mapbox-improve-map'): any);
        }

        const params = [
            {key: 'owner', value: this.styleOwner},
            {key: 'id', value: this.styleId},
            {key: 'access_token', value: this._map._requestManager._customAccessToken || config.ACCESS_TOKEN}
        ];

        if (editLink) {
            const paramString = params.reduce((acc, next, i) => {
                if (next.value) {
                    acc += `${next.key}=${next.value}${i < params.length - 1 ? '&' : ''}`;
                }
                return acc;
            }, `?`);
            editLink.href = `${config.FEEDBACK_URL}/${paramString}#${getHashString(this._map, true)}`;
            editLink.rel = 'noopener nofollow';
            this._setElementTitle(editLink, 'MapFeedback');
        }
    }

    _updateData(e: any) {
        if (e && (e.sourceDataType === 'metadata' || e.sourceDataType === 'visibility' || e.dataType === 'style')) {
            this._updateAttributions();
            this._updateEditLink();
        }
    }

    _updateAttributions() {
        if (!this._map.style) return;
        let attributions: Array<string> = [];

        if (this._map.style.stylesheet) {
            const stylesheet: any = this._map.style.stylesheet;
            this.styleOwner = stylesheet.owner;
            this.styleId = stylesheet.id;
        }

        const sourceCaches = this._map.style._mergedSourceCaches;
        for (const id in sourceCaches) {
            const sourceCache = sourceCaches[id];
            if (sourceCache.used) {
                const source = sourceCache.getSource();
                if (source.attribution && attributions.indexOf(source.attribution) < 0) {
                    // $FlowFixMe[incompatible-call] - Flow can't infer that attribution is a string
                    attributions.push(source.attribution);
                }
            }
        }

        // remove any entries that are substrings of another entry.
        // first sort by length so that substrings come first
        attributions.sort((a, b) => a.length - b.length);
        attributions = attributions.filter((attrib, i) => {
            for (let j = i + 1; j < attributions.length; j++) {
                if (attributions[j].indexOf(attrib) >= 0) { return false; }
            }
            return true;
        });

        if (this.options.customAttribution) {
            if (Array.isArray(this.options.customAttribution)) {
                attributions = [...this.options.customAttribution, ...attributions];
            } else {
                attributions.unshift(this.options.customAttribution);
            }
        }

        // check if attribution string is different to minimize DOM changes
        const attribHTML = attributions.join(' | ');
        if (attribHTML === this._attribHTML) return;

        this._attribHTML = attribHTML;

        if (attributions.length) {
            this._innerContainer.innerHTML = attribHTML;
            this._container.classList.remove('mapboxgl-attrib-empty');
        } else {
            this._container.classList.add('mapboxgl-attrib-empty');
        }
        // remove old DOM node from _editLink
        this._editLink = null;
    }

    _updateCompact() {
        if (this._map.getCanvasContainer().offsetWidth <= 640) {
            this._container.classList.add('mapboxgl-compact');
        } else {
            this._container.classList.remove('mapboxgl-compact', 'mapboxgl-compact-show');
        }
    }

}

export default AttributionControl;
