/*
 * Copyright (C) 2009 eXo Platform SAS.
 *
 * This 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 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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 this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.exoplatform.services.jcr.impl.core;

import org.exoplatform.services.jcr.core.nodetype.ExtendedNodeTypeManager;
import org.exoplatform.services.jcr.core.nodetype.ItemDefinitionData;
import org.exoplatform.services.jcr.core.nodetype.PropertyDefinitionData;
import org.exoplatform.services.jcr.core.nodetype.PropertyDefinitionDatas;
import org.exoplatform.services.jcr.datamodel.InternalQName;
import org.exoplatform.services.jcr.datamodel.ItemData;
import org.exoplatform.services.jcr.datamodel.NodeData;
import org.exoplatform.services.jcr.datamodel.PropertyData;
import org.exoplatform.services.jcr.impl.core.nodetype.PropertyDefinitionImpl;
import org.exoplatform.services.jcr.impl.core.value.BaseValue;
import org.exoplatform.services.jcr.impl.core.value.ReferenceValue;
import org.exoplatform.services.jcr.impl.dataflow.ValueDataUtil;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;

import java.io.InputStream;
import java.util.Calendar;

import javax.jcr.ItemVisitor;
import javax.jcr.Node;
import javax.jcr.Property;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Value;
import javax.jcr.ValueFormatException;
import javax.jcr.lock.LockException;
import javax.jcr.nodetype.ConstraintViolationException;
import javax.jcr.nodetype.PropertyDefinition;
import javax.jcr.version.VersionException;

/**
 * Created by The eXo Platform SAS.
 * 
 * @author Gennady Azarenkov
 * @version $Id: PropertyImpl.java 11907 2008-03-13 15:36:21Z ksm $
 */
public class PropertyImpl extends ItemImpl implements Property
{

   /**
    * Logger.
    */
   protected static final Log LOG = ExoLogger.getLogger("exo.jcr.component.core.PropertyImpl");

   /**
    * Value type.
    */
   protected int type;

   private PropertyDefinitionData propertyDef;

   private PropertyData propertyData;

   /**
    * PropertyImpl constructor.
    * 
    * @param data
    *          ItemData object
    * @param session
    *          Session object
    * @throws RepositoryException
    * @throws ConstraintViolationException
    */
   PropertyImpl(ItemData data, SessionImpl session) throws RepositoryException, ConstraintViolationException
   {
      super(data, session);
      loadData(data);
   }

   /**
    * PropertyImpl constructor.
    * 
    * @param data
    *          ItemData object
    * @param parent NodeData Property's parent
    * @param session
    *          Session object
    * @throws RepositoryException
    * @throws ConstraintViolationException
    */
   PropertyImpl(ItemData data, NodeData parent, SessionImpl session) throws RepositoryException,
      ConstraintViolationException
   {
      super(data, session);
      loadData(data, parent);
   }

   /**
    * {@inheritDoc}
    */
   void loadData(ItemData data) throws RepositoryException, ConstraintViolationException
   {
      loadData(data, (NodeData)null);
   }

   /**
    * {@inheritDoc}
    */
   void loadData(ItemData data, NodeData parent) throws RepositoryException, ConstraintViolationException
   {
      if (data.isNode())
      {
         throw new RepositoryException("Load data failed: Property expected");
      }

      this.data = data;
      this.propertyData = (PropertyData)data;
      this.type = propertyData.getType();

      this.qpath = data.getQPath();
      this.location = null;

      initDefinitions(this.propertyData.isMultiValued(), parent);
   }

   /**
    * {@inheritDoc}
    */
   public ItemDefinitionData getItemDefinitionData()
   {
      return propertyDef;
   }

   /**
    * {@inheritDoc}
    */
   public Value getValue() throws ValueFormatException, RepositoryException
   {
      checkValid();

      if (isMultiValued())
      {
         throw new ValueFormatException("The property " + getPath() + " is multi-valued (6.2.4)");
      }

      if (propertyData.getValues() != null && propertyData.getValues().size() == 0)
      {
         throw new ValueFormatException("The single valued property " + getPath() + " is empty");
      }

      return valueFactory.loadValue(propertyData.getValues().get(0), propertyData.getType());

   }

