/**
 *
 */
package org.richfaces.el;

import java.beans.FeatureDescriptor;
import java.util.Collection;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;

import javax.el.ELContext;
import javax.el.ELResolver;
import javax.el.FunctionMapper;
import javax.el.ValueExpression;
import javax.el.VariableMapper;
import javax.faces.context.FacesContext;
import javax.faces.el.CompositeComponentExpressionHolder;

import org.richfaces.validator.GraphValidatorState;

/**
 * This class wraps original ELContext and capture whole call stack to the target object so it could be used to extract semantic
 * information like annotations or Jena Model properties.
 *
 * @author asmirnov
 *
 */
public class CapturingELContext extends ELContext {
    private final ELContext parent;
    private ValueReference reference = null;
    private final InterceptingResolver resolver;

    public CapturingELContext(ELContext parent,Map<Object, GraphValidatorState> states) {
        this.parent = parent;
        resolver = new InterceptingResolver(parent.getELResolver(),states);
    }

    public ValueReference getReference() {
        return reference;
    }

    private boolean isContainerObject(Object base) {
        return base instanceof Collection || base instanceof Map || base.getClass().isArray();
    }

    public boolean hasReferenceExpression() {
        return reference != null && reference.getBase() instanceof CompositeComponentExpressionHolder;
    }

    public ValueExpression getReferenceExpression() {
        CompositeComponentExpressionHolder expressionHolder = (CompositeComponentExpressionHolder) reference.getBase();
        return expressionHolder.getExpression(reference.getProperty().toString());
    }

    public ValueDescriptor getDescriptor() {
        ValueReference localReference = reference;

        while (true) {
            if (localReference == null || localReference.getBase() == null || localReference.getProperty() == null) {
                return null;
            }

            Object base = localReference.getBase();

            if (isContainerObject(base) && localReference.hasNext()) {
                localReference = localReference.next();
            } else {
                return new ValueDescriptor(base.getClass(), localReference.getProperty().toString());
            }
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.el.ELContext#getELResolver()
     */
    @Override
    public ELResolver getELResolver() {
        return resolver;
    }

    @SuppressWarnings("unchecked")
    @Override
    public Object getContext(Class key) {
        return parent.getContext(key);
    }

    @Override
    public FunctionMapper getFunctionMapper() {
        return parent.getFunctionMapper();
    }

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

    @Override
    public VariableMapper getVariableMapper() {
        return parent.getVariableMapper();
    }

    @SuppressWarnings("unchecked")
    @Override
    public void putContext(Class key, Object contextObject) {
        parent.putContext(key, contextObject);
    }

    @Override
    public void setLocale(Locale locale) {
        parent.setLocale(locale);
    }

    /**
     * This resolver records all intermediate objects from the EL-expression that can be used to detect Semantic Beans
     * annotations or Jena Model properties.
     *
     * @author asmirnov
     *
     */
    private final class InterceptingResolver extends ELResolver {
        private final ELResolver delegate;
        private boolean clonedObject;
        private final Map<Object, GraphValidatorState> states;

        public InterceptingResolver(ELResolver delegate, Map<Object, GraphValidatorState> states) {
            this.delegate = delegate;
            this.states = states;
        }

        // Capture the base and property rather than write the value
        @Override
        public void setValue(ELContext context, Object base, Object property, Object value) {
            if (base != null) {
                // TODO - detect value object from inderect references ( e.g. data table variables ).
                if (this.clonedObject) {
                    delegate.setValue(context, base, property, value);
                }

                context.setPropertyResolved(true);
                reference = new ValueReference(base, property, reference);
            }
        }

        // The rest of the methods simply delegate to the existing context

        @Override
        public Object getValue(ELContext context, Object base, Object property) {
            reference = new ValueReference(base, property, reference);
            Object value = delegate.getValue(context, base, property);
            if (null != value && context.isPropertyResolved() && states.containsKey(value)) {
                GraphValidatorState graphValidatorState = states.get(value);
                if (graphValidatorState.isActive()) {
                    this.clonedObject = true;
                    Object clone = graphValidatorState.getCloned();
                    return clone;
                }
            }
            return value;
        }

        @Override
        public Class<?> getType(ELContext context, Object base, Object property) {
            if (base != null) {
                context.setPropertyResolved(true);
                reference = new ValueReference(base, property, reference);
            }
            return delegate.getType(context, base, property);
        }

        @Override
        public boolean isReadOnly(ELContext context, Object base, Object property) {
            return delegate.isReadOnly(context, base, property);
        }

        @Override
        public Iterator<FeatureDescriptor> getFeatureDescriptors(ELContext context, Object base) {
            return delegate.getFeatureDescriptors(context, base);
        }

        @Override
        public Class<?> getCommonPropertyType(ELContext context, Object base) {
            return delegate.getCommonPropertyType(context, base);
        }
    }
}
