/*
 * 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.ext.replication;

import org.exoplatform.services.jcr.config.RepositoryConfigurationException;
import org.exoplatform.services.jcr.dataflow.ChangesLogIterator;
import org.exoplatform.services.jcr.dataflow.ItemDataKeeper;
import org.exoplatform.services.jcr.dataflow.ItemStateChangesLog;
import org.exoplatform.services.jcr.dataflow.PlainChangesLog;
import org.exoplatform.services.jcr.dataflow.TransactionChangesLog;
import org.exoplatform.services.jcr.ext.replication.recovery.RecoveryManager;
import org.exoplatform.services.jcr.ext.replication.transport.AbstractPacket;
import org.exoplatform.services.jcr.ext.replication.transport.ChannelManager;
import org.exoplatform.services.jcr.ext.replication.transport.MemberAddress;
import org.exoplatform.services.jcr.ext.replication.transport.PacketListener;
import org.exoplatform.services.jcr.impl.util.io.FileCleaner;
import org.exoplatform.services.jcr.util.IdGenerator;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;

import java.util.HashMap;

/**
 * Created by The eXo Platform SAS.
 * 
 * @author <a href="mailto:alex.reshetnyak@exoplatform.com.ua">Alex Reshetnyak</a>
 * @version $Id: AbstractWorkspaceDataReceiver.java 34445 2009-07-24 07:51:18Z dkatayev $
 */

public abstract class AbstractWorkspaceDataReceiver implements PacketListener
{

   /**
    * The apache logger.
    */
   private static Log log = ExoLogger.getLogger("exo.jcr.component.ext.AbstractWorkspaceDataReceiver");

   /**
    * The definition INIT_MODE for AbstractWorkspaceDataReceiver.
    */
   public static final int INIT_MODE = -1;

   /**
    * The definition NORMAL_MODE for AbstractWorkspaceDataReceiver.
    */
   public static final int NORMAL_MODE = 0;

   /**
    * The definition RECOVERY_MODE for AbstractWorkspaceDataReceiver.
    */
   public static final int RECOVERY_MODE = 1;

   /**
    * The definition start timeout.
    */
   private static final int START_TIMEOUT = 1000;

   /**
    * The state of AbstractWorkspaceDataReceiver.
    */
   private int state;

   /**
    * System identification string.
    */
   private String systemId;

   /**
    * The ChannalManager will be transmitted the Packets.
    */
   private ChannelManager channelManager;

   /**
    * The HashMap with mapPendingBinaryFiles.
    */
   private HashMap<String, PendingBinaryFile> mapPendingBinaryFile;

   /**
    * The ChangesLogs will be saved on ItemDataKeeper.
    */
   protected ItemDataKeeper dataKeeper;

   /**
    * The FileCleaner will be deleted temporary files.
    */
   private FileCleaner fileCleaner;

   /**
    * The own name in cluster.
    */
   private String ownName;

   /**
    * The RecoveryManager will be saved ChangesLogs on FS(file system).
    */
   private RecoveryManager recoveryManager;

   /**
    * AbstractWorkspaceDataReceiver constructor.
    * 
    * @throws RepositoryConfigurationException
    *           will be generated the RepositoryConfigurationException
    */
   public AbstractWorkspaceDataReceiver() throws RepositoryConfigurationException
   {
      this.fileCleaner = new FileCleaner(ReplicationService.FILE_CLEANRE_TIMEOUT);
      mapPendingBinaryFile = new HashMap<String, PendingBinaryFile>();

      state = INIT_MODE;
   }

   /**
    * init.
    * 
    * @param channelManager
    *          the ChannelManager
    * @param systemId
    *          system identification string
    * @param ownName
    *          own name
    * @param recoveryManager
    *          the RecoveryManager
    */
   public void init(ChannelManager channelManager, String systemId, String ownName, RecoveryManager recoveryManager)
   {
      this.systemId = systemId;
      this.channelManager = channelManager;

      this.channelManager.addPacketListener(this);

      this.ownName = ownName;
      this.recoveryManager = recoveryManager;

   }

   /**
    * The call 'start()' for information other participants.
    */
   public void start()
   {
      try
      {
         Packet memberStartedPacket = new Packet(Packet.PacketType.MEMBER_STARTED, IdGenerator.generate(), ownName);
         channelManager.sendPacket(memberStartedPacket);

         Thread.sleep(START_TIMEOUT);

         Packet initedPacket = new Packet(Packet.PacketType.INITED_IN_CLUSTER, IdGenerator.generate(), ownName);
         channelManager.sendPacket(initedPacket);
      }
      catch (Exception e)
      {
         log.error("Can't initialized AbstractWorkspaceDataReceiver", e);
      }
   }

