/* *
 *
 *  (c) 2009-2019 Øystein Moseng
 *
 *  Accessibility component for chart info region and table.
 *
 *  License: www.highcharts.com/license
 *
 *  !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
 *
 * */
'use strict';
import H from '../../../parts/Globals.js';
var doc = H.win.document, format = H.format;
import U from '../../../parts/Utilities.js';
var extend = U.extend, pick = U.pick;
import AccessibilityComponent from '../AccessibilityComponent.js';
import ChartUtilities from '../utils/chartUtilities.js';
var unhideChartElementFromAT = ChartUtilities.unhideChartElementFromAT, getChartTitle = ChartUtilities.getChartTitle, getAxisDescription = ChartUtilities.getAxisDescription;
import HTMLUtilities from '../utils/htmlUtilities.js';
var addClass = HTMLUtilities.addClass, setElAttrs = HTMLUtilities.setElAttrs, escapeStringForHTML = HTMLUtilities.escapeStringForHTML, stripHTMLTagsFromString = HTMLUtilities.stripHTMLTagsFromString, getElement = HTMLUtilities.getElement, visuallyHideElement = HTMLUtilities.visuallyHideElement;
/* eslint-disable no-invalid-this, valid-jsdoc */
/**
 * @private
 */
function getTypeDescForMapChart(chart, formatContext) {
    return formatContext.mapTitle ?
        chart.langFormat('accessibility.chartTypes.mapTypeDescription', formatContext) :
        chart.langFormat('accessibility.chartTypes.unknownMap', formatContext);
}
/**
 * @private
 */
function getTypeDescForCombinationChart(chart, formatContext) {
    return chart.langFormat('accessibility.chartTypes.combinationChart', formatContext);
}
/**
 * @private
 */
function getTypeDescForEmptyChart(chart, formatContext) {
    return chart.langFormat('accessibility.chartTypes.emptyChart', formatContext);
}
/**
 * @private
 */
function buildTypeDescriptionFromSeries(chart, types, context) {
    var firstType = types[0], typeExplaination = chart.langFormat('accessibility.seriesTypeDescriptions.' + firstType, context), multi = chart.series && chart.series.length < 2 ? 'Single' : 'Multiple';
    return (chart.langFormat('accessibility.chartTypes.' + firstType + multi, context) ||
        chart.langFormat('accessibility.chartTypes.default' + multi, context)) + (typeExplaination ? ' ' + typeExplaination : '');
}
/**
 * @private
 */
function getTableSummary(chart) {
    return chart.langFormat('accessibility.table.tableSummary', { chart: chart });
}
/**
 * @private
 */
function stripEmptyHTMLTags(str) {
    return str.replace(/<(\w+)[^>]*?>\s*<\/\1>/g, '');
}
/**
 * @private
 */
