import { dispatch as d3_dispatch } from 'd3-dispatch';
import { select as d3_select } from 'd3-selection';

import { t, localizer } from '../core/localizer';
import { locationManager } from '../core/location_manager';
import { svgIcon } from '../svg/icon';
import { uiTooltip } from './tooltip';
import { geoExtent } from '../geo/extent';
import { uiFieldHelp } from './field_help';
import { uiFields } from './fields';
import { LANGUAGE_SUFFIX_REGEX } from './fields/localized';
import { uiTagReference } from './tag_reference';
import { utilRebind, utilUniqueDomId } from '../util';


export function uiField(context, presetField, entityIDs, options) {
    options = Object.assign({
        show: true,
        wrap: true,
        remove: true,
        revert: true,
        info: true
    }, options);

    var dispatch = d3_dispatch('change', 'revert');
    var field = Object.assign({}, presetField);   // shallow copy
    field.domId = utilUniqueDomId('form-field-' + field.safeid);
    var _show = options.show;
    var _state = '';
    var _tags = {};

    var _entityExtent;
    if (entityIDs && entityIDs.length) {
        _entityExtent = entityIDs.reduce(function(extent, entityID) {
            var entity = context.graph().entity(entityID);
            return extent.extend(entity.extent(context.graph()));
        }, geoExtent());
    }

    var _locked = false;
    var _lockedTip = uiTooltip()
        .title(() => t.append('inspector.lock.suggestion', { label: field.label() }))
        .placement('bottom');

    // only create the fields that are actually being shown
    if (_show && !field.impl) {
        createField();
    }

    // Creates the field.. This is done lazily,
    // once we know that the field will be shown.
    function createField() {
        field.impl = uiFields[field.type](field, context)
            .on('change', function(t, onInput) {
                dispatch.call('change', field, t, onInput);
            });

        if (entityIDs) {
            field.entityIDs = entityIDs;
            // if this field cares about the entities, pass them along
            if (field.impl.entityIDs) {
                field.impl.entityIDs(entityIDs);
            }
        }
    }


    function allKeys(tags) {
        return field.allKeys(tags);
    }


    function isModified() {
        if (!entityIDs || !entityIDs.length) return false;
        return entityIDs.some(function(entityID) {
            var original = context.graph().base().entities[entityID];
            var latest = context.graph().entity(entityID);
            if (original) {
                const originalKeys = allKeys(original.tags);
                const latestKeys = allKeys(_tags);
                return latestKeys.concat(originalKeys).some(function (key) {
                    return latest.tags[key] !== original.tags[key];
                });
            } else {
                return allKeys(_tags).some(function (key) {
                    return !!latest.tags[key];
                });
            }
        });
    }


    function tagsContainFieldKey() {
        return allKeys(_tags).some(function(key) {
            if (field.type === 'multiCombo') {
                for (var tagKey in _tags) {
                    if (tagKey.indexOf(key) === 0) {
                        return true;
                    }
                }
                return false;
            }
            if (field.type === 'localized') {
                for (let tagKey in _tags) {
                    // matches for field:<code>, where <code> is a BCP 47 locale code
                    let match = tagKey.match(LANGUAGE_SUFFIX_REGEX);
                    if (match && match[1] === field.key && match[2]) {
                        return true;
                    }
                }
            }
            return _tags[key] !== undefined;
        });
    }


    function revert(d3_event, d) {
        d3_event.stopPropagation();
        d3_event.preventDefault();
        if (!entityIDs || _locked) return;

        let keys = allKeys(_tags);

        if (entityIDs) {
            for (let i = 0; i < entityIDs.length; i++) {
                const entityID = entityIDs[i];
                const original = context.graph().base().entities[entityID];
                if (original) {
                    allKeys(original.tags).forEach(function(key) {
                        if (keys.indexOf(key) === -1) {
                            keys.push(key);
                        }
                    });
                }
            }
        }

        dispatch.call('revert', d, keys);
    }


    function remove(d3_event, d) {
        d3_event.stopPropagation();
        d3_event.preventDefault();
        if (_locked) return;

        var t = {};
        allKeys(_tags).forEach(function(key) {
            t[key] = undefined;
        });

        dispatch.call('change', d, t);
    }


    field.render = function(selection) {
        var container = selection.selectAll('.form-field')
            .data([field]);

        // Enter
        var enter = container.enter()
            .append('div')
            .attr('class', function(d) { return 'form-field form-field-' + d.safeid; })
            .classed('nowrap', !options.wrap);

        if (options.wrap) {
            var labelEnter = enter
                .append('label')
                .attr('class', 'field-label')
                .attr('for', function(d) { return d.domId; });

            var textEnter = labelEnter
                .append('span')
                .attr('class', 'label-text');

            textEnter
                .append('span')
                .attr('class', 'label-textvalue')
                .each(function(d) { d.label()(d3_select(this)); });

            textEnter
                .append('span')
                .attr('class', 'label-textannotation');

            if (options.remove) {
                labelEnter
                    .append('button')
                    .attr('class', 'remove-icon')
                    .attr('title', t('icons.remove'))
                    .call(svgIcon('#iD-operation-delete'));
            }

            if (options.revert) {
                labelEnter
                    .append('button')
                    .attr('class', 'modified-icon')
                    .attr('title', t('icons.undo'))
                    .call(svgIcon((localizer.textDirection() === 'rtl') ? '#iD-icon-redo' : '#iD-icon-undo'));
            }
        }


        // Update
        container = container
            .merge(enter);

        container.select('.field-label > .remove-icon')  // propagate bound data
            .on('click', remove);

        container.select('.field-label > .modified-icon')  // propagate bound data
            .on('click', revert);

        container
            .each(function(d) {
                var selection = d3_select(this);

                if (!d.impl) {
                    createField();
                }

                var reference, help;

                // instantiate field help
                if (options.wrap && field.type === 'restrictions') {
                    help = uiFieldHelp(context, 'restrictions');
                }

                // instantiate tag reference
                if (options.wrap && options.info) {
                    var referenceKey = d.key || '';
                    if (d.type === 'multiCombo') {   // lookup key without the trailing ':'
                        referenceKey = referenceKey.replace(/:$/, ':*');
                    }

                    var referenceOptions = d.reference || {
                        key: referenceKey,
                        value: _tags[referenceKey]
                    };
                    reference = uiTagReference(referenceOptions, context);
                    if (_state === 'hover') {
                        reference.showing(false);
                    }
                }

                selection
                    .call(d.impl);

                // add field help components
                if (help) {
                    selection
                        .call(help.body)
                        .select('.field-label')
                        .call(help.button);
                }

                // add tag reference components
                if (reference) {
                    selection
                        .call(reference.body)
                        .select('.field-label')
                        .call(reference.button);
                }

                d.impl.tags(_tags);
            });


            container
                .classed('locked', _locked)
                .classed('modified', isModified())
                .classed('present', tagsContainFieldKey());


            // show a tip and lock icon if the field is locked
            var annotation = container.selectAll('.field-label .label-textannotation');
            var icon = annotation.selectAll('.icon')
                .data(_locked ? [0]: []);

            icon.exit()
                .remove();

            icon.enter()
                .append('svg')
                .attr('class', 'icon')
                .append('use')
                .attr('xlink:href', '#fas-lock');

            container.call(_locked ? _lockedTip : _lockedTip.destroy);
    };


    field.state = function(val) {
        if (!arguments.length) return _state;
        _state = val;
        return field;
    };


    field.tags = function(val) {
        if (!arguments.length) return _tags;
        _tags = val;

        if (tagsContainFieldKey() && !_show) {
            // always show a field if it has a value to display
            _show = true;
            if (!field.impl) {
                createField();
            }
        }

        return field;
    };


    field.locked = function(val) {
        if (!arguments.length) return _locked;
        _locked = val;
        return field;
    };


    field.show = function() {
        _show = true;
        if (!field.impl) {
            createField();
        }
        if (field.default && field.key && _tags[field.key] !== field.default) {
            var t = {};
            t[field.key] = field.default;
            dispatch.call('change', this, t);
        }
    };

    // A shown field has a visible UI, a non-shown field is in the 'Add field' dropdown
    field.isShown = function() {
        return _show;
    };


    // An allowed field can appear in the UI or in the 'Add field' dropdown.
    // A non-allowed field is hidden from the user altogether
    field.isAllowed = function() {

        if (entityIDs &&
            entityIDs.length > 1 &&
            uiFields[field.type].supportsMultiselection === false) return false;

        if (field.geometry && !entityIDs.every(function(entityID) {
            return field.matchGeometry(context.graph().geometry(entityID));
        })) return false;

        if (entityIDs && _entityExtent && field.locationSetID) {   // is field allowed in this location?
            var validHere = locationManager.locationSetsAt(_entityExtent.center());
            if (!validHere.has(field.locationSetID)) return false;
        }

        var prerequisiteTag = field.prerequisiteTag;

        if (entityIDs &&
            !tagsContainFieldKey() && // ignore tagging prerequisites if a value is already present
            prerequisiteTag) {

            if (!entityIDs.every(function(entityID) {
                var entity = context.graph().entity(entityID);
                if (prerequisiteTag.key) {
                    var value = entity.tags[prerequisiteTag.key] || '';

                    if (prerequisiteTag.valuesNot) {
                        return !prerequisiteTag.valuesNot.includes(value);
                    }
                    if (prerequisiteTag.valueNot) {
                        return prerequisiteTag.valueNot !== value;
                    }
                    if (prerequisiteTag.values) {
                        return prerequisiteTag.values.includes(value);
                    }
                    if (prerequisiteTag.value) {
                        return prerequisiteTag.value === value;
                    }
                    if (!value) return false;
                } else if (prerequisiteTag.keyNot) {
                    if (entity.tags[prerequisiteTag.keyNot]) return false;
                }
                return true;
            })) return false;
        }

        return true;
    };


    field.focus = function() {
        if (field.impl) {
            field.impl.focus();
        }
    };


    return utilRebind(field, dispatch, 'on');
}
