/*
 * 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.backup.impl;

import org.exoplatform.commons.utils.ClassLoading;
import org.exoplatform.services.jcr.RepositoryService;
import org.exoplatform.services.jcr.config.RepositoryConfigurationException;
import org.exoplatform.services.jcr.core.ManageableRepository;
import org.exoplatform.services.jcr.ext.backup.BackupChain;
import org.exoplatform.services.jcr.ext.backup.BackupChainLog;
import org.exoplatform.services.jcr.ext.backup.BackupConfig;
import org.exoplatform.services.jcr.ext.backup.BackupConfigurationException;
import org.exoplatform.services.jcr.ext.backup.BackupJob;
import org.exoplatform.services.jcr.ext.backup.BackupJobListener;
import org.exoplatform.services.jcr.ext.backup.BackupManager;
import org.exoplatform.services.jcr.ext.backup.BackupOperationException;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;

import java.io.File;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;

import javax.jcr.RepositoryException;

/**
 * Created by The eXo Platform SARL .<br/>
 * 
 * @author Gennady Azarenkov
 * @version $Id: $
 */

public class BackupChainImpl implements BackupChain
{

   private final BackupConfig config;

   private List<BackupJob> jobs;

   private AbstractFullBackupJob fullBackup;

   private AbstractIncrementalBackupJob incrementalBackup;

   private final BackupChainLog chainLog;

   private final String backupId;

   private int state;

   private PeriodConroller periodConroller;

   private Timer timer;

   private final Calendar timeStamp;

   private Set<BackupJobListener> listeners = new LinkedHashSet<BackupJobListener>();

   public BackupChainImpl(BackupConfig config, File logDirectory,
            RepositoryService repositoryService,
            String fullBackupType, String incrementalBackupType, String backupId, File rootDir, Calendar startTime)
            throws BackupOperationException,
      BackupConfigurationException
   {
      this.config = config;
      this.jobs = new ArrayList<BackupJob>();
      this.timeStamp = startTime;

      this.chainLog =
               new BackupChainLog(logDirectory, config, fullBackupType, incrementalBackupType, backupId,
                        repositoryService.getConfig(), rootDir);

      this.backupId = backupId;
      
      ManageableRepository repository = null;
      try
      {
         repository = repositoryService.getRepository(config.getRepository());
      }
      catch (RepositoryConfigurationException e)
      {
         throw new BackupOperationException("Can not get repository \"" + config.getRepository() + "\"", e);
      }
      catch (RepositoryException e)
      {
         throw new BackupOperationException("Can not get repository \"" + config.getRepository() + "\"", e);
      }

      try
      {
         this.fullBackup = (AbstractFullBackupJob)ClassLoading.forName(fullBackupType, this).newInstance();
      }
      catch (ClassNotFoundException e)
      {
         throw new BackupConfigurationException("FullBackupType error, " + e, e);
      }
      catch (InstantiationException e)
      {
         throw new BackupConfigurationException("FullBackupType error, " + e, e);
      }
      catch (IllegalAccessException e)
      {
         throw new BackupConfigurationException("FullBackupType error, " + e, e);
      }
      fullBackup.init(repository, config.getWorkspace(), config, timeStamp);

      if (config.getBackupType() == BackupManager.FULL_AND_INCREMENTAL)
      {
         try
         {
            this.incrementalBackup =
               (AbstractIncrementalBackupJob)ClassLoading.forName(incrementalBackupType, this).newInstance();
         }
         catch (ClassNotFoundException e)
         {
            throw new BackupConfigurationException("IncrementalBackupType error, " + e, e);
         }
         catch (InstantiationException e)
         {
            throw new BackupConfigurationException("IncrementalBackupType error, " + e, e);
         }
         catch (IllegalAccessException e)
         {
            throw new BackupConfigurationException("IncrementalBackupType error, " + e, e);
         }
         incrementalBackup.init(repository, config.getWorkspace(), config, timeStamp);

         periodConroller = new PeriodConroller(config.getIncrementalJobPeriod() * 1000); // sec --> ms
      }
      this.state = INITIALIZED;
      this.timer =
         new Timer("BackupChain_" + getBackupConfig().getRepository() + "@" + getBackupConfig().getWorkspace()
            + "_PeriodTimer_" + new SimpleDateFormat("yyyyMMdd.HHmmss.SSS").format(new Date()), true);
   }

