/*
 * 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.xcmis.search.lucene.index;


import java.io.BufferedOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;

import org.xcmis.spi.utils.Logger;

/**
 * Created by The eXo Platform SAS.
 * 
 * @author <a href="mailto:Sergey.Kabashnyuk@gmail.com">Sergey Kabashnyuk</a>
 * @version $Id: FileSystemTransactionLog.java 2 2010-02-04 17:21:49Z andrew00x $
 */
public class FileSystemTransactionLog implements TransactionLog
{

   public static final int LOG_DIR_COUNT = 2;

   private static final int ADDED_DOCUMENTS = 1;

   /**
    * Class logger.
    */
   private static final Logger LOG = Logger.getLogger(FileSystemTransactionLog.class);

   private static final int REMOVED_DOCUMENTS = 3;

   private static final int UUID_SIZE = 32;

   private Set<String> addedDocumentsUuids; // =

   private File file;

   private Set<String> removedDocumentsUuids; // =

   private final FSIndexTransactionService storage;

   /**
    * Constructor for recovery case.
    * 
    * @param srcFile - source file.
    * @param storage - transaction storage.
    */
   public FileSystemTransactionLog(final File srcFile, final FSIndexTransactionService storage)
      throws TransactionLogException
   {
      this.storage = storage;
      this.file = srcFile;
      if (!this.file.exists())
      {
         throw new TransactionLogException("File not exist " + this.file.getAbsolutePath());
      }
   }

   /**
    * Constructor for ordinary case.
    *
    * @param addedDocumentsUuids added documents
    * @param removedDocumentsUuids removed documents
    * @param storage - transaction storage.
    */
   public FileSystemTransactionLog(final Set<String> addedDocumentsUuids, final Set<String> removedDocumentsUuids,
      final FSIndexTransactionService storage)
   {
      this.addedDocumentsUuids = addedDocumentsUuids;
      this.removedDocumentsUuids = removedDocumentsUuids;
      this.storage = storage;

   }

   /**
    * {@inheritDoc}
    * 
    * @throws TransactionLogException
    */
   public Set<String> getAddedList() throws TransactionLogException
   {
      if (this.addedDocumentsUuids == null)
      {
         this.load();
      }
      return this.addedDocumentsUuids;
   }

   /**
    * {@inheritDoc}
    * 
    * @throws TransactionLogException
    */
   public Set<String> getRemovedList() throws TransactionLogException
   {
      if (this.removedDocumentsUuids == null)
      {
         this.load();
      }
      return this.removedDocumentsUuids;
   }

   /**
    * {@inheritDoc}
    */
   public void load() throws TransactionLogException
   {
      this.addedDocumentsUuids = new HashSet<String>();

      this.removedDocumentsUuids = new HashSet<String>();
      FileInputStream in = null;
      try
      {
         in = new FileInputStream(this.file);
         int type;
         while ((type = in.read()) != -1)
         {
            final byte[] buf = new byte[FileSystemTransactionLog.UUID_SIZE];
            if (in.read(buf) != FileSystemTransactionLog.UUID_SIZE)
            {
               throw new TransactionLogException(" TransactionLog file is corrupted. Unexpected end of file: "
                  + this.file.getAbsolutePath());
            }
            final String uuid = new String(buf);
            switch (type)
            {
               case ADDED_DOCUMENTS :
                  this.addedDocumentsUuids.add(uuid);
                  break;
               case REMOVED_DOCUMENTS :
                  this.removedDocumentsUuids.add(uuid);
                  break;
               default :

                  throw new TransactionLogException(" TransactionLog file is corrupted. Unexpected type of record "
                     + type + ". file : " + this.file.getAbsolutePath());
            }
         }
      }
      catch (final EOFException e)
      {
         throw new TransactionLogException(" TransactionLog file is uncomplete: " + this.file.getAbsolutePath());

      }
      catch (final IOException e)
      {
         throw new TransactionLogException("TransactionLog read exception: " + e.getMessage(), e);
      }
      finally
      {
         if (in != null)
         {
            try
            {
               in.close();
            }
            catch (final IOException e)
            {
               FileSystemTransactionLog.LOG.warn("Can not close log file " + this.file.getAbsolutePath());
            }
         }
      }
   }

   /**
    * {@inheritDoc}
    */
   public void log() throws TransactionLogException
   {
      OutputStream out = null;
      try
      {
         //TODO Check uuid
         final String fileName = UUID.randomUUID().toString();

         final File dir =
            new File(this.storage.getStorageDir().getAbsoluteFile()
               + this.buildPathFromName(fileName, FileSystemTransactionLog.LOG_DIR_COUNT));

         this.file = this.storage.getLockFactory().createFile(dir, fileName, FileSystemTransactionLog.LOG_DIR_COUNT);

         out = new BufferedOutputStream(new FileOutputStream(this.file));

         this.writeSet(out, this.addedDocumentsUuids, FileSystemTransactionLog.ADDED_DOCUMENTS);
         this.writeSet(out, this.removedDocumentsUuids, FileSystemTransactionLog.REMOVED_DOCUMENTS);
         out.flush();
      }
      catch (final IOException e)
      {
         throw new TransactionLogException("TransactionLog write exception: " + e.getMessage(), e);
      }
      finally
      {
         if (out != null)
         {
            try
            {
               out.close();
            }
            catch (final IOException e)
            {
               FileSystemTransactionLog.LOG.warn("Can not close log file" + this.file.getAbsolutePath());
            }
         }
      }
   }

   /**
    * {@inheritDoc}
    */
   public void removeLog() throws TransactionLogException
   {
      if (!this.file.exists())
      {
         FileSystemTransactionLog.LOG.warn("TransactionLog file not exist. " + this.file.getAbsolutePath());
      }
      if (!this.removeLogFile(this.file))
      {
         throw new TransactionLogException("TransactionLog file was not delete. " + this.file.getAbsolutePath());
      }
      else
      {
         if (FileSystemTransactionLog.LOG.isDebugEnabled())
         {
            FileSystemTransactionLog.LOG.debug("Transaction log removed " + this.file.getAbsolutePath());
         }
      }
   }

   /**
    * Create path to file using its name.
    * 
    * @param fileName - fileName as a source to generated path
    * @param dirCount - parent dirs count
    * @return String - generated path to file.
    */
   private String buildPathFromName(final String fileName, final int dirCount)
   {
      final char[] chs = fileName.toCharArray();
      StringBuilder path = new StringBuilder();
      for (int i = 1; i <= dirCount; i++)
      {
         path.append(File.separator);
         path.append(chs[chs.length - i]);
      }
      return path.toString();
   }

   /**
    * Remove log file. Depends from creation file algorithm. See builfPathX2C1
    * method.
    * 
    * @param file to remove.
    * @return <code>true</code> if remove successfull.
    */
   private boolean removeLogFile(final File file) throws TransactionLogException
   {
      final File dir = file.getParentFile();
      if (!file.delete())
      {
         return false;
      }
      else
      {
         // try to delete parents
         this.storage.getLockFactory().removeDirectory(dir, FileSystemTransactionLog.LOG_DIR_COUNT);
         return true;

      }
   }

   /**
    * Write set of string to out with flag.
    * 
    * @param out - OutputStream
    * @param keys - Set of String to Store
    * @param flag - ADDED_DOCUMENTS, UPDATED_DOCUMENTS or REMOVED_DOCUMENTS
    * @throws IOException if write to file IOException occurs.
    */
   private void writeSet(final OutputStream out, final Set<String> keys, final int flag) throws IOException
   {
      for (final String key : keys)
      {
         out.write(flag);
         out.write(key.getBytes());
      }
   }

}
