/**
 * Copyright (C) 2010 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.xcmis.sp.inmemory;

import org.xcmis.spi.BaseItemsIterator;
import org.xcmis.spi.CmisConstants;
import org.xcmis.spi.CmisRuntimeException;
import org.xcmis.spi.ConstraintException;
import org.xcmis.spi.DocumentData;
import org.xcmis.spi.FolderData;
import org.xcmis.spi.ItemsIterator;
import org.xcmis.spi.NameConstraintViolationException;
import org.xcmis.spi.ObjectData;
import org.xcmis.spi.ObjectDataVisitor;
import org.xcmis.spi.ObjectNotFoundException;
import org.xcmis.spi.PolicyData;
import org.xcmis.spi.PropertyFilter;
import org.xcmis.spi.RelationshipData;
import org.xcmis.spi.StorageException;
import org.xcmis.spi.TypeNotFoundException;
import org.xcmis.spi.UpdateConflictException;
import org.xcmis.spi.VersioningException;
import org.xcmis.spi.model.AccessControlEntry;
import org.xcmis.spi.model.BaseType;
import org.xcmis.spi.model.ChangeEvent;
import org.xcmis.spi.model.ChangeType;
import org.xcmis.spi.model.Property;
import org.xcmis.spi.model.PropertyDefinition;
import org.xcmis.spi.model.PropertyType;
import org.xcmis.spi.model.RelationshipDirection;
import org.xcmis.spi.model.TypeDefinition;
import org.xcmis.spi.model.Updatability;
import org.xcmis.spi.model.impl.BooleanProperty;
import org.xcmis.spi.model.impl.DateTimeProperty;
import org.xcmis.spi.model.impl.DecimalProperty;
import org.xcmis.spi.model.impl.HtmlProperty;
import org.xcmis.spi.model.impl.IdProperty;
import org.xcmis.spi.model.impl.IntegerProperty;
import org.xcmis.spi.model.impl.StringProperty;
import org.xcmis.spi.model.impl.UriProperty;
import org.xcmis.spi.utils.CmisUtils;
import org.xcmis.spi.utils.Logger;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * @author <a href="mailto:andrew00x@gmail.com">Andrey Parfonov</a>
 * @version $Id: BaseObjectData.java 1197 2010-05-28 08:15:37Z
 *          alexey.zavizionov@gmail.com $
 */
abstract class BaseObjectData implements ObjectData
{

   private static final Logger LOG = Logger.getLogger(BaseObjectData.class);

   /**
    * Util method for obtaining deep copy of property.
    *
    * @param source source property
    * @return
    */
   @SuppressWarnings("unchecked")
   static Property<?> createCopyOfProperty(Property<?> source)
   {
      switch (source.getType())
      {
         case BOOLEAN :
            return new BooleanProperty(source.getId(), source.getQueryName(), source.getLocalName(), source
               .getDisplayName(), new ArrayList<Boolean>((List<Boolean>)source.getValues()));
         case DATETIME :
            List<Calendar> dates = (List<Calendar>)source.getValues();
            List<Calendar> work = new ArrayList<Calendar>(dates.size());
            for (Calendar c : dates)
            {
               work.add((Calendar)c.clone());
            }
            return new DateTimeProperty(source.getId(), source.getQueryName(), source.getLocalName(), source
               .getDisplayName(), work);
         case DECIMAL :
            return new DecimalProperty(source.getId(), source.getQueryName(), source.getLocalName(), source
               .getDisplayName(), new ArrayList<BigDecimal>((List<BigDecimal>)source.getValues()));
         case HTML :
            return new HtmlProperty(source.getId(), source.getQueryName(), source.getLocalName(), source
               .getDisplayName(), new ArrayList<String>((List<String>)source.getValues()));
         case ID :
            return new IdProperty(source.getId(), source.getQueryName(), source.getLocalName(),
               source.getDisplayName(), new ArrayList<String>((List<String>)source.getValues()));
         case INTEGER :
            return new IntegerProperty(source.getId(), source.getQueryName(), source.getLocalName(), source
               .getDisplayName(), new ArrayList<BigInteger>((List<BigInteger>)source.getValues()));
         case STRING :
            return new StringProperty(source.getId(), source.getQueryName(), source.getLocalName(), source
               .getDisplayName(), new ArrayList<String>((List<String>)source.getValues()));
         case URI :
            return new UriProperty(source.getId(), source.getQueryName(), source.getLocalName(), source
               .getDisplayName(), new ArrayList<URI>((List<URI>)source.getValues()));
         default :
            return null;
      }
   }

