/*
 * Copyright (C) 2003-2010 eXo Platform SAS.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Affero General Public License
 * as published by the Free Software Foundation; either version 3
 * of the License, or (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see<http://www.gnu.org/licenses/>.
 */
package org.exoplatform.services.jcr.ext.backup;

import org.exoplatform.commons.utils.PrivilegedFileHelper;
import org.exoplatform.commons.utils.SecurityHelper;
import org.exoplatform.container.xml.Deserializer;
import org.exoplatform.services.jcr.config.RepositoryEntry;
import org.exoplatform.services.jcr.config.RepositoryServiceConfiguration;
import org.exoplatform.services.jcr.ext.backup.server.RepositoryRestoreExeption;
import org.exoplatform.services.jcr.impl.Constants;
import org.exoplatform.services.jcr.impl.util.JCRDateFormat;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import org.jibx.runtime.BindingDirectory;
import org.jibx.runtime.IBindingFactory;
import org.jibx.runtime.IMarshallingContext;
import org.jibx.runtime.IUnmarshallingContext;
import org.jibx.runtime.JiBXException;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.regex.PatternSyntaxException;

import javax.xml.stream.FactoryConfigurationError;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.XMLStreamWriter;
import javax.xml.stream.events.StartElement;

/**
 * Created by The eXo Platform SAS.
 * 
 * <br>Date: 2010
 *
 * @author <a href="mailto:alex.reshetnyak@exoplatform.com.ua">Alex Reshetnyak</a> 
 * @version $Id$
 */
public class RepositoryBackupChainLog
{
   private class LogWriter
   {

      protected Log logger = ExoLogger.getLogger("exo.jcr.component.ext.LogWriter");

      private File logFile;

      XMLStreamWriter writer;

      public LogWriter(File file) throws FileNotFoundException, XMLStreamException, FactoryConfigurationError
      {
         this.logFile = file;

         try
         {
            writer = SecurityHelper.doPrivilegedExceptionAction(new PrivilegedExceptionAction<XMLStreamWriter>()
            {
               public XMLStreamWriter run() throws Exception
               {
                  return XMLOutputFactory.newInstance().createXMLStreamWriter(new FileOutputStream(logFile),
                           Constants.DEFAULT_ENCODING);
               }
            });
         }
         catch (PrivilegedActionException pae)
         {
            Throwable cause = pae.getCause();
            if (cause instanceof FileNotFoundException)
            {
               throw (FileNotFoundException)cause;
            }
            else if (cause instanceof XMLStreamException)
            {
               throw (XMLStreamException)cause;
            }
            else if (cause instanceof FactoryConfigurationError)
            {
               throw (FactoryConfigurationError)cause;
            }
            else if (cause instanceof RuntimeException)
            {
               throw (RuntimeException)cause;
            }
            else
            {
               throw new RuntimeException(cause);
            }
         };

         writer.writeStartDocument();
         writer.writeStartElement("repository-backup-chain-log");

         writer.writeStartElement("version-log");
         writer.writeCharacters(versionLog);
         writer.writeEndElement();

         writer.writeStartElement("start-time");
         writer.writeCharacters(JCRDateFormat.format(startedTime));
         writer.writeEndElement();

         writer.flush();
      }

      public void writeSystemWorkspaceName(String wsName) throws XMLStreamException
      {
         writer.writeStartElement("system-workspace");
         writer.writeCharacters(wsName);
         writer.writeEndElement();
         writer.flush();
      }

      public void writeBackupsPath(List<String> wsLogFilePathList, RepositoryBackupConfig config)
               throws XMLStreamException,
               IOException
      {
         writer.writeStartElement("workspaces-backup-info");

         for (String path : wsLogFilePathList)
         {
            writer.writeStartElement("url");
            writer.writeCharacters(RepositoryChainLogPathHelper.getRelativePath(path, PrivilegedFileHelper.getCanonicalPath(config
                     .getBackupDir())));
            writer.writeEndElement();
         }

         writer.writeEndElement();

         writer.flush();
      }

