/*
 * #%L
 * Alfresco Repository
 * %%
 * Copyright (C) 2005 - 2016 Alfresco Software Limited
 * %%
 * This file is part of the Alfresco software. 
 * If the software was purchased under a paid Alfresco license, the terms of 
 * the paid license agreement will prevail.  Otherwise, the software is 
 * provided under the following open source license terms:
 * 
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Alfresco is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 * #L%
 */
package org.alfresco.repo.domain.propval;

import java.io.Serializable;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.alfresco.util.EqualsHelper;
import org.alfresco.util.Pair;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;


/**
 * Entity bean for <b>alf_prop_value</b> table.
 * <p>
 * Values here are either simple values that can be stored in a <code>long</code>
 * or will be references to data in other tables.
 * 
 * @author Derek Hulley
 * @since 3.2
 */
public class PropertyValueEntity
{
    public static final Long LONG_ZERO = new Long(0L);
    public static final Long LONG_ONE = new Long(1L);

    public static final Short ORDINAL_NULL = 0;
    public static final Short ORDINAL_LONG = 1;
    public static final Short ORDINAL_DOUBLE = 2;
    public static final Short ORDINAL_STRING = 3;
    public static final Short ORDINAL_SERIALIZABLE = 4;
    public static final Short ORDINAL_CONSTRUCTABLE = 5;
    public static final Short ORDINAL_ENUM = 6;
    
    /**
     * Enumeration of persisted types for <b>alf_prop_value.persisted_type</b>.
     * <p/>
     * This enumeration is a helper for the default implementation of the {@link PropertyTypeConverter}
     * and should not be used in public interfaces.
     * 
     * @author Derek Hulley
     * @since 3.2
     */
    public static enum PersistedType
    {
        NULL
        {
            @Override
            public Short getOrdinalNumber()
            {
                return ORDINAL_NULL;
            }
            @Override
            public Class<?> getAssociatedClass()
            {
                throw new UnsupportedOperationException("NULL is a special case and has no associated class.");
            }
        },
        LONG
        {
            @Override
            public Short getOrdinalNumber()
            {
                return ORDINAL_LONG;
            }
            @Override
            public Class<?> getAssociatedClass()
            {
                return Long.class;
            }
        },
        DOUBLE
        {
            @Override
            public Short getOrdinalNumber()
            {
                return ORDINAL_DOUBLE;
            }
            @Override
            public Class<?> getAssociatedClass()
            {
                return Double.class;
            }
        },
        STRING
        {
            @Override
            public Short getOrdinalNumber()
            {
                return ORDINAL_STRING;
            }
            @Override
            public Class<?> getAssociatedClass()
            {
                return String.class;
            }
        },
        SERIALIZABLE
        {
            @Override
            public Short getOrdinalNumber()
            {
                return ORDINAL_SERIALIZABLE;
            }
            @Override
            public Class<?> getAssociatedClass()
            {
                return Serializable.class;
            }
        },
        CONSTRUCTABLE
        {
            @Override
            public Short getOrdinalNumber()
            {
                return ORDINAL_CONSTRUCTABLE;
            }
            @Override
            public Class<?> getAssociatedClass()
            {
                return Class.class;
            }
        },
        ENUM
        {
            @Override
            public Short getOrdinalNumber()
            {
                return ORDINAL_ENUM;
            }
            @Override
            public Class<?> getAssociatedClass()
            {
                return Enum.class;
            }
        };
        
        /**
         * Fetch the numerical value that will represent the the persisted type.  This is done
         * explicitly to prevent ordering issues if further types are added.
         * 
         * @return              Returns the ordinal number
         */
        public abstract Short getOrdinalNumber();
        
        /**
         * Get the persisted type's class.  This is used for determining the source type when
         * converting from persisted values.
         * 
         * @return              Returns the class associated with the persisted type
         */
        public abstract Class<?> getAssociatedClass();
    }

