/*
 * Copyright (C) 2003-2007 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.impl.core.query;

import org.exoplatform.commons.utils.PrivilegedFileHelper;
import org.exoplatform.commons.utils.SecurityHelper;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

/**
 * All changes that must be in index but interrupted by IOException are here.
 * Created by The eXo Platform SAS Author : Sergey Karpenko
 * sergey.karpenko@exoplatform.com.ua
 * 
 * @version $Id: $
 */

public class ErrorLog
{

   /**
    * Logger instance for this class
    */
   private static final Log LOG = ExoLogger.getLogger("exo.jcr.component.core.ErrorLog");

   /**
    * REMOVE term.
    */
   public static final String REMOVE = "rem";

   /**
    * ADD term.
    */
   public static final String ADD = "add";

   /**
    * The log file
    */
   private final File logFile;

   /**
    * Writer to the log file
    */
   private FileChannel out;

   /**
    * File size in Kb. Used on create and clear(truncate) methods.
    */
   private int fileSize = 0; // Kb

   /**
    * ErrorLog constructor.
    * 
    * @param file
    * @param errorLogSize
    * @throws IOException
    */
   public ErrorLog(File file, int errorLogSize) throws IOException
   {
      fileSize = errorLogSize;
      logFile = file;
      openFile(file);
   }

   /**
    * openFile.
    * 
    * @param log
    * @throws IOException
    */
   private void openFile(final File log) throws IOException
   {
      SecurityHelper.doPrivilegedIOExceptionAction(new PrivilegedExceptionAction<Void>()
      {
         @SuppressWarnings("resource")
         public Void run() throws Exception
         {
            // set file size;
            if (!log.exists())
            {
               log.getParentFile().mkdirs();
               log.createNewFile();
               out = new FileOutputStream(log).getChannel();
               out.position(1024 * fileSize - 1);
               out.write(ByteBuffer.wrap(new byte[]{0}));
               out.position(0);
               out.force(false);
            }
            else
            {
               out = new FileOutputStream(log, true).getChannel();
            }
            return null;
         }
      });
   }

   /**
    * Appends an action to the log.
    * 
    * @param action
    *            the action to append.
    * @throws IOException
    *             if the node cannot be written to the redo log.
    */
   public void append(final String action, final String uuid) throws IOException
   {
      initOut();
      out.write(ByteBuffer.wrap((action + " " + uuid + "\n").getBytes()));
   }

   /**
    * Flushes all pending writes to the underlying file.
    * 
    * @throws IOException
    *             if an error occurs while writing.
    */
   public void flush() throws IOException
   {
      if (out != null)
      {
         out.force(false);
      }
   }

   /**
    * Clears the redo log.
    * 
    * @throws IOException
    *             if the redo log cannot be cleared.
    */
   public void clear() throws IOException
   {
      if (out != null)
      {
         out.truncate(0);
         out.close();
         out = PrivilegedFileHelper.fileOutputStream(logFile).getChannel();
         out.position(1024 * fileSize - 1);
         out.write(ByteBuffer.wrap(new byte[]{0}));
         out.position(0);
         out.force(false);
      }
   }

   /**
    * Initializes the {@link #out} stream if it is not yet set.
    * 
    * @throws IOException
    *             if an error occurs while creating the output stream.
    */
   private void initOut() throws IOException
   {
      if (out == null)
      {
         FileOutputStream os = PrivilegedFileHelper.fileOutputStream(logFile, false);
         out = os.getChannel();
      }
   }

   /**
    * Reads the log file .
    * 
    * @throws IOException
    *             if an error occurs while reading from the log file.
    */
   public List<String> readList() throws IOException
   {
      InputStream in = PrivilegedFileHelper.fileInputStream(logFile);
      try
      {
         List<String> list = new ArrayList<String>();
         BufferedReader reader = new BufferedReader(new InputStreamReader(in));
         String line;
         while ((line = reader.readLine()) != null)
         {
            if (!line.matches("\\x00++"))
            {
               list.add(line);
            }
         }
         return list;

      }
      finally
      {
         if (in != null)
         {
            try
            {
               in.close();
            }
            catch (IOException e)
            {
               LOG.warn("Exception while closing error log: " + e.toString());
            }
         }
      }
   }

   public void readChanges(Set<String> rem, Set<String> add) throws IOException
   {
      List<String> list = readList();

      Iterator<String> it = list.iterator();
      while (it.hasNext())
      {
         String[] str = it.next().split(" ");
         if (str.length == 2)
         {
            if (str[0].equals(ADD))
            {
               add.add(str[1]);
            }
            else if (str[0].equals(REMOVE))
            {
               rem.add(str[1]);
            }
         }
      }
   }

   public void writeChanges(Set<String> removed, Set<String> added) throws IOException
   {
      try
      {
         if (!removed.isEmpty())
         {
            Iterator<String> rem = removed.iterator();
            while (rem.hasNext())
            {
               append(ErrorLog.REMOVE, rem.next());
            }
         }
         if (!added.isEmpty())
         {
            Iterator<String> add = added.iterator();
            while (add.hasNext())
            {
               append(ErrorLog.ADD, add.next());
            }
         }
      }
      finally
      {
         flush();
      }
   }

   /**
    * Closes ErrorLog and frees resources associated with it.
    */
   public void close()
   {
      if (out != null)
      {
         try
         {
            out.close();
         }
         catch (IOException e)
         {
            out = null;
         }
      }
   }

}