      public synchronized void write(RepositoryBackupConfig config, String fullBackupType,
               String incrementalBackupType, File serviceBackupDir)
               throws XMLStreamException, IOException
      {
         writer.writeStartElement("repository-backup-config");

         writer.writeStartElement("backup-type");
         writer.writeCharacters(String.valueOf(config.getBackupType()));
         writer.writeEndElement();

         writer.writeStartElement("full-backup-type");
         writer.writeCharacters(fullBackupType);
         writer.writeEndElement();

         writer.writeStartElement("incremental-backup-type");
         writer.writeCharacters(incrementalBackupType);
         writer.writeEndElement();

         if (config.getBackupDir() != null)
         {
            String backupDir = PrivilegedFileHelper.getCanonicalPath(config.getBackupDir());
            String serviceBackupDirPath = PrivilegedFileHelper.getCanonicalPath(serviceBackupDir);
            if (backupDir.startsWith(serviceBackupDirPath))
            {

               backupDir = "." + backupDir.replace(serviceBackupDirPath, "");

               if (File.separator.equals("\\"))
               {
                  backupDir = backupDir.replaceAll("\\\\", "/");
               }
            }
            
            writer.writeStartElement("backup-dir");
            writer.writeCharacters(backupDir);
            writer.writeEndElement();
         }

         if (config.getRepository() != null)
         {
            writer.writeStartElement("repository");
            writer.writeCharacters(config.getRepository());
            writer.writeEndElement();
         }

         writer.writeStartElement("incremental-job-period");
         writer.writeCharacters(Long.toString(config.getIncrementalJobPeriod()));
         writer.writeEndElement();

         writer.writeStartElement("incremental-job-number");
         writer.writeCharacters(Integer.toString(config.getIncrementalJobNumber()));
         writer.writeEndElement();

         writer.writeEndElement();

         writer.flush();
      }

      public synchronized void writeEndLog()
      {
         try
         {
            writer.writeStartElement("finish-time");
            writer.writeCharacters(JCRDateFormat.format(finishedTime));
            writer.writeEndElement();

            writer.writeEndElement();
            writer.writeEndDocument();
            writer.flush();
         }
         catch (XMLStreamException e)
         {
            logger.error("Can't write end log", e);
         }
      }

      public synchronized void writeRepositoryEntry(RepositoryEntry rEntry,
               RepositoryServiceConfiguration serviceConfiguration) throws XMLStreamException, IOException,
               JiBXException
      {
         File config =
                  new File(PrivilegedFileHelper.getCanonicalPath(RepositoryBackupChainLog.this.config.getBackupDir())
                     + File.separator + "original-repository-config.xml");
         PrivilegedFileHelper.createNewFile(config);
         OutputStream saveStream = PrivilegedFileHelper.fileOutputStream(config);
      
         ArrayList<RepositoryEntry> repositoryEntries = new ArrayList<RepositoryEntry>();
         repositoryEntries.add(rEntry);
      
         RepositoryServiceConfiguration newRepositoryServiceConfiguration =
                  new RepositoryServiceConfiguration(serviceConfiguration.getDefaultRepositoryName(), repositoryEntries);
      
         IBindingFactory bfact;
         try
         {
            bfact = SecurityHelper.doPrivilegedExceptionAction(new PrivilegedExceptionAction<IBindingFactory>()
            {
               public IBindingFactory run() throws Exception
               {
                  return BindingDirectory.getFactory(RepositoryServiceConfiguration.class);
               }
            });
         }
         catch (PrivilegedActionException pae)
         {
            Throwable cause = pae.getCause();
            if (cause instanceof JiBXException)
            {
               throw (JiBXException) cause;
            }
            else if (cause instanceof RuntimeException)
            {
               throw (RuntimeException) cause;
            }
            else
            {
               throw new RuntimeException(cause);
            }
         }
         IMarshallingContext mctx = bfact.createMarshallingContext();
      
         mctx.marshalDocument(newRepositoryServiceConfiguration, "ISO-8859-1", null, saveStream);
         saveStream.close();
      
         writer.writeStartElement("original-repository-config");
         writer.writeCharacters(config.getName());
         writer.writeEndElement();
      }
   }

   private class LogReader
   {
      protected Log logger = ExoLogger.getLogger("exo.jcr.component.ext.LogReader");

      private File logFile;

      private XMLStreamReader reader;

      private String version;

      public LogReader(File logFile) throws FileNotFoundException, XMLStreamException, FactoryConfigurationError
      {
         this.logFile = logFile;
         reader =
                  XMLInputFactory.newInstance().createXMLStreamReader(PrivilegedFileHelper.fileInputStream(logFile),
                           Constants.DEFAULT_ENCODING);
      }

