/*
 * Copyright (C) 2005-2010 Alfresco Software Limited.
 *
 * This file is part of Alfresco
 *
 * 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 
QName to the corresponding value type */
    private static MapQName into a ValueType
     * 
     * @return Returns the ValueType  - never null
     */
    private static ValueType makeValueType(QName typeQName)
    {
        ValueType valueType = valueTypesByPropertyType.get(typeQName);
        if (valueType == null)
        {
            throw new AlfrescoRuntimeException(
                    "Property type not recognised: \n" +
                    "   type: " + typeQName);
        }
        return valueType;
    }
    
    /**
     * Given an actual type qualified name, returns the int ordinal number
     * that represents it in the database.
     * 
     * @param typeQName the type qualified name
     * @return Returns the int representation of the type,
     *      e.g. CONTENT.getOrdinalNumber() for type d:content.
     */
    public static int convertToTypeOrdinal(QName typeQName)
    {
        ValueType valueType = makeValueType(typeQName);
        return valueType.getOrdinalNumber();
    }
    
    /**
     * If property value of the type QName is supported
     * 
     * @param typeQName the type qualified name
     */
    public static boolean isDataTypeSupported(QName typeQName)
    {
        return valueTypesByPropertyType.keySet().contains(typeQName);
    }
    
    /** the type of the property, prior to serialization persistence */
    private ValueType actualType;
    /** the type of persistence used */
    private ValueType persistedType;
    
    private Boolean booleanValue;
    private Long longValue;
    private Float floatValue;
    private Double doubleValue;
    private String stringValue;
    private Serializable serializableValue;
    
    /**
     * default constructor
     */
    public NodePropertyValue()
    {
    }
    /**
     * Construct a new property value.
     * 
     * @param typeQName         the dictionary-defined property type to store the property as
     * @param value             the value to store.  This will be converted into a format compatible
     *                          with the type given
     * 
     * @throws java.lang.UnsupportedOperationException if the value cannot be converted to the type given
     */
    public NodePropertyValue(QName typeQName, Serializable value)
    {
        ParameterCheck.mandatory("typeQName", typeQName);
        if (value == null)
        {
            this.actualType = NodePropertyValue.getActualType(value);
            setPersistedValue(ValueType.NULL, null);
        }
        else
        {
            // Convert the value to the type required.  This ensures that any type conversion issues
            // are caught early and prevent the scenario where the data in the DB cannot be given
            // back out because it is unconvertable.
            ValueType valueType = makeValueType(typeQName);
            value = valueType.convert(value);
            
            this.actualType = NodePropertyValue.getActualType(value);
            // get the persisted type
            ValueType persistedValueType = this.actualType.getPersistedType(value);
            // convert to the persistent type
            value = persistedValueType.convert(value);
            setPersistedValue(persistedValueType, value);
        }
    }
    @Override
    public boolean equals(Object obj)
    {
        if (this == obj)
        {
            return true;
        }
        if (obj == null)
        {
            return false;
        }
        if (obj instanceof NodePropertyValue)
        {
            NodePropertyValue that = (NodePropertyValue) obj;
            return (this.actualType.equals(that.actualType) &&
                    EqualsHelper.nullSafeEquals(this.getPersistedValue(), that.getPersistedValue()));
        }
        else
        {
            return false;
        }
    }
    
    @Override
    public int hashCode()
    {
        int h = 0;
        if (actualType != null)
            h = actualType.hashCode();
        Serializable persistedValue = getPersistedValue();
        if (persistedValue != null)
            h += 17 * persistedValue.hashCode();
        return h;
    }
    
    @Override
    public Object clone() throws CloneNotSupportedException
    {
        return super.clone();
    }
    @Override
    public String toString()
    {
        StringBuilder sb = new StringBuilder(128);
        sb.append("PropertyValue")
          .append("[actual-type=").append(actualType)
          .append(", value-type=").append(persistedType)
          .append(", value=").append(getPersistedValue())
          .append("]");
        return sb.toString();
    }
    public Integer getActualType()
    {
        return actualType == null ? null : actualType.getOrdinalNumber();
    }
    /**
     * @return          Returns the actual type's String representation
     */
    public String getActualTypeString()
    {
        return actualType == null ? null : actualType.toString();
    }
    public void setActualType(Integer actualType)
    {
        ValueType type = NodePropertyValue.valueTypesByOrdinalNumber.get(actualType);
        if (type == null)
        {
            logger.error("Unknown property actual type ordinal number: " + actualType);
        }
        this.actualType = type;
    }
    public Integer getPersistedType()
    {
        return persistedType == null ? null : persistedType.getOrdinalNumber();
    }
    public void setPersistedType(Integer persistedType)
    {
        ValueType type = NodePropertyValue.valueTypesByOrdinalNumber.get(persistedType);
        if (type == null)
        {
            logger.error("Unknown property persisted type ordinal number: " + persistedType);
        }
        this.persistedType = type;
    }
    
    /**
     * Stores the value in the correct slot based on the type of persistence requested.
     * No conversion is done.
     * 
     * @param persistedType the value type
     * @param value the value - it may only be null if the persisted type is {@link ValueType#NULL}
     */
    public void setPersistedValue(ValueType persistedType, Serializable value)
    {
        switch (persistedType)
        {
            case NULL:
                if (value != null)
                {
                    throw new AlfrescoRuntimeException("Value must be null for persisted type: " + persistedType);
                }
                break;
            case BOOLEAN:
                this.booleanValue = (Boolean) value;
                break;
            case LONG:
                this.longValue = (Long) value;
                break;
            case FLOAT:
                this.floatValue = (Float) value;
                break;
            case DOUBLE:
                this.doubleValue = (Double) value;
                break;
            case STRING:
                this.stringValue = (String) value;
                break;
            case SERIALIZABLE:
                this.serializableValue = cloneSerializable(value);
                break;
//            case ENCRYPTED_STRING:
//                this.serializableValue = encrypt(value);
//                break;
            default:
                throw new AlfrescoRuntimeException("Unrecognised value type: " + persistedType);
        }
        // we store the type that we persisted as
        this.persistedType = persistedType;
    }
    
    /**
     * Clones a serializable object to disconnect the original instance from the persisted instance.
     * 
     * @param original          the original object
     * @return                  the new cloned object
     */
    private Serializable cloneSerializable(Serializable original)
    {
       ObjectOutputStream objectOut = null;
       ByteArrayOutputStream byteOut = null;
       ObjectInputStream objectIn = null;
        try
        {
           // Write the object out to a byte array
           byteOut = new ByteArrayOutputStream();
           objectOut = new ObjectOutputStream(byteOut);
           objectOut.writeObject(original);
           objectOut.flush();
           objectIn = new ObjectInputStream(new ByteArrayInputStream(byteOut.toByteArray()));
           Object target = objectIn.readObject();
           // Done
           return (Serializable) target;
        }
        catch (Throwable e)
        {
            throw new AlfrescoRuntimeException("Failed to clone serializable object: " + original, e);
        }
        finally
        {
           if (objectOut != null)
           {
              try { objectOut.close(); } catch (Throwable e) {}
           }
           if (byteOut != null)
           {
              try { byteOut.close(); } catch (Throwable e) {}
           }
           if (objectIn != null)
           {
              try { objectIn.close(); } catch (Throwable e) {}
           }
        }
    }
    /**
     * @return Returns the persisted value, keying off the persisted value type
     */
    private Serializable getPersistedValue()
    {
        switch (persistedType)
        {
            case NULL:
                return null;
            case BOOLEAN:
                return this.booleanValue;
            case LONG:
                return this.longValue;
            case FLOAT:
                return this.floatValue;
            case DOUBLE:
                return this.doubleValue;
            case STRING:
                // Oracle stores empty strings as 'null'...
                if (this.stringValue == null)
                {
                    // We know that we stored a non-null string, but now it is null.
                    // It can only mean one thing - Oracle
                    if (loggerOracle.isDebugEnabled())
                    {
                        logger.debug("string_value is 'null'.  Forcing to empty String");
                    }
                    return NodePropertyValue.STRING_EMPTY;
                }
                else
                {
                    return this.stringValue;
                }
            case SERIALIZABLE:
                return this.serializableValue;
            default:
                throw new AlfrescoRuntimeException("Unrecognised value type: " + persistedType);
        }
    }
    /**
     * Fetches the value as a desired type.  Collections (i.e. multi-valued properties)
     * will be converted as a whole to ensure that all the values returned within the
     * collection match the given type.
     * 
     * @param typeQName the type required for the return value
     * @return Returns the value of this property as the desired type, or a Collection
     *      of values of the required type
     * 
     * @throws AlfrescoRuntimeException
     *      if the type given is not recognized
     * @throws org.alfresco.service.cmr.repository.datatype.TypeConversionException
     *      if the conversion to the required type fails
     * 
     * @see DataTypeDefinition#ANY The static qualified names for the types
     */
    public Serializable getValue(QName typeQName)
    {
        // first check for null
        ValueType requiredType = makeValueType(typeQName);
        if (requiredType == ValueType.SERIALIZABLE)
        {
            // the required type must be the actual type
            requiredType = this.actualType;
        }
        
        // we need to convert
        Serializable ret = null;
        if (actualType == ValueType.COLLECTION && persistedType == ValueType.NULL)
        {
            // This is a special case of an empty collection
            ret = (Serializable) Collections.emptyList();
        }
        else if (persistedType == ValueType.NULL)
        {
            ret = null;
        }
        else
        {
            Serializable persistedValue = getPersistedValue();
            // convert the type
            // In order to cope with historical data, where collections were serialized
            // regardless of type.
            if (persistedValue instanceof Collection>)
            {
                // We assume that the collection contained the correct type values.  They would
                // have been converted on the way in.
                ret = (Serializable) persistedValue;
            }
            else if(persistedValue instanceof SealedObject)
            {
                ret = (Serializable) persistedValue;
            }
            else
            {
                ret = requiredType.convert(persistedValue);
            }
        }
        // done
        if (logger.isDebugEnabled())
        {
            logger.debug("Fetched value: \n" +
                    "   property value: " + this + "\n" +
                    "   requested type: " + requiredType + "\n" +
                    "   result: " + ret);
        }
        return ret;
    }
    
    /**
     * Gets the value or values as a guaranteed collection.
     * 
     * @see #getValue(QName)
     */
    @SuppressWarnings("unchecked")
    public Collection