   protected final TypeDefinition type;

   protected final StorageImpl storage;

   protected final Entry entry;

   public BaseObjectData(Entry entry, TypeDefinition type, StorageImpl storage)
   {
      this.entry = entry;
      this.type = type;
      this.storage = storage;
   }

   /**
    * {@inheritDoc}
    */
   public void accept(ObjectDataVisitor visitor)
   {
      visitor.visit(this);
   }

   /**
    * {@inheritDoc}
    */
   public void applyPolicy(PolicyData policy)
   {
      entry.addPolicy(policy);
      try
      {
         save();
      }
      catch (StorageException e)
      {
         throw new CmisRuntimeException("Unable apply policy. " + e.getMessage(), e);
      }
      storage.changes.add(new ChangeEvent(StorageImpl.generateId(), getObjectId(), ChangeType.SECURITY, Calendar
         .getInstance(), null, new HashSet<String>(entry.getPolicies()), null));
   }

   public boolean equals(Object obj)
   {
      if (obj == null)
      {
         return false;
      }
      if (obj.getClass() != getClass())
      {
         return false;
      }
      return ((BaseObjectData)obj).getObjectId().equals(getObjectId());
   }

   /**
    * {@inheritDoc}
    */
   public List<AccessControlEntry> getACL(boolean onlyBasicPermissions)
   {
      if (!type.isControllableACL())
      {
         return Collections.emptyList();
      }
      return CmisUtils.createAclFromPermissionMap(entry.getPermissions());
   }

   /**
    * {@inheritDoc}
    */
   public BaseType getBaseType()
   {
      return type.getBaseId();
   }

   /**
    * {@inheritDoc}
    */
   public String getChangeToken()
   {
      return getString(CmisConstants.CHANGE_TOKEN);
   }

   /**
    * {@inheritDoc}
    */
   public String getCreatedBy()
   {
      return getString(CmisConstants.CREATED_BY);
   }

   /**
    * {@inheritDoc}
    */
   public Calendar getCreationDate()
   {
      return getDate(CmisConstants.CREATION_DATE);
   }

   /**
    * {@inheritDoc}
    */
   public Calendar getLastModificationDate()
   {
      return getDate(CmisConstants.LAST_MODIFICATION_DATE);
   }

   /**
    * {@inheritDoc}
    */
   public String getLastModifiedBy()
   {
      return getString(CmisConstants.LAST_MODIFIED_BY);
   }

   /**
    * {@inheritDoc}
    */
   public String getName()
   {
      return getString(CmisConstants.NAME);
   }

   /**
    * {@inheritDoc}
    */
   public String getObjectId()
   {
      return entry.getId();
   }

   /**
    * {@inheritDoc}
    */
   public FolderData getParent() throws ConstraintException
   {
      if (StorageImpl.ROOT_FOLDER_ID.equals(getObjectId()))
      {
         throw new ConstraintException("Unable get parent of root folder.");
      }

      Collection<FolderData> parents = getParents();
      if (parents.size() > 1)
      {
         throw new ConstraintException("Object has more then one parent.");
      }
      if (parents.size() == 1)
      {
         return parents.iterator().next();
      }
      return null;
   }

   /**
    * {@inheritDoc}
    */
   public Collection<FolderData> getParents()
   {
      Set<String> parentIds = storage.parents.get(getObjectId());
      Set<FolderData> parents = new HashSet<FolderData>(parentIds.size());

      if (parentIds != null)
      {
         for (String id : parentIds)
         {
            try
            {
               parents.add((FolderData)storage.getObjectById(id));
            }
            catch (ObjectNotFoundException e)
            {
               LOG.warn("Not found folder " + id);
            }
         }
      }
      return parents;
   }

   /**
    * {@inheritDoc}
    */
   public Collection<PolicyData> getPolicies()
   {
      if (!type.isControllablePolicy())
      {
         return Collections.emptyList();
      }

      List<PolicyData> policies = new ArrayList<PolicyData>();
      for (String id : entry.getPolicies())
      {
         try
         {
            policies.add((PolicyData)storage.getObjectById(id));
         }
         catch (ObjectNotFoundException e)
         {
            LOG.warn("Not found policy " + id);
         }
      }
      return policies;
   }

   /**
    * {@inheritDoc}
    */
   public Map<String, Property<?>> getProperties()
   {
      Map<String, Property<?>> properties = new HashMap<String, Property<?>>();
      for (PropertyDefinition<?> definition : type.getPropertyDefinitions())
      {
         properties.put(definition.getId(), doGetProperty(definition));
      }
      return properties;
   }

