/*
 * Copyright 2011 JBoss Inc 
 *
 * Licensed 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 org.jbpm.formapi.client.form;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.jbpm.formapi.client.CommonGlobals;
import org.jbpm.formapi.client.FormBuilderException;
import org.jbpm.formapi.client.bus.FormItemSelectionEvent;
import org.jbpm.formapi.client.effect.FBFormEffect;
import org.jbpm.formapi.client.menu.EffectsPopupPanel;
import org.jbpm.formapi.client.validation.FBValidationItem;
import org.jbpm.formapi.common.handler.ControlKeyHandler;
import org.jbpm.formapi.common.handler.EventHelper;
import org.jbpm.formapi.common.handler.RightClickEvent;
import org.jbpm.formapi.common.handler.RightClickHandler;
import org.jbpm.formapi.common.reflect.ReflectionHelper;
import org.jbpm.formapi.shared.api.ExternalData;
import org.jbpm.formapi.shared.api.FBScript;
import org.jbpm.formapi.shared.api.FBValidation;
import org.jbpm.formapi.shared.api.FormItemRepresentation;
import org.jbpm.formapi.shared.api.InputData;
import org.jbpm.formapi.shared.api.OutputData;

import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.event.dom.client.BlurEvent;
import com.google.gwt.event.dom.client.BlurHandler;
import com.google.gwt.event.dom.client.FocusEvent;
import com.google.gwt.event.dom.client.FocusHandler;
import com.google.gwt.event.shared.EventBus;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.FocusPanel;
import com.google.gwt.user.client.ui.Widget;

/**
 * Base class for UI components. Contains most of the edition definitions: right
 * click functionality, inplace editor invocation, desired positioning, width,
 * height, validations, input association and output association.
 */
public abstract class FBFormItem extends FocusPanel {

    private List<FBValidationItem> validations = new ArrayList<FBValidationItem>();
    private Map<String, FBScript> eventActions = new HashMap<String, FBScript>();
    private List<FBFormEffect> effects = new ArrayList<FBFormEffect>();

    private int desiredX;
    private int desiredY;

    private String widgetWidth;
    private String widgetHeight;

    private boolean alreadyEditing = false;
    private Widget auxiliarWidget = null;

    private InputData input = null;
    private OutputData output = null;
    private ExternalData external = null;

    public FBFormItem(List<FBFormEffect> formEffects) {
        this.effects.addAll(formEffects);
        addStyleName("fbFormItemThinBorder");
        EventHelper.addRightClickHandler(this, new RightClickHandler() {
            @Override
            public void onRightClick(RightClickEvent event) {
                EffectsPopupPanel popupPanel = new EffectsPopupPanel(
                        FBFormItem.this, true);
                if (getFormEffects() != null && !getFormEffects().isEmpty()) {
                    popupPanel.setPopupPosition(event.getX(), event.getY());
                    popupPanel.show();
                }
            }
        });
        EventHelper.addKeyboardCopyHandler(this, new ControlKeyHandler() {
            @Override
            public void onKeyboardControl() {
                CommonGlobals.getInstance().copy().append(FBFormItem.this)
                        .execute();
            }
        });
        EventHelper.addKeyboardCutHandler(this, new ControlKeyHandler() {
            @Override
            public void onKeyboardControl() {
                CommonGlobals.getInstance().cut().append(FBFormItem.this)
                        .execute();
            }
        });
        EventHelper.addKeyboardPasteHandler(this, new ControlKeyHandler() {
            @Override
            public void onKeyboardControl() {
                CommonGlobals.getInstance().paste().append(FBFormItem.this)
                        .execute();
            }
        });
        EventHelper.addBlurHandler(this, new BlurHandler() {
            @Override
            public void onBlur(BlurEvent event) {
                reset();
            }
        });
        EventHelper.addFocusHandler(this, new FocusHandler() {
            @Override
            public void onFocus(FocusEvent event) {
                makeEditor();
            }
        });
    }

    private void makeEditor() {
        if (!getFormItemPropertiesMap().isEmpty() && !isAlreadyEditing()) {
            fireSelectionEvent(new FormItemSelectionEvent(this, true));
        }
        FBInplaceEditor inplaceEditor = createInplaceEditor();
        if (inplaceEditor != null && !isAlreadyEditing()) {
            auxiliarWidget = getWidget();
            clear();
            setWidget(inplaceEditor);
            setAlreadyEditing(true);
            inplaceEditor.focus();
        }
    }

    public boolean isAlreadyEditing() {
        return alreadyEditing;
    }

    public void setAlreadyEditing(boolean alreadyEditing) {
        this.alreadyEditing = alreadyEditing;
    }

    public void reset() {
        if (auxiliarWidget != null && !getEditor().isFocused()) {
            clear();
            add(auxiliarWidget);
            setAlreadyEditing(false);
            fireSelectionEvent(new FormItemSelectionEvent(this, false));
        }
    }