   /**
    * {@inheritDoc}
    */
   public Value[] getValues() throws ValueFormatException, RepositoryException
   {
      checkValid();

      // Check property definition and life-state flag both
      if (!isMultiValued())
      {
         throw new ValueFormatException("The property " + getPath() + " is single-valued (6.2.4)");
      }

      // The array returned is a copy of the stored values
      return getValueArray();
   }

   /**
    * {@inheritDoc}
    */
   public String getString() throws ValueFormatException, RepositoryException
   {
      try
      {
         return getValue().getString();
      }
      catch (ValueFormatException e)
      {
         throw new ValueFormatException("PropertyImpl.getString() for " + getPath() + " failed: " + e);
      }
      catch (IllegalStateException e)
      {
         throw new ValueFormatException("PropertyImpl.getString() for " + getPath() + " failed: " + e);
      }
   }

   /**
    * {@inheritDoc}
    */
   public double getDouble() throws ValueFormatException, RepositoryException
   {
      try
      {
         return getValue().getDouble();
      }
      catch (IllegalStateException e)
      {
         throw new ValueFormatException("PropertyImpl.getDouble() failed: " + e);
      }
   }

   /**
    * {@inheritDoc}
    */
   public long getLong() throws ValueFormatException, RepositoryException
   {
      try
      {
         return getValue().getLong();
      }
      catch (IllegalStateException e)
      {
         throw new ValueFormatException("PropertyImpl.getLong() failed: " + e);
      }
   }

   /**
    * {@inheritDoc}
    */
   public InputStream getStream() throws ValueFormatException, RepositoryException
   {
      try
      {
         return getValue().getStream();
      }
      catch (IllegalStateException e)
      {
         throw new ValueFormatException("PropertyImpl.getStream() failed: " + e);
      }
   }

   /**
    * {@inheritDoc}
    */
   public Calendar getDate() throws ValueFormatException, RepositoryException
   {
      try
      {
         return getValue().getDate();
      }
      catch (IllegalStateException e)
      {
         throw new ValueFormatException("PropertyImpl.getDate() failed: " + e);
      }
   }

   /**
    * {@inheritDoc}
    */
   public boolean getBoolean() throws ValueFormatException, RepositoryException
   {
      try
      {
         return getValue().getBoolean();
      }
      catch (IllegalStateException e)
      {
         throw new ValueFormatException("PropertyImpl.getBoolean() failed: " + e);
      }
   }

   /**
    * {@inheritDoc}
    */
   public Node getNode() throws ValueFormatException, RepositoryException
   {
      Value value = getValue();

      if (value instanceof ReferenceValue)
      {
         String identifier = ((BaseValue)value).getReference();
         return session.getNodeByUUID(identifier);
      }
      else
      {
         throw new ValueFormatException("Property cannot be converted to a reference");
      }
   }

   /**
    * {@inheritDoc}
    */
   public long getLength() throws ValueFormatException, RepositoryException
   {
      return ((BaseValue)getValue()).getLength();
   }

   /**
    * {@inheritDoc}
    */
   public long[] getLengths() throws ValueFormatException, RepositoryException
   {
      Value[] thisValues = getValues();

      long[] lengths = new long[thisValues.length];
      for (int i = 0; i < lengths.length; i++)
      {
         lengths[i] = ((BaseValue)thisValues[i]).getLength();
      }
      return lengths;
   }