   /**
    * {@inheritDoc}
    */
   public Map<String, Property<?>> getProperties(PropertyFilter filter)
   {
      Map<String, Property<?>> properties = new HashMap<String, Property<?>>();
      for (PropertyDefinition<?> definition : type.getPropertyDefinitions())
      {
         String queryName = definition.getQueryName();
         if (filter.accept(queryName))
         {
            String id = definition.getId();
            properties.put(id, doGetProperty(definition));
         }
      }
      return properties;
   }

   /**
    * {@inheritDoc}
    */
   public Property<?> getProperty(String id)
   {
      PropertyDefinition<?> definition = type.getPropertyDefinition(id);
      if (definition != null)
      {
         return doGetProperty(definition);
      }
      return null;
   }

   /**
    * {@inheritDoc}
    */
   public ItemsIterator<RelationshipData> getRelationships(RelationshipDirection direction, TypeDefinition type,
      boolean includeSubRelationshipTypes)
   {
      Set<String> relationshipIds = storage.relationships.get(getObjectId());
      if (relationshipIds == null)
      {
         return CmisUtils.emptyItemsIterator();
      }

      Collection<String> typeFilter = new HashSet<String>();
      typeFilter.add(type.getId());

      if (includeSubRelationshipTypes)
      {
         Collection<TypeDefinition> subTypes = null;
         try
         {
            subTypes = storage.getSubTypes(type.getId(), false);
         }
         catch (TypeNotFoundException e)
         {
            // Should never happen.
            throw new CmisRuntimeException(e.getMessage(), e);
         }
         for (TypeDefinition t : subTypes)
         {
            typeFilter.add(t.getId());
         }
      }

      Set<RelationshipData> relationships = new java.util.HashSet<RelationshipData>();
      for (String id : relationshipIds)
      {
         RelationshipData relationship = null;
         try
         {
            relationship = (RelationshipData)storage.getObjectById(id);
         }
         catch (ObjectNotFoundException e)
         {
            LOG.warn("Not found relationship " + id + ".");
            continue;
         }
         if ((direction == RelationshipDirection.EITHER //
            || (direction == RelationshipDirection.SOURCE && relationship.getSourceId().equals(getObjectId())) //
         || (direction == RelationshipDirection.TARGET && relationship.getTargetId().equals(getObjectId())))
            && typeFilter.contains(relationship.getTypeDefinition().getId()))
         {
            relationships.add(relationship);
         }
      }
      return new BaseItemsIterator<RelationshipData>(relationships);
   }

   /**
    * {@inheritDoc}
    */
   public TypeDefinition getTypeDefinition()
   {
      return type;
   }

   /**
    * {@inheritDoc}
    */
   public String getTypeId()
   {
      return type.getId();
   }

   public int hashCode()
   {
      int hash = 8;
      return hash * 31 + getObjectId().hashCode();
   }

   /**
    * {@inheritDoc}
    */
   public void removePolicy(PolicyData policy)
   {
      entry.removePolicy(policy);
      try
      {
         save();
      }
      catch (StorageException e)
      {
         throw new CmisRuntimeException("Unable remove policy. " + e.getMessage(), e);
      }

      storage.changes.add(new ChangeEvent(StorageImpl.generateId(), getObjectId(), ChangeType.SECURITY, Calendar
         .getInstance(), null, new HashSet<String>(entry.getPolicies()), null));
   }

   /**
    * {@inheritDoc}
    */
   public void setACL(List<AccessControlEntry> acl)
   {
      Map<String, Set<String>> permissions = entry.getPermissions();
      permissions.clear();
      CmisUtils.addAclToPermissionMap(permissions, acl);
      try
      {
         save();
      }
      catch (StorageException e)
      {
         throw new CmisRuntimeException("Unable set ACL. " + e.getMessage(), e);
      }
      if (acl != null)
      {
         // null may mind replace all applied ACL
         List<AccessControlEntry> copy = new ArrayList<AccessControlEntry>(acl.size());
         for (AccessControlEntry ace : acl)
         {
            copy.add(new AccessControlEntry(ace.getPrincipal(), new HashSet<String>(ace.getPermissions()), ace
               .isDirect()));
         }
         storage.changes.add(new ChangeEvent(StorageImpl.generateId(), getObjectId(), ChangeType.SECURITY, Calendar
            .getInstance(), null, null, copy));
      }
   }

