'use strict';

import Mustache from 'mustache';
import React from 'react';

import createReactClass from 'create-react-class';

import PropTypes from 'prop-types';

import CesiumMath from 'terriajs-cesium/Source/Core/Math';
import classNames from 'classnames';
import dateFormat from 'dateformat';
import defined from 'terriajs-cesium/Source/Core/defined';
import Ellipsoid from 'terriajs-cesium/Source/Core/Ellipsoid';
import isArray from 'terriajs-cesium/Source/Core/isArray';
import JulianDate from 'terriajs-cesium/Source/Core/JulianDate';

import CustomComponents from '../Custom/CustomComponents';
import FeatureInfoDownload from './FeatureInfoDownload';
import formatNumberForLocale from '../../Core/formatNumberForLocale';
import Icon from '../Icon.jsx';
import ObserveModelMixin from '../ObserveModelMixin';
import propertyGetTimeValues from '../../Core/propertyGetTimeValues';
import parseCustomMarkdownToReact from '../Custom/parseCustomMarkdownToReact';

import Styles from './feature-info-section.scss';

// We use Mustache templates inside React views, where React does the escaping; don't escape twice, or eg. " => &quot;
Mustache.escape = function(string) {
    return string;
};

// Individual feature info section
const FeatureInfoSection = createReactClass({
    displayName: 'FeatureInfoSection',
    mixins: [ObserveModelMixin],

    propTypes: {
        viewState: PropTypes.object.isRequired,
        template: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
        feature: PropTypes.object,
        position: PropTypes.object,
        catalogItem: PropTypes.object,  // Note this may not be known (eg. WFS).
        isOpen: PropTypes.bool,
        onClickHeader: PropTypes.func,
        printView: PropTypes.bool
    },

    getInitialState() {
        return {
            removeClockSubscription: undefined,
            timeoutIds: [],
            showRawData: false
        };
    },

    /* eslint-disable-next-line camelcase */
    UNSAFE_componentWillMount() {
        setSubscriptionsAndTimeouts(this, this.props.feature);
    },

    /* eslint-disable-next-line camelcase */
    UNSAFE_componentWillReceiveProps(nextProps) {
        // If the feature changed (without an unmount/mount),
        // change the subscriptions that handle time-varying data.
        if (nextProps.feature !== this.props.feature) {
            removeSubscriptionsAndTimeouts(this);
            setSubscriptionsAndTimeouts(this, nextProps.feature);
        }
    },

    componentWillUnmount() {
        removeSubscriptionsAndTimeouts(this);
    },

    getPropertyValues() {
        return getPropertyValuesForFeature(this.props.feature, currentTimeIfAvailable(this), this.props.template && this.props.template.formats);
    },

    getTemplateData() {
        const propertyData = this.getPropertyValues();
        if (defined(propertyData)) {

            propertyData.terria = {
                formatNumber: mustacheFormatNumberFunction,
                formatDateTime: mustacheFormatDateTime,
                urlEncodeComponent: mustacheURLEncodeTextComponent,
                urlEncode: mustacheURLEncodeText
            };
            if (this.props.position) {
                const latLngInRadians = Ellipsoid.WGS84.cartesianToCartographic(this.props.position);
                propertyData.terria.coords = {
                    latitude: CesiumMath.toDegrees(latLngInRadians.latitude),
                    longitude: CesiumMath.toDegrees(latLngInRadians.longitude)
                };
            }
            if (this.props.catalogItem) {
                propertyData.terria.currentTime = this.props.catalogItem.discreteTime;
            }
            propertyData.terria.timeSeries = getTimeSeriesChartContext(this.props.catalogItem, this.props.feature, propertyData._terria_getChartDetails);
        }
        return propertyData;
    },

    clickHeader() {
        if (defined(this.props.onClickHeader)) {
            this.props.onClickHeader(this.props.feature);
        }
    },

    hasTemplate() {
        return this.props.template && (typeof this.props.template === 'string' || this.props.template.template);
    },

    descriptionFromTemplate() {
        const template = this.props.template;
        const templateData = this.getTemplateData();
        // If property names were changed, let the template access the original property names too.
        if (defined(templateData) && defined(templateData._terria_columnAliases)) {
            for (let i = 0; i < templateData._terria_columnAliases.length; i++) {
                const alias = templateData._terria_columnAliases[i];
                templateData[alias.id] = templateData[alias.name];
            }
        }
        // templateData may not be defined if a re-render gets triggered in the middle of a feature updating.
        // (Recall we re-render whenever feature.definitionChanged triggers.)
        if (defined(templateData)) {
            return typeof template === 'string' ?
                Mustache.render(template, templateData) :
                Mustache.render(template.template, templateData, template.partials);
        } else {
            return 'No information available';
        }
    },

    descriptionFromFeature() {
        const feature = this.props.feature;
        // This description could contain injected <script> tags etc.
        // Before rendering, we will pass it through parseCustomMarkdownToReact, which applies
        //     markdownToHtml (which applies MarkdownIt.render and DOMPurify.sanitize), and then
        //     parseCustomHtmlToReact (which calls htmlToReactParser).
        // Note that there is an unnecessary HTML encoding and decoding in this combination which would be good to remove.
        const currentTime = currentTimeIfAvailable(this) ? currentTimeIfAvailable(this) : JulianDate.now();
        let description = feature.currentDescription || getCurrentDescription(feature, currentTime);
        if (!defined(description) && defined(feature.properties)) {
            description = describeFromProperties(feature.properties, currentTime);
        }
        return description;
    },

    renderDataTitle() {
        const template = this.props.template;
        if (typeof template === 'object' && defined(template.name)) {
            return Mustache.render(template.name, this.getPropertyValues());
        }
        const feature = this.props.feature;
        return (feature && feature.name) || 'Site Data';
    },

    isFeatureTimeVarying(feature) {
        // The feature is NOT time-varying if:
        // 1. There is no info (ie. no description and no properties).
        // 2. A template is provided and all feature.properties are constant.
        // OR
        // 3. No template is provided, and feature.description is either not defined, or defined and constant.
        // If info is time-varying, we need to keep updating the description.
        if (!defined(feature.description) && !defined(feature.properties)) {
            return false;
        }
        if (defined(this.props.template)) {
            return !areAllPropertiesConstant(feature.properties);
        }
        if (defined(feature.description)) {
            return !feature.description.isConstant; // This should always be a "Property" eg. a ConstantProperty.
        }
        return false;
    },

    toggleRawData() {
        this.setState({
            showRawData: !this.state.showRawData
        });
    },

    render() {
        const catalogItemName = (this.props.catalogItem && this.props.catalogItem.name) || '';
        let baseFilename = catalogItemName;
        // Add the Lat, Lon to the baseFilename if it is possible and not already present.
        if (this.props.position) {
            const position = Ellipsoid.WGS84.cartesianToCartographic(this.props.position);
            const latitude = CesiumMath.toDegrees(position.latitude);
            const longitude = CesiumMath.toDegrees(position.longitude);
            const precision = 5;
            // Check that baseFilename doesn't already contain the lat, lon with the similar or better precision.
            if ((typeof baseFilename !== 'string') || !contains(baseFilename, latitude, precision) || !contains(baseFilename, longitude, precision)) {
                baseFilename += ' - Lat ' + latitude.toFixed(precision) + ' Lon ' + longitude.toFixed(precision);
            }
        }
        const fullName = (catalogItemName ? (catalogItemName + ' - ') : '') + this.renderDataTitle();
        const reactInfo = getInfoAsReactComponent(this);

        return (
            <li className={classNames(Styles.section)}>
                <If condition={this.props.printView}>
                    <h2>{fullName}</h2>
                </If>
                <If condition={!this.props.printView}>
                    <button type='button' onClick={this.clickHeader} className={Styles.title}>
                        <span>{fullName}</span>
                        {this.props.isOpen ? <Icon glyph={Icon.GLYPHS.opened}/> : <Icon glyph={Icon.GLYPHS.closed}/>}
                    </button>
                </If>
                <If condition={this.props.isOpen}>
                    <section className={Styles.content}>
                    <If condition={!this.props.printView && this.hasTemplate()}>
                        <button type="button" className={Styles.rawDataButton} onClick={this.toggleRawData}>
                            {this.state.showRawData ? 'Show Curated Data' : 'Show Raw Data'}
                        </button>
                    </If>
                    <div>
                        <Choose>
                            <When condition={reactInfo.showRawData || !this.hasTemplate()}>
                                <If condition={reactInfo.hasRawData}>
                                    {reactInfo.rawData}
                                </If>
                                <If condition={!reactInfo.hasRawData}>
                                    <div ref="no-info" key="no-info">No information available.</div>
                                </If>
                                <If condition={!this.props.printView && reactInfo.timeSeriesChart}>
                                    <div className={Styles.timeSeriesChart}>
                                        <h4>{reactInfo.timeSeriesChartTitle}</h4>
                                        {reactInfo.timeSeriesChart}
                                    </div>
                                </If>
                                <If condition={!this.props.printView && defined(reactInfo.downloadableData)}>
                                    <FeatureInfoDownload key='download'
                                        viewState={this.props.viewState}
                                        data={reactInfo.downloadableData}
                                        name={baseFilename} />
                                </If>
                            </When>
                            <Otherwise>
                                {reactInfo.info}
                            </Otherwise>
                        </Choose>
                        <For each="ExtraComponent" index="i" of={FeatureInfoSection.extraComponents}>
                            <ExtraComponent key={i} viewState={this.props.viewState} // eslint-disable-line react/jsx-no-undef
                                                    template={this.props.template}
                                                    feature={this.props.feature}
                                                    position={this.props.position}
                                                    // We should deprecate clock here and remove it alltogether, but currently leaving so don't break API.
                                                    // Clients can and should use catalogItem.clock and catalogItem.currentTime.
                                                    clock={clockIfAvailable(this)}
                                                    catalogItem={this.props.catalogItem}
                                                    isOpen={this.props.isOpen}
                                                    onClickHeader={this.props.onClickHeader}/>
                        </For>
                    </div>
                    </section>
                </If>
            </li>
        );
    },
});

