/*
 * 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 org.apache.empire.jsf2.utils;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.Iterator;
import java.util.Locale;

import javax.el.ValueExpression;
import javax.faces.FacesWrapper;
import javax.faces.application.FacesMessage;
import javax.faces.component.NamingContainer;
import javax.faces.component.UIComponent;
import javax.faces.component.UIData;
import javax.faces.component.UIInput;
import javax.faces.component.UIOutput;
import javax.faces.component.html.HtmlOutputLabel;
import javax.faces.component.html.HtmlOutputText;
import javax.faces.component.html.HtmlPanelGroup;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.BeanUtilsBean;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.beanutils.PropertyUtilsBean;
import org.apache.empire.commons.ObjectUtils;
import org.apache.empire.commons.Options;
import org.apache.empire.commons.StringUtils;
import org.apache.empire.data.Column;
import org.apache.empire.data.ColumnExpr;
import org.apache.empire.data.DataType;
import org.apache.empire.data.Record;
import org.apache.empire.data.RecordData;
import org.apache.empire.db.DBColumn;
import org.apache.empire.db.DBDatabase;
import org.apache.empire.db.DBRecord;
import org.apache.empire.db.DBRowSet;
import org.apache.empire.db.exceptions.FieldNotNullException;
import org.apache.empire.exceptions.BeanPropertyGetException;
import org.apache.empire.exceptions.BeanPropertySetException;
import org.apache.empire.exceptions.InvalidArgumentException;
import org.apache.empire.exceptions.InvalidPropertyException;
import org.apache.empire.exceptions.NotSupportedException;
import org.apache.empire.exceptions.PropertyReadOnlyException;
import org.apache.empire.jsf2.app.FacesUtils;
import org.apache.empire.jsf2.app.TextResolver;
import org.apache.empire.jsf2.app.WebApplication;
import org.apache.empire.jsf2.components.ControlTag;
import org.apache.empire.jsf2.components.InputTag;
import org.apache.empire.jsf2.components.LabelTag;
import org.apache.empire.jsf2.components.LinkTag;
import org.apache.empire.jsf2.components.RecordTag;
import org.apache.empire.jsf2.controls.InputControl;
import org.apache.empire.jsf2.controls.InputControl.DisabledType;
import org.apache.empire.jsf2.controls.InputControlManager;
import org.apache.empire.jsf2.controls.SelectInputControl;
import org.apache.empire.jsf2.controls.TextAreaInputControl;
import org.apache.empire.jsf2.controls.TextInputControl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class TagEncodingHelper implements NamingContainer
{
    private final static String SPACE = " ";
    
    /**
     * ColumnExprWrapper
     * wraps a ColumnExpr object into a Column interface object
     * @author doebele
     */
    protected static class ColumnExprWrapper implements Column
    {
        private final ColumnExpr expr;

        public ColumnExprWrapper(ColumnExpr expr)
        {
            this.expr = expr;
        }
        
        public ColumnExpr getExpr()
        {
            return this.expr;
        }

        @Override
        public DataType getDataType()
        {
            return expr.getDataType();
        }

        @Override
        public String getName()
        {
            return expr.getName();
        }

        @Override
        public String getTitle()
        {
            return expr.getTitle();
        }

        @Override
        public String getControlType()
        {
            return expr.getControlType();
        }

        @Override
        public Object getAttribute(String name)
        {
            return expr.getAttribute(name);
        }

        @Override
        public Options getOptions()
        {
            return expr.getOptions();
        }

        @Override
        public String getBeanPropertyName()
        {
            return expr.getBeanPropertyName();
        }

        @Override
        public Column getSourceColumn()
        {
            return expr.getSourceColumn();
        }

        @Override
        public double getSize()
        {
            return 0;
        }

        @Override
        public boolean isRequired()
        {
            return false;
        }

        @Override
        public boolean isAutoGenerated()
        {
            return false;
        }

        @Override
        public boolean isReadOnly()
        {
            return true;
        }

        @Override
        public Class<Enum<?>> getEnumType()
        {
            return getSourceColumn().getEnumType();
        }

        @Override
        public Object validate(Object value)
        {
            log.warn("validate not supported for {}", expr.getName());
            return value;
        }
    }

    /**
     * ValueInfoImpl
     * Provides information necessary to render a data value (non editable) 
     * @author doebele
     */
    protected class ValueInfoImpl implements InputControl.ValueInfo
    {
        public ValueInfoImpl(Column column, TextResolver resolver)
        {
            if (column==null)
                throw new InvalidArgumentException("column", resolver);
            if (resolver==null)
                throw new InvalidArgumentException("resolver", resolver);
        }

        /* Value Options */
        protected boolean hasColumn()
        {
            return (column != null || TagEncodingHelper.this.hasColumn());
        }

        @Override
        public Column getColumn()
        {
            return column;
        }

        @Override
        public Object getValue(boolean evalExpression)
        {
            return getDataValue(evalExpression);
        }

        @Override
        public Options getOptions()
        {
            return getValueOptions();
        }

        /*
        @Override
        public Object getNullValue()
        {
            // null value
            Object attr = getTagAttributeValue("default");
            if (attr != null)
                return attr;
            // Use Column default
            if (hasColumn())
                return column.getAttribute("default");
            // not available
            return null;
        }
        */

        @Override
        public String getStyleClass(String addlStyle)
        {
            String style = getTagStyleClass(addlStyle);
            return style; 
        }

        @Override
        public String getFormat()
        {
            // null value
            String attr = getTagAttributeString("format");
            if (attr != null)
                return attr;
            // Use Column default
            if (hasColumn())
            { // from column
                Object format = column.getAttribute("format");
                if (format != null)
                    return format.toString();
            }
            // not available
            return null;
        }

        @Override
        public Locale getLocale()
        {
            return textResolver.getLocale();
        }

        @Override
        public String getText(String text)
        {
            return textResolver.resolveText(text);
        }

        @Override
        public TextResolver getTextResolver()
        {
            return textResolver;
        }
        
        @Override 
        public boolean isInsideUIData()
        {
            return TagEncodingHelper.this.isInsideUIData();
        }
    }

    /**
     * InputInfoImpl
     * Provides information necessary to render an input control (editable) 
     * @author doebele
     */
    protected class InputInfoImpl extends ValueInfoImpl implements InputControl.InputInfo
    {
        public InputInfoImpl(Column column, TextResolver resolver)
        {
            super(column, resolver);
        }

        @Override
        public void setValue(Object value)
        {
            setDataValue(value);
        }

        @Override
        public void validate(Object value)
        {
            // skip?
            if (skipValidation)
                return;
            // Make sure null values are not forced to be required
            boolean isNull = ObjectUtils.isEmpty(value);
            if (isNull && validateNullValue())
            {   // OK, null allowed
                return;
            }
            // validate through record (if any)
            if ((getRecord() instanceof Record))
               ((Record)getRecord()).validateValue(column, value);
            else
                column.validate(value);
        }

        @Override
        public boolean isRequired()
        {
            return isValueRequired();
        }

        @Override
        public boolean isModified()
        {
            return isValueModified();
        }

        @Override
        public boolean isDisabled()
        {
            DisabledType disabled = TagEncodingHelper.this.getDisabled(); 
            return (disabled!=null && disabled!=DisabledType.NO);
        }
        
        @Override
        public DisabledType getDisabled()
        {            
            return TagEncodingHelper.this.getDisabled();
        }
        
        @Override
        public String getInputId()
        {   /**
             * obsolete since ControlTag or InputTag will set column name as id
             * 
             Column c = getColumn();
             return c.getName();
            */
            return "inp";
        }
        
        @Override
        public boolean hasError()
        {
            return hasError;
        }

        @Override
        public Object getAttribute(String name)
        {
            return getTagAttributeValue(name);
        }
        
        @Override
        public Object getAttributeEx(String name)
        {
            return getAttributeValueEx(name);
        }
    }

    // Logger
    private static final Logger log = LoggerFactory.getLogger(TagEncodingHelper.class);

    public static final String ORIGINAL_COMPONENT_ID  = "ORIGINAL_COMPONENT_ID"; 

    public static final String COLATTR_TOOLTIP        = "TOOLTIP";          // Column tooltip
    public static final String COLATTR_ABBR_TITLE     = "ABBR_TITLE";       // Column title for abbreviations
    
    protected final UIOutput      component;
    protected final String        cssStyleClass;
    protected Column              column       = null;
    protected Object              record       = null;
    protected RecordTag           recordTag    = null;
    protected UIData              uiDataTag    = null;
    // protected Boolean          tagRequired  = null;
    protected Boolean             hasValueExpr = null;
    protected InputControl        control      = null;
    protected TextResolver        textResolver = null;
    protected Object              mostRecentValue = null;
    protected boolean             skipValidation = false;
    protected boolean             hasError     = false;
    protected Boolean             insideUIData = null;

    protected TagEncodingHelper(UIOutput component, String cssStyleClass)
    {
        this.component = component;
        this.cssStyleClass = cssStyleClass;
    }

    public void encodeBegin()
    {
        if (component instanceof UIInput)
        {   // has local value?
            if (((UIInput)component).isLocalValueSet())
            {   /* clear local value */
                if (log.isDebugEnabled())
                    log.debug("clearing local value for {}. value is {}.", getColumnName(), ((UIInput)component).getLocalValue());
                // reset
                ((UIInput)component).setValue(null);
                ((UIInput)component).setLocalValueSet(false);
            }
            /*
            ValueExpression ve = findValueExpression("required", true);
            if (ve!=null)
            {   Object req = ve.getValue(FacesContext.getCurrentInstance().getELContext());
                if (req!=null)
                    tagRequired = new Boolean(ObjectUtils.getBoolean(req));
            }
            */
        }
        // check record
        checkRecord();
    }

    public void prepareData()
    {
        checkRecord();
    }
    
    protected static final String PH_COLUMN_NAME = "@";  // placeholder for column name
    protected static final String PH_COLUMN_FULL = "&";  // placeholder for column full name including table

    public String completeInputTagId(String id)
    {
        // EmptyString or AT
        if (StringUtils.isEmpty(id) || PH_COLUMN_NAME.equals(id))
            return getColumnName();
        // replace placeholder
        if (id.indexOf(PH_COLUMN_NAME)>=0)
        {   // column name only
            id = id.replace(PH_COLUMN_NAME, getColumnName());
        }
        else if (id.indexOf(PH_COLUMN_FULL)>=0) 
        {   // column full name including table
            String name= null;
            if (column==null)
                column = findColumn();
            if (column instanceof DBColumn)
                name = ((DBColumn)column).getFullName().replace('.', '_');
            else if (column!=null)
                name = column.getName();
            id = id.replace(PH_COLUMN_FULL, String.valueOf(name));
        }
        // done 
        return id;
    }
    
    public InputControl getInputControl()
    {
        // Create
        if (getColumn() == null)
        	throw new NotSupportedException(this, "getInputControl");
        // Get Control from column
        String controlType = getTagAttributeString("controlType");
        if (controlType==null)
        {   controlType = column.getControlType();
            // Always use SelectInputControl
            if (TextInputControl.NAME.equalsIgnoreCase(controlType))
            {   Object attr = getTagAttributeValue("options");
                if (attr != null && (attr instanceof Options) && !((Options)attr).isEmpty())
                    controlType = SelectInputControl.NAME;
            }
        }
        // detect Control
        control = detectInputControl(controlType, column.getDataType(), column.getOptions()!=null);
        // check record
        return control;
    }

    protected InputControl detectInputControl(String controlType, DataType dataType, boolean hasOptions)
    {
        // Create
        if (dataType==null)
            throw new InvalidArgumentException("dataType", dataType);
        // find control type
        InputControl control = null;
        if (StringUtils.isNotEmpty(controlType))
            control = InputControlManager.getControl(controlType);
        if (control == null)
        {   // Auto-detect
            if (hasOptions)
                controlType = SelectInputControl.NAME;
            else
            {   // get from data type
                switch (dataType)
                {
                    case CLOB:
                        controlType = TextAreaInputControl.NAME;
                        break;
                    default:
                        controlType = TextInputControl.NAME;
                }
            }
            // get default control
            control = InputControlManager.getControl(controlType);
            // Still not? Use Text Control
            if (control == null)
                control = InputControlManager.getControl(TextInputControl.NAME);
        }
        // check record
        return control;
    }
    
    protected void checkRecord()
    {
        // Record may change even for the same instance
        if (this.record== null)
            return;
        // Check direct record property
        Object rec = getTagAttributeValue("record");
        if (rec!=null)
        {   // record directly specified
            if (rec!=this.record)
            {   // Record has changed
                if (log.isTraceEnabled())
                {   // Debug output
                    if ((rec instanceof DBRecord) && (this.record instanceof DBRecord))
                    {   // a database record change
                        String keyOld = StringUtils.toString(((DBRecord)this.record).getKeyValues());
                        String keyNew = StringUtils.toString(((DBRecord)rec).getKeyValues());
                        String rowSet = StringUtils.valueOf(((DBRecord)rec).getRowSet().getName());
                        log.trace("Changing "+component.getClass().getSimpleName()+" record of rowset "+rowSet+" from {} to {}", keyOld, keyNew);
                    }
                    else
                    {   // probably a bean change
                        log.trace("Changing "+component.getClass().getSimpleName()+" record of class "+rec.getClass().getSimpleName());
                    }
                }
                // change now
                setRecord(rec);
            }    
        }
        else 
        {   // Do we have a record-tag?
            if (recordTag!=null)
            {   // Check Record change
                rec = recordTag.getRecord();
                if (rec!=this.record)
                {   // Record has changed
                    setRecord(rec);
                }
            }  
            // Invalidate if inside UIData
            else if (isInsideUIData())
            {   // reset record
                setRecord(null);
            }
        }
    }

    public InputControl.ValueInfo getValueInfo(FacesContext ctx)
    {
        return new ValueInfoImpl(getColumn(), getTextResolver(ctx));
    }

    public InputControl.InputInfo getInputInfo(FacesContext ctx)
    {
        // Skip validate
        skipValidation = FacesUtils.isSkipInputValidation(ctx);
        // check whether we have got an error
        hasError = detectError(ctx);            
        // create
        return new InputInfoImpl(getColumn(), getTextResolver(ctx));
    }
    
    public boolean isPartialSubmit(FacesContext ctx)
    {
        return FacesUtils.getWebApplication().isPartialSubmit(ctx);
    }

    public boolean isSkipValidation()
    {
        return skipValidation;
    }

    public boolean hasColumn()
    {
        if (column == null)
            column = findColumn();
        if (column == null)
        {   // @deprecated: for compatibility only!
            if (getTagAttributeValue("column")!=null)
                return false; // provided but not found
            // find value
            column = findColumnFromValue();  
            if (column!=null)
                log.warn("Providing the column as the value is deprecated. Use column attribute insteam. This might be removed in future versions!");
        }
        return (column != null);
    }

    public Column getColumn()
    {
        if (hasColumn())
            return this.column;
        else
            throw new InvalidArgumentException("column", column);
    }
    
    public String getColumnName()
    {
        // don't use hasColumn() or getColumn() here!
        if (column==null)
            column = findColumn(); 
        return (column!=null ? column.getName() : "null");
    }

    public void setColumn(Column column)
    {
        this.column = column;
    }

    public Object getRecord()
    {
        if (record == null && (record=findRecord())!=null)
            setRecord(record);
        return record;
    }

    public void setRecord(Object record)
    {
        this.record = record;
        this.mostRecentValue = null; 
    }
    
    public Object findRecordComponent()
    {
        // already present?
        if (this.recordTag != null)
            return this.recordTag.getRecord();
        if (this.uiDataTag != null)
        {   // check row available
            if (this.uiDataTag.isRowAvailable())
                return this.uiDataTag.getRowData();
            // row not available (possibly deleted)
            return null;
        }
        // walk upwards the parent component tree and return the first record component found (if any)
        UIComponent parent = component;
        while ((parent = parent.getParent()) != null)
        {
            if (parent instanceof RecordTag)
            {
                this.recordTag = (RecordTag) parent;
                return this.recordTag.getRecord();
            }
            if (parent instanceof UIData)
            {
                this.uiDataTag = (UIData)parent;
                this.insideUIData = true;
                return (this.uiDataTag.isRowAvailable() ? this.uiDataTag.getRowData() : null);
            }
        }
        return null;
    }

    protected boolean isDetectFieldChange()
    {
        Object v = this.getTagAttributeValue("detectFieldChange");
        if (v==null && recordTag != null)
            v = recordTag.getAttributes().get("detectFieldChange");
        return (v!=null ? ObjectUtils.getBoolean(v) : true);
    }

    public Object getDataValue(boolean evalExpression)
    {
        if (getRecord() != null)
        {   // value
            if (record instanceof RecordData)
            { // a record
                ColumnExpr col = unwrapColumnExpr(getColumn());
                mostRecentValue = ((RecordData) record).getValue(col);
                return mostRecentValue;
            }   
            else
            { // a normal bean
                String prop = getColumn().getBeanPropertyName();
                return getBeanPropertyValue(record, prop);
            }
        }
        else
        {   // Get from tag
            if (evalExpression)
                return component.getValue();
            else
            {   // return value or value expression
                Object value = component.getLocalValue();
                if (value!=null && (component instanceof UIInput) && !((UIInput)component).isLocalValueSet())
                    value= null; /* should never come here! */
                if (value==null)
                    value = findValueExpression("value", false);
                
                // value = tag.getValue();
                return value;
            }
        }
    }

    public void setDataValue(Object value)
    {
        if (getRecord() != null)
        {   // value
            if (record instanceof Record)
            {   getColumn();
                /* special case
                if (value==null && getColumn().isRequired())
                {   // ((Record)record).isFieldRequired(column)==false
                    log.warn("Unable to set null for required field!");
                    return;
                }
                */
                if (mostRecentValue!=null && isDetectFieldChange())
                {   // DetectFieldChange by comparing current and most recent value
                    Object currentValue = ((Record) record).getValue(column);
                    if (!ObjectUtils.compareEqual(currentValue, mostRecentValue))
                    {   // Value has been changed by someone else!
                        log.info("Concurrent data change for "+column.getName()+". Current Value is {}. Ignoring new value {}", currentValue, value);
                        return;
                    }
                }
                // check whether to skip validation
                boolean reenableValidation = false;
                if (skipValidation && (record instanceof DBRecord))
                {   // Ignore read only values
                    if (this.isReadOnly())
                        return;
                    // Column required?
                    if (ObjectUtils.isEmpty(value) && ((Record) this.record).isFieldRequired(column))
                        return; // Cannot set required value to null
                    // Disable Validation
                    reenableValidation = ((DBRecord)record).isValidateFieldValues();
                    if (reenableValidation)
                        ((DBRecord)record).setValidateFieldValues(false);
                    // Validation skipped for
                    if (log.isDebugEnabled())
                        log.debug("Input Validation skipped for {}.", column.getName());
                }
                // Now, set the value
                try {
                    ((Record) record).setValue(column, value);
                    mostRecentValue = value;
                } finally {
                    // re-enable validation
                    if (reenableValidation)
                        ((DBRecord)record).setValidateFieldValues(true);
                }
            }
            else if (record instanceof RecordData)
            { // a record
                throw new PropertyReadOnlyException("record");
            }
            else
            { // a normal bean
                String prop = getColumn().getBeanPropertyName();
                setBeanPropertyValue(record, prop, value);
            }
        }
        else
        { // Get from tag
          // tag.setValue(value);
            ValueExpression ve = component.getValueExpression("value");
            if (ve == null)
                throw new PropertyReadOnlyException("value");

            FacesContext ctx = FacesContext.getCurrentInstance();
            ve.setValue(ctx.getELContext(), value);
        }
    }

    public boolean isRenderValueComponent()
    {
        return isReadOnly();
    }

    public boolean isRecordReadOnly()
    {
        return (getRecordReadOnly()==Boolean.TRUE);
    }
    
    public Boolean getRecordReadOnly()
    {
        // Do we have a record?
        if (getRecord() instanceof RecordData)
        {   // Only a RecordData?
            if (!(record instanceof Record))
                return true;
        }
        else if (!hasValueExpression())
        {   // No Value expression given
            return true;
        }
        // check attribute
        Object val = getTagAttributeValue("readonly");
        if (!ObjectUtils.isEmpty(val))
        {   // override
            return ObjectUtils.getBoolean(val);
        }
        // check record component
        if (recordTag != null && recordTag.isReadOnly())
            return true;
        // Do we have a record?
        if ((record instanceof Record) && ((Record)record).isReadOnly())
            return true;
        // not defined
        return null;
    }

    public boolean isVisible()
    {
        // reset record
        if (this.record!=null)
        {   // Check attribute
            Object recordTagValue = getTagAttributeValue("record");
            if ((recordTagValue instanceof DBRecord) && this.record!=recordTagValue)
            {   // shoud not come here
                log.warn("Record in call to IsVisible has unexpectedly changed!");
                this.record=null;
            }
        }
        // Check Record
        if ((getRecord() instanceof Record))
        {   // Ask Record
            Record r = (Record) record;
            return r.isFieldVisible(getColumn());
        }
        // column
        return true;
    }

    public boolean isReadOnly()
    {
        // component 
        if (!(component instanceof UIInput))
        {   log.warn("Component is not of type UIInput");
            return true;
        }
        // Check Record
        Boolean readOnly = getRecordReadOnly();
        if (readOnly!=null)
            return readOnly;
        // Check Record
        if ((getRecord() instanceof Record))
        { // Ask Record
            Record r = (Record) record;
            return r.isFieldReadOnly(getColumn());
        }
        // column
        return getColumn().isReadOnly();
    }
    
    public DisabledType getDisabled()
    {
        // component 
        if (!(component instanceof UIInput))
        {   log.warn("Component is not of type UIInput");
            return DisabledType.READONLY;
        }
        // Check attribute
        Object dis = getAttributeValueEx("disabled");
        if (ObjectUtils.isEmpty(dis))
            return null; // not provided!
        // direct
        if (dis instanceof DisabledType)
            return (DisabledType)dis;
        // readonly
        if (String.valueOf(dis).equalsIgnoreCase("readonly"))
            return DisabledType.READONLY;
        // other
        return (ObjectUtils.getBoolean(dis) ? DisabledType.DISABLED : DisabledType.NO);
    }

    public boolean isValueRequired()
    {
        // See if the tag is required (don't use the "required" attribute or tag.isRequired()!)
        Object mandatory = getTagAttributeValue("mandatory");
        if (mandatory!=null)
            return ObjectUtils.getBoolean(mandatory);
        // Check Record
        if ((getRecord() instanceof Record))
        {   // Ask Record
            Record r = (Record) record;
            return r.isFieldRequired(getColumn());
        }
        // Check Value Attribute
        if (hasValueExpression())
            return false;
        // Required
        return getColumn().isRequired();
    }
    
    public boolean isValueModified()
    {
        Object modified = getTagAttributeValue("modified");
        if (modified!=null)
            return ObjectUtils.getBoolean(modified);
        // Check Record
        if ((getRecord() instanceof Record))
        {   // Ask Record
            Record r = (Record) record;
            return r.wasModified(getColumn());
        }
        // not detectable
        return false;
    }

    public boolean validateNullValue()
    {
        if (isValueRequired())
            throw new FieldNotNullException(column);
        // OK, null allowed
        return true;
    }
    
    /**
     * used for partial submits to detect whether the value of this field can be set to null
     */
    public boolean isTempoaryNullable()
    {
        if (getColumn().isRequired())
            return false;
        return true;        
    }

    /* Helpers */
    protected Column findColumn()
    {
        // if parent is a record tag, get the record from there
        Object col = getTagAttributeValue("column");
        if (col instanceof Column)
        {   // cast to column
            return (Column) col;
        }
        if (col instanceof ColumnExpr)
        {   // check component
            if ((component instanceof InputTag || component instanceof ControlTag))
            {   log.warn("ColumnExpresion cannot be used with InputTag or ControlTag");
                throw new InvalidPropertyException("column", column);
            }
            // wrap expression
            return createColumnExprWrapper((ColumnExpr) col);
        }
        if (col instanceof String)
        {   // parse String
            String name = String.valueOf(col);
            int dbix = name.indexOf('.');
            if (dbix<=0)
            {
                log.error("Invalid column expression '{}'!", name);
                return null; // not found
            }
            DBDatabase db = DBDatabase.findById(name.substring(0,dbix));
            if (db==null)
            {
                log.error("Database '{}' not found!", name.substring(0,dbix));
                return null; // not found
            }
            int co = name.lastIndexOf('.');
            int to = name.lastIndexOf('.', co - 1);
            String cn = name.substring(co + 1);
            String tn = name.substring(to + 1, co);
            DBRowSet rs = db.getRowSet(tn);
            if (rs == null)
            {
                log.error("Table/View '{}' not found in database!", tn);
                return null; // not found
            }
            Column column = rs.getColumn(cn);
            if (column == null)
            {
                log.error("Column '{}' not found in table/view '{}'!", cn, tn);
                return null; // not found
            }
            // done
            return column;
        }
        // No column!
        if (log.isDebugEnabled() && !(component instanceof LinkTag))
            log.warn("No Column provided for value tag!");
        return null;
    }

    /**
     * Checks whether the value attribute contains a column reference and returns it
     * @return the column
     */
    protected Column findColumnFromValue()
    {   // Try value
        Object col = component.getValue();
        // Column supplied?
        if (col instanceof Column)
        {
            return (Column) col;
        }
        // Column expression supplied?
        if (col instanceof ColumnExpr)
        { // Use source column instead 
            Column source = ((ColumnExpr) col).getSourceColumn();
            if (source != null)
                return source;
            // No source column? --> wrap 
            return createColumnExprWrapper((ColumnExpr) col);
        }
        return null;
    }
    
    protected Column createColumnExprWrapper(ColumnExpr colExpr)
    {
        return new ColumnExprWrapper(colExpr);
    }
    
    protected ColumnExpr unwrapColumnExpr(Column col)
    {
        if (col instanceof ColumnExprWrapper)
            return ((ColumnExprWrapper) col).getExpr();
        return col;
    }
    
    protected Column unwrapColumn(Column col)
    {
        if (col instanceof ColumnExprWrapper)
            return ((ColumnExprWrapper) col).getSourceColumn();
        return col;
    }

    protected Object findRecord()
    {
        Object rec = getTagAttributeValue("record");
        if (rec != null)
            return rec;
        // Value expression
        if (hasValueExpression())
        {   // See if the record is in value
            return null;
        }       
        // if parent is a record tag, get the record from there
        rec = findRecordComponent();
        if (rec==null)
        {   // not supplied
            if ((component instanceof ControlTag) && !((ControlTag)component).isCustomInput())
                log.warn("No record supplied for {} and column {}.", component.getClass().getSimpleName(), getColumnName()); 
        }
        return rec;
    }
    
    protected boolean hasValueExpression()
    {
        // Find expression
        if (hasValueExpr != null)
            return hasValueExpr.booleanValue();
        // Find expression
        ValueExpression ve = findValueExpression("value", false);
        if (ve != null)
        {   // We have a ValueExpression!
            // Now unwrap for Facelet-Tags to work
            String originalExpr = (log.isDebugEnabled() ? ve.getExpressionString() : null);
            ve = FacesUtils.getFacesImplementation().unwrapValueExpression(ve);
            if (originalExpr!=null)
            {   // log result
                if (ve!=null && !originalExpr.equals(ve.getExpressionString()))
                    log.debug("ValueExpression \"{}\" has been resolved to \"{}\" from class {}", originalExpr, ve.getExpressionString(), ve.getClass().getName());
                else if (ve==null)
                    log.debug("ValueExpression \"{}\" has been resolved to NULL", originalExpr);
            }
        }
        // store result to avoid multiple detection 
        hasValueExpr = Boolean.valueOf(ve!=null);
        return hasValueExpr.booleanValue();
    }
        
    protected static final String CC_ATTR_EXPR = "#{cc.attrs.";
    
    @SuppressWarnings("unchecked")
    protected ValueExpression findValueExpression(String attribute, boolean allowLiteral)
    {
        // Check for expression
        ValueExpression ve = component.getValueExpression(attribute);
        if (ve == null)
            return null;
        // Find expression
        UIComponent parent = component;
        String expr = ve.getExpressionString();
        while (expr.startsWith(CC_ATTR_EXPR))
        {
            // Unwrap
            if (ve instanceof FacesWrapper<?>)
                ve = ((FacesWrapper<ValueExpression>)ve).getWrapped();
            // find parent
            UIComponent valueParent = FacesUtils.getFacesImplementation().getValueParentComponent(ve);
            if (valueParent!=null)
            {	// use the value parent
            	parent = valueParent;
            }
            else
            {   // find parent
                parent = UIComponent.getCompositeComponentParent(parent);
            }
            if (parent == null)
                return null;
            // check expression
            int end = expr.indexOf('}');
            String attrib = expr.substring(CC_ATTR_EXPR.length(), end);
            if (attrib.indexOf('.')>0)
                return ve; // do not investigate any further
            // find attribute
            ValueExpression next = parent.getValueExpression(attrib);
            if (next == null)
            {   // allow literal
                if (allowLiteral && (parent.getAttributes().get(attrib)!=null))
                    return ve;
                // not found
                return null;
            }
            // get new expression String
            ve = next;
            expr = ve.getExpressionString();
        }
        // found
        return ve;
    }
    
    protected Options getValueOptions()
    {
        // null value
        Object attr = getTagAttributeValue("options");
        if (attr != null && (attr instanceof Options))
            return ((Options) attr);
        if (getColumn() != null)
        { // Do we have a record?
            if (getRecord() instanceof Record)
                return ((Record) record).getFieldOptions(unwrapColumn(column));
            // get From Column
            return column.getOptions();
        }
        // not available
        return null;
    }

    protected Object getBeanPropertyValue(Object bean, String property)
    {
        try
        { // Get Property Value
            PropertyUtilsBean pub = BeanUtilsBean.getInstance().getPropertyUtils();
            return pub.getSimpleProperty(bean, property);

        }
        catch (IllegalAccessException e)
        {
            log.error(bean.getClass().getName() + ": unable to get property '" + property + "'");
            throw new BeanPropertyGetException(bean, property, e);
        }
        catch (InvocationTargetException e)
        {
            log.error(bean.getClass().getName() + ": unable to get property '" + property + "'");
            throw new BeanPropertyGetException(bean, property, e);
        }
        catch (NoSuchMethodException e)
        {
            log.warn(bean.getClass().getName() + ": no getter available for property '" + property + "'");
            throw new BeanPropertyGetException(bean, property, e);
        }
    }

    protected void setBeanPropertyValue(Object bean, String property, Object value)
    {
        // Get Property Name
        try
        { // Get Property Value
            if (ObjectUtils.isEmpty(value))
                value = null;
            // Set Property Value
            if (value != null)
            { // Bean utils will convert if necessary
                BeanUtils.setProperty(bean, property, value);
            }
            else
            { // Don't convert, just set
                PropertyUtils.setProperty(bean, property, null);
            }
        }
        catch (IllegalArgumentException e)
        {
            log.error(bean.getClass().getName() + ": invalid argument for property '" + property + "'");
            throw new BeanPropertySetException(bean, property, e);
        }
        catch (IllegalAccessException e)
        {
            log.error(bean.getClass().getName() + ": unable to set property '" + property + "'");
            throw new BeanPropertySetException(bean, property, e);
        }
        catch (InvocationTargetException e)
        {
            log.error(bean.getClass().getName() + ": unable to set property '" + property + "'");
            throw new BeanPropertySetException(bean, property, e);
        }
        catch (NoSuchMethodException e)
        {
            log.error(bean.getClass().getName() + ": no setter available for property '" + property + "'");
            throw new BeanPropertySetException(bean, property, e);
        }
    }
    
    public String getValueTooltip(Object value)
    {
        if (value == null)
            return null;
        // is it a column
        if (value instanceof Column)
        {   // find column value   
            Column ttc = ((Column)value);
            if (getRecord() != null)
            {   // value
                if (record instanceof RecordData)
                { // a record
                    value = ((RecordData) record).getValue(ttc);
                }
                else
                { // a normal bean
                    String prop = ttc.getBeanPropertyName();
                    value = getBeanPropertyValue(record, prop);
                }
                // translate
                // ValueInfoImpl vi = new MiscValueInfoImpl(ttc, textResolver);
                // InputControl ctrl = detectInputControl(ttc.getControlType(), ttc.getDataType(), ttc.getOptions()!=null);
                return StringUtils.valueOf(value);
            } 
            else
            {   // Error
                log.warn("Unable to resolve Tooltip-value for column {}.", ttc.getName());
                return null;
            }
        }
        // is it a template?
        String templ = StringUtils.valueOf(value);
        int valIndex = templ.indexOf("{}");
        if (valIndex >= 0)
            value = getDataValue(true);
        // Check Options
        String text;
        Options options = getValueOptions();
        if (options != null && !hasFormat("notitlelookup"))
        { // Lookup the title
            String optValue = options.get(value);
            text = getDisplayText(optValue);
        }
        else
            text = getDisplayText(StringUtils.toString(value));
        // Check for template
        if (valIndex >= 0 && text!=null)
            text = StringUtils.replace(templ, "{}", text);
        return text;
    }

    public String getLabelTooltip(Column column)
    {
        String title = getTagAttributeString("title");
        if (title == null)
            title = StringUtils.toString(column.getAttribute(Column.COLATTR_TOOLTIP));
        if (title != null)
            return getDisplayText(title);
        // Check for short form
        if (hasFormat("short") && !ObjectUtils.isEmpty(column.getAttribute(COLATTR_ABBR_TITLE)))
            return getDisplayText(column.getTitle());
        
        // No Title
        return null;
    }

    public boolean hasFormat(String format)
    { // null value
        String f = getTagAttributeString("format");
        return (f != null && f.indexOf(format) >= 0);
    }

    public boolean hasFormat(InputControl.ValueInfo vi, String format)
    {
        String f = vi.getFormat();
        return (f != null && f.indexOf(format) >= 0);
    }

    public void writeAttribute(ResponseWriter writer, String attribute, Object value)
        throws IOException
    {
        if (value != null)
            writer.writeAttribute(attribute, value, null);
    }
    
    public String getDisplayText(String text)
    {
        if (textResolver==null)
            getTextResolver(FacesContext.getCurrentInstance());
        return textResolver.resolveText(text);
    }

    public TextResolver getTextResolver(FacesContext context)
    {
        if (textResolver==null)
            textResolver=WebApplication.getInstance().getTextResolver(context);
        return textResolver;
    }
    
    protected boolean detectError(FacesContext context)
    {
        Iterator<FacesMessage> iter = context.getMessages(component.getClientId());
        while (iter.hasNext())
        {   // Check for error
            FacesMessage m = iter.next();
            if (m.getSeverity()==FacesMessage.SEVERITY_ERROR)
                return true;
        }
        return false;
    }
    
    public void addErrorMessage(FacesContext context, Exception e)
    {
        String msgText = getTextResolver(context).getExceptionMessage(e);
        FacesMessage msg = new FacesMessage(FacesMessage.SEVERITY_ERROR, msgText, msgText);
        context.addMessage(component.getClientId(), msg);
    }

    public Object getAttributeValueEx(String name)
    { 
        Object value = getTagAttributeValue(name);
        if (value==null && hasColumn())
        {   // Check Column
            value = column.getAttribute(name);
        }
        // Checks whether it's another column    
        if (value instanceof Column)
        {   // Special case: Value is a column
            Column col = ((Column)value);
            Object rec = getRecord();
            if (rec instanceof Record)
                return ((Record)rec).getValue(col);
            else if (rec!=null)
            {   // Get Value from a bean
                String property = col.getBeanPropertyName();
                try
                {   // Use Beanutils to get Property
                    PropertyUtilsBean pub = BeanUtilsBean.getInstance().getPropertyUtils();
                    return pub.getSimpleProperty(rec, property);
                }
                catch (Exception e)
                {   log.error("BeanUtils.getSimpleProperty failed for "+property, e);
                    return null;
                }
            }    
            return null;
        }
        return value;
    }

    public String getTagAttributeStringEx(String name)
    {
        Object value = getAttributeValueEx(name);
        return (value!=null) ? StringUtils.toString(value) : null;
    }
    
    public Object getTagAttributeValue(String name)
    {
        return TagEncodingHelper.getTagAttributeValue(component, name);
    }

    public static Object getTagAttributeValue(UIComponent comp, String name)
    {
        Object value = comp.getAttributes().get(name);
        if (value==null)
        {   // try value expression
            ValueExpression ve = comp.getValueExpression(name);
            if (ve!=null)
            {   // It's a value expression
                FacesContext ctx = FacesContext.getCurrentInstance();
                value = ve.getValue(ctx.getELContext());
            }
        }
        return value;
    }
    
    public String getTagAttributeString(String name, String defValue)
    {
        Object  v = getTagAttributeValue(name);
        return (v!=null) ? StringUtils.toString(v) : defValue;
    }

    public String getTagAttributeString(String name)
    {
        return getTagAttributeString(name, null);
    }

    /* ********************** label ********************** */

    protected String getLabelValue(Column column, boolean colon)
    {
        String label = getTagAttributeString("label");
        if (label==null)
        {   // Check for short form    
            if (hasFormat("short"))
            {
                label = StringUtils.toString(column.getAttribute(COLATTR_ABBR_TITLE));
                if (label==null)
                    log.warn("No Abbreviation available for column {}. Using normal title.", column.getName());
            }
            // Use normal title
            if (label==null)
                label=column.getTitle();
            // translate
            label = getDisplayText(label);
        }    
        // handle empty string
        if (StringUtils.isEmpty(label))
            return "";
        // getColon
        if (colon) 
            label = label.trim() + ":";
        // done
        return label;
    }
    
    public HtmlOutputLabel createLabelComponent(FacesContext context, String forInput, String styleClass, String style, boolean colon)
    {
        Column column = null;
        boolean readOnly=false;
        boolean required=false;
        // forInput provided
        if (StringUtils.isNotEmpty(forInput))
        {   // find the component
            if (!forInput.equals("*"))
            {   // Set Label input Id
                UIComponent input = FacesUtils.getWebApplication().findComponent(context, forInput, component);
                if (input!=null && (input instanceof InputTag))
                {   // Check Read-Only
                    InputTag inputTag = ((InputTag)input);
                    column = inputTag.getInputColumn();
                    readOnly = inputTag.isInputReadOnly();
                    required = inputTag.isInputRequired();
                }
                else
                {   // Not found (<e:input id="ABC"...> must match <e:label for="ABC"...>
                    log.warn("Input component {} not found for label {}.", forInput, getColumn().getName());
                }                
            }
            else if (component instanceof LabelTag)
            {   // for LabelTag
                forInput = this.getColumnName();
                // readOnly
                Object val = getAttributeValueEx("readOnly");
                if (val!=null)
                    readOnly = ObjectUtils.getBoolean(val);
                else 
                    readOnly = this.isReadOnly();
            }
            else 
            {   // for ControlTag
                readOnly = this.isReadOnly();
            }
        }
        
        // Column provided?
        if (column==null)
        {   // Get from LinkTag
            column = getColumn();
            required = isValueRequired(); // does only check Attribute
        }
            
        // Check column
        if (column==null)
            throw new InvalidArgumentException("column", column);

        // create label now
        HtmlOutputLabel label = InputControlManager.createComponent(context, InputControlManager.getLabelComponentClass());
        
        // value
        String labelText = getLabelValue(column, colon);
        if (StringUtils.isEmpty(labelText))
            label.setRendered(false);
        else
            label.setValue(labelText);

        // styleClass
        if (StringUtils.isNotEmpty(styleClass))
            label.setStyleClass(completeLabelStyleClass(styleClass, required));
        
        // for 
        if (StringUtils.isNotEmpty(forInput) && !readOnly)
        {   // Set Label input Id
            InputControl.InputInfo ii = getInputInfo(context);
            String inputId = getInputControl().getLabelForId(ii);
            if (StringUtils.isNotEmpty(inputId))
            {   // input_id was given
                if (forInput.equals("*"))
                    label.setFor(inputId);
                else
                    label.setFor(forInput+":"+inputId);
            }
            else
            {   // No input-id available
                log.info("No input-id provided for {}.", getColumn().getName());
            }    
        }    

        // style
        if (StringUtils.isNotEmpty(style))
            label.setStyle(style);

        // title
        String title = getLabelTooltip(column);
        if (title!=null)
            label.setTitle(title);
        
        // required
        if (required && InputControlManager.isShowLabelRequiredMark())
            addRequiredMark(label);

        return label;
    }
    
    public void updateLabelComponent(FacesContext context, HtmlOutputLabel label, String forInput)
    {
        // Find Input Control (only if forInput Attribute has been set!)
        InputTag inputTag = null;
        if (StringUtils.isNotEmpty(forInput) && !forInput.equals("*"))
        {   // Set Label input Id
            UIComponent input = FacesUtils.getWebApplication().findComponent(context, forInput, component);
            if (input!=null && (input instanceof InputTag))
            {   // Check Read-Only
                inputTag = ((InputTag)input);
            }
        }
        // Is the Mark required?
        boolean required = (inputTag!=null ? inputTag.isInputRequired() : isValueRequired());
        // Style Class
        String styleClass = label.getStyleClass();
        label.setStyleClass(completeLabelStyleClass(styleClass, required));
        // set mark
        boolean hasMark = (label.getChildCount()>0);
        if (required==hasMark)
            return;
        // Add or remove the mark
        if (required && InputControlManager.isShowLabelRequiredMark())
            addRequiredMark(label);
        else
            label.getChildren().clear();
    }

    protected String completeLabelStyleClass(String styleClass, boolean required)
    {
        final String LABEL_REQ_STYLE = InputControl.STYLECLASS_REQUIRED;

        boolean hasRequired = StringUtils.contains(styleClass, LABEL_REQ_STYLE);
        if (required==hasRequired)
            return styleClass; // no change
        // must be empty at least
        if (styleClass==null)
            styleClass="";
        // add or remove
        if (required) {
            styleClass += LABEL_REQ_STYLE;
        }    
        else
        {   // remove both   
            styleClass = StringUtils.remove(styleClass, LABEL_REQ_STYLE);
        }    
        // done
        return styleClass;
    }
    
    protected void addRequiredMark(HtmlOutputLabel label)
    {
        HtmlPanelGroup span = new HtmlPanelGroup();
        span.setStyleClass("required");
        HtmlOutputText text = new HtmlOutputText();
        text.setValue("*");
        span.getChildren().add(text);
        label.getChildren().add(span);
    }
    
    /* ********************** CSS-generation ********************** */

    public static final String assembleStyleClassString(String tagCssStyle, String typeClass, String addlStyle, String userStyle)
    {
        // handle simple case
        if (StringUtils.isEmpty(userStyle) && StringUtils.isEmpty(addlStyle))
            return StringUtils.isEmpty(typeClass) ? tagCssStyle : tagCssStyle + typeClass;
        // assemble 
        StringBuilder b = new StringBuilder(tagCssStyle);
        if (StringUtils.isNotEmpty(typeClass))
        {   // type class must begin with a space!
            b.append(typeClass);
        }
        if (StringUtils.isNotEmpty(addlStyle))
        {   // add additional style class
            if (!addlStyle.startsWith(SPACE))
                b.append(SPACE);
            b.append(addlStyle);
        }
        if (StringUtils.isNotEmpty(userStyle) && !StringUtils.compareEqual(userStyle, addlStyle, false))
        {   // add user style class
            if (!userStyle.startsWith(SPACE))
                b.append(SPACE);
            b.append(userStyle);
        }
        return b.toString();
    }

    public static final String CSS_DATA_TYPE_NONE     = "";
    public static final String CSS_DATA_TYPE_IDENT    = " eTypeIdent";
    public static final String CSS_DATA_TYPE_NUMBER   = " eTypeNumber";
    public static final String CSS_DATA_TYPE_TEXT     = " eTypeText";
    public static final String CSS_DATA_TYPE_LONGTEXT = " eTypeLongText";
    public static final String CSS_DATA_TYPE_DATE     = " eTypeDate";
    public static final String CSS_DATA_TYPE_BOOL     = " eTypeBool";

    protected String getDataTypeClass(DataType type)
    {
        switch (type)
        {
            case AUTOINC:
                return CSS_DATA_TYPE_IDENT;
            case INTEGER:
            case DECIMAL:
            case FLOAT:
                return CSS_DATA_TYPE_NUMBER;
            case TEXT:
            case VARCHAR:
            case CHAR:
                return CSS_DATA_TYPE_TEXT;
            case DATE:
            case DATETIME:
            case TIMESTAMP:
                return CSS_DATA_TYPE_DATE;
            case BOOL:
                return CSS_DATA_TYPE_BOOL;
            case CLOB:
                return CSS_DATA_TYPE_LONGTEXT;
            default:
                return CSS_DATA_TYPE_NONE; // Not provided
        }
    }

    public String getTagStyleClass(DataType dataType, String addlStyle, String userStyle)
    {
        String typeClass = (dataType!=null) ? getDataTypeClass(dataType) : null;
        String contextStyle = getContextStyleClass(addlStyle);
        return assembleStyleClassString(cssStyleClass, typeClass, contextStyle, userStyle);
    }

    public String getTagStyleClass(DataType dataType, String addlStyle)
    {
        String userStyle = getTagAttributeStringEx("styleClass");
        return getTagStyleClass(dataType, addlStyle, userStyle);
    }

    public String getTagStyleClass(String addlStyle)
    {
        DataType dataType = (hasColumn() ? column.getDataType() : null);
        return getTagStyleClass(dataType, addlStyle);
    }

    public final String getTagStyleClass()
    {
        return getTagStyleClass((String)null);
    }
    
    protected String getContextStyleClass(String addlStyle)
    {
        String contextStyle = null;
        if ((getRecord() instanceof TagContextInfo) && hasColumn())
        {
            contextStyle = ((TagContextInfo)getRecord()).getContextStyleClass(getColumn());
        }
        if (StringUtils.isEmpty(addlStyle))
        {
            return contextStyle;
        }
        if (StringUtils.isEmpty(contextStyle))
        {
            return addlStyle;
        }
        if (contextStyle.startsWith(SPACE))
        {
            return addlStyle + contextStyle;
        }
        return addlStyle + SPACE + contextStyle;
    }
    
    public boolean isInsideUIData()
    {
        if (component==null)
            return false;
        if (this.insideUIData!=null)
            return this.insideUIData;
        // detect
        this.insideUIData = false;
        for (UIComponent p = component.getParent(); p!=null; p=p.getParent())
        {   // Check whether inside UIData
            if (p instanceof UIData) {
                this.insideUIData = true;
                break;
            }
        }
        return this.insideUIData;
    }
    
    public void saveComponentId(UIComponent comp)
    {
        if (comp==null || comp.getId()==null)
            return;
        String compId = comp.getId();
        comp.getAttributes().put(ORIGINAL_COMPONENT_ID, compId);
        comp.setId(compId); // reset
    }

    public void restoreComponentId(UIComponent comp)
    {
        if (comp==null)
            return;
        String compId = StringUtils.toString(comp.getAttributes().get(ORIGINAL_COMPONENT_ID));
        if (compId==null)
            return; // not set
        if (StringUtils.compareEqual(compId, comp.getId(), false)==false)
        {   // someone changed the id. Restore original Id
            log.warn("Restoring original Component-id from {} to {}", comp.getId(), compId);
            comp.setId(compId);
        }
    }
    
    public void resetComponentId(UIComponent comp)
    {
        if (comp==null || comp.getId()==null)
            return;
        if (isInsideUIData()) 
        {
            String resetId = comp.getId();
            if (log.isInfoEnabled())
                log.info("Resetting Component-id inside UIData to {}", resetId);
            comp.setId(resetId);
            
            /* needed ? 
            for (UIComponent c : comp.getChildren())
            {
                resetComponentId(c);
            }
            */
        }
    }
    
}
