/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 */
package groovy.swing.factory

import groovy.swing.SwingBuilder
import groovy.swing.binding.AbstractButtonProperties
import groovy.swing.binding.JComboBoxProperties
import groovy.swing.binding.JComponentProperties
import groovy.swing.binding.JListProperties
import groovy.swing.binding.JScrollBarProperties
import groovy.swing.binding.JSliderProperties
import groovy.swing.binding.JSpinnerProperties
import groovy.swing.binding.JTableProperties
import groovy.swing.binding.JTextComponentProperties
import org.codehaus.groovy.binding.AggregateBinding
import org.codehaus.groovy.binding.BindingUpdatable
import org.codehaus.groovy.binding.ClosureSourceBinding
import org.codehaus.groovy.binding.ClosureTriggerBinding
import org.codehaus.groovy.binding.EventTriggerBinding
import org.codehaus.groovy.binding.FullBinding
import org.codehaus.groovy.binding.MutualPropertyBinding
import org.codehaus.groovy.binding.PropertyBinding
import org.codehaus.groovy.binding.SourceBinding
import org.codehaus.groovy.binding.TargetBinding
import org.codehaus.groovy.binding.TriggerBinding

import java.util.Map.Entry

/**
 * @since Groovy 1.1
 */
class BindFactory extends AbstractFactory {

    public static final String CONTEXT_DATA_KEY = "BindFactoryData";

    final Map<String, TriggerBinding> syntheticBindings

    BindFactory() {
        syntheticBindings = new HashMap()

        // covers JTextField.text
        // covers JTextPane.text
        // covers JTextArea.text
        // covers JEditorPane.text
        syntheticBindings.putAll(JTextComponentProperties.syntheticProperties)

        // covers JCheckBox.selected
        // covers JCheckBoxMenuItem.selected
        // covers JRadioButton.selected
        // covers JRadioButtonMenuItem.selected
        // covers JToggleButton.selected
        syntheticBindings.putAll(AbstractButtonProperties.syntheticProperties)

        // covers JSlider.value
        syntheticBindings.putAll(JSliderProperties.syntheticProperties)

        // covers JScrollBar.value
        syntheticBindings.putAll(JScrollBarProperties.syntheticProperties)

        // JComboBox.elements / items
        // JComboBox.selectedElement / selectedItem
        syntheticBindings.putAll(JComboBoxProperties.syntheticProperties)

        // JList.selectedElement / selectedItem / selectedElements / selectedItems / selectedIndex
        syntheticBindings.putAll(JListProperties.syntheticProperties)

        // JSpinner.value
        syntheticBindings.putAll(JSpinnerProperties.syntheticProperties)

        // other properties handled in JSR-295
        // JTable.elements
        // JTable.selectedElement
        // JTable.selectedElements
        syntheticBindings.putAll(JTableProperties.syntheticProperties)

        // JTree.root
        // JTree.selectedElement
        // JTree.selectedElements

        // covers JComponent.size
        // covers JComponent.width
        // covers JComponent.height
        // covers JComponent.bounds
        // covers JComponent.x
        // covers JComponent.y
        // covers JComponent.visible
        syntheticBindings.putAll(JComponentProperties.syntheticProperties)

    }

