/*
 *  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 java.util.Map.Entry
import org.codehaus.groovy.binding.*
import groovy.swing.binding.JComponentProperties
import groovy.swing.binding.JSliderProperties
import groovy.swing.binding.JTextComponentProperties
import groovy.swing.binding.JTableProperties
import groovy.swing.binding.JListProperties
import groovy.swing.binding.AbstractButtonProperties
import groovy.swing.binding.JScrollBarProperties
import groovy.swing.binding.JComboBoxProperties
import groovy.swing.binding.JSpinnerProperties

/**
 * @author <a href="mailto:shemnon@yahoo.com">Danno Ferrin</a>
 * @since Groovy 1.1
 */
public class BindFactory extends AbstractFactory {

    public static final String CONTEXT_DATA_KEY = "BindFactoryData";

    final Map<String, TriggerBinding> syntheticBindings

    public 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
     */
    public 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
    }

    public 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
            }
        }
    }

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

    public boolean isLeaf() {
        return false;
    }

    public boolean isHandlesNodeChildren() {
        return true;
    }

    public 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")
    }

    public 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
    }

    public 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 def 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)
    }

}
