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

import { presetManager } from '../../presets';
import { localizer, t } from '../../core/localizer';
import { uiField } from '../field';
import { utilArrayUnion, utilRebind } from '../../util';
import { formatTag } from './tag_title';


export { uiFieldRadio as uiFieldStructureRadio };


export function uiFieldRadio(field, context) {
    var dispatch = d3_dispatch('change');
    var placeholder = d3_select(null);
    var wrap = d3_select(null);
    var labels = d3_select(null);
    var radios = d3_select(null);
    var typeField;
    var layerField;
    let _tags = {};
    var _oldType = {};
    var _entityIDs = [];


    function selectedKey() {
        var node = wrap.selectAll('.form-field-input-radio label.active input');
        return !node.empty() && node.datum();
    }


    function radio(selection) {
        selection.classed('preset-radio', true);

        wrap = selection.selectAll('.form-field-input-wrap')
            .data([0]);

        const enter = wrap.enter()
            .append('div')
            .attr('class', 'form-field-input-wrap form-field-input-radio');

        enter
            .append('span')
            .attr('class', 'placeholder');

        wrap = wrap
            .merge(enter);


        placeholder = wrap.selectAll('.placeholder');

        updateRadios();


    }


    function updateRadios() {
        const allOptions = [...(field.options || field.keys)];
        if (_tags[field.key] && !allOptions.includes(_tags[field.key])) {
            // include other values as (temporary) option #12082
            //allOptions.push(`"${_tags[field.key]}"`);
            allOptions.push(_tags[field.key]);
        }

        labels = wrap.selectAll('label')
            .data(allOptions, d => d);

        labels.exit().remove();
        const enter = labels.enter()
            .append('label');

        enter
            .append('input')
            .attr('type', 'radio')
            .attr('name', field.id)
            .attr('value', (d) => field.t('options.' + d, { 'default': d }))
            .attr('checked', false);

        enter
            .append('span')
            .each(function(d) {
                const labelId = field.hasTextForStringId('options.' + d + '.title')
                    ? 'options.' + d + '.title'
                    : 'options.' + d;
                field.t.append(labelId, {
                    'default': selection => selection
                        .classed('raw-value', true)
                        .text(`"${d}"`)
                })(d3_select(this));
            });

        labels = labels
            .merge(enter);

        radios = labels.selectAll('input')
            .on('change', changeRadio);


        function isOptionChecked(d) {
            if (field.key) {
                return _tags[field.key] === d;
            }
            return !!(typeof _tags[d] === 'string' && _tags[d].toLowerCase() !== 'no');
        }

        radios.property('checked', function(d) {
            return isOptionChecked(d) &&
                (field.key || allOptions.filter(isOptionChecked).length === 1);
        });
    }


    function updateLayout() {
        const wrapNode = wrap.node();
        if (!wrapNode || labels.empty()) return;

        wrap.classed('one-line', false);
        wrap.classed('two-column', false);

        // Temporarily measure each label at its natural content width (no grow/shrink)
        // by applying inline styles, forcing a layout read, then removing them.
        const labelNodes = labels.nodes();
        labelNodes.forEach(node => {
            node._originalStyle = node.style;
            node.style.flex = '0 0 auto';
            node.style.width = 'auto';
        });

        const containerWidth = wrapNode.getBoundingClientRect().width;
        const labelWidths = labelNodes.map(node => node.getBoundingClientRect().width);
        const sumLabelWidth = labelWidths.reduce((a, b) => a + b);
        const maxLabelWidth = Math.max(...labelWidths);
        if (labelWidths.length % 2 === 1) {
            // for odd number of entries, we can skip the last entry as there is the
            // full width available
            labelWidths.pop();
        }
        const maxLabelWidthTwoColumns = Math.max(...labelWidths);

        labelNodes.forEach(node => {
            node.style = node._originalStyle;
            delete node._originalStyle;
        });

        // All labels fit on one equal-width line without truncation when the widest
        // label's natural width fits within each cell's equal share of the container.
        wrap.classed('one-line', sumLabelWidth <= containerWidth);
        wrap.classed('equal-spacing', maxLabelWidth * labelNodes.length <= containerWidth);
        // Otherwise, if all labels fit on half width of the container -> we can use
        // a more compact two column layout
        wrap.classed('two-column',
            sumLabelWidth > containerWidth // not if already one-line layout
            && maxLabelWidthTwoColumns <= Math.ceil(containerWidth / 2) // has to fit in half-width column
            && labelNodes.length > 3 // skip if only 3 or fewer options -> looks unbalanced
        );
    }


    function structureExtras(selection, tags) {
        var selected = selectedKey() || tags.layer !== undefined;
        var type = presetManager.field(selected);
        var layer = presetManager.field('layer');
        var showLayer = (selected === 'bridge' || selected === 'tunnel' || tags.layer !== undefined);


        var extrasWrap = selection.selectAll('.structure-extras-wrap')
            .data(selected ? [0] : []);

        extrasWrap.exit()
            .remove();

        extrasWrap = extrasWrap.enter()
            .append('div')
            .attr('class', 'structure-extras-wrap')
            .merge(extrasWrap);

        var list = extrasWrap.selectAll('ul')
            .data([0]);

        list = list.enter()
            .append('ul')
            .attr('class', 'rows')
            .merge(list);


        // Type
        if (type) {
            if (!typeField || typeField.id !== selected) {
                typeField = uiField(context, type, _entityIDs, { wrap: false })
                    .on('change', changeType);
            }
            typeField.tags(tags);
        } else {
            typeField = null;
        }

        var typeItem = list.selectAll('.structure-type-item')
            .data(typeField ? [typeField] : [], function(d) { return d.id; });

        // Exit
        typeItem.exit()
            .remove();

        // Enter
        var typeEnter = typeItem.enter()
            .insert('li', ':first-child')
            .attr('class', 'labeled-input structure-type-item');

        typeEnter
            .append('div')
            .attr('class', 'label structure-label-type')
            .attr('for', 'preset-input-' + selected)
            .call(t.append('inspector.radio.structure.type'));

        typeEnter
            .append('div')
            .attr('class', 'structure-input-type-wrap');

        // Update
        typeItem = typeItem
            .merge(typeEnter);

        if (typeField) {
            typeItem.selectAll('.structure-input-type-wrap')
                .call(typeField.render);
        }


        // Layer
        if (layer && showLayer) {
            if (!layerField) {
                layerField = uiField(context, layer, _entityIDs, { wrap: false })
                    .on('change', changeLayer);
            }
            layerField.tags(tags);
            field.keys = utilArrayUnion(field.keys, ['layer']);
        } else {
            layerField = null;
            field.keys = field.keys.filter(function(k) { return k !== 'layer'; });
        }

        var layerItem = list.selectAll('.structure-layer-item')
            .data(layerField ? [layerField] : []);

        // Exit
        layerItem.exit()
            .remove();

        // Enter
        var layerEnter = layerItem.enter()
            .append('li')
            .attr('class', 'labeled-input structure-layer-item');

        layerEnter
            .append('div')
            .attr('class', 'label structure-label-layer')
            .attr('for', 'preset-input-layer')
            .call(t.append('inspector.radio.structure.layer'));

        layerEnter
            .append('div')
            .attr('class', 'structure-input-layer-wrap');

        // Update
        layerItem = layerItem
            .merge(layerEnter);

        if (layerField) {
            layerItem.selectAll('.structure-input-layer-wrap')
                .call(layerField.render);
        }
    }


    function changeType(t, onInput) {
        var key = selectedKey();
        if (!key) return;

        var val = t[key];
        if (val !== 'no') {
            _oldType[key] = val;
        }

        if (field.type === 'structureRadio') {
            // remove layer if it should not be set
            if (val === 'no' ||
                (key !== 'bridge' && key !== 'tunnel') ||
                (key === 'tunnel' && val === 'building_passage')) {
                t.layer = undefined;
            }
            // add layer if it should be set
            if (t.layer === undefined) {
                if (key === 'bridge' && val !== 'no') {
                    t.layer = '1';
                }
                if (key === 'tunnel' && val !== 'no' && val !== 'building_passage') {
                    t.layer = '-1';
                }
            }
         }

        dispatch.call('change', this, t, onInput);
    }


    function changeLayer(t, onInput) {
        dispatch.call('change', this, t, onInput);
    }


    function changeRadio() {
        var t = {};
        var activeKey;

        if (field.key) {
            t[field.key] = undefined;
        }

        radios.each(function(d) {
            var active = d3_select(this).property('checked');
            if (active) activeKey = d;

            if (field.key) {
                if (active) t[field.key] = d;
            } else {
                var val = _oldType[activeKey] || 'yes';
                t[d] = active ? val : undefined;
            }
        });

        if (field.type === 'structureRadio') {
            if (activeKey === 'bridge') {
                // if there already is an a layer tag, respect it if it's >0
                const hasExistingLayer = !Number.isNaN(+_tags.layer) && +_tags.layer > 0;

                t.layer = hasExistingLayer ? _tags.layer : '1';
            } else if (activeKey === 'tunnel' && t.tunnel !== 'building_passage') {
                // if there already is an a layer tag, respect it if it's <0
                const hasExistingLayer = !Number.isNaN(+_tags.layer) && +_tags.layer < 0;

                t.layer = hasExistingLayer ? _tags.layer : '-1';
            } else {
                t.layer = undefined;
            }
        }

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


    radio.tags = function(tags) {
        _tags = tags;

        function isMixed(d) {
            if (field.key) {
                return Array.isArray(tags[field.key]) && tags[field.key].includes(d);
            }
            return Array.isArray(tags[d]);
        }

        updateRadios();

        labels
            .classed('active', function(d) {
                if (field.key) {
                    return (Array.isArray(tags[field.key]) && tags[field.key].includes(d))
                        || tags[field.key] === d;
                }
                return Array.isArray(tags[d]) && tags[d].some(v => typeof v === 'string' && v.toLowerCase() !== 'no') ||
                    !!(typeof tags[d] === 'string' && tags[d].toLowerCase() !== 'no');
            })
            .classed('mixed', isMixed)
            .attr('title', function(d) {
                if (isMixed(d)) return t('inspector.unshared_value_tooltip');
                if (!field.key) return null;
                const desc = localizer.hasTextForStringId('options.' + d + '.description')
                    ? t('options.' + d + '.description') : undefined;
                const tag = formatTag(field.key, d);
                return desc ? `${desc}\n${tag}` : tag;
            });


        var selection = radios.filter(function() { return this.checked; });

        if (selection.empty()) {
            placeholder.text('');
            placeholder.call(t.append('inspector.none'));
        } else {
            placeholder.text(selection.attr('value'));
            _oldType[selection.datum()] = tags[selection.datum()];
        }

        if (field.type === 'structureRadio') {
            if (!!tags.waterway && !_oldType.tunnel) {
                // default waterway tunnels to 'culvert'
                _oldType.tunnel = 'culvert';
            }
            if (!!tags.waterway && !_oldType.bridge) {
                // default waterway bridges to 'aqueduct'
                _oldType.bridge = 'aqueduct';
            }

            wrap.call(structureExtras, tags);
        }

        updateLayout();
    };


    radio.focus = function() {
        radios.node().focus();
    };


    radio.entityIDs = function(val) {
        if (!arguments.length) return _entityIDs;
        _entityIDs = val;
        _oldType = {};
        return radio;
    };


    radio.isAllowed = function() {
        return _entityIDs.length === 1;
    };


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