    /**
     * Accepted Properties...
     *
     * group?
     * source ((sourceProperty) | (sourceEvent sourceValue))
     * (target targetProperty)? (? use default javabeans property if targetProperty is not present?)
     *
     * @param builder
     * @param name
     * @param value
     * @param attributes
     * @return the newly created instance
     * @throws InstantiationException
     * @throws IllegalAccessException
     */
    Object newInstance(FactoryBuilderSupport builder, Object name, Object value, Map attributes) throws InstantiationException, IllegalAccessException {
        Object source = attributes.remove("source")
        Object target = attributes.remove("target")
        Object update = attributes.get("update")
        Map bindContext = builder.context.get(CONTEXT_DATA_KEY) ?: [:]
        if (bindContext.isEmpty()) {
            builder.context.put(CONTEXT_DATA_KEY, bindContext)
        }

        TargetBinding tb = null
        if (target != null) {
            Object targetProperty = attributes.remove("targetProperty") ?: value
            if (!(targetProperty instanceof CharSequence)) {
                throw new IllegalArgumentException("Invalid value for targetProperty: (or node value)." +
                        " Value for this attribute must be a String but it is " + (targetProperty != null ? targetProperty.getClass().getName() : null))
            }
            tb = new PropertyBinding(target, targetProperty.toString(), update)
            if (source == null) {
                // if we have a target but no source assume the build context is the source and return
                def result
                if (attributes.remove("mutual")) {
                    result = new MutualPropertyBinding(null, null, tb, this.&getTriggerBinding)
                } else {
                    result = tb
                }
                def newAttributes = [:]
                newAttributes.putAll(attributes)
                bindContext.put(result, newAttributes)
                attributes.clear()
                return result
            }
        }

        FullBinding fb
        boolean sea = attributes.containsKey("sourceEvent")
        boolean sva = attributes.containsKey("sourceValue")
        boolean spa = attributes.containsKey("sourceProperty") || value

        if (sea && sva && !spa) {
            // entirely event triggered binding
            Closure queryValue = (Closure) attributes.remove("sourceValue")
            ClosureSourceBinding csb = new ClosureSourceBinding(queryValue)
            String trigger = (String) attributes.remove("sourceEvent")
            EventTriggerBinding etb = new EventTriggerBinding(source, trigger)
            fb = etb.createBinding(csb, tb)
        } else if (spa && !(sea && sva)) {
            // partially property driven binding
            Object property = attributes.remove("sourceProperty") ?: value
            if (!(property instanceof CharSequence)) {
                throw new IllegalArgumentException("Invalid value for sourceProperty: (or node value). " +
                        "Value for this attribute must be a String but it is " + (property != null ? property.getClass().getName() : null))
            }

            if (source == null) {
                // if we have a sourceProperty but no source then we're in trouble
                throw new IllegalArgumentException("Missing value for source: even though sourceProperty: (or node value) " +
                        "was specified. Please check you didn't write bind(model.someProperty) instead of bind{ model.someProperty }")
            }

            PropertyBinding pb = new PropertyBinding(source, property.toString(), update)

            TriggerBinding trigger
            if (sea) {
                // source trigger comes from an event
                String triggerName = (String) attributes.remove("sourceEvent")
                trigger = new EventTriggerBinding(source, triggerName)
            } else {
                // source trigger comes from a property change
                // this method will also check for synthetic properties
                trigger = getTriggerBinding(pb)
            }

            SourceBinding sb
            if (sva) {
                // source value comes from a value closure
                Closure queryValue = (Closure) attributes.remove("sourceValue")
                sb = new ClosureSourceBinding(queryValue)
            } else {
                // source value is the property value
                sb = pb
            }

            // check for a mutual binding (bi-directional)
            if (attributes.remove("mutual")) {
                fb = new MutualPropertyBinding(trigger, sb, tb, this.&getTriggerBinding)
            } else {
                fb = trigger.createBinding(sb, tb)
            }
        } else if (!(sea || sva || spa)) {
            // if no sourcing is defined then assume we are a closure binding and return
            def newAttributes = [:]
            newAttributes.putAll(attributes)
            def ctb = new ClosureTriggerBinding(syntheticBindings)
            bindContext.put(ctb, newAttributes)
            attributes.clear()
            return ctb
        } else {
            throw new RuntimeException("Both sourceEvent: and sourceValue: cannot be specified along with sourceProperty: or a value argument")
        }

        if (attributes.containsKey("value")) {
            bindContext.put(fb, [value: attributes.remove("value")])
        }

        bindContext.get(fb, [:]).put('update', update)

        Object o = attributes.remove("bind")
        if (((o == null) && !attributes.containsKey('group'))
                || ((o instanceof Boolean) && ((Boolean) o).booleanValue())) {
            fb.bind()
        }

        if ((attributes.group instanceof AggregateBinding) && (fb instanceof BindingUpdatable)) {
            attributes.remove('group').addBinding(fb)
        }

        builder.addDisposalClosure(fb.&unbind)
        return fb
    }

    void onNodeCompleted(FactoryBuilderSupport builder, Object parent, Object node) {
        super.onNodeCompleted(builder, parent, node);

        if (node instanceof FullBinding && node.sourceBinding && node.targetBinding) {
            try {
                node.update()
            } catch (Exception ignored) {
                // don't throw out to top
            }
            try {
                node.rebind()
            } catch (Exception ignored) {
                // don't throw out to top
            }
        }
    }

    boolean onHandleNodeAttributes(FactoryBuilderSupport builder, Object node, Map attributes) {
        attributes.remove('update')
        true
    }

    boolean isLeaf() {
        return false
    }