/**
 * Returns the clockForDisplay for the catalogItem if it is avaliable, otherwise returns undefined.
 * @private
 */
function clockIfAvailable(featureInfoSection) {
    if (defined(featureInfoSection.props.catalogItem)) {
        return featureInfoSection.props.catalogItem.clock;
    }

    return undefined;
}

/**
 * Returns the currentTime for the catalogItem if it is avaliable, otherwise returns undefined.
 * @private
 */
function currentTimeIfAvailable(featureInfoSection) {
    if (defined(featureInfoSection.props.catalogItem)) {
        return featureInfoSection.props.catalogItem.currentTime;
    }

    return undefined;
}

/**
 * Do we need to dynamically update this feature info over time?
 * There are three situations in which we would:
 * 1. When the feature description or properties are time-varying.
 * 2. When a custom component self-updates.
 *    Eg. <chart poll-seconds="60" src="xyz.csv"> must reload data from xyz.csv every 60 seconds.
 * 3. When a catalog item changes a feature's properties, eg. changing from a daily view to a monthly view.
 *
 * For (1), use catalogItem.clock.currentTime knockout observable so don't need to do anything specific here.
 * For (2), use a regular javascript setTimeout to update a counter in feature's currentProperties.
 * For (3), use an event listener on the Feature's underlying Entity's "definitionChanged" event.
 *   Conceivably it could also be handled by the catalog item itself changing, if its change is knockout tracked, and the
 *   change leads to a change in what is rendered (unlikely).
 * Since the catalogItem is also a prop, this will trigger a rerender.
 * @private
 */