    private FBInplaceEditor getEditor() {
        return (FBInplaceEditor) getWidget();
    }

    public final void fireSelectionEvent(FormItemSelectionEvent event) {
        EventBus bus = CommonGlobals.getInstance().getEventBus();
        bus.fireEvent(event);
    }

    @Override
    public void onBrowserEvent(Event event) {
        EventHelper.onBrowserEvent(this, event);
    }

    public void addEffect(FBFormEffect effect) {
        if (!effects.contains(effect)) {
            effects.add(effect);
        }
    }

    public void removeEffect(FBFormEffect effect) {
        if (effects.contains(effect)) {
            effects.remove(effect);
        }
    }

    protected Integer extractInt(Object obj) {
        String s = extractString(obj);
        return s.equals("") ? null : Integer.valueOf(s);
    }

    protected Boolean extractBoolean(Object obj) {
        if (obj != null && obj instanceof Boolean) {
            return (Boolean) obj;
        }
        String s = extractString(obj);
        return s.equals("") ? Boolean.FALSE : Boolean.valueOf(s);
    }

    protected String extractString(Object obj) {
        return obj == null ? "" : obj.toString();
    }

    protected Double extractDouble(Object obj) {
        String s = extractString(obj);
        return s.equals("") ? null : Double.valueOf(s);
    }

    public List<FBFormEffect> getFormEffects() {
        return this.effects;
    }

    public int getDesiredX() {
        return desiredX;
    }

    public void setDesiredX(int desiredX) {
        this.desiredX = desiredX;
    }

    public int getDesiredY() {
        return desiredY;
    }

    public void setDesiredY(int desiredY) {
        this.desiredY = desiredY;
    }

    public void setDesiredPosition(int desiredX, int desiredY) {
        this.desiredX = desiredX;
        this.desiredY = desiredY;
    }

    public String getHeight() {
        return widgetHeight;
    }

    public String getWidth() {
        return widgetWidth;
    }

    @Override
    public void setWidth(String width) {
        if (width != null) {
            super.setWidth(width);
            this.widgetWidth = width;
        }
    }

    @Override
    public void setHeight(String height) {
        if (height != null) {
            super.setHeight(height);
            this.widgetHeight = height;
        }
    }

    public void setInput(InputData input) {
        this.input = input;
    }

    public void setOutput(OutputData output) {
        this.output = output;
    }

    public void setExternal(ExternalData external) {
        this.external = external;
    }

    public OutputData getOutput() {
        return output;
    }

    public InputData getInput() {
        return input;
    }

    public ExternalData getExternal() {
        return external;
    }

    protected void setWidgetHeight(String widgetHeight) {
        this.widgetHeight = widgetHeight;
    }

    protected void setWidgetWidth(String widgetWidth) {
        this.widgetWidth = widgetWidth;
    }

    protected void setEffects(List<FBFormEffect> effects) {
        this.effects = effects;
    }

    protected <T extends FBFormItem> T cloneItem(T clone) {
        clone.setValidations(this.validations);
        clone.setWidgetHeight(this.widgetHeight);
        clone.setWidgetWidth(this.widgetWidth);
        clone.setEffects(this.effects);
        clone.setInput(this.input);
        clone.setOutput(this.output);
        return clone;
    }

    protected <T extends FormItemRepresentation> T getRepresentation(T rep) {
        rep.setInput(getInput());
        rep.setOutput(getOutput());
        rep.setHeight(getHeight());
        rep.setWidth(getWidth());
        List<FBValidation> repValidations = new ArrayList<FBValidation>();
        for (FBValidationItem item : getValidations()) {
            repValidations.add(item.createValidation());
        }
        rep.setItemValidations(repValidations);
        for (FBFormEffect effect : getFormEffects()) {
            rep.addEffectClass(effect.getClass());
        }
        rep.setEventActions(getEventActions());
        rep.setExternal(getExternal());
        return rep;
    }

    public static FBFormItem createItem(FormItemRepresentation rep)
            throws FormBuilderException {
        if (rep == null) {
            return null;
        }
        String className = rep.getItemClassName();
        try {
            FBFormItem item = (FBFormItem) ReflectionHelper
                    .newInstance(className);
            item.populate(rep);
            return item;
        } catch (Exception e) {
            throw new FormBuilderException("Couldn't instantiate class "
                    + className, e);
        }
    }

    public void setValidations(List<FBValidationItem> validations) {
        if (validations == null) {
            validations = new ArrayList<FBValidationItem>();
        }
        this.validations = validations;
    }

    public List<FBValidationItem> getValidations() {
        return validations;
    }

    public void setEventActions(Map<String, FBScript> eventActions) {
        if (eventActions == null) {
            eventActions = new HashMap<String, FBScript>();
        }
        this.eventActions = eventActions;
    }

