package com.sleepycat.je.log;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.util.List;
import java.util.zip.Checksum;
import com.sleepycat.je.DatabaseException;
import com.sleepycat.je.RunRecoveryException;
import com.sleepycat.je.cleaner.TrackedFileSummary;
import com.sleepycat.je.cleaner.UtilizationTracker;
import com.sleepycat.je.config.EnvironmentParams;
import com.sleepycat.je.dbi.DbConfigManager;
import com.sleepycat.je.dbi.EnvironmentImpl;
import com.sleepycat.je.log.entry.LogEntry;
import com.sleepycat.je.utilint.Adler32;
import com.sleepycat.je.utilint.DbLsn;
import com.sleepycat.je.utilint.TestHook;
import com.sleepycat.je.utilint.Tracer;
import de.ovgu.cide.jakutil.*;
/** 
 * The LogManager supports reading and writing to the JE log.
 */
abstract public class LogManager {
  private static final String DEBUG_NAME=LogManager.class.getName();
  static final int HEADER_BYTES=14;
  static final int CHECKSUM_BYTES=4;
  static final int PREV_BYTES=4;
  static final int HEADER_ENTRY_TYPE_OFFSET=4;
  static final int HEADER_VERSION_OFFSET=5;
  static final int HEADER_PREV_OFFSET=6;
  static final int HEADER_SIZE_OFFSET=6 + 4;
  static int HEADER_CONTENT_BYTES(){
    int r=HEADER_BYTES;
    r=hook504(r);
    return r;
  }
  protected LogBufferPool logBufferPool;
  private FileManager fileManager;
  protected EnvironmentImpl envImpl;
  private boolean readOnly;
  private int readBufferSize;
  private long lastLsnAtRecovery=DbLsn.NULL_LSN;
  private TestHook readHook;
  /** 
 * There is a single log manager per database environment.
 */
  public LogManager(  EnvironmentImpl envImpl,  boolean readOnly) throws DatabaseException {
    this.envImpl=envImpl;
    this.fileManager=envImpl.getFileManager();
    DbConfigManager configManager=envImpl.getConfigManager();
    this.readOnly=readOnly;
    logBufferPool=new LogBufferPool(fileManager,envImpl);
    this.hook505(configManager);
    this.hook502(envImpl);
    readBufferSize=configManager.getInt(EnvironmentParams.LOG_FAULT_READ_SIZE);
    this.hook498(envImpl);
  }
  public long getLastLsnAtRecovery(){
    return lastLsnAtRecovery;
  }
  public void setLastLsnAtRecovery(  long lastLsnAtRecovery){
    this.lastLsnAtRecovery=lastLsnAtRecovery;
  }
  /** 
 * Reset the pool when the cache is resized. This method is called after the
 * memory budget has been calculated.
 */
  public void resetPool(  DbConfigManager configManager) throws DatabaseException {
    logBufferPool.reset(configManager);
  }
  /** 
 * Log this single object and force a write of the log files.
 * @param itemobject to be logged
 * @param fsyncRequiredif true, log files should also be fsynced.
 * @return LSN of the new log entry
 */
  public long logForceFlush(  LoggableObject item,  boolean fsyncRequired) throws DatabaseException {
    return log(item,false,true,fsyncRequired,false,DbLsn.NULL_LSN);
  }
  /** 
 * Log this single object and force a flip of the log files.
 * @param itemobject to be logged
 * @param fsyncRequiredif true, log files should also be fsynced.
 * @return LSN of the new log entry
 */
  public long logForceFlip(  LoggableObject item) throws DatabaseException {
    return log(item,false,true,false,true,DbLsn.NULL_LSN);
  }
  /** 
 * Write a log entry.
 * @return LSN of the new log entry
 */
  public long log(  LoggableObject item) throws DatabaseException {
    return log(item,false,false,false,false,DbLsn.NULL_LSN);
  }
  /** 
 * Write a log entry.
 * @return LSN of the new log entry
 */
  public long log(  LoggableObject item,  boolean isProvisional,  long oldNodeLsn) throws DatabaseException {
    return log(item,isProvisional,false,false,false,oldNodeLsn);
  }
  /** 
 * Write a log entry.
 * @param itemis the item to be logged.
 * @param isProvisionaltrue if this entry should not be read during recovery.
 * @param flushRequiredif true, write the log to the file after adding the item. i.e.
 * call java.nio.channel.FileChannel.write().
 * @param fsyncRequiredif true, fsync the last file after adding the item.
 * @param oldNodeLsnis the previous version of the node to be counted as obsolete,
 * or null if the item is not a node or has no old LSN.
 * @return LSN of the new log entry
 */
  private long log(  LoggableObject item,  boolean isProvisional,  boolean flushRequired,  boolean fsyncRequired,  boolean forceNewLogFile,  long oldNodeLsn) throws DatabaseException {
    if (readOnly) {
      return DbLsn.NULL_LSN;
    }
    boolean marshallOutsideLatch=item.marshallOutsideWriteLatch();
    ByteBuffer marshalledBuffer=null;
    UtilizationTracker tracker=envImpl.getUtilizationTracker();
    LogResult logResult=null;
    try {
      if (marshallOutsideLatch) {
        int itemSize=item.getLogSize();
        int entrySize=itemSize + HEADER_BYTES;
        marshalledBuffer=marshallIntoBuffer(item,itemSize,isProvisional,entrySize);
      }
      logResult=logItem(item,isProvisional,flushRequired,forceNewLogFile,oldNodeLsn,marshallOutsideLatch,marshalledBuffer,tracker);
    }
 catch (    BufferOverflowException e) {
      throw new RunRecoveryException(envImpl,e);
    }
catch (    IOException e) {
      throw new DatabaseException(Tracer.getStackTrace(e),e);
    }
    this.hook501(fsyncRequired);
    this.hook499(logResult);
    if (logResult.wakeupCleaner) {
      tracker.activateCleaner();
    }
    return logResult.currentLsn;
  }
  abstract protected LogResult logItem(  LoggableObject item,  boolean isProvisional,  boolean flushRequired,  boolean forceNewLogFile,  long oldNodeLsn,  boolean marshallOutsideLatch,  ByteBuffer marshalledBuffer,  UtilizationTracker tracker) throws IOException, DatabaseException ;
  /** 
 * Called within the log write critical section.
 */
  protected LogResult logInternal(  LoggableObject item,  boolean isProvisional,  boolean flushRequired,  boolean forceNewLogFile,  long oldNodeLsn,  boolean marshallOutsideLatch,  ByteBuffer marshalledBuffer,  UtilizationTracker tracker) throws IOException, DatabaseException {
    LogEntryType entryType=item.getLogType();
    if (oldNodeLsn != DbLsn.NULL_LSN) {
      tracker.countObsoleteNode(oldNodeLsn,entryType);
    }
    int entrySize;
    if (marshallOutsideLatch) {
      entrySize=marshalledBuffer.limit();
    }
 else {
      entrySize=item.getLogSize() + HEADER_BYTES;
    }
    if (forceNewLogFile) {
      fileManager.forceNewLogFile();
    }
    boolean flippedFile=fileManager.bumpLsn(entrySize);
    long currentLsn=DbLsn.NULL_LSN;
    boolean wakeupCleaner=false;
    boolean usedTemporaryBuffer=false;
    try {
      currentLsn=fileManager.getLastUsedLsn();
      wakeupCleaner=tracker.countNewLogEntry(currentLsn,entryType,entrySize);
      if (item.countAsObsoleteWhenLogged()) {
        tracker.countObsoleteNodeInexact(currentLsn,entryType);
      }
      if (!marshallOutsideLatch) {
        marshalledBuffer=marshallIntoBuffer(item,entrySize - HEADER_BYTES,isProvisional,entrySize);
      }
      if (entrySize != marshalledBuffer.limit()) {
        throw new DatabaseException("Logged item entrySize= " + entrySize + " but marshalledSize="+ marshalledBuffer.limit()+ " type="+ entryType+ " currentLsn="+ DbLsn.getNoFormatString(currentLsn));
      }
      LogBuffer useLogBuffer=logBufferPool.getWriteBuffer(entrySize,flippedFile);
      marshalledBuffer=addPrevOffsetAndChecksum(marshalledBuffer,fileManager.getPrevEntryOffset(),entrySize);
      usedTemporaryBuffer=this.hook503(marshalledBuffer,entrySize,currentLsn,usedTemporaryBuffer,useLogBuffer);
    }
 catch (    Exception e) {
      fileManager.restoreLastPosition();
      if (e instanceof DatabaseException) {
        throw (DatabaseException)e;
      }
 else       if (e instanceof IOException) {
        throw (IOException)e;
      }
 else {
        throw new DatabaseException(e);
      }
    }
    if (!usedTemporaryBuffer) {
      logBufferPool.writeCompleted(currentLsn,flushRequired);
    }
    item.postLogWork(currentLsn);
    boolean wakeupCheckpointer=false;
    wakeupCheckpointer=this.hook500(item,entrySize,wakeupCheckpointer);
    return new LogResult(currentLsn,wakeupCheckpointer,wakeupCleaner);
  }
  /** 
 * Serialize a loggable object into this buffer.
 */
  private ByteBuffer marshallIntoBuffer(  LoggableObject item,  int itemSize,  boolean isProvisional,  int entrySize) throws DatabaseException {
    ByteBuffer destBuffer=ByteBuffer.allocate(entrySize);
    destBuffer.position(CHECKSUM_BYTES);
    writeHeader(destBuffer,item.getLogType(),itemSize,isProvisional);
    item.writeToLog(destBuffer);
    destBuffer.flip();
    return destBuffer;
  }
  private ByteBuffer addPrevOffsetAndChecksum(  ByteBuffer destBuffer,  long lastOffset,  int entrySize){
    Checksum checksum=Adler32.makeChecksum();
    destBuffer.position(HEADER_PREV_OFFSET);
    LogUtils.writeUnsignedInt(destBuffer,lastOffset);
    checksum.update(destBuffer.array(),CHECKSUM_BYTES,(entrySize - CHECKSUM_BYTES));
    destBuffer.position(0);
    LogUtils.writeUnsignedInt(destBuffer,checksum.getValue());
    destBuffer.position(0);
    return destBuffer;
  }
  /** 
 * Serialize a loggable object into this buffer. Return it ready for a copy.
 */
  ByteBuffer putIntoBuffer(  LoggableObject item,  int itemSize,  long prevLogEntryOffset,  boolean isProvisional,  int entrySize) throws DatabaseException {
    ByteBuffer destBuffer=marshallIntoBuffer(item,itemSize,isProvisional,entrySize);
    return addPrevOffsetAndChecksum(destBuffer,0,entrySize);
  }
  /** 
 * Helper to write the common entry header.
 * @param destBufferdestination
 * @param itemobject being logged
 * @param itemSizeWe could ask the item for this, but are passing it as a
 * parameter for efficiency, because it's already available
 */
  private void writeHeader(  ByteBuffer destBuffer,  LogEntryType itemType,  int itemSize,  boolean isProvisional){
    byte typeNum=itemType.getTypeNum();
    destBuffer.put(typeNum);
    byte version=itemType.getVersion();
    if (isProvisional)     version=LogEntryType.setProvisional(version);
    destBuffer.put(version);
    destBuffer.position(HEADER_SIZE_OFFSET);
    LogUtils.writeInt(destBuffer,itemSize);
  }
  /** 
 * Instantiate all the objects in the log entry at this LSN.
 * @param lsnlocation of entry in log.
 * @return log entry that embodies all the objects in the log entry.
 */
  public LogEntry getLogEntry(  long lsn) throws DatabaseException {
    envImpl.checkIfInvalid();
    LogSource logSource=getLogSource(lsn);
    return getLogEntryFromLogSource(lsn,logSource);
  }
  LogEntry getLogEntry(  long lsn,  RandomAccessFile file) throws DatabaseException {
    return getLogEntryFromLogSource(lsn,new FileSource(file,readBufferSize,fileManager));
  }
  /** 
 * Instantiate all the objects in the log entry at this LSN. This will
 * release the log source at the first opportunity.
 * @param lsnlocation of entry in log
 * @return log entry that embodies all the objects in the log entry
 */
  private LogEntry getLogEntryFromLogSource(  long lsn,  LogSource logSource) throws DatabaseException {
    return new LogManager_getLogEntryFromLogSource(this,lsn,logSource).execute();
  }
  /** 
 * Fault in the first object in the log entry log entry at this LSN.
 * @param lsnlocation of object in log
 * @return the object in the log
 */
  public Object get(  long lsn) throws DatabaseException {
    LogEntry entry=getLogEntry(lsn);
    return entry.getMainItem();
  }
  /** 
 * Find the LSN, whether in a file or still in the log buffers.
 */
  private LogSource getLogSource(  long lsn) throws DatabaseException {
    LogBuffer logBuffer=logBufferPool.getReadBuffer(lsn);
    if (logBuffer == null) {
      try {
        return new FileHandleSource(fileManager.getFileHandle(DbLsn.getFileNumber(lsn)),readBufferSize,fileManager);
      }
 catch (      LogFileNotFoundException e) {
        throw new LogFileNotFoundException(DbLsn.getNoFormatString(lsn) + ' ' + e.getMessage());
      }
    }
 else {
      return logBuffer;
    }
  }
  /** 
 * Flush all log entries, fsync the log file.
 */
  public void flush() throws DatabaseException {
    if (readOnly) {
      return;
    }
    flushInternal();
    fileManager.syncLogEnd();
  }
  abstract protected void flushInternal() throws LogException, DatabaseException ;
  /** 
 * Returns a tracked summary for the given file which will not be flushed.
 * Used for watching changes that occur while a file is being cleaned.
 */
  abstract public TrackedFileSummary getUnflushableTrackedSummary(  long file) throws DatabaseException ;
  protected TrackedFileSummary getUnflushableTrackedSummaryInternal(  long file) throws DatabaseException {
    return envImpl.getUtilizationTracker().getUnflushableTrackedSummary(file);
  }
  /** 
 * Count node as obsolete under the log write latch. This is done here
 * because the log write latch is managed here, and all utilization counting
 * must be performed under the log write latch.
 */
  abstract public void countObsoleteNode(  long lsn,  LogEntryType type) throws DatabaseException ;
  protected void countObsoleteNodeInternal(  UtilizationTracker tracker,  long lsn,  LogEntryType type) throws DatabaseException {
    tracker.countObsoleteNode(lsn,type);
  }
  /** 
 * Counts file summary info under the log write latch.
 */
  abstract public void countObsoleteNodes(  TrackedFileSummary[] summaries) throws DatabaseException ;
  protected void countObsoleteNodesInternal(  UtilizationTracker tracker,  TrackedFileSummary[] summaries) throws DatabaseException {
    for (int i=0; i < summaries.length; i+=1) {
      TrackedFileSummary summary=summaries[i];
      tracker.addSummary(summary.getFileNumber(),summary);
    }
  }
  /** 
 * Counts the given obsolete IN LSNs under the log write latch.
 */
  abstract public void countObsoleteINs(  List lsnList) throws DatabaseException ;
  protected void countObsoleteINsInternal(  List lsnList) throws DatabaseException {
    UtilizationTracker tracker=envImpl.getUtilizationTracker();
    for (int i=0; i < lsnList.size(); i+=1) {
      Long offset=(Long)lsnList.get(i);
      tracker.countObsoleteNode(offset.longValue(),LogEntryType.LOG_IN);
    }
  }
  public void setReadHook(  TestHook hook){
    readHook=hook;
  }
@MethodObject static class LogManager_getLogEntryFromLogSource {
    LogManager_getLogEntryFromLogSource(    LogManager _this,    long lsn,    LogSource logSource){
      this._this=_this;
      this.lsn=lsn;
      this.logSource=logSource;
    }
    LogEntry execute() throws DatabaseException {
      try {
        fileOffset=DbLsn.getFileOffset(lsn);
        entryBuffer=logSource.getBytes(fileOffset);
        this.hook507();
        loggableType=entryBuffer.get();
        version=entryBuffer.get();
        entryBuffer.position(entryBuffer.position() + _this.PREV_BYTES);
        itemSize=LogUtils.readInt(entryBuffer);
        if (entryBuffer.remaining() < itemSize) {
          entryBuffer=logSource.getBytes(fileOffset + _this.HEADER_BYTES,itemSize);
          this.hook508();
        }
        this.hook506();
        assert LogEntryType.isValidType(loggableType) : "Read non-valid log entry type: " + loggableType;
        logEntry=LogEntryType.findType(loggableType,version).getNewLogEntry();
        logEntry.readEntry(entryBuffer,itemSize,version,true);
        if (_this.readHook != null) {
          _this.readHook.doIOHook();
        }
        return logEntry;
      }
 catch (      DatabaseException e) {
        throw e;
      }
catch (      ClosedChannelException e) {
        throw new RunRecoveryException(_this.envImpl,"Channel closed, may be " + "due to thread interrupt",e);
      }
catch (      Exception e) {
        throw new DatabaseException(e);
      }
 finally {
        if (logSource != null) {
          logSource.release();
        }
      }
    }
    protected LogManager _this;
    protected long lsn;
    protected LogSource logSource;
    protected long fileOffset;
    protected ByteBuffer entryBuffer;
    protected ChecksumValidator validator;
    protected long storedChecksum;
    protected byte loggableType;
    protected byte version;
    protected int itemSize;
    protected LogEntry logEntry;
    protected void hook506() throws DatabaseException, ClosedChannelException, Exception {
    }
    protected void hook507() throws DatabaseException, ClosedChannelException, Exception {
    }
    protected void hook508() throws DatabaseException, ClosedChannelException, Exception {
    }
  }
  protected void hook498(  EnvironmentImpl envImpl) throws DatabaseException {
  }
  protected void hook499(  LogResult logResult) throws DatabaseException {
  }
  protected boolean hook500(  LoggableObject item,  int entrySize,  boolean wakeupCheckpointer) throws IOException, DatabaseException {
    return wakeupCheckpointer;
  }
  protected void hook501(  boolean fsyncRequired) throws DatabaseException {
  }
  protected void hook502(  EnvironmentImpl envImpl) throws DatabaseException {
  }
  protected boolean hook503(  ByteBuffer marshalledBuffer,  int entrySize,  long currentLsn,  boolean usedTemporaryBuffer,  LogBuffer useLogBuffer) throws IOException, DatabaseException, Exception {
    ByteBuffer useBuffer=useLogBuffer.getDataBuffer();
    if (useBuffer.capacity() - useBuffer.position() < entrySize) {
      fileManager.writeLogBuffer(new LogBuffer(marshalledBuffer,currentLsn));
      usedTemporaryBuffer=true;
      assert useBuffer.position() == 0;
      this.hook509();
    }
 else {
      useBuffer.put(marshalledBuffer);
    }
    return usedTemporaryBuffer;
  }
  protected static int hook504(  int r){
    return r;
  }
  protected void hook505(  DbConfigManager configManager) throws DatabaseException {
  }
  protected void hook509() throws IOException, DatabaseException, Exception {
  }
}