   /**
    * receive.
    * 
    * @param itemStatechangesLog
    *          the received ChangesLog
    * @param identifier
    *          the PandingChangeLog or PendingBinaryFile identifier string
    * @throws Exception
    *           will be generated the Exception
    */
   public void receive(ItemStateChangesLog itemStatechangesLog, String identifier) throws Exception
   {
      TransactionChangesLog changesLog = (TransactionChangesLog)itemStatechangesLog;
      if (changesLog.getSystemId() == null)
      {
         throw new Exception("Invalid or same systemId " + changesLog.getSystemId());
      }
      else if (!changesLog.getSystemId().equals(this.systemId))
      {

         if (state != RECOVERY_MODE)
         {
            // dump log
            if (log.isDebugEnabled())
            {
               ChangesLogIterator logIterator = changesLog.getLogIterator();
               while (logIterator.hasNextLog())
               {
                  PlainChangesLog pcl = logIterator.nextLog();
                  log.info(pcl.dump());
               }
            }

            dataKeeper.save(changesLog);

            Packet packet = new Packet(Packet.PacketType.ADD_OK, identifier, ownName);
            channelManager.sendPacket(packet);

            if (log.isDebugEnabled())
            {
               log.info("After save message: the owner systemId --> " + changesLog.getSystemId());
               log.info("After save message: --> " + systemId);
            }
         }
      }
   }

   /**
    * {@inheritDoc}
    */
   public void receive(AbstractPacket p, MemberAddress sourceAddress)
   {
      try
      {
         Packet packet = (Packet)p;
         Packet bigPacket = null;

         switch (packet.getPacketType())
         {

            case Packet.PacketType.BINARY_CHANGESLOG_PACKET :

               PendingBinaryFile container = mapPendingBinaryFile.get(packet.getIdentifier());
               if (container == null)
               {
                  container = new PendingBinaryFile();
                  mapPendingBinaryFile.put(packet.getIdentifier(), container);
               }

               ChangesFile chf;

               synchronized (container)
               {
                  chf = container.getChangesFile(packet.getOwnerName(), packet.getFileName());
                  if (chf == null)
                  {
                     chf =
                        container.addChangesFile(packet.getOwnerName(), packet.getFileName(), packet.getSystemId(),
                           packet.getTotalPacketCount());
                  }
               }

               chf.write(packet.getOffset(), packet.getByteArray());

               // save to JCR
               if (chf.isStored())
               {
                  saveChangesLog(chf, packet.getIdentifier());

                  // remove
                  if (!chf.getFile().delete())
                     fileCleaner.addFile(chf.getFile());
                  mapPendingBinaryFile.remove(packet.getIdentifier());

                  if (log.isDebugEnabled())
                     log.debug("Last packet of file has been received : " + packet.getFileName());
               }
               break;
         }

         if (bigPacket != null)
         {
            state = recoveryManager.processing(bigPacket, state);
            bigPacket = null;
         }
         else
            state = recoveryManager.processing(packet, state);

      }
      catch (Exception e)
      {
         log.error("An error in processing packet : ", e);
      }
   }

   /**
    * {@inheritDoc}
    */
   public void onError(MemberAddress sourceAddress)
   {

   }

   /**
    * getDataKeeper.
    * 
    * @return ItemDataKeeper return the dataKeeper
    */
   public ItemDataKeeper getDataKeeper()
   {
      return dataKeeper;
   }

   /**
    * saveChangesLog.
    * 
    * @param fileDescriptor
    *          the FileDescriptor
    * @param identifire
    *          the PendingBinaryFile identification string
    * @throws Exception
    *           will be generated the Exception
    */
   private void saveChangesLog(ChangesFile fileDescriptor, String identifire) throws Exception
   {
      TransactionChangesLog transactionChangesLog =
         recoveryManager.getRecoveryReader().getChangesLog(fileDescriptor.getFile().getAbsolutePath());

      if (log.isDebugEnabled())
      {
         log.debug("Save to JCR : " + fileDescriptor.getFile().getAbsolutePath());
         log.debug("SystemID : " + transactionChangesLog.getSystemId());
      }

      // dump log
      if (log.isDebugEnabled())
      {
         ChangesLogIterator logIterator = transactionChangesLog.getLogIterator();
         while (logIterator.hasNextLog())
         {
            PlainChangesLog pcl = logIterator.nextLog();
            log.debug(pcl.dump());
         }
      }

      this.receive((ItemStateChangesLog)transactionChangesLog, identifire);
   }
}