function setSubscriptionsAndTimeouts(featureInfoSection, feature) {
    featureInfoSection.setState({
        removeFeatureChangedSubscription: feature.definitionChanged.addEventListener(function(changedFeature) {
            setCurrentFeatureValues(changedFeature, currentTimeIfAvailable(featureInfoSection));
        })
    });

    setTimeoutsForUpdatingCustomComponents(featureInfoSection);
}

/**
 * Remove the clock subscription (event listener) and timeouts.
 * @private
 */
function removeSubscriptionsAndTimeouts(featureInfoSection) {
    if (defined(featureInfoSection.state.removeFeatureChangedSubscription)) {
        featureInfoSection.state.removeFeatureChangedSubscription();
        featureInfoSection.setState({removeFeatureChangedSubscription: undefined});
    }
    featureInfoSection.state.timeoutIds.forEach(id => {
        clearTimeout(id);
    });
}

/**
 * Gets a map of property labels to property values for a feature at the provided current time.
 * @private
 * @param {Entity} feature A feature to get values for.
 * @param {JulianDate} currentTime A knockout observable containing the currentTime.
 * @param {Object} [formats] A map of property labels to the number formats that should be applied for them.
 */
function getPropertyValuesForFeature(feature, currentTime, formats) {
    // Manipulate the properties before templating them.
    // If they require .getValue, apply that.
    // If they have bad keys, fix them.
    // If they have formatting, apply it.
    const properties = feature.currentProperties || propertyGetTimeValues(feature.properties, currentTime);
    // Try JSON.parse on values that look like JSON arrays or objects
    let result = parseValues(properties);
    result = replaceBadKeyCharacters(result);
    if (defined(formats)) {
        applyFormatsInPlace(result, formats);
    }
    return result;
}