   /**
    * {@inheritDoc}
    */
   public PropertyDefinition getDefinition() throws RepositoryException
   {
      checkValid();

      if (propertyDef == null)
      {
         throw new RepositoryException("FATAL: property definition is NULL " + getPath() + " "
            + propertyData.getValues());
      }

      Value[] defaultValues = new Value[propertyDef.getDefaultValues().length];
      String[] propVal = propertyDef.getDefaultValues();
      // there can be null in definition but should not be null value
      if (propVal != null)
      {
         for (int i = 0; i < propVal.length; i++)
         {
            if (propertyDef.getRequiredType() == PropertyType.UNDEFINED)
               defaultValues[i] = valueFactory.createValue(propVal[i]);
            else
               defaultValues[i] = valueFactory.createValue(propVal[i], propertyDef.getRequiredType());
         }
      }

      return new PropertyDefinitionImpl(propertyDef, session.getWorkspace().getNodeTypesHolder(),
         (ExtendedNodeTypeManager)session.getWorkspace().getNodeTypeManager(), session.getSystemLocationFactory(),
         session.getValueFactory(), session.getTransientNodesManager());
   }

   /**
    * @param multiple
    * @param parent
    * @throws RepositoryException
    * @throws ConstraintViolationException
    */
   private void initDefinitions(boolean multiple, NodeData parent) throws RepositoryException,
      ConstraintViolationException
   {
      InternalQName pname = getData().getQPath().getName();

      if (parent == null)
      {
         parent = parentData();
      }

      PropertyDefinitionDatas definitions =
         session.getWorkspace().getNodeTypesHolder().getPropertyDefinitions(pname, parent.getPrimaryTypeName(),
            parent.getMixinTypeNames());

      if (definitions == null)
      {
         throw new ConstraintViolationException("Definition for property " + getPath() + " not found.");
      }

      propertyDef = definitions.getDefinition(multiple);
   }

   /**
    * {@inheritDoc}
    */
   public int getType()
   {
      return type;
   }

   /**
    * {@inheritDoc}
    */
   public void setValue(Value value) throws ValueFormatException, VersionException, LockException,
      ConstraintViolationException, RepositoryException
   {
      checkValid();

      doUpdateProperty(parent(), getInternalName(), value, false, PropertyType.UNDEFINED);
   }

   /**
    * {@inheritDoc}
    */
   public void setValue(Value[] values) throws ValueFormatException, VersionException, LockException,
      ConstraintViolationException, RepositoryException
   {
      checkValid();

      doUpdateProperty(parent(), getInternalName(), values, true, PropertyType.UNDEFINED);
   }

   /**
    * Check if property is multi valued.
    * 
    * @return multiValued property of data field (PropertyData) it's a life-state property field
    *         which contains multiple-valued flag for value(s) data. Can be set in property creation
    *         time or from persistent storage.
    */
   public boolean isMultiValued()
   {
      return ((PropertyData)data).isMultiValued();
   }

   /**
    * {@inheritDoc}
    */
   public void setValue(String value) throws ValueFormatException, VersionException, LockException,
      ConstraintViolationException, RepositoryException
   {
      setValue(valueFactory.createValue(value));
   }

   /**
    * {@inheritDoc}
    */
   public void setValue(InputStream stream) throws ValueFormatException, VersionException, LockException,
      ConstraintViolationException, RepositoryException
   {
      setValue(valueFactory.createValue(stream));
   }

   /**
    * {@inheritDoc}
    */
   public void setValue(double number) throws ValueFormatException, VersionException, LockException,
      ConstraintViolationException, RepositoryException
   {
      setValue(valueFactory.createValue(number));
   }

   /**
    * {@inheritDoc}
    */
   public void setValue(long number) throws ValueFormatException, VersionException, LockException,
      ConstraintViolationException, RepositoryException
   {
      setValue(valueFactory.createValue(number));
   }

   /**
    * {@inheritDoc}
    */
   public void setValue(Calendar date) throws ValueFormatException, VersionException, LockException,
      ConstraintViolationException, RepositoryException
   {
      setValue(valueFactory.createValue(date));
   }

   /**
    * {@inheritDoc}
    */
   public void setValue(boolean b) throws ValueFormatException, VersionException, LockException,
      ConstraintViolationException, RepositoryException
   {
      setValue(valueFactory.createValue(b));
   }