    public Map<String, FBScript> getEventActions() {
        return eventActions;
    }

    /**
     * If you wish that on clicking your UI component, it becomes replaced by a
     * custom editor, this is where you must create it
     * 
     * @return A custom subclass of {@link FBInplaceEditor} to replace component
     *         and be rechanged after lost of focus. Default returns null
     */
    public FBInplaceEditor createInplaceEditor() {
        return null;
    }

    /**
     * This method must be defined to tell outside default editors what
     * properties this UI component has. Outside editors will then provide
     * functionality to edit these properties and invoke
     * {@link #saveValues(Map)}
     * 
     * @return a map of the properties of this UI component
     */
    public abstract Map<String, Object> getFormItemPropertiesMap();

    /**
     * This method must be defined so that outside default editor can tell this
     * UI component the new value of its properties. It's the entire
     * responsibility of this UI component to repopulate itself from these
     * properties
     * 
     * @param asPropertiesMap
     *            a map of the proeprties to set on this UI component
     */
    public abstract void saveValues(Map<String, Object> asPropertiesMap);

    /**
     * This method is used to create a POJO representation of the UI component
     * that any java service can understand.
     * 
     * @return a POJO representation of this UI component
     */
    public abstract FormItemRepresentation getRepresentation();

    /**
     * This method must be overriden by each {@link FBFormItem} subclass to
     * repopulate its properties from an outside POJO representation.
     * 
     * @param rep
     *            the POJO representation of this UI component. It's the
     *            responsibility of each {@link FBFormItem} instance to validate
     *            the POJO representation for itself, call the superclass
     *            method, and define what and how properties of its UI component
     *            should be updated.
     * @throws FormBuilderException
     *             in case of error or invalid content
     */
    public void populate(FormItemRepresentation rep)
            throws FormBuilderException {
        if (rep.getEffectClasses() != null) {
            this.effects = new ArrayList<FBFormEffect>(rep.getEffectClasses()
                    .size());
            for (String className : rep.getEffectClasses()) {
                try {
                    FBFormEffect effect = (FBFormEffect) ReflectionHelper
                            .newInstance(className);
                    this.effects.add(effect);
                } catch (Exception e) {
                    throw new FormBuilderException(
                            "Couldn't instantiate class " + className, e);
                }
            }
        }
        if (rep.getEventActions() != null) {
            for (String key : eventActions.keySet()) {
                eventActions.put(key, null);
            }
            this.eventActions.putAll(rep.getEventActions());
        }
        this.validations.clear();
        if (rep.getItemValidations() != null) {
            for (FBValidation validation : rep.getItemValidations()) {
                FBValidationItem validationItem = FBValidationItem
                        .createValidation(validation);
                this.validations.add(validationItem);
            }
        }
        setHeight(rep.getHeight());
        setWidth(rep.getWidth());
        this.input = rep.getInput();
        this.output = rep.getOutput();
        this.external = rep.getExternal();
        this.eventActions = rep.getEventActions();
    }

    /**
     * This methods is similar to {@link #clone()}, but returns a proper type
     * and forces implementation
     * 
     * @return a clone of this very object
     */
    public abstract FBFormItem cloneItem();

    /**
     * Similar to {@link #cloneItem()}, but only clones the underlying UI GWT
     * component.
     * 
     * @return
     */
    public abstract Widget cloneDisplay(Map<String, Object> formData);

    protected void populateActions(Element element) {
        for (Map.Entry<String, FBScript> entry : getEventActions().entrySet()) {
            element.setPropertyJSO(entry.getKey(), toJsFunction(entry
                    .getValue().getContent()));
        }
    }

    protected Object getInputValue(Map<String, Object> data) {
        if (getInput() != null && getInput().getName() != null) {
            if (data != null && data.containsKey(getInput().getName())) {
                return data.get(getInput().getName());
            }
        }
        return null;
    }

    private native JavaScriptObject toJsFunction(String value) /*-{
        var r = function() {
            eval(value);
        }
        return r;
    }-*/;

    public boolean removeEffectOfType(Class<? extends FBFormEffect> effectClass) {
        FBFormEffect effectToRemove = null;
        if (getFormEffects() != null) {
            for (FBFormEffect effect : getFormEffects()) {
                if (effect.getClass().getName().equals(effectClass.getName())) {
                    effectToRemove = effect;
                    break;
                }
            }
        }
        if (effectToRemove != null) {
            return effects.remove(effectToRemove);
        }
        return false;
    }

    public boolean hasEffectOfType(Class<? extends FBFormEffect> effectClass) {
        if (getFormEffects() != null) {
            for (FBFormEffect effect : getFormEffects()) {
                if (effect.getClass().getName().equals(effectClass.getName())) {
                    return true;
                }
            }
        }
        return false;
    }
}