   /**
    * Add all listeners to a given job. Used in startBackup() which itself is synchronized.
    * 
    * @param job
    */
   private void addJobListeners(BackupJob job)
   {
      for (BackupJobListener jl : listeners)
         job.addListener(jl);
   }

   /**
    * Remove all listeners from a given job. Used in stoptBackup() which itself is synchronized.
    * 
    * @param job
    */
   private void removeJobListeners(BackupJob job)
   {
      for (BackupJobListener jl : listeners)
         job.removeListener(jl);
   }

   public void addListener(BackupJobListener listener)
   {
      if (listener != null)
      {
         synchronized (jobs)
         {
            for (BackupJob job : jobs)
               job.addListener(listener);
         }
         synchronized (listeners)
         {
            listeners.add(listener);
         }
      }
   }

   public void removeListener(BackupJobListener listener)
   {
      if (listener != null)
      {
         try
         {
            synchronized (jobs)
            {
               for (BackupJob job : jobs)
                  job.removeListener(listener);
            }
         }
         finally
         {
            // remove anyway
            synchronized (listeners)
            {
               listeners.remove(listener);
            }
         }
      }
   }

   public List<BackupJob> getBackupJobs()
   {
      return jobs;
   }

   public final synchronized void startBackup()
   {

      addJobListeners(fullBackup);

      Thread fexecutor =
         new Thread(fullBackup, config.getRepository() + "@" + config.getWorkspace() + "-" + fullBackup.getId());
      fexecutor.start();
      state |= FULL_WORKING;
      chainLog.addJobEntry(fullBackup);
      jobs.add(fullBackup);

      if (incrementalBackup != null)
      {
         addJobListeners(incrementalBackup);

         Thread iexecutor =
            new Thread(incrementalBackup, config.getRepository() + "@" + config.getWorkspace() + "-"
               + incrementalBackup.getId());
         iexecutor.start();
         state |= INCREMENTAL_WORKING;
         chainLog.addJobEntry(incrementalBackup);
         jobs.add(incrementalBackup);

         if (config.getIncrementalJobPeriod() > 0)
            periodConroller.start();
      }
   }

   public final synchronized void stopBackup()
   {
      if (!chainLog.isFinilized())
      {
         fullBackup.stop();
         chainLog.addJobEntry(fullBackup);
         removeJobListeners(fullBackup);

         if (incrementalBackup != null)
         {
            if (config.getIncrementalJobPeriod() > 0)
               periodConroller.stop();

            incrementalBackup.stop();
            chainLog.addJobEntry(incrementalBackup);
            removeJobListeners(incrementalBackup);
         }

         this.state |= FINISHED;
         chainLog.endLog();
      }
   }

   public void restartIncrementalBackup()
   {
      incrementalBackup.suspend();
      incrementalBackup.resume();
      chainLog.addJobEntry(incrementalBackup);
   }

   public BackupConfig getBackupConfig()
   {
      return config;
   }

   public int getFullBackupState()
   {
      return fullBackup.getState();
   }
   
   public int getIncrementalBackupState()
   {
      if (incrementalBackup == null)
      {
         throw new IllegalStateException("The incremental bacup was not configured. Only full backup.");
      }
         
      return incrementalBackup.getState();
   }

   public String getLogFilePath()
   {
      return chainLog.getLogFilePath();
   }

   private class PeriodConroller extends TimerTask
   {
      protected Log log = ExoLogger.getLogger("exo.jcr.component.ext.PeriodConroller");

      protected Long period;

      private boolean isFirst = false;

      public PeriodConroller(long period)
      {
         this.period = period;
      }

      @Override
      public void run()
      {
         if (incrementalBackup.getState() == BackupJob.FINISHED)
         {
            this.stop();
         }
         else if (!isFirst)
         {
            isFirst = true;
         }
         else
         {
            Thread starter = new Thread("IncrementalBackup Starter")
            {
               @Override
               public void run()
               {
                  restartIncrementalBackup();
                  if (log.isDebugEnabled())
                     log.debug("Restart incrementalBackup :" + new Date().toString());
               }
            };
            starter.start();
         }
      }

      public void start()
      {
         timer.schedule(this, new Date(), period);
      }

      public boolean stop()
      {
         log.info("Stop period controller");
         return this.cancel();
      }
   }

   public int getState()
   {
      return state;
   }

   public boolean isFinished()
   {
      return ((state & BackupChain.FINISHED) == BackupChain.FINISHED ? true : false);
   }

   public String getBackupId()
   {
      return backupId;
   }

   public Calendar getStartedTime()
   {
      return timeStamp;
   }
}