      public void readLogFile() throws UnsupportedEncodingException, Exception
      {
         boolean endDocument = false;

         while (!endDocument)
         {
            int eventCode = reader.next();
            switch (eventCode)
            {

               case StartElement.START_ELEMENT :
                  String name = reader.getLocalName();

                  if (name.equals("repository-backup-config"))
                     config = readBackupConfig();

                  if (name.equals("system-workspace"))
                     workspaceSystem = readContent();

                  if (name.equals("backup-config"))
                     config = readBackupConfig();

                  if (name.equals("workspaces-backup-info"))
                     workspaceBackupsInfo = readWorkspaceBackupInfo();

                  if (name.equals("start-time"))
                     startedTime = JCRDateFormat.parse(readContent());

                  if (name.equals("finish-time"))
                     finishedTime = JCRDateFormat.parse(readContent());

                  if (name.equals("original-repository-config"))
                     originalRepositoryEntry = readRepositoryEntry();

                  if (name.equals("version-log"))
                  {
                     this.version = readContent();
                  }


                  break;

               case StartElement.END_DOCUMENT :
                  endDocument = true;
                  break;
            }
         }
      }

      private RepositoryEntry readRepositoryEntry() throws UnsupportedEncodingException, Exception
      {
         String configName = readContent();

         File configFile =
                  new File(PrivilegedFileHelper.getCanonicalPath(getBackupConfig().getBackupDir()) + File.separator
                           + configName);

         if (!PrivilegedFileHelper.exists(configFile))
         {
            throw new RepositoryRestoreExeption("The backup set is not contains original repository configuration : "
                     + PrivilegedFileHelper.getCanonicalPath(getBackupConfig().getBackupDir()));
         }

         IBindingFactory factory;
         try
         {
            factory = SecurityHelper.doPrivilegedExceptionAction(new PrivilegedExceptionAction<IBindingFactory>()
            {
               public IBindingFactory run() throws Exception
               {
                  return BindingDirectory.getFactory(RepositoryServiceConfiguration.class);
               }
            });
         }
         catch (PrivilegedActionException pae)
         {
            Throwable cause = pae.getCause();
            if (cause instanceof JiBXException)
            {
               throw (JiBXException)cause;
            }
            else if (cause instanceof RuntimeException)
            {
               throw (RuntimeException)cause;
            }
            else
            {
               throw new RuntimeException(cause);
            }
         }

         IUnmarshallingContext uctx = factory.createUnmarshallingContext();
         RepositoryServiceConfiguration conf =
                  (RepositoryServiceConfiguration) uctx.unmarshalDocument(PrivilegedFileHelper
                           .fileInputStream(configFile), null);

         if (conf.getRepositoryConfigurations().size() != 1)
         {
            throw new RepositoryRestoreExeption(
                     "The oririginal configuration should be contains only one repository entry :"
                     + PrivilegedFileHelper.getCanonicalPath(configFile));
         }

         if (!conf.getRepositoryConfiguration(getBackupConfig().getRepository()).getName().equals(
                  getBackupConfig().getRepository()))
         {
            throw new RepositoryRestoreExeption(
                     "The oririginal configuration should be contains only one repository entry with name \""
                              + getBackupConfig().getRepository() + "\" :"
                              + PrivilegedFileHelper.getCanonicalPath(configFile));
         }
         
         

         return conf.getRepositoryConfiguration(getBackupConfig().getRepository());
      }

      private List<String> readWorkspaceBackupInfo() throws XMLStreamException, IOException
      {
         List<String> wsBackupInfo = new ArrayList<String>();

         boolean endWorkspaceBackupInfo = false;

         while (!endWorkspaceBackupInfo)
         {
            int eventCode = reader.next();
            switch (eventCode)
            {

               case StartElement.START_ELEMENT :
                  String name = reader.getLocalName();

                  if (name.equals("url"))
                  {
                     if (version != null && version.equals(VERSION_LOG_1_1))
                     {
                        String path = readContent();
                        wsBackupInfo.add(RepositoryChainLogPathHelper.getPath(path, PrivilegedFileHelper.getCanonicalPath(config
                                 .getBackupDir())));
                     }
                     else
                     {
                        wsBackupInfo.add(readContent());
                     }
                  }

                  break;

               case StartElement.END_ELEMENT :
                  String tagName = reader.getLocalName();

                  if (tagName.equals("workspaces-backup-info"))
                     endWorkspaceBackupInfo = true;
                  break;
            }
         }

         return wsBackupInfo;
      }