function enableSimpleHTML(str) {
    return str
        .replace(/&lt;(h[1-7]|p|div)&gt;/g, '<$1>')
        .replace(/&lt;&#x2F;(h[1-7]|p|div|a|button)&gt;/g, '</$1>')
        .replace(/&lt;(div|a|button) id=&quot;([a-zA-Z\-0-9#]*?)&quot;&gt;/g, '<$1 id="$2">');
}
/**
 * @private
 */
function stringToSimpleHTML(str) {
    return stripEmptyHTMLTags(enableSimpleHTML(escapeStringForHTML(str)));
}
/**
 * Return simplified explaination of chart type. Some types will not be familiar
 * to most users, but in those cases we try to add an explaination of the type.
 *
 * @private
 * @function Highcharts.Chart#getTypeDescription
 * @param {Array<string>} types The series types in this chart.
 * @return {string} The text description of the chart type.
 */
H.Chart.prototype.getTypeDescription = function (types) {
    var firstType = types[0], firstSeries = this.series && this.series[0] || {}, formatContext = {
        numSeries: this.series.length,
        numPoints: firstSeries.points && firstSeries.points.length,
        chart: this,
        mapTitle: firstSeries.mapTitle
    };
    if (!firstType) {
        return getTypeDescForEmptyChart(this, formatContext);
    }
    if (firstType === 'map') {
        return getTypeDescForMapChart(this, formatContext);
    }
    if (this.types.length > 1) {
        return getTypeDescForCombinationChart(this, formatContext);
    }
    return buildTypeDescriptionFromSeries(this, types, formatContext);
};
/**
 * The InfoRegionsComponent class
 *
 * @private
 * @class
 * @name Highcharts.InfoRegionsComponent
 */
var InfoRegionsComponent = function () { };
InfoRegionsComponent.prototype = new AccessibilityComponent();
extend(InfoRegionsComponent.prototype, /** @lends Highcharts.InfoRegionsComponent */ {
    /**
     * Init the component
     * @private
     */
    init: function () {
        var chart = this.chart, component = this;
        this.initRegionsDefinitions();
        this.addEvent(chart, 'afterGetTable', function (e) {
            component.onDataTableCreated(e);
        });
        this.addEvent(chart, 'afterViewData', function (tableDiv) {
            component.dataTableDiv = tableDiv;
            // Use small delay to give browsers & AT time to register new table
            setTimeout(function () {
                component.focusDataTable();
            }, 300);
        });
    },
    /**
     * @private
     */
    initRegionsDefinitions: function () {
        var component = this;
        this.screenReaderSections = {
            before: {
                element: null,
                buildContent: function (chart) {
                    var formatter = chart.options.accessibility
                        .screenReaderSection.beforeChartFormatter;
                    return formatter ? formatter(chart) :
                        component.defaultBeforeChartFormatter(chart);
                },
                insertIntoDOM: function (el, chart) {
                    chart.renderTo.insertBefore(el, chart.renderTo.firstChild);
                },
                afterInserted: function () {
                    if (typeof component.dataTableButtonId !== 'undefined') {
                        component.initDataTableButton(component.dataTableButtonId);
                    }
                }
            },
            after: {
                element: null,
                buildContent: function (chart) {
                    var formatter = chart.options.accessibility.screenReaderSection
                        .afterChartFormatter;
                    return formatter ? formatter(chart) :
                        component.defaultAfterChartFormatter();
                },
                insertIntoDOM: function (el, chart) {
                    chart.renderTo.insertBefore(el, chart.container.nextSibling);
                }
            }
        };
    },
    /**
     * Called on first render/updates to the chart, including options changes.
     */
    onChartUpdate: function () {
        var component = this;
        this.linkedDescriptionElement = this.getLinkedDescriptionElement();
        this.setLinkedDescriptionAttrs();
        Object.keys(this.screenReaderSections).forEach(function (regionKey) {
            component.updateScreenReaderSection(regionKey);
        });
    },
    /**
     * @private
     */
    getLinkedDescriptionElement: function () {
        var chartOptions = this.chart.options, linkedDescOption = chartOptions.accessibility.linkedDescription;
        if (!linkedDescOption) {
            return;
        }
        if (typeof linkedDescOption !== 'string') {
            return linkedDescOption;
        }
        var query = format(linkedDescOption, this.chart), queryMatch = doc.querySelectorAll(query);
        if (queryMatch.length === 1) {
            return queryMatch[0];
        }
    },
    /**
     * @private
     */
    setLinkedDescriptionAttrs: function () {
        var el = this.linkedDescriptionElement;
        if (el) {
            el.setAttribute('aria-hidden', 'true');
            addClass(el, 'highcharts-linked-description');
        }
    },
    /**
     * @private
     * @param {string} regionKey The name/key of the region to update
     */
    updateScreenReaderSection: function (regionKey) {
        var chart = this.chart, region = this.screenReaderSections[regionKey], content = region.buildContent(chart), sectionDiv = region.element = (region.element || this.createElement('div')), hiddenDiv = (sectionDiv.firstChild || this.createElement('div'));
        this.setScreenReaderSectionAttribs(sectionDiv, regionKey);
        hiddenDiv.innerHTML = content;
        sectionDiv.appendChild(hiddenDiv);
        region.insertIntoDOM(sectionDiv, chart);
        visuallyHideElement(hiddenDiv);
        unhideChartElementFromAT(chart, hiddenDiv);
        if (region.afterInserted) {
            region.afterInserted();
        }
    },
    /**
     * @private
     * @param {Highcharts.HTMLDOMElement} sectionDiv The section element
     * @param {string} regionKey Name/key of the region we are setting attrs for
     */
    setScreenReaderSectionAttribs: function (sectionDiv, regionKey) {
        var labelLangKey = ('accessibility.screenReaderSection.' + regionKey + 'RegionLabel'), chart = this.chart, labelText = chart.langFormat(labelLangKey, { chart: chart }), sectionId = 'highcharts-screen-reader-region-' + regionKey + '-' +
            chart.index;
        setElAttrs(sectionDiv, {
            id: sectionId,
            'aria-label': labelText
        });
        // Sections are wrapped to be positioned relatively to chart in case
        // elements inside are tabbed to.
        sectionDiv.style.position = 'relative';
        if (chart.options.accessibility.landmarkVerbosity === 'all' &&
            labelText) {
            sectionDiv.setAttribute('role', 'region');
        }
    },
    /**
     * @private
     * @return {string}
     */
    defaultBeforeChartFormatter: function () {
        var chart = this.chart, format = chart.options.accessibility
            .screenReaderSection.beforeChartFormat, axesDesc = this.getAxesDescription(), dataTableButtonId = 'hc-linkto-highcharts-data-table-' +
            chart.index, context = {
            chartTitle: getChartTitle(chart),
            typeDescription: this.getTypeDescriptionText(),
            chartSubtitle: this.getSubtitleText(),
            chartLongdesc: this.getLongdescText(),
            xAxisDescription: axesDesc.xAxis,
            yAxisDescription: axesDesc.yAxis,
            viewTableButton: chart.getCSV ?
                this.getDataTableButtonText(dataTableButtonId) : ''
        }, formattedString = H.i18nFormat(format, context, chart);
        this.dataTableButtonId = dataTableButtonId;
        return stringToSimpleHTML(formattedString);
    },
    /**
     * @private
     * @return {string}
     */
    defaultAfterChartFormatter: function () {
        var chart = this.chart, format = chart.options.accessibility
            .screenReaderSection.afterChartFormat, context = {
            endOfChartMarker: this.getEndOfChartMarkerText()
        }, formattedString = H.i18nFormat(format, context, chart);
        return stringToSimpleHTML(formattedString);
    },
    /**
     * @private
     * @return {string}
     */
    getLinkedDescription: function () {
        var el = this.linkedDescriptionElement, content = el && el.innerHTML || '';
        return stripHTMLTagsFromString(content);
    },
    /**
     * @private
     * @return {string}
     */
    getLongdescText: function () {
        var chartOptions = this.chart.options, captionOptions = chartOptions.caption, captionText = captionOptions && captionOptions.text, linkedDescription = this.getLinkedDescription();
        return (chartOptions.accessibility.description ||
            linkedDescription ||
            captionText ||
            '');
    },
    /**
     * @private
     * @return {string}
     */
    getTypeDescriptionText: function () {
        var chart = this.chart;
        return chart.types ?
            chart.options.accessibility.typeDescription ||
                chart.getTypeDescription(chart.types) : '';
    },
    /**
     * @private
     * @param {string} buttonId
     * @return {string}
     */
    getDataTableButtonText: function (buttonId) {
        var chart = this.chart, buttonText = chart.langFormat('accessibility.table.viewAsDataTableButtonText', { chart: chart, chartTitle: getChartTitle(chart) });
        return '<a id="' + buttonId + '">' + buttonText + '</a>';
    },
    /**
     * @private
     * @return {string}
     */
    getSubtitleText: function () {
        var subtitle = (this.chart.options.subtitle);
        return stripHTMLTagsFromString(subtitle && subtitle.text || '');
    },
    /**
     * @private
     * @return {string}
     */
    getEndOfChartMarkerText: function () {
        var chart = this.chart, markerText = chart.langFormat('accessibility.screenReaderSection.endOfChartMarker', { chart: chart }), id = 'highcharts-end-of-chart-marker-' + chart.index;
        return '<div id="' + id + '">' + markerText + '</div>';
    },
    /**
     * @private
     * @param {Highcharts.Dictionary<string>} e
     */
    onDataTableCreated: function (e) {
        var chart = this.chart;
        if (chart.options.accessibility.enabled) {
            if (this.viewDataTableButton) {
                this.viewDataTableButton.setAttribute('aria-expanded', 'true');
            }
            e.html = e.html.replace('<table ', '<table tabindex="0" summary="' + getTableSummary(chart) + '"');
        }
    },
    /**
     * @private
     */
    focusDataTable: function () {
        var tableDiv = this.dataTableDiv, table = tableDiv && tableDiv.getElementsByTagName('table')[0];
        if (table && table.focus) {
            table.focus();
        }
    },
    /**
     * Set attribs and handlers for default viewAsDataTable button if exists.
     * @private
     * @param {string} tableButtonId
     */
    initDataTableButton: function (tableButtonId) {
        var el = this.viewDataTableButton = getElement(tableButtonId), chart = this.chart, tableId = tableButtonId.replace('hc-linkto-', '');
        if (el) {
            setElAttrs(el, {
                role: 'button',
                tabindex: '-1',
                'aria-expanded': !!getElement(tableId),
                href: '#' + tableId
            });
            el.onclick = chart.options.accessibility
                .screenReaderSection.onViewDataTableClick ||
                function () {
                    chart.viewData();
                };
        }
    },
    /**
     * Return object with text description of each of the chart's axes.
     * @private
     * @return {Highcharts.Dictionary<string>}
     */
    getAxesDescription: function () {
        var chart = this.chart, shouldDescribeColl = function (collectionKey, defaultCondition) {
            var axes = chart[collectionKey];
            return axes.length > 1 || axes[0] &&
                pick(axes[0].options.accessibility &&
                    axes[0].options.accessibility.enabled, defaultCondition);
        }, hasNoMap = !!chart.types && chart.types.indexOf('map') < 0, hasCartesian = !!chart.hasCartesianSeries, showXAxes = shouldDescribeColl('xAxis', !chart.angular && hasCartesian && hasNoMap), showYAxes = shouldDescribeColl('yAxis', hasCartesian && hasNoMap), desc = {};
        if (showXAxes) {
            desc.xAxis = this.getAxisDescriptionText('xAxis');
        }
        if (showYAxes) {
            desc.yAxis = this.getAxisDescriptionText('yAxis');
        }
        return desc;
    },
    /**
     * @private
     * @param {string} collectionKey
     * @return {string}
     */
    getAxisDescriptionText: function (collectionKey) {
        var component = this, chart = this.chart, axes = chart[collectionKey];
        return chart.langFormat('accessibility.axis.' + collectionKey + 'Description' + (axes.length > 1 ? 'Plural' : 'Singular'), {
            chart: chart,
            names: axes.map(function (axis) {
                return getAxisDescription(axis);
            }),
            ranges: axes.map(function (axis) {
                return component.getAxisRangeDescription(axis);
            }),
            numAxes: axes.length
        });
    },
    /**
     * Return string with text description of the axis range.
     * @private
     * @param {Highcharts.Axis} axis The axis to get range desc of.
     * @return {string} A string with the range description for the axis.
     */
    getAxisRangeDescription: function (axis) {
        var axisOptions = axis.options || {};
        // Handle overridden range description
        if (axisOptions.accessibility &&
            typeof axisOptions.accessibility.rangeDescription !== 'undefined') {
            return axisOptions.accessibility.rangeDescription;
        }
        // Handle category axes
        if (axis.categories) {
            return this.getCategoryAxisRangeDesc(axis);
        }
        // Use time range, not from-to?
        if (axis.isDatetimeAxis && (axis.min === 0 || axis.dataMin === 0)) {
            return this.getAxisTimeLengthDesc(axis);
        }
        // Just use from and to.
        // We have the range and the unit to use, find the desc format
        return this.getAxisFromToDescription(axis);
    },
    /**
     * @private
     * @param {Highcharts.Axis} axis
     * @return {string}
     */
    getCategoryAxisRangeDesc: function (axis) {
        var chart = this.chart;
        if (axis.dataMax && axis.dataMin) {
            return chart.langFormat('accessibility.axis.rangeCategories', {
                chart: chart,
                axis: axis,
                numCategories: axis.dataMax - axis.dataMin + 1
            });
        }
        return '';
    },
    /**
     * @private
     * @param {Highcharts.Axis} axis
     * @return {string}
     */
    getAxisTimeLengthDesc: function (axis) {
        var chart = this.chart, range = {}, rangeUnit = 'Seconds';
        range.Seconds = ((axis.max || 0) - (axis.min || 0)) / 1000;
        range.Minutes = range.Seconds / 60;
        range.Hours = range.Minutes / 60;
        range.Days = range.Hours / 24;
        ['Minutes', 'Hours', 'Days'].forEach(function (unit) {
            if (range[unit] > 2) {
                rangeUnit = unit;
            }
        });
        var rangeValue = range[rangeUnit].toFixed(rangeUnit !== 'Seconds' &&
            rangeUnit !== 'Minutes' ? 1 : 0 // Use decimals for days/hours
        );
        // We have the range and the unit to use, find the desc format
        return chart.langFormat('accessibility.axis.timeRange' + rangeUnit, {
            chart: chart,
            axis: axis,
            range: rangeValue.replace('.0', '')
        });
    },
    /**
     * @private
     * @param {Highcharts.Axis} axis
     * @return {string}
     */
    getAxisFromToDescription: function (axis) {
        var chart = this.chart, dateRangeFormat = chart.options.accessibility
            .screenReaderSection.axisRangeDateFormat, format = function (axisKey) {
            return axis.isDatetimeAxis ? chart.time.dateFormat(dateRangeFormat, axis[axisKey]) : axis[axisKey];
        };
        return chart.langFormat('accessibility.axis.rangeFromTo', {
            chart: chart,
            axis: axis,
            rangeFrom: format('min'),
            rangeTo: format('max')
        });
    }
});
export default InfoRegionsComponent;
