package org.prevayler.implementation.journal;
import org.prevayler.foundation.Chunk;
import org.prevayler.foundation.DurableInputStream;
import org.prevayler.foundation.DurableOutputStream;
import org.prevayler.foundation.StopWatch;
import org.prevayler.implementation.PrevaylerDirectory;
import org.prevayler.implementation.TransactionGuide;
import org.prevayler.implementation.TransactionTimestamp;
import org.prevayler.implementation.publishing.TransactionSubscriber;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;

/** 
 * A Journal that will write all transactions to .journal files.
 */
public class PersistentJournal implements Journal {
  private final PrevaylerDirectory _directory;
  private DurableOutputStream _outputJournal;
  private final long _journalSizeThresholdInBytes;
  private final long _journalAgeThresholdInMillis;
  private StopWatch _journalAgeTimer;
  private long _nextTransaction;
  private boolean _nextTransactionInitialized=false;
  private final String _journalSuffix;
  public PersistentJournal(  PrevaylerDirectory directory,  long journalSizeThresholdInBytes,  long journalAgeThresholdInMillis,  String journalSuffix) throws IOException {
    PrevaylerDirectory.checkValidJournalSuffix(journalSuffix);
    _directory=directory;
    _directory.produceDirectory();
    _journalSizeThresholdInBytes=journalSizeThresholdInBytes;
    _journalAgeThresholdInMillis=journalAgeThresholdInMillis;
    _journalSuffix=journalSuffix;
  }
  public void append(  TransactionGuide guide){
    if (!_nextTransactionInitialized)     throw new IllegalStateException("Journal.update() has to be called at least once before Journal.append().");
    DurableOutputStream myOutputJournal;
    DurableOutputStream outputJournalToClose=null;
    guide.startTurn();
    try {
      guide.checkSystemVersion(_nextTransaction);
      if (!isOutputJournalStillValid()) {
        outputJournalToClose=_outputJournal;
        _outputJournal=createOutputJournal(_nextTransaction);
        _journalAgeTimer=StopWatch.start();
      }
      _nextTransaction++;
      myOutputJournal=_outputJournal;
    }
  finally {
      guide.endTurn();
    }
    try {
      myOutputJournal.sync(guide);
    }
 catch (    IOException iox) {
      handle(iox,_outputJournal.file(),"writing to");
    }
    guide.startTurn();
    try {
      try {
        if (outputJournalToClose != null)         outputJournalToClose.close();
      }
 catch (      IOException iox) {
        handle(iox,outputJournalToClose.file(),"closing");
      }
    }
  finally {
      guide.endTurn();
    }
  }
  private boolean isOutputJournalStillValid(){
    return _outputJournal != null && !isOutputJournalTooBig() && !isOutputJournalTooOld();
  }
  private boolean isOutputJournalTooOld(){
    return _journalAgeThresholdInMillis != 0 && _journalAgeTimer.millisEllapsed() >= _journalAgeThresholdInMillis;
  }
  private boolean isOutputJournalTooBig(){
    return _journalSizeThresholdInBytes != 0 && _outputJournal.file().length() >= _journalSizeThresholdInBytes;
  }
  private DurableOutputStream createOutputJournal(  long transactionNumber){
    File file=_directory.journalFile(transactionNumber,_journalSuffix);
    try {
      return new DurableOutputStream(file);
    }
 catch (    IOException iox) {
      handle(iox,file,"creating");
      return null;
    }
  }
  /** 
 * IMPORTANT: This method cannot be called while the log() method is being
 * called in another thread. If there are no journal files in the directory
 * (when a snapshot is taken and all journal files are manually deleted, for
 * example), the initialTransaction parameter in the first call to this
 * method will define what the next transaction number will be. We have to
 * find clearer/simpler semantics.
 */
  public void update(  TransactionSubscriber subscriber,  long initialTransactionWanted) throws IOException, ClassNotFoundException {
    File initialJournal=_directory.findInitialJournalFile(initialTransactionWanted);
    if (initialJournal == null) {
      initializeNextTransaction(initialTransactionWanted,1);
      return;
    }
    long nextTransaction=recoverPendingTransactions(subscriber,initialTransactionWanted,initialJournal);
    initializeNextTransaction(initialTransactionWanted,nextTransaction);
  }
  private void initializeNextTransaction(  long initialTransactionWanted,  long nextTransaction) throws IOException {
    if (_nextTransactionInitialized) {
      if (_nextTransaction < initialTransactionWanted)       throw new IOException("The transaction log has not yet reached transaction " + initialTransactionWanted + ". The last logged transaction was "+ (_nextTransaction - 1)+ ".");
      if (nextTransaction < _nextTransaction)       throw new IOException("Unable to find journal file containing transaction " + nextTransaction + ". Might have been manually deleted.");
      if (nextTransaction > _nextTransaction)       throw new IllegalStateException();
      return;
    }
    _nextTransactionInitialized=true;
    _nextTransaction=initialTransactionWanted > nextTransaction ? initialTransactionWanted : nextTransaction;
  }
  private long recoverPendingTransactions(  TransactionSubscriber subscriber,  long initialTransaction,  File initialJournal) throws IOException {
    long recoveringTransaction=PrevaylerDirectory.journalVersion(initialJournal);
    File journal=initialJournal;
    DurableInputStream input;
    input=this.hook76(journal,input);
    while (true) {
      try {
        Chunk chunk=input.readChunk();
        if (recoveringTransaction >= initialTransaction) {
          if (!journal.getName().endsWith(_journalSuffix)) {
            throw new IOException("There are transactions needing to be recovered from " + journal + ", but only "+ _journalSuffix+ " files are supported");
          }
          TransactionTimestamp entry=TransactionTimestamp.fromChunk(chunk);
          if (entry.systemVersion() != recoveringTransaction) {
            throw new IOException("Expected " + recoveringTransaction + " but was "+ entry.systemVersion());
          }
          subscriber.receive(entry);
        }
        recoveringTransaction++;
      }
 catch (      EOFException eof) {
        File nextFile=_directory.journalFile(recoveringTransaction,_journalSuffix);
        if (journal.equals(nextFile))         PrevaylerDirectory.renameUnusedFile(journal);
        journal=nextFile;
        if (!journal.exists())         break;
        this.hook77(journal,input);
      }
    }
    return recoveringTransaction;
  }
  protected void handle(  IOException iox,  File journal,  String action){
    String message="All transaction processing is now blocked. An IOException was thrown while " + action + " a .journal file.";
    this.hook78(iox,journal,message);
    hang();
  }
  static private void hang(){
    while (true) {
      try {
        Thread.sleep(5000);
      }
 catch (      InterruptedException ignored) {
      }
    }
  }
  public void close() throws IOException {
    if (_outputJournal != null)     _outputJournal.close();
  }
  public long nextTransaction(){
    if (!_nextTransactionInitialized)     throw new IllegalStateException("update() must be called at least once");
    return _nextTransaction;
  }
  protected DurableInputStream hook76(  File journal,  DurableInputStream input) throws IOException {
    return input;
  }
  protected void hook77(  File journal,  DurableInputStream input) throws IOException {
  }
  protected void hook78(  IOException iox,  File journal,  String message){
  }
}