      private BackupConfig readBackupConfig() throws XMLStreamException, IOException
      {
         BackupConfig conf = new BackupConfig();

         boolean endBackupConfig = false;

         while (!endBackupConfig)
         {
            int eventCode = reader.next();
            switch (eventCode)
            {

               case StartElement.START_ELEMENT :
                  String name = reader.getLocalName();

                  if (name.equals("backup-dir"))
                  {
                     if (version != null && version.equals(VERSION_LOG_1_1))
                     {
                        String dir = readContent();
                        if (dir.equals("."))
                        {
                           String path = PrivilegedFileHelper.getCanonicalPath(logFile.getParentFile());

                           conf.setBackupDir(new File(path));
                        }
                        else if (dir.startsWith("./"))
                        {
                           String path = PrivilegedFileHelper.getCanonicalPath(logFile.getParentFile());

                           dir = dir.replace("./", "/");
                           
                           if (File.separator.equals("\\"))
                           {
                              dir = dir.replaceAll("/", "\\\\");
                           }

                           conf.setBackupDir(new File(path + dir));
                        }
                        else
                        {
                           conf.setBackupDir(new File(Deserializer.resolveVariables(dir)));
                        }
                     }
                     else
                     {
                        conf.setBackupDir(new File(readContent()));
                     }
                  }

                  if (name.equals("backup-type"))
                     conf.setBackupType(Integer.valueOf(readContent()));

                  if (name.equals("repository"))
                     conf.setRepository(readContent());

                  if (name.equals("workspace"))
                     conf.setWorkspace(readContent());

                  if (name.equals("incremental-job-period"))
                     conf.setIncrementalJobPeriod(Long.valueOf(readContent()).longValue());

                  if (name.equals("incremental-job-number"))
                     conf.setIncrementalJobNumber(Integer.valueOf(readContent()).intValue());

                  if (name.equals("full-backup-type"))
                     fullBackupType = readContent();

                  if (name.equals("incremental-backup-type"))
                     increnetalBackupType = readContent();

                  break;

               case StartElement.END_ELEMENT :
                  String tagName = reader.getLocalName();

                  if (tagName.equals("repository-backup-config"))
                     endBackupConfig = true;
                  break;
            }
         }

         return conf;
      }

      private String readContent() throws XMLStreamException
      {
         String content = null;

         int eventCode = reader.next();

         if (eventCode == StartElement.CHARACTERS)
            content = reader.getText();

         return content;
      }

      public String getVersionLog()
      {
         return version;
      }
   }

   protected static Log logger = ExoLogger.getLogger("exo.jcr.component.ext.BackupChainLog");

   /**
    * Start for 1.1 version log will be stored relative paths. 
    */
   protected static String VERSION_LOG_1_1 = "1.1";

   public static final String PREFIX = "repository-backup-";

   private static final String SUFFIX = ".xml";

   private File log;

   private LogWriter logWriter;

   private LogReader logReader;

   private RepositoryBackupConfig config;

   private String backupId;

   private Calendar startedTime;

   private Calendar finishedTime;

   private boolean finalized;

   private List<String> workspaceBackupsInfo;

   private String workspaceSystem;

   private String fullBackupType;

   private String increnetalBackupType;

   private RepositoryEntry originalRepositoryEntry;

   private final String versionLog;

   /**
    * @param logDirectory
    * @param config
    * @param systemWorkspace
    * @param wsLogFilePathList
    * @param backupId
    * @param startTime
    * @param rEntry
    * @throws BackupOperationException
    */
   public RepositoryBackupChainLog(File logDirectory, RepositoryBackupConfig config, String fullBackupType,
            String incrementalBackupType, String systemWorkspace, List<String> wsLogFilePathList, String backupId,
            Calendar startTime, RepositoryEntry rEntry, RepositoryServiceConfiguration repositoryServiceConfiguration)
            throws BackupOperationException
   {
      try
      {
         this.finalized = false;
         this.versionLog = VERSION_LOG_1_1;
         this.log =
                  new File(PrivilegedFileHelper.getCanonicalPath(logDirectory) + File.separator
                           + (PREFIX + backupId + SUFFIX));
         PrivilegedFileHelper.createNewFile(this.log);
         this.backupId = backupId;
         this.config = config;
         this.startedTime = Calendar.getInstance();
         this.fullBackupType = fullBackupType;
         this.increnetalBackupType = incrementalBackupType;
         this.originalRepositoryEntry = rEntry;

         logWriter = new LogWriter(log);
         logWriter.write(config, fullBackupType, incrementalBackupType, logDirectory);
         logWriter.writeSystemWorkspaceName(systemWorkspace);
         logWriter.writeBackupsPath(wsLogFilePathList, config);
         logWriter.writeRepositoryEntry(rEntry, repositoryServiceConfiguration);

         this.workspaceBackupsInfo = wsLogFilePathList;
         this.workspaceSystem = systemWorkspace;
      }
      catch (IOException e)
      {
         throw new BackupOperationException("Can not create backup log ...", e);
      }
      catch (XMLStreamException e)
      {
         throw new BackupOperationException("Can not create backup log ...", e);
      }
      catch (FactoryConfigurationError e)
      {
         throw new BackupOperationException("Can not create backup log ...", e);
      }
      catch (JiBXException e)
      {
         throw new BackupOperationException("Can not create backup log ...", e);
      }
   }

