/**************************************************************************************
 * Copyright (C) 2008 EsperTech, Inc. All rights reserved.                            *
 * http://esper.codehaus.org                                                          *
 * http://www.espertech.com                                                           *
 * ---------------------------------------------------------------------------------- *
 * The software in this package is published under the terms of the GPL license       *
 * a copy of which has been included with this distribution in the license.txt file.  *
 **************************************************************************************/
package com.espertech.esper.event.vaevent;

import com.espertech.esper.util.JavaClassHelper;
import com.espertech.esper.util.SimpleTypeCaster;
import com.espertech.esper.util.SimpleTypeCasterFactory;
import com.espertech.esper.client.EventBean;
import com.espertech.esper.client.EventType;
import com.espertech.esper.client.EventPropertyGetter;
import com.espertech.esper.client.PropertyAccessException;

import java.util.LinkedHashSet;
import java.util.Set;

/**
 * A property resolution strategy that allows only the preconfigured types, wherein all properties
 * that are common (name and type) to all properties are considered.
 */
public class VariantPropResolutionStrategyDefault implements VariantPropResolutionStrategy
{
    private int currentPropertyNumber;
    private VariantPropertyGetterCache propertyGetterCache;

    /**
     * Ctor.
     * @param variantSpec specified the preconfigured types
     */
    public VariantPropResolutionStrategyDefault(VariantSpec variantSpec)
    {
        propertyGetterCache = new VariantPropertyGetterCache(variantSpec.getEventTypes());
    }

    public VariantPropertyDesc resolveProperty(String propertyName, EventType[] variants)
    {
        boolean existsInAll = true;
        Class commonType = null;
        boolean mustCoerce = false;
        for (int i = 0; i < variants.length; i++)
        {
            Class type = JavaClassHelper.getBoxedType(variants[i].getPropertyType(propertyName));
            if (type == null)
            {
                existsInAll = false;
                continue;
            }

            if (commonType == null)
            {
                commonType = type;
                continue;
            }

            // compare types
            if (type.equals(commonType))
            {
                continue;
            }

            // coercion
            if (JavaClassHelper.isNumeric(type))
            {
                if (JavaClassHelper.canCoerce(type, commonType))
                {
                    mustCoerce = true;
                    continue;
                }
                if (JavaClassHelper.canCoerce(commonType, type))
                {
                    mustCoerce = true;
                    commonType = type;
                }
            }
            else if (commonType == Object.class)
            {
                continue;
            }
            // common interface or base class
            else if (!JavaClassHelper.isJavaBuiltinDataType(type))
            {
                Set<Class> supersForType = new LinkedHashSet<Class>();
                JavaClassHelper.getSuper(type, supersForType);
                supersForType.remove(Object.class);

                if (supersForType.contains(commonType))
                {
                    continue;   // type implements or extends common type
                }
                if (JavaClassHelper.isSubclassOrImplementsInterface(commonType, type))
                {
                    commonType = type;  // common type implements type
                    continue;
                }

                // find common interface or type both implement
                Set<Class> supersForCommonType = new LinkedHashSet<Class>();
                JavaClassHelper.getSuper(commonType, supersForCommonType);
                supersForCommonType.remove(Object.class);

                // Take common classes first, ignoring interfaces
                boolean found = false;
                for (Class superClassType : supersForType)
                {
                    if (!superClassType.isInterface() && (supersForCommonType.contains(superClassType)))
                    {
                        commonType = superClassType;
                        found = true;
                        break;
                    }
                }
                if (found)
                {
                    continue;
                }
                // Take common interfaces
                for (Class superClassType : supersForType)
                {
                    if (superClassType.isInterface() && (supersForCommonType.contains(superClassType)))
                    {
                        break;
                    }
                }
            }

            commonType = Object.class;
        }

        if (!existsInAll)
        {
            return null;
        }

        if (commonType == null)
        {
            return null;
        }

        // property numbers should start at zero since the serve as array index
        final int assignedPropertyNumber = currentPropertyNumber;
        currentPropertyNumber++;
        propertyGetterCache.addGetters(assignedPropertyNumber, propertyName);

        EventPropertyGetter getter;
        if (mustCoerce)
        {
            final SimpleTypeCaster caster = SimpleTypeCasterFactory.getCaster(null, commonType);
            getter = new EventPropertyGetter()
            {
                public Object get(EventBean eventBean) throws PropertyAccessException
                {
                    VariantEvent variant = (VariantEvent) eventBean;
                    EventPropertyGetter getter = propertyGetterCache.getGetter(assignedPropertyNumber, variant.getUnderlyingEventBean().getEventType());
                    if (getter == null)
                    {
                        return null;
                    }
                    Object value = getter.get(variant.getUnderlyingEventBean());
                    if (value == null)
                    {
                        return value;
                    }
                    return caster.cast(value);
                }

                public boolean isExistsProperty(EventBean eventBean)
                {
                    VariantEvent variant = (VariantEvent) eventBean;
                    EventPropertyGetter getter = propertyGetterCache.getGetter(assignedPropertyNumber, variant.getUnderlyingEventBean().getEventType());
                    if (getter == null)
                    {
                        return false;
                    }
                    return getter.isExistsProperty(variant.getUnderlyingEventBean());
                }

                public Object getFragment(EventBean eventBean)
                {
                    return null;
                }
            };
        }
        else
        {
            getter = new EventPropertyGetter()
            {
                public Object get(EventBean eventBean) throws PropertyAccessException
                {
                    VariantEvent variant = (VariantEvent) eventBean;
                    EventPropertyGetter getter = propertyGetterCache.getGetter(assignedPropertyNumber, variant.getUnderlyingEventBean().getEventType());
                    if (getter == null)
                    {
                        return null;
                    }
                    return getter.get(variant.getUnderlyingEventBean());
                }

                public boolean isExistsProperty(EventBean eventBean)
                {
                    VariantEvent variant = (VariantEvent) eventBean;
                    EventPropertyGetter getter = propertyGetterCache.getGetter(assignedPropertyNumber, variant.getUnderlyingEventBean().getEventType());
                    if (getter == null)
                    {
                        return false;
                    }
                    return getter.isExistsProperty(variant.getUnderlyingEventBean());
                }

                public Object getFragment(EventBean eventBean)
                {
                    return null;
                }
            };
        }

        return new VariantPropertyDesc(commonType, getter, true);
    }
}
