/*
 * Copyright 2010 Traction Software, 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 com.tractionsoftware.gwt.user.client.ui;


import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.event.dom.client.HasKeyDownHandlers;
import com.google.gwt.event.dom.client.HasKeyUpHandlers;
import com.google.gwt.event.dom.client.KeyCodeEvent;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyDownEvent;
import com.google.gwt.event.dom.client.KeyDownHandler;
import com.google.gwt.event.dom.client.KeyUpEvent;
import com.google.gwt.event.dom.client.KeyUpHandler;
import com.google.gwt.event.logical.shared.HasValueChangeHandlers;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.Focusable;
import com.google.gwt.user.client.ui.HasText;
import com.google.gwt.user.client.ui.HasValue;
import com.google.gwt.user.client.ui.Widget;
import com.tractionsoftware.gwt.user.client.util.MiscUtils;

/**
 * This attaches to an input, listening for KeyDown/KeyUp, and
 * automatically resizing the text area. It does this using a shadow
 * control that matches the text of the input.
 */
public abstract class AutoSizingBase<T extends Widget & HasTextSelection & HasValue<String> & HasValueChangeHandlers<String> & HasKeyDownHandlers & HasKeyUpHandlers & Focusable & HasText, S extends Widget> extends Composite 
    implements KeyDownHandler, KeyUpHandler, ValueChangeHandler<String>,
	       // for PillList
	       Focusable, HasText {

    public static final int DEFAULT_MAX = 10000;
    public static final int DEFAULT_MIN = 0;

    // ----------------------------------------------------------------------
    // abstract methods for subclass

    /**
     * Returns the size of the shadow element
     */
    public abstract int getShadowSize();

    /**
     * @param text the text that should be set on the shadow to
     * determine the appropriate size of the widget
     */
    public abstract void setShadowText(String text);

    /**
     * @param size will take into account minSize, maxSize, and
     * extraSize. the implementation should just call setWidth or
     * setHeight as appropriate.
     */
    public abstract void setSize(int size);

    // ----------------------------------------------------------------------
    // shared configuration

    // size is either width or height depending on the control
    // (TextBox vs. TextArea)

    public final int getMinSize() {
	return minSize;
    }
    public final void setMinSize(int minSize) {
	this.minSize = minSize;
    }

    public final int getMaxSize() {
	return maxSize;
    }
    public final void setMaxSize(int maxSize) {
	this.maxSize = maxSize;
    }

    /**
     * This is the amount of extra horizontal or vertical space that
     * will be added.
     */
    public final int getExtraSize() {
	return extraSize;
    }
    public final void setExtraSize(int extraSize) {
	this.extraSize = extraSize;
    }

    // ----------------------------------------------------------------------
    // check for max-height and min-height properties and use them
    // instead of anything configured. if you need to control the
    // min/max in code, don't set those css properties.
    //
    // also note that we remove the properties from the textarea AND
    // the shadow. this is important because otherwise they interfere
    // with the auto-sizing
    //

    public final void setMinFromCss(String property) {
	int min = getAndResetValueFromCss(property, "0");
	if (min > 0) {
	    setMinSize(min);
	}
    }

    public final void setMaxFromCss(String property) {
	int max = getAndResetValueFromCss(property, "none");
	if (max > 0) {
	    setMaxSize(max);
	}
    }

    public final int getAndResetValueFromCss(String property, String reset) {
	int value = MiscUtils.getComputedStyleInt(box.getElement(), property);
	if (value > 0) {
	    box.getElement().getStyle().setProperty(property, reset);
	    shadow.getElement().getStyle().setProperty(property, reset);
	}	
	return value;
    }
    
    // ----------------------------------------------------------------------
    
    protected int minSize = DEFAULT_MIN;
    protected int maxSize = DEFAULT_MAX;
    protected int extraSize;
    
    protected final T box;
    protected final S shadow;
    protected final FlowPanel div = new FlowPanel();

    public AutoSizingBase(T box, S shadow) {
	this.box = box;
	this.shadow = shadow;

 	box.addKeyDownHandler(this);
 	box.addKeyUpHandler(this);
	box.addValueChangeHandler(this);

	div.setStyleName("gwt-traction-input-autosize");
	shadow.setStyleName("gwt-traction-input-shadow");
	
	// make sure the shadow isn't in the tab order
	if (shadow instanceof Focusable) {
	    // we can't use -1 because FocusWidget.onAttach looks for
	    // that and sets it to 0. any negative value will remove
	    // it from the tab order.
	    ((Focusable) shadow).setTabIndex(-2);
	}

	// note this has to be in a FlowPanel to work
	div.add(box);
	div.add(shadow);

	initWidget(div);	
    }

    /**
     * Matches the styles and adjusts the size. This needs to be
     * called after the input is added to the DOM, so we do it in
     * onLoad.
     */
    @Override
    protected void onLoad() {
	super.onLoad();

	// these styles need to be the same for the box and shadow so
	// that we can measure properly
	matchStyles("display");
	matchStyles("fontSize");
	matchStyles("fontFamily");
	matchStyles("fontWeight");
	matchStyles("lineHeight");
	matchStyles("paddingTop");
	matchStyles("paddingRight");
	matchStyles("paddingBottom");
	matchStyles("paddingLeft");

	adjustSize();
    }

    @Override
    public T getWidget() {
	return box;
    }

    // ----------------------------------------------------------------------
    // style manipulation

    public void matchStyles(String name) {
        String value = MiscUtils.getComputedStyle(box.getElement(), name);
        if (value != null) {
            try {
                // we might have a bogus value (e.g. width: -10px). we
                // just let it fail quietly.
                shadow.getElement().getStyle().setProperty(name, value);	
            }
            catch (Exception e) {
                GWT.log("Exception in matchStyles for name="+name+" value="+value, e);
            }
	}
    }

    public void setStyles(String name, String value) {
	box.getElement().getStyle().setProperty(name, value);
	shadow.getElement().getStyle().setProperty(name, value);
    }

    // ----------------------------------------------------------------------
    // event handling code

    /**
     * On key down we assume the key will go at the end. It's the most
     * common case and not that distracting if that's not true.
     */
    @Override
    public void onKeyDown(KeyDownEvent event) {
	char c = MiscUtils.getCharCode(event.getNativeEvent());
	onKeyCodeEvent(event, box.getValue()+c);
    }    

    @Override
    public void onKeyUp(KeyUpEvent event) {
	onKeyCodeEvent(event, box.getValue());
    }    

    protected void onKeyCodeEvent(KeyCodeEvent event, String newShadowText) {
	// ignore arrow keys
	switch (event.getNativeKeyCode()) {
	case KeyCodes.KEY_UP:
	case KeyCodes.KEY_DOWN:
	case KeyCodes.KEY_LEFT:
	case KeyCodes.KEY_RIGHT:
	    break;
	default:
	    // don't do this if there's a selection because it will get smaller
	    if (box.getSelectionLength() == 0) {
		setShadowText(newShadowText);
		adjustSize();	    
		break;
	    }
	}	
    }
        
    @Override
    public void onValueChange(ValueChangeEvent<String> event) {
	// here, we just match them and adjust the size again. this
	// will handle backspace and typing over a selection.
	sync();
    }

    public void sync() {
	setShadowText(box.getValue());
	adjustSize();
    }

    // ----------------------------------------------------------------------
    // the meat (not very meaty)

    public void resetSize() {
	setSize(Math.max(minSize, extraSize));
    }
    
    public void adjustSize() {
	int size = getShadowSize() + extraSize;
	if (size < minSize) {
	    size = minSize;
	}
	else if (size > maxSize) {
	    size = maxSize;
	}
	setSize(size);
    }

    public void setWidth(int width) {
	box.setWidth(width+"px");
	div.setWidth(width+"px");
    }
    
    public void setHeight(int height) {
	box.setHeight(height+"px");
	div.setHeight(height+"px");	
    }
    
    // ----------------------------------------------------------------------
    // Focusable (proxy to SuggestBox)
    
    @Override
    public final int getTabIndex() {
	return box.getTabIndex();
    }
    
    @Override
    public final void setTabIndex(int index) {
	box.setTabIndex(index);
    }
    
    @Override
    public final void setFocus(boolean focus) {
	if (focus) {
	    Scheduler.get().scheduleDeferred(new ScheduledCommand() {
                @Override
                public void execute() {
                    box.setFocus(true);                      
                }
            });
	}
	else {
	    box.setFocus(false);
	}
    }

    @Override
    public final void setAccessKey(char key) {
	box.setAccessKey(key);
    }

    // ----------------------------------------------------------------------
    // HasText

    @Override
    public final String getText() {
	return box.getText();
    }

    @Override
    public abstract void setText(String text);

}