   /**
    * {@inheritDoc}
    */
   public void setProperties(Map<String, Property<?>> properties) throws NameConstraintViolationException,
      UpdateConflictException, VersioningException, StorageException
   {
      List<Property<?>> chl = new ArrayList<Property<?>>();
      for (Property<?> property : properties.values())
      {
         if (doSetProperty(property))
         {
            chl.add(createCopyOfProperty(property));
         }
      }
      save();
      storage.changes.add(new ChangeEvent(StorageImpl.generateId(), getObjectId(), ChangeType.UPDATED, Calendar
         .getInstance(), chl, null, null));
   }

   /**
    * {@inheritDoc}
    */
   public void setProperty(Property<?> property) throws NameConstraintViolationException, StorageException,
      UpdateConflictException, VersioningException
   {
      doSetProperty(property);
      save();
      List<Property<?>> chl = new ArrayList<Property<?>>(1);
      // Create copy of property to be sure it will unchangeable in changes log.
      chl.add(createCopyOfProperty(property));
      storage.changes.add(new ChangeEvent(StorageImpl.generateId(), getObjectId(), ChangeType.UPDATED, Calendar
         .getInstance(), chl, null, null));
   }

   /**
    * {@inheritDoc}
    */
   public String toString()
   {
      return "type: " + getTypeId() + ", name: " + getName() + ", id: " + getObjectId();
   }

   /**
    * To create the new property.
    *
    * @param def the property definition
    * @param value the value
    * @return the new property
    */
   private Property<?> createProperty(PropertyDefinition<?> def, Value value)
   {
      if (def.getPropertyType() == PropertyType.BOOLEAN)
      {
         return new BooleanProperty(def.getId(), def.getQueryName(), def.getLocalName(), def.getDisplayName(),
            value == null ? null : Arrays.asList(value.getBooleans()));
      }
      else if (def.getPropertyType() == PropertyType.DATETIME)
      {
         return new DateTimeProperty(def.getId(), def.getQueryName(), def.getLocalName(), def.getDisplayName(),
            value == null ? null : Arrays.asList(value.getDates()));
      }
      else if (def.getPropertyType() == PropertyType.DECIMAL)
      {
         return new DecimalProperty(def.getId(), def.getQueryName(), def.getLocalName(), def.getDisplayName(),
            value == null ? null : Arrays.asList(value.getDecimals()));
      }
      else if (def.getPropertyType() == PropertyType.HTML)
      {
         return new HtmlProperty(def.getId(), def.getQueryName(), def.getLocalName(), def.getDisplayName(),
            value == null ? null : Arrays.asList(value.getStrings()));
      }
      else if (def.getPropertyType() == PropertyType.ID)
      {
         return new IdProperty(def.getId(), def.getQueryName(), def.getLocalName(), def.getDisplayName(), value == null
            ? null : Arrays.asList(value.getStrings()));
      }
      else if (def.getPropertyType() == PropertyType.INTEGER)
      {
         return new IntegerProperty(def.getId(), def.getQueryName(), def.getLocalName(), def.getDisplayName(),
            value == null ? null : Arrays.asList(value.getIntegers()));
      }
      else if (def.getPropertyType() == PropertyType.STRING)
      {
         return new StringProperty(def.getId(), def.getQueryName(), def.getLocalName(), def.getDisplayName(),
            value == null ? null : Arrays.asList(value.getStrings()));
      }
      else if (def.getPropertyType() == PropertyType.URI)
      {
         return new UriProperty(def.getId(), def.getQueryName(), def.getLocalName(), def.getDisplayName(),
            value == null ? null : Arrays.asList(value.getURI()));
      }
      else
      {
         throw new CmisRuntimeException("Unknown property type.");
      }
   }

   protected abstract void delete() throws StorageException, UpdateConflictException, VersioningException;

   protected Property<?> doGetProperty(PropertyDefinition<?> definition)
   {
      Value value = entry.getValue(definition.getId());
      if (value == null && CmisConstants.PATH.equals(definition.getId()) && type.getBaseId() == BaseType.FOLDER)
      {
         value = new StringValue(((FolderData)this).getPath());
         // add other virtual property
      }
      return createProperty(definition, value);
   }