    boolean isHandlesNodeChildren() {
        return true
    }

    boolean onNodeChildren(FactoryBuilderSupport builder, Object node, Closure childContent) {
        if ((node instanceof FullBinding) && (node.converter == null)) {
            node.converter = childContent
            return false
        } else if (node instanceof ClosureTriggerBinding) {
            node.closure = childContent
            return false
        } else if (node instanceof TriggerBinding) {
            def bindAttrs = builder.context.get(CONTEXT_DATA_KEY)[node] ?: [:]
            if (!bindAttrs.containsKey("converter")) {
                bindAttrs["converter"] = childContent
                return false
            }
        }

        throw new RuntimeException("Binding nodes do not accept child content when a converter is already specified")
    }

    TriggerBinding getTriggerBinding(PropertyBinding psb) {
        String property = psb.propertyName
        Class currentClass = psb.bean.getClass()
        while (currentClass != null) {
            // should we check interfaces as well?  if so at what level?
            def trigger = (TriggerBinding) syntheticBindings.get("$currentClass.name#$property" as String)
            if (trigger != null) {
                return trigger
            }
            currentClass = currentClass.getSuperclass()
        }
        //TODO inspect the bean info and throw an error if the property is not observable and not bind:false?
        return psb
    }

    def bindingAttributeDelegate(FactoryBuilderSupport builder, def node, def attributes) {
        Iterator iter = attributes.entrySet().iterator()
        Map bindContext = builder.context.get(CONTEXT_DATA_KEY) ?: [:]

        while (iter.hasNext()) {
            Entry entry = (Entry) iter.next()
            String property = entry.key.toString()
            Object value = entry.value

            def bindAttrs = bindContext.get(value) ?: [:]
            def idAttr = builder.getAt(SwingBuilder.DELEGATE_PROPERTY_OBJECT_ID) ?: SwingBuilder.DEFAULT_DELEGATE_PROPERTY_OBJECT_ID
            def id = bindAttrs.remove(idAttr)
            if (bindAttrs.containsKey("value")) {
                node."$property" = bindAttrs.remove("value")
            }
            def update = bindAttrs.get('update')

            FullBinding fb
            if (value instanceof MutualPropertyBinding) {
                fb = (FullBinding) value
                PropertyBinding psb = new PropertyBinding(node, property, update)
                if (fb.sourceBinding == null) {
                    fb.sourceBinding = psb
                    finishContextualBinding(fb, builder, bindAttrs, id)
                } else if (fb.targetBinding == null) {
                    fb.targetBinding = psb
                }
            } else if (value instanceof FullBinding) {
                fb = (FullBinding) value
                fb.targetBinding = new PropertyBinding(node, property, update)
            } else if (value instanceof TargetBinding) {
                PropertyBinding psb = new PropertyBinding(node, property, update)
                fb = getTriggerBinding(psb).createBinding(psb, value)
                finishContextualBinding(fb, builder, bindAttrs, id)
            } else if (value instanceof ClosureTriggerBinding) {
                PropertyBinding psb = new PropertyBinding(node, property, update)
                fb = value.createBinding(value, psb)
                finishContextualBinding(fb, builder, bindAttrs, id)
            } else {
                continue
            }
            try {
                fb.update()
            } catch (Exception e) {
                // just eat it?
            }
            try {
                fb.rebind()
            } catch (Exception e) {
                // just eat it?
            }
            // this is why we cannot use entrySet().each { }
            iter.remove()
        }
    }

    private finishContextualBinding(FullBinding fb, FactoryBuilderSupport builder, bindAttrs, id) {
        bindAttrs.remove('update')
        Object bindValue = bindAttrs.remove("bind")
        List propertiesToBeSkipped = ['group']
        bindAttrs.each { k, v -> if (!(k in propertiesToBeSkipped)) fb."$k" = v }

        if ((bindAttrs.group instanceof AggregateBinding) && (fb instanceof BindingUpdatable)) {
            bindAttrs.group.addBinding(fb)
        }

        if ((bindValue == null)
                || ((bindValue instanceof Boolean) && ((Boolean) bindValue).booleanValue())) {
            fb.bind()
        }

        builder.addDisposalClosure(fb.&unbind)

        // replaces ourselves in the variables
        // id: is lost to us by now, so we just assume that any storage of us is a goner as well
        //builder.getVariables().each{ Map.Entry me -> if (value.is(me.value)) me.setValue fb}
        if (id) builder.setVariable(id, fb)
    }

}