   /**
    * @param log
    * @throws BackupOperationException
    */
   public RepositoryBackupChainLog(File log) throws BackupOperationException
   {
      this.log = log;
      this.backupId = log.getName().replaceAll(PREFIX, "").replaceAll(SUFFIX, "");

      try
      {
         logReader = new LogReader(log);
         logReader.readLogFile();
         this.versionLog = logReader.getVersionLog();
      }
      catch (FileNotFoundException e)
      {
         throw new BackupOperationException("Can not read RepositoryBackupChainLog from file :"
                  + PrivilegedFileHelper.getAbsolutePath(log), e);
      }
      catch (XMLStreamException e)
      {
         throw new BackupOperationException("Can not read RepositoryBackupChainLog from file :"
                  + PrivilegedFileHelper.getAbsolutePath(log), e);
      }
      catch (UnsupportedEncodingException e)
      {
         throw new BackupOperationException("Can not read RepositoryBackupChainLog from file :"
                  + PrivilegedFileHelper.getAbsolutePath(log), e);
      }
      catch (Exception e)
      {
         throw new BackupOperationException("Can not read RepositoryBackupChainLog from file :"
                  + PrivilegedFileHelper.getAbsolutePath(log), e);
      }
   }

   /**
    * Getting log file path.
    *
    * @return String
    *           return the path to backup log
    */
   public String getLogFilePath()
   {
      return PrivilegedFileHelper.getAbsolutePath(log);
   }

   /**
    * Getting repository backup configuration.
    *
    * @return ReposiotoryBackupConfig
    *           return the repository backup configuration
    */
   public RepositoryBackupConfig getBackupConfig()
   {
      return config;
   }

   /**
    * Getting the started time.
    *
    * @return Calendar
    *           return the started time
    */
   public Calendar getStartedTime()
   {
      return startedTime;
   }

   /**
    * Getting the finished time.
    *
    * @return Calendar
    *           return the finished time 
    */
   public Calendar getFinishedTime()
   {
      return finishedTime;
   }

   public boolean isFinilized()
   {
      return finalized;
   }

   /**
    * Finalize log.
    *
    */
   public synchronized void endLog()
   {
      if (!finalized)
      {
         finishedTime = Calendar.getInstance();
         finalized = true;
         logWriter.writeEndLog();

         //copy backup chain log file in into Backupset files itself for portability (e.g. on another server)
         try
         {
            InputStream in = PrivilegedFileHelper.fileInputStream(log);

            File dest = new File(config.getBackupDir() + File.separator + log.getName());
            if (!PrivilegedFileHelper.exists(dest))
            {
               OutputStream out = PrivilegedFileHelper.fileOutputStream(dest);

               byte[] buf = new byte[(int) (PrivilegedFileHelper.length(log))];
               in.read(buf);

               String sConfig = new String(buf, Constants.DEFAULT_ENCODING);
               sConfig = sConfig.replaceAll("<backup-dir>.+</backup-dir>", "<backup-dir>.</backup-dir>");

               out.write(sConfig.getBytes(Constants.DEFAULT_ENCODING));

               in.close();
               out.close();
            }
         }
         catch (PatternSyntaxException e)
         {
            logger.error("Can't write log", e);
         }
         catch (FileNotFoundException e)
         {
            logger.error("Can't write log", e);
         }
         catch (IOException e)
         {
            logger.error("Can't write log", e);
         }
      }
   }

   /**
    * Getting the system workspace name.
    * 
    * @return String
    *           return the system workspace name.
    */
   public String getSystemWorkspace()
   {
      return workspaceSystem;
   }

   /**
    * Getting the workspace backups info.
    * 
    * @return Collection
    *           return the list with path to backups.
    */
   public List<String> getWorkspaceBackupsInfo()
   {
      return workspaceBackupsInfo;
   }

   /**
    * Getting the backup id.
    *
    * @return int
    *           return the backup id
    */
   public String getBackupId()
   {
      return backupId;
   }

   /**
    * Getting original repository configuration
    * 
    * @return RepositoryEntry
    *           return the original repository configuration
    */
   public RepositoryEntry getOriginalRepositoryEntry()
   {
      return originalRepositoryEntry;
   }

}