   /**
    * Update properties, skip on-create and read-only properties
    *
    * @param property property to be updated
    * @return <code>true</code> if property was updated and <code>false</code>
    *         otherwise, e.g. property was not updated since it is read-only
    *         property
    */
   protected boolean doSetProperty(Property<?> property) throws NameConstraintViolationException
   {
      PropertyDefinition<?> definition = type.getPropertyDefinition(property.getId());
      Updatability updatability = definition.getUpdatability();
      if (updatability == Updatability.READWRITE //
         || (updatability == Updatability.WHENCHECKEDOUT && getBaseType() == BaseType.DOCUMENT && ((DocumentData)this)
            .isPWC()))
      {

         // Do not store nulls
         for (Iterator<?> i = property.getValues().iterator(); i.hasNext();)
         {
            Object v = i.next();
            if (v == null)
            {
               i.remove();
            }
         }

         if (CmisConstants.NAME.equals(property.getId()))
         {
            String name = null;
            List<?> values = property.getValues();
            if (values.size() > 0)
            {
               name = (String)values.get(0);
            }

            if (name == null || name.length() == 0)
            {
               throw new NameConstraintViolationException("Name can't be null or empty string.");
            }
            if (name.equals(getName()))
            {
               return false;
            }

            for (FolderData parent : getParents())
            {
               for (ItemsIterator<ObjectData> iterator = parent.getChildren(null); iterator.hasNext();)
               {
                  if (name.equals(iterator.next().getName()))
                  {
                     throw new NameConstraintViolationException("Object with name " + name
                        + " already exists in parent folder.");
                  }
               }
            }
         }

         entry.setProperty(property);
         return true;
      }
      else
      {
         if (LOG.isDebugEnabled())
         {
            LOG.debug("Property " + property.getId() + " is not updatable.");
         }
         return false;
      }
   }

   protected Boolean getBoolean(String id)
   {
      Value value = entry.getValue(id);
      if (value != null)
      {
         Boolean[] booleans = value.getBooleans();
         return booleans.length > 0 ? booleans[0] : null;
      }
      return null;
   }

   protected Boolean[] getBooleans(String id)
   {
      Value value = entry.getValue(id);
      if (value != null)
      {
         return value.getBooleans();
      }
      return null;
   }

   protected Calendar getDate(String id)
   {
      Value value = entry.getValue(id);
      if (value != null)
      {
         Calendar[] dates = value.getDates();
         return dates.length > 0 ? dates[0] : null;
      }
      return null;
   }

   protected Calendar[] getDates(String id)
   {
      Value value = entry.getValue(id);
      if (value != null)
      {
         return value.getDates();
      }
      return null;
   }

   protected BigDecimal getDecimal(String id)
   {
      Value value = entry.getValue(id);
      if (value != null)
      {
         BigDecimal[] decimals = value.getDecimals();
         return decimals.length > 0 ? decimals[0] : null;
      }
      return null;
   }

   protected BigDecimal[] getDecimals(String id)
   {
      Value value = entry.getValue(id);
      if (value != null)
      {
         return value.getDecimals();
      }
      return null;
   }

   protected Entry getEntry()
   {
      return entry;
   }

   protected String getId(String id)
   {
      return getString(id);
   }

   protected String[] getIds(String id)
   {
      return getStrings(id);
   }

   protected BigInteger getInteger(String id)
   {
      Value value = entry.getValue(id);
      if (value != null)
      {
         BigInteger[] integers = value.getIntegers();
         return integers.length > 0 ? integers[0] : null;
      }
      return null;
   }

   protected BigInteger[] getIntegers(String id)
   {
      Value value = entry.getValue(id);
      if (value != null)
      {
         return value.getIntegers();
      }
      return null;
   }

   protected String getString(String id)
   {
      Value value = entry.getValue(id);
      if (value != null)
      {
         String[] strings = value.getStrings();
         return strings.length > 0 ? strings[0] : null;
      }
      return null;
   }

   protected String[] getStrings(String id)
   {
      Value value = entry.getValue(id);
      if (value != null)
      {
         return value.getStrings();
      }
      return null;
   }

   protected void save() throws StorageException
   {
      if (storage.entries.get(entry.getId()) == null)
      {
         throw new CmisRuntimeException("Object was removed from storage.");
      }

      entry.setValue(CmisConstants.LAST_MODIFIED_BY, new StringValue(storage.getCurrentUser()));
      entry.setValue(CmisConstants.LAST_MODIFICATION_DATE, new DateValue(Calendar.getInstance()));
      entry.setValue(CmisConstants.CHANGE_TOKEN, new StringValue(StorageImpl.generateId()));

      storage.entries.put(entry.getId(), entry);
      storage.indexListener.updated(this);
   }

}