function parseValues(properties) {
    // JSON.parse property values that look like arrays or objects
    const result = {};
    for (const key in properties) {
        if (properties.hasOwnProperty(key)) {
            let val = properties[key];
            if (val && (typeof val === 'string' || val instanceof String) && (/^\s*[[{]/).test(val)) {
                try {
                    val = JSON.parse(val);
                } catch (e) {}
            }
            result[key] = val;
        }
    }
    return result;
}

/**
 * Formats values in an object if their keys match the provided formats object.
 * @private
 * @param {Object} properties a map of property labels to property values.
 * @param {Object} formats A map of property labels to the number formats that should be applied for them.
 */
function applyFormatsInPlace(properties, formats) {
    // Optionally format each property. Updates properties in place, returning nothing.
    for (const key in formats) {
        if (properties.hasOwnProperty(key)) {
            // Default type if not provided is number.
            if (!defined(formats[key].type) || (defined(formats[key].type) && formats[key].type === "number")) {
                properties[key] = formatNumberForLocale(properties[key], formats[key]);
            }
            if (defined(formats[key].type)) {
                if (formats[key].type === "dateTime") {
                    properties[key] = formatDateTime(properties[key], formats[key]);
                }
            }
        }
    }
}

/**
 * Recursively replace '.' and '#' in property keys with _, since Mustache cannot reference keys with these characters.
 * @private
 */
function replaceBadKeyCharacters(properties) {
    // if properties is anything other than an Object type, return it. Otherwise recurse through its properties.
    if (!properties || typeof properties !== 'object' || isArray(properties)) {
        return properties;
    }
    const result = {};
    for (const key in properties) {
        if (properties.hasOwnProperty(key)) {
            const cleanKey = key.replace(/[.#]/g, '_');
            result[cleanKey] = replaceBadKeyCharacters(properties[key]);
        }
    }
    return result;
}

/**
 * Determines whether all properties in the provided properties object have an isConstant flag set - otherwise they're
 * assumed to be time-varying.
 * @private
 * @returns {boolean}
 */
function areAllPropertiesConstant(properties) {
    // test this by assuming property is time-varying only if property.isConstant === false.
    // (so if it is undefined or true, it is constant.)
    let result = true;
    if (defined(properties.isConstant)) {
        return properties.isConstant;
    }
    for (const key in properties) {
        if (properties.hasOwnProperty(key)) {
            result = result && defined(properties[key]) && (properties[key].isConstant !== false);
        }
    }
    return result;
}

/**
 * Gets a text description for the provided feature at a certain time.
 * @private
 * @param {Entity} feature
 * @param {JulianDate} currentTime
 * @returns {String}
 */
function getCurrentDescription(feature, currentTime) {
    if (feature.description && typeof feature.description.getValue === 'function') {
        return feature.description.getValue(currentTime);
    }
    return feature.description;
}

/**
 * Updates {@link Entity#currentProperties} and {@link Entity#currentDescription} with the values at the provided time.
 * @private
 * @param {Entity} feature
 * @param {JulianDate} currentTime
 */
function setCurrentFeatureValues(feature, currentTime) {
    const newProperties = propertyGetTimeValues(feature.properties, currentTime);
    if (newProperties !== feature.currentProperties) {
        feature.currentProperties = newProperties;
    }
    const newDescription = getCurrentDescription(feature, currentTime);
    if (newDescription !== feature.currentDescription) {
        feature.currentDescription = newDescription;
    }
}

/**
 * Returns a function which extracts JSON elements from the content of a Mustache section template and calls the
 * supplied customProcessing function with the extracted JSON options, example syntax processed:
 * {optionKey: optionValue}{{value}}
 * @private
 */
function mustacheJsonSubOptions(customProcessing) {
    return function(text, render) {
        // Eg. "{foo:1}hi there".match(optionReg) = ["{foo:1}hi there", "{foo:1}", "hi there"].
        // Note this won't work with nested objects in the options (but these aren't used yet).
        // Note I use [\s\S]* instead of .* at the end - .* does not match newlines, [\s\S]* does.
        const optionReg = /^(\{[^}]+\})([\s\S]*)/;
        const components = text.match(optionReg);
        // This regex unfortunately matches double-braced text like {{number}}, so detect that separately and do not treat it as option json.
        const startsWithdoubleBraces = (text.length > 4) && (text[0] === '{') && (text[1] === '{');
        if (!components || startsWithdoubleBraces) {
            // If no options were provided, just use the defaults.
            return customProcessing(render(text));
        }
        // Allow {foo: 1} by converting it to {"foo": 1} for JSON.parse.
        const quoteReg = /([{,])(\s*)([A-Za-z0-9_\-]+?)\s*:/g;
        const jsonOptions = components[1].replace(quoteReg, '$1"$3":');
        const options = JSON.parse(jsonOptions);
        return customProcessing(render(components[2]), options);
    };
}

/**
 * Returns a function which implements number formatting in Mustache templates, using this syntax:
 * {{#terria.formatNumber}}{useGrouping: true}{{value}}{{/terria.formatNumber}}
 * @private
 */
function mustacheFormatNumberFunction() {
    return mustacheJsonSubOptions(formatNumberForLocale);
}

/**
 * Formats the date according to the date format string.
 * If the date expression can't be parsed using Date.parse() it will be returned unmodified.
 *
 * @param {String} text The date to format.
 * @param {Object} options Object with the following properties:
 * @param {String} options.format If present, will override the default date format using the npm datefromat package
 *                                format (see https://www.npmjs.com/package/dateformat). E.g. "isoDateTime"
 *                                or "dd-mm-yyyy HH:MM:ss". If not supplied isoDateTime will be used.
 * @private
 */
function formatDateTime(text, options) {
    const date = Date.parse(text);

    if (!defined(date) || isNaN(date)) {
        return text;
    }

    if (defined(options) && defined(options.format)) {
       return dateFormat(date, options.format);
    }

    return dateFormat(date, "isoDateTime");
}

/**
 * Returns a function which implements date/time formatting in Mustache templates, using this syntax:
 * {{#terria.formatDateTime}}{format: "npm dateFormat string"}DateExpression{{/terria.formatDateTime}}
 * format If present, will override the default date format (see https://www.npmjs.com/package/dateformat)
 * Eg. "isoDateTime" or "dd-mm-yyyy HH:MM:ss".
 * If the Date_Expression can't be parsed using Date.parse() it will be used(returned) unmodified by the terria.formatDateTime section expression.
 * If no valid date formatting options are present in the terria.formatDateTime section isoDateTime will be used.
 * @private
 */
function mustacheFormatDateTime() {
    return mustacheJsonSubOptions(formatDateTime);
}

/**
 * URL Encodes provided text: {{#terria.urlEncodeComponent}}{{value}}{{/terria.urlEncodeComponent}}.
 * See encodeURIComponent for details.
 *
 * {{#terria.urlEncodeComponent}}W/HO:E#1{{/terria.urlEncodeComponent}} -> W%2FHO%3AE%231
 * @private
 */
function mustacheURLEncodeTextComponent() {
    return function(text, render) {
        return encodeURIComponent(render(text));
    };
}

/**
 * URL Encodes provided text: {{#terria.urlEncode}}{{value}}{{/terria.urlEncode}}.
 * See encodeURI for details.
 *
 * {{#terria.urlEncode}}http://example.com/a b{{/terria.urlEncode}} -> http://example.com/a%20b
 * @private
 */
function mustacheURLEncodeText() {
    return function(text, render) {
        return encodeURI(render(text));
    };
}

const simpleStyleIdentifiers = ['title', 'description',
    'marker-size', 'marker-symbol', 'marker-color', 'stroke',
    'stroke-opacity', 'stroke-width', 'fill', 'fill-opacity'];

/**
 * A way to produce a description if properties are available but no template is given.
 * Derived from Cesium's geoJsonDataSource, but made to work with possibly time-varying properties.
 * @private
 */
function describeFromProperties(properties, time) {
    let html = '';
    if (typeof properties.getValue === 'function') {
        properties = properties.getValue(time);
    }
    if (typeof properties === 'object') {
        for (const key in properties) {
            if (properties.hasOwnProperty(key)) {
                if (simpleStyleIdentifiers.indexOf(key) !== -1) {
                    continue;
                }
                let value = properties[key];
                if (defined(value)) {
                    if (typeof value.getValue === 'function') {
                        value = value.getValue(time);
                    }
                    if (Array.isArray(properties)) {
                        html += '<tr><td>' + describeFromProperties(value, time) + '</td></tr>';
                    } else if (typeof value === 'object') {
                        html += '<tr><th>' + key + '</th><td>' + describeFromProperties(value, time) + '</td></tr>';
                    } else {
                        html += '<tr><th>' + key + '</th><td>' + value + '</td></tr>';
                    }
                }
            }
        }
    } else {
        // properties is only a single value.
        html += '<tr><th>' + '</th><td>' + properties + '</td></tr>';
    }
    if (html.length > 0) {
        html = '<table class="cesium-infoBox-defaultTable"><tbody>' + html + '</tbody></table>';
    }
    return html;
}

/**
 * Get parameters that should be exposed to the template, to help show a timeseries chart of the feature data.
 * @private
 */
function getTimeSeriesChartContext(catalogItem, feature, getChartDetails) {
    // Only show it as a line chart if the details are available, the data is sampled (so a line chart makes sense), and charts are available.
    if (defined(getChartDetails) && defined(catalogItem) && catalogItem.isSampled && CustomComponents.isRegistered('chart')) {
        const chartDetails = getChartDetails();
        const distinguishingId = catalogItem.dataViewId;
        const featureId = defined(distinguishingId) ? (distinguishingId + '--' + feature.id) : feature.id;
        if (chartDetails) {
            const result = {
                xName: chartDetails.xName.replace(/\"/g, ''),
                yName: chartDetails.yName.replace(/\"/g, ''),
                title: chartDetails.yName,
                id: featureId.replace(/\"/g, ''),
                data: chartDetails.csvData.replace(/\\n/g, '\\n'),
                units: chartDetails.units.join(',').replace(/\"/g, '')
            };
            const xAttribute = 'x-column="' + result.xName + '" ';
            const yAttribute = 'y-column="' + result.yName + '" ';
            const idAttribute = 'id="' + result.id + '" ';
            const unitsAttribute = 'column-units = "' + result.units + '" ';
            result.chart = '<chart ' + xAttribute + yAttribute + unitsAttribute + idAttribute + '>' + result.data + '</chart>';
            return result;
        }
    }
}

/**
 * Wrangle the provided feature data into more convenient forms.
 * @private
 * @param  {ReactClass} that The FeatureInfoSection.
 * @return {Object} Returns {info, rawData, showRawData, hasRawData, ...}.
 *                  info is the main body of the info section, as a react component.
 *                  rawData is the same for the raw data, if it needs to be shown.
 *                  showRawData is whether to show the rawData.
 *                  hasRawData is whether there is any rawData to show.
 *                  timeSeriesChart - if the feature has timeseries data that could be shown in chart, this is the chart.
 *                  downloadableData is the same as template data, but numerical.
 */
function getInfoAsReactComponent(that) {
    const templateData = that.getPropertyValues();
    const downloadableData = defined(templateData) ? (templateData._terria_numericalProperties || templateData) : undefined;
    const updateCounters = that.props.feature.updateCounters;
    const context = {
        catalogItem: that.props.catalogItem,
        feature: that.props.feature,
        updateCounters: updateCounters
    };
    let timeSeriesChart;
    let timeSeriesChartTitle;

    if (defined(templateData)) {
        const timeSeriesChartContext = getTimeSeriesChartContext(that.props.catalogItem, that.props.feature, templateData._terria_getChartDetails);
        if (defined(timeSeriesChartContext)) {
            timeSeriesChart = parseCustomMarkdownToReact(timeSeriesChartContext.chart, context);
            timeSeriesChartTitle = timeSeriesChartContext.title;
        }
    }
    const showRawData = !that.hasTemplate() || that.state.showRawData;
    let rawDataHtml;
    let rawData;
    if (showRawData) {
        rawDataHtml = that.descriptionFromFeature();
        if (defined(rawDataHtml)) {
            rawData = parseCustomMarkdownToReact(rawDataHtml, context);
        }
    }
    return {
        info: that.hasTemplate() ? parseCustomMarkdownToReact(that.descriptionFromTemplate(), context) : rawData,
        rawData: rawData,
        showRawData: showRawData,
        hasRawData: !!rawDataHtml,
        timeSeriesChartTitle: timeSeriesChartTitle,
        timeSeriesChart: timeSeriesChart,
        downloadableData: downloadableData
    };
}

function setTimeoutsForUpdatingCustomComponents(that) { // eslint-disable-line require-jsdoc
    const {info} = getInfoAsReactComponent(that);
    const foundCustomComponents = CustomComponents.find(info);
    foundCustomComponents.forEach((match, componentNumber) => {
        const updateSeconds = match.type.selfUpdateSeconds(match.reactComponent);
        if (updateSeconds > 0) {
            setTimeoutForUpdatingCustomComponent(that, match.reactComponent, updateSeconds, componentNumber);
        }
    });
}

function setTimeoutForUpdatingCustomComponent(that, reactComponent, updateSeconds, componentNumber) { // eslint-disable-line require-jsdoc
    const timeoutId = setTimeout(() => {
        // Update the counter for this component. Handle various undefined cases.
        const updateCounters = that.props.feature.updateCounters;
        const counterObject = {
            reactComponent: reactComponent,
            counter: (defined(updateCounters) && defined(updateCounters[componentNumber])) ? updateCounters[componentNumber].counter + 1 : 1
        };
        if (!defined(that.props.feature.updateCounters)) {
            const counters = {};
            counters[componentNumber] = counterObject;
            that.props.feature.updateCounters = counters;
        } else {
            that.props.feature.updateCounters[componentNumber] = counterObject;
        }
        // And finish by triggering the next timeout, but do this in another timeout so we aren't nesting setStates.
        setTimeout(() => {
            setTimeoutForUpdatingCustomComponent(that, reactComponent, updateSeconds, componentNumber);
            // console.log('Removing ' + timeoutId + ' from', that.state.timeoutIds);
            that.setState({timeoutIds: that.state.timeoutIds.filter(id => timeoutId !== id)});
        }, 5);
    }, updateSeconds * 1000);
    const timeoutIds = that.state.timeoutIds;
    that.setState({timeoutIds: timeoutIds.concat(timeoutId)});
}

// See if text contains the number (to a precision number of digits (after the dp) either fixed up or down on the last digit).
function contains(text, number, precision) {
    // Take Math.ceil or Math.floor and use it to calculate the number with a precision number of digits (after the dp).
    function fixed(round, number) {
        const scale = Math.pow(10, precision);
        return (round(number*scale)/scale).toFixed(precision);
    }
    return (text.indexOf(fixed(Math.floor, number)) !== -1) || (text.indexOf(fixed(Math.ceil, number)) !== -1);
}

/**
 * Add your own react components to have them rendered in a FeatureInfoSection. ViewState will be passed as a prop.
 */
FeatureInfoSection.extraComponents = [];

module.exports = FeatureInfoSection;
