/*
 * Copyright (C) 2012 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.quota;

import org.exoplatform.services.jcr.impl.Constants;
import org.exoplatform.services.jcr.storage.WorkspaceDataContainer;

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

/**
 * Wraps information of changed data size of particular save. First, is put into pending changes
 * and after save is performed being moved into changes log.
 *
 * @author <a href="abazko@exoplatform.com">Anatoliy Bazko</a>
 * @version $Id: ChangesItem.java 34360 2009-07-22 23:58:59Z tolusha $
 */
class ChangesItem implements Externalizable
{
   /**
    * ChangesItem constructor.
    */
   public ChangesItem()
   {
   }

   /**
    * Contains calculated workspace data size changes of particular save.
    */
   private long workspaceChangedSize;

   /**
    * Contains calculated node data size changes of particular save.
    * Represents {@link Map} with absolute node path as key and changed node
    * data size as value respectively.
    */
   private Map<String, Long> calculatedChangedNodesSize = new HashMap<String, Long>();

   /**
    * Set of absolute nodes paths for which changes were made but changed size is unknown. Most
    * famous case when {@link WorkspaceDataContainer#TRIGGER_EVENTS_FOR_DESCENDANTS_ON_RENAME} is
    * set to false and move operation is performed.
    */
   private Set<String> unknownChangedNodesSize = new HashSet<String>();

   /**
    * Collects node paths for which data size updating must be performed asynchronously.
    */
   private Set<String> asyncUpdate = new HashSet<String>();

   /**
    * Getter for {@link #workspaceChangedSize}.
    */
   public long getWorkspaceChangedSize()
   {
      return workspaceChangedSize;
   }

   /**
    * Updates {@link #workspaceChangedSize}.
    */
   public void updateWorkspaceChangedSize(long delta)
   {
      workspaceChangedSize += delta;
   }

   /**
    * Returns node data changed size if exists or zero otherwise.
    */
   public long getNodeChangedSize(String nodePath)
   {
      Long delta = calculatedChangedNodesSize.get(nodePath);
      return delta == null ? 0 : delta;
   }

   /**
    * Getter for {@link #calculatedChangedNodesSize}.
    */
   public Map<String, Long> getAllNodesCalculatedChangedSize()
   {
      return calculatedChangedNodesSize;
   }

   /**
    * Getter for {@link #unknownChangedNodesSize}.
    */
   public Set<String> getAllNodesUnknownChangedSize()
   {
      return unknownChangedNodesSize;
   }

   /**
    * Updates {@link #calculatedChangedNodesSize} for particular
    * node path.
    */
   public void updateNodeChangedSize(String nodePath, long delta)
   {
      Long oldDelta = calculatedChangedNodesSize.get(nodePath);
      Long newDelta = delta + (oldDelta != null ? oldDelta : 0);

      calculatedChangedNodesSize.put(nodePath, newDelta);
   }

   /**
    * Adds new node absolute path for {@link #unknownChangedNodesSize} collection.
    */
   public void addPathWithUnknownChangedSize(String nodePath)
   {
      unknownChangedNodesSize.add(nodePath);
   }

   /**
    * Adds new node absolute path for {@link #asyncUpdate} collection.
    */
   public void addPathWithAsyncUpdate(String nodePath)
   {
      asyncUpdate.add(nodePath);
   }

   /**
    * Merges current changes with new one.
    */
   public void merge(ChangesItem changesItem)
   {
      workspaceChangedSize += changesItem.getWorkspaceChangedSize();

      for (Entry<String, Long> changesEntry : changesItem.calculatedChangedNodesSize.entrySet())
      {
         String nodePath = changesEntry.getKey();
         Long currentDelta = changesEntry.getValue();

         Long oldDelta = calculatedChangedNodesSize.get(nodePath);
         Long newDelta = currentDelta + (oldDelta == null ? 0 : oldDelta);

         calculatedChangedNodesSize.put(nodePath, newDelta);
      }

      for (String path : changesItem.unknownChangedNodesSize)
      {
         unknownChangedNodesSize.add(path);
      }

      for (String path : changesItem.asyncUpdate)
      {
         asyncUpdate.add(path);
      }
   }

   /**
    * Checks if there is any changes.
    */
   public boolean isEmpty()
   {
      return workspaceChangedSize == 0 && calculatedChangedNodesSize.isEmpty() && unknownChangedNodesSize.isEmpty();
   }

   /**
    * Leave in {@link ChangesItem} only changes being apply asynchronously
    * and return ones to apply instantly.
    */
   public ChangesItem extractSyncChanges()
   {
      ChangesItem syncChangesItem = new ChangesItem();

      Iterator<String> iter = calculatedChangedNodesSize.keySet().iterator();
      while (iter.hasNext() && !asyncUpdate.isEmpty())
      {
         String nodePath = iter.next();

         if (!asyncUpdate.contains(nodePath))
         {
            Long chanagedSize = calculatedChangedNodesSize.get(nodePath);
            syncChangesItem.calculatedChangedNodesSize.put(nodePath, chanagedSize);
            syncChangesItem.workspaceChangedSize += chanagedSize;

            this.asyncUpdate.remove(nodePath);
            this.workspaceChangedSize -= chanagedSize;

            iter.remove();
         }
      }

      return syncChangesItem;
   }

   /**
    * {@inheritDoc}
    */
   public void writeExternal(ObjectOutput out) throws IOException
   {
      out.writeLong(workspaceChangedSize);

      out.writeInt(calculatedChangedNodesSize.size());
      for (Entry<String, Long> entry : calculatedChangedNodesSize.entrySet())
      {
         writeString(out, entry.getKey());
         out.writeLong(entry.getValue());
      }

      out.writeInt(unknownChangedNodesSize.size());
      for (String path : unknownChangedNodesSize)
      {
         writeString(out, path);
      }

      out.writeInt(asyncUpdate.size());
      for (String path : asyncUpdate)
      {
         writeString(out, path);
      }
   }

   private void writeString(ObjectOutput out, String str) throws IOException
   {
      byte[] data = str.getBytes(Constants.DEFAULT_ENCODING);
      out.writeInt(data.length);
      out.write(data);
   }

   /**
    * {@inheritDoc}
    */
   public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException
   {
      this.workspaceChangedSize = in.readLong();

      int size = in.readInt();
      this.calculatedChangedNodesSize = new HashMap<String, Long>(size);
      for (int i = 0; i < size; i++)
      {
         String nodePath = readString(in);
         Long delta = in.readLong();

         calculatedChangedNodesSize.put(nodePath, delta);
      }

      size = in.readInt();
      this.unknownChangedNodesSize = new HashSet<String>(size);
      for (int i = 0; i < size; i++)
      {
         String nodePath = readString(in);
         unknownChangedNodesSize.add(nodePath);
      }

      size = in.readInt();
      this.asyncUpdate = new HashSet<String>(size);
      for (int i = 0; i < size; i++)
      {
         String nodePath = readString(in);
         asyncUpdate.add(nodePath);
      }
   }

   private String readString(ObjectInput in) throws IOException
   {
      byte[] data = new byte[in.readInt()];
      in.readFully(data);

      return new String(data, Constants.DEFAULT_ENCODING);
   }
}