   /**
    * {@inheritDoc}
    */
   public void setValue(Node value) throws ValueFormatException, VersionException, LockException,
      ConstraintViolationException, RepositoryException
   {
      setValue(valueFactory.createValue(value));
   }

   /**
    * {@inheritDoc}
    */
   public void setValue(String[] values) throws ValueFormatException, VersionException, LockException,
      ConstraintViolationException, RepositoryException
   {
      Value[] strValues = null;
      if (values != null)
      {
         strValues = new Value[values.length];
         for (int i = 0; i < values.length; i++)
            strValues[i] = valueFactory.createValue(values[i]);
      }
      setValue(strValues);
   }

   // ////////////////// Item implementation ////////////////////

   /**
    * {@inheritDoc}
    */
   public void accept(ItemVisitor visitor) throws RepositoryException
   {
      checkValid();

      visitor.visit(this);
   }

   /**
    * {@inheritDoc}
    */
   public boolean isNode()
   {
      return false;
   }

   // // ----------------------- ExtendedProperty -----------------------
   //
   // public void updateValue(int index, InputStream value, long length,
   // long position) throws ValueFormatException, VersionException,
   // LockException, ConstraintViolationException, RepositoryException {
   //
   // checkValid();
   //    
   // PropertyData pdata = (PropertyData) getData();
   // TransientValueData vdata = (TransientValueData)
   // pdata.getValues().get(index);
   //
   // doUpdateProperty(parent(), getInternalName(),
   // valueFactory.loadValue(vdata, PropertyType.BINARY), false,
   // PropertyType.BINARY);
   //    
   // // get new data
   // vdata = (TransientValueData) ((PropertyData)
   // getData()).getValues().get(index);
   //    
   // try {
   // vdata.update(value, length, position);
   // } catch (IOException e) {
   // throw new RepositoryException(e);
   // }
   //    
   // //setValue(valueFactory.loadValue(vdata, PropertyType.BINARY));
   // }

   // ////////////////////////////////

   /**
    * Copies property values into array.
    * 
    * @return array of property values
    * @throws RepositoryException
    *           if any Exception is occurred
    */
   public Value[] getValueArray() throws RepositoryException
   {
      Value[] values = new Value[propertyData.getValues().size()];
      for (int i = 0; i < values.length; i++)
      {
         values[i] = valueFactory.loadValue(propertyData.getValues().get(i), propertyData.getType());
      }
      return values;
   }

   /**
    * Get info about property values.
    * 
    * @return string with property values
    */
   public String dump()
   {
      StringBuilder vals = new StringBuilder("Property ");
      try
      {
         vals = new StringBuilder(getPath()).append(" values: ");
         for (int i = 0; i < getValueArray().length; i++)
         {
            vals.append(ValueDataUtil.getString(((BaseValue)getValueArray()[i]).getInternalData())).append(";");
         }
      }
      catch (Exception e)
      {
         LOG.error(e.getLocalizedMessage(), e);
      }
      return vals.toString();
   }

   // ----------------------- Object -----------------------

   /**
    * {@inheritDoc}
    */
   @Override
   public boolean equals(Object obj)
   {
      if (obj instanceof PropertyImpl)
      {
         try
         {
            // by path
            return getLocation().equals(((PropertyImpl)obj).getLocation());
         }
         catch (Exception e)
         {
            return false;
         }
      }
      return false;
   }

   @Override
   public String toString()
   {
      String typeName;
      try
      {
         typeName = PropertyType.nameFromValue(type);
      }
      catch (IllegalArgumentException e)
      {
         // Value has abnormal type
         typeName = String.valueOf(type);
      }
      return String.format("Property {\n id: %s;\n path: %s;\n type: %s;\n multi-valued: %b\n}", data == null
         ? "not valid property" : data.getIdentifier(), qpath == null ? "undefined" : qpath.getAsString(), typeName,
         data == null ? false : isMultiValued());
   }

}