    public static final Pair<Short, Serializable> PERSISTED_TYPE_NULL;
    /**
     * An unmodifiable map of persisted type enums keyed by their ordinal number
     */
    public static final Map<Short, PersistedType> persistedTypesByOrdinal;
    
    static
    {
        // Create a pair for null values
        PERSISTED_TYPE_NULL = new Pair<Short, Serializable>(PersistedType.NULL.getOrdinalNumber(), new Long(0));
        // Create the map of ordinal-type
        Map<Short, PersistedType> mapOrdinal = new HashMap<Short, PersistedType>(15);
        for (PersistedType persistedType : PersistedType.values())
        {
            mapOrdinal.put(persistedType.getOrdinalNumber(), persistedType);
        }
        persistedTypesByOrdinal = Collections.unmodifiableMap(mapOrdinal);
    }

    private static final Log logger = LogFactory.getLog(PropertyValueEntity.class);
    
    private Long id;
    private Long actualTypeId;
    private Short persistedType;
    private PersistedType persistedTypeEnum;            // Derived
    private Long longValue;
    private String stringValue;
    private Double doubleValue;
    private Serializable serializableValue;
    
    public PropertyValueEntity()
    {
        this.persistedType = PersistedType.NULL.getOrdinalNumber();
        this.longValue = LONG_ZERO;
    }
    
    @Override
    public int hashCode()
    {
        return (actualTypeId == null ? 0 : actualTypeId.intValue()) + (longValue == null ? 0 : longValue.intValue());
    }
    
    @Override
    public boolean equals(Object obj)
    {
        if (this == obj)
        {
            return true;
        }
        else if (obj != null && obj instanceof PropertyValueEntity)
        {
            PropertyValueEntity that = (PropertyValueEntity) obj;
            return EqualsHelper.nullSafeEquals(this.actualTypeId, that.actualTypeId) &&
                   EqualsHelper.nullSafeEquals(this.longValue, that.longValue);
        }
        else
        {
            return false;
        }
    }
    
    @Override
    public String toString()
    {
        StringBuilder sb = new StringBuilder(512);
        sb.append("PropertyValueEntity")
          .append("[ ID=").append(id)
          .append(", actualTypeId=").append(actualTypeId)
          .append(", persistedType=").append(persistedType)
          .append(", value=").append(longValue)
          .append("]");
        return sb.toString();
    }
    
    /**
     * Helper method to get the value based on the persisted type. 
     * 
     * @param actualType        the type to convert to
     * @param converter         the data converter to use
     * @return                  Returns the converted value
     */
    public Serializable getValue(Class<Serializable> actualType, PropertyTypeConverter converter)
    {
        switch (persistedTypeEnum)
        {
        case NULL:
            return null;
        case LONG:
            return converter.convert(actualType, Long.valueOf(longValue));
        case DOUBLE:
            return converter.convert(actualType, Double.valueOf(doubleValue));
        case STRING:
            if (stringValue != null && stringValue.equals(PropertyStringValueEntity.EMPTY_STRING_REPLACEMENT))
            {
                return converter.convert(actualType, PropertyStringValueEntity.EMPTY_STRING);
            }
            else
            {
                return converter.convert(actualType, stringValue);
            }
        case SERIALIZABLE:
            return converter.convert(actualType, serializableValue);
        case CONSTRUCTABLE:
            // Construct an instance using the converter (it knows best!)
            return converter.constructInstance(stringValue);
        case ENUM:
            // The converter should handle enumeration types
            return converter.convert(actualType, stringValue);
        default:
            throw new IllegalStateException("Should not be able to get through switch");
        }
    }
    
    /**
     * Shortcut method to set the value.  It will be converted as required and the necessary fields
     * will be populated.
     * 
     * @param value         the value to persist (may be <tt>null</tt>)
     * @param converter     the converter that will perform and type conversion
     */
    public void setValue(Serializable value, PropertyTypeConverter converter)
    {
        if (value == null)
        {
            this.persistedType = ORDINAL_NULL;
            this.persistedTypeEnum = PersistedType.NULL;
            this.longValue = LONG_ZERO;
        }
        else
        {
            // The converter will be responsible for deserializing, so let it choose
            // how the data is to be stored.
            persistedTypeEnum = converter.getPersistentType(value);
            persistedType = persistedTypeEnum.getOrdinalNumber();
            // Get the class to persist as
            switch (persistedTypeEnum)
            {
                case LONG:
                    longValue = converter.convert(Long.class, value);
                    break;
                case DOUBLE:
                    doubleValue = converter.convert(Double.class, value);
                    break;
                case STRING:
                    stringValue = converter.convert(String.class, value);
                    if (stringValue.equals(PropertyStringValueEntity.EMPTY_STRING))
                    {
                        // Oracle: We can't insert empty strings into the column.
                        stringValue = PropertyStringValueEntity.EMPTY_STRING_REPLACEMENT;
                    }
                    break;
                case CONSTRUCTABLE:
                    // A special case.  There is no conversion, so just Store the name of the class.
                    stringValue = value.getClass().getName();
                    break;
                case ENUM:
                    // A special case.  Store the string-equivalent representation
                    stringValue = converter.convert(String.class, value);
                    break;
                case SERIALIZABLE:
                    serializableValue = value;
                    break;
                default:
                    throw new IllegalStateException(
                            "PropertyTypeConverter.convertToPersistentType returned illegal type: " +
                            "   Converter:      " + converter + "\n" +
                            "   Type Returned:  " + persistedTypeEnum + "\n" +
                            "   From Value:     " + value);
            }
        }
    }
    
    /**
     * Helper method to determine how the given value will be stored.
     * 
     * @param value         the value to check
     * @param converter     the type converter
     * @return              Returns the persisted type
     * 
     * @see PropertyTypeConverter#getPersistentType(Serializable)
     */
    public static PersistedType getPersistedTypeEnum(Serializable value, PropertyTypeConverter converter)
    {
        PersistedType persistedTypeEnum;
        if (value == null)
        {
            persistedTypeEnum = PersistedType.NULL;
        }
        else
        {
            persistedTypeEnum = converter.getPersistentType(value);
        }
        return persistedTypeEnum;
    }
    
    public PersistedType getPersistedTypeEnum()
    {
        return persistedTypeEnum;
    }
    
    public Long getId()
    {
        return id;
    }

    public void setId(Long id)
    {
        this.id = id;
    }

    public Long getActualTypeId()
    {
        return actualTypeId;
    }

    public void setActualTypeId(Long actualTypeId)
    {
        this.actualTypeId = actualTypeId;
    }

    public Short getPersistedType()
    {
        return persistedType;
    }

    public void setPersistedType(Short persistedType)
    {
        this.persistedType = persistedType;
        this.persistedTypeEnum = persistedTypesByOrdinal.get(persistedType);
        if (persistedTypeEnum == null)
        {
            logger.error("Persisted type '" + persistedType + "' is not recognised.");
            this.persistedTypeEnum = PersistedType.LONG;
        }
    }

    public Long getLongValue()
    {
        return longValue;
    }

    public void setLongValue(Long longValue)
    {
        this.longValue = longValue;
    }

    public String getStringValue()
    {
        return stringValue;
    }

    public void setStringValue(String stringValue)
    {
        if (stringValue == null)
        {
            // Oracle!  It pulls nulls out in place of empty strings.
            //  Since we don't put nulls into the DB (the column doesn't allow it)
            //  we can be sure that this is an Oracle empty string
            stringValue = "";
        }
        this.stringValue = stringValue;
    }

    public Double getDoubleValue()
    {
        return doubleValue;
    }

    public void setDoubleValue(Double doubleValue)
    {
        this.doubleValue = doubleValue;
    }

    public Serializable getSerializableValue()
    {
        return serializableValue;
    }

    public void setSerializableValue(Serializable serializableValue)
    {
        this.serializableValue = serializableValue;
    }
}