/**
Copyright (c) 2007-2013 Alysson Bessani, Eduardo Alchieri, Paulo Sousa, and the authors indicated in the @author tags

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package bftsmart.tom.server.defaultservices.durability;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;

import bftsmart.statemanagement.strategy.durability.CSTRequest;
import bftsmart.statemanagement.strategy.durability.CSTRequestF1;
import bftsmart.statemanagement.strategy.durability.CSTState;
import bftsmart.tom.MessageContext;
import bftsmart.tom.server.defaultservices.CommandsInfo;
import bftsmart.tom.server.defaultservices.FileRecoverer;
import bftsmart.tom.server.defaultservices.StateLog;
import bftsmart.tom.util.TOMUtil;

public class DurableStateLog extends StateLog {
	private final static org.apache.log4j.Logger logger = org.apache.log4j.Logger.getLogger(DurableStateLog.class);

	private int id;
	public final static String DEFAULT_DIR = "files".concat(System
			.getProperty("file.separator"));
	private static final int INT_BYTE_SIZE = 4;
	private static final int EOF = 0;

	private RandomAccessFile log;
	private boolean syncLog;
	private String logPath;
	private String lastCkpPath;
	private boolean syncCkp;
	private boolean isToLog;
	private ReentrantLock checkpointLock = new ReentrantLock();
	private Map<Integer, Long> logPointers;
	private FileRecoverer fr;
	
	public DurableStateLog(int id, byte[] initialState, byte[] initialHash,
			boolean isToLog, boolean syncLog, boolean syncCkp) {
		super(id, initialState, initialHash);
		this.id = id;
		this.isToLog = isToLog;
		this.syncLog = syncLog;
		this.syncCkp = syncCkp;
		this.logPointers = new HashMap<Integer, Long>();
		this.fr = new FileRecoverer(id, DEFAULT_DIR);
	}

	private void createLogFile() {
		logPath = DEFAULT_DIR + String.valueOf(id) + "."
				+ System.currentTimeMillis() + ".log";
		try {
			log = new RandomAccessFile(logPath, (syncLog ? "rwd" : "rw"));
		} catch (FileNotFoundException e) {
			logger.error("", e);
		}
	}

	/**
	 * Adds a message batch to the log. This batches should be added to the log
	 * in the same order in which they are delivered to the application. Only
	 * the 'k' batches received after the last checkpoint are supposed to be
	 * kept
         * @param commands The batch of messages to be kept.
         * @param msgCtx
         * @param consensusId the das id added to the batch
	 */
        @Override
	public void addMessageBatch(byte[][] commands, MessageContext[] msgCtx, int consensusId) {
//		logger.info("DurableStateLog#addMessageBatch. consensusId: " + consensusId);
		CommandsInfo command = new CommandsInfo(commands, msgCtx);
		if (isToLog) {
			if(log == null)
				createLogFile();
			writeCommandToDisk(command, consensusId);
		}
	}

	private void writeCommandToDisk(CommandsInfo commandsInfo, int consensusId) {
		ByteArrayOutputStream bos = new ByteArrayOutputStream();
		try {
			ObjectOutputStream oos = new ObjectOutputStream(bos);
			oos.writeObject(commandsInfo);
			oos.flush();

			byte[] batchBytes = bos.toByteArray();

			ByteBuffer bf = ByteBuffer.allocate(3 * INT_BYTE_SIZE
					+ batchBytes.length);
			bf.putInt(batchBytes.length);
			bf.put(batchBytes);
			bf.putInt(EOF);
			bf.putInt(consensusId);
			
			log.write(bf.array());
			log.seek(log.length() - 2 * INT_BYTE_SIZE);// Next write will overwrite
													// the EOF mark
		} catch (IOException e) {
			// TODO Auto-generated catch block
			logger.error("", e);
	    }
	}
	
        @Override
	public void newCheckpoint(byte[] state, byte[] stateHash, int consensusId) {
		String ckpPath = DEFAULT_DIR + String.valueOf(id) + "."
				+ System.currentTimeMillis() + ".tmp";
		try {
			checkpointLock.lock();
			RandomAccessFile ckp = new RandomAccessFile(ckpPath,
					(syncCkp ? "rwd" : "rw"));

			ByteBuffer bf = ByteBuffer.allocate(state.length + stateHash.length
					+ 4 * INT_BYTE_SIZE);
			bf.putInt(state.length);
			bf.put(state);
			bf.putInt(stateHash.length);
			bf.put(stateHash);
			bf.putInt(EOF);
			bf.putInt(consensusId);

			byte[] ckpState = bf.array();
			
			ckp.write(ckpState);
			ckp.close();

			if (isToLog)
				deleteLogFile();
			deleteLastCkp();
			renameCkp(ckpPath);
			if (isToLog)
				createLogFile();
			
		} catch (FileNotFoundException e) {
			// TODO Auto-generated catch block
			logger.error("", e);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			logger.error("", e);
		} finally {
			checkpointLock.unlock();
		}
	}

	private void renameCkp(String ckpPath) {
		String finalCkpPath = ckpPath.replace(".tmp", ".ckp");
		new File(ckpPath).renameTo(new File(finalCkpPath));
		lastCkpPath = finalCkpPath;
	}

	private void deleteLastCkp() {
		if (lastCkpPath != null)
			new File(lastCkpPath).delete();
	}

	private void deleteLogFile() {
		try {
			if(log != null)
				log.close();
			new File(logPath).delete();
		} catch (IOException e) {
			logger.error("", e);
		}
	}

	public CSTState getState(CSTRequest cstRequest) {
		int cid = cstRequest.getCID();

		int lastCheckpointCID = getLastCheckpointCID();
		int lastCID = getLastCID();
		logger.info("LAST CKP CID = " + lastCheckpointCID);
		logger.info("CID = " + cid);
		logger.info("LAST CID = " + lastCID);
		
		if(cstRequest instanceof CSTRequestF1) {
			CSTRequestF1 requestF1 = (CSTRequestF1)cstRequest;
			if(id == requestF1.getCheckpointReplica()) {
				// This replica is expected to send the checkpoint plus the hashes of lower and upper log portions
				checkpointLock.lock();
				byte[] ckpState = fr.getCkpState(lastCkpPath);
				checkpointLock.unlock();
	    		logger.info("--- sending checkpoint: " + ckpState.length);
	    		CommandsInfo[] logLower = fr.getLogState(requestF1.getLogLowerSize(), logPath);
	    		CommandsInfo[] logUpper = fr.getLogState(logPointers.get(requestF1.getLogUpper()), 0, requestF1.getLogUpperSize(), logPath);
	    		byte[] logLowerBytes = TOMUtil.getBytes(logLower);
	    		logger.info(logLower.length + " Log lower bytes size: " + logLowerBytes.length);
	    		byte[] logLowerHash = TOMUtil.computeHash(logLowerBytes);
	    		byte[] logUpperBytes = TOMUtil.getBytes(logUpper);
	    		logger.info(logUpper.length + " Log upper bytes size: " + logUpperBytes.length);
	    		byte[] logUpperHash = TOMUtil.computeHash(logUpperBytes);
	    		CSTState cstState = new CSTState(ckpState, null, null, logLowerHash, null, logUpperHash, lastCheckpointCID, lastCID, this.id);
	    		return cstState;
			} else if(id == requestF1.getLogLower()) {
				// This replica is expected to send the lower part of the log
	    		logger.info("--- sending lower log: " + requestF1.getLogLowerSize() + " from " + logPointers.get(requestF1.getCheckpointReplica())); ;
	    		CommandsInfo[] logLower = fr.getLogState(logPointers.get(requestF1.getCheckpointReplica()), 0, requestF1.getLogLowerSize(), logPath);
	    		logger.info(" " + TOMUtil.getBytes(logLower).length + " bytes");
	    		CSTState cstState = new CSTState(null, null, logLower, null, null, null, lastCheckpointCID, lastCID, this.id);
	    		return cstState;
			} else {
				// This replica is expected to send the upper part of the log plus the hash for its checkpoint
	    		logger.info("--- sending upper log: " + requestF1.getLogUpperSize());
				checkpointLock.lock();
				fr.recoverCkpHash(lastCkpPath);
				byte[] ckpHash = fr.getCkpStateHash();
				byte[] ckpState = fr.getCkpState(lastCkpPath);
				checkpointLock.unlock();
	    		CommandsInfo[] logUpper = fr.getLogState(requestF1.getLogUpperSize(), logPath);
	    		logger.info(" " + TOMUtil.getBytes(logUpper).length + " bytes");
	    		logger.info("--- State size: " + ckpState.length + " Current state Hash: " + ckpHash);
	    		int lastCIDInState = lastCheckpointCID + requestF1.getLogUpperSize();
	    		CSTState cstState = new CSTState(null, ckpHash, null, null, logUpper, null, lastCheckpointCID, lastCIDInState, this.id);
	    		return cstState;
			}
		}
//		else if(cstRequest instanceof CSTRequestFGT1) {
//			CSTRequestFGT1 requestFGT1 = (CSTRequestFGT1)cstRequest;
//			if(id == requestFGT1.getCheckpointReplica()) {
//				checkpointLock.lock();
//				byte[] ckpState = fr.getCkpState();
//				checkpointLock.unlock(); 
//	    		batches = fr.getLogState(requestFGT1.getLogSize());
//	    		logger.info("--- sending checkpoint: " + ckpState.length);
//	            return new DefaultApplicationState(batches, lastCheckpointCID, cid, ckpState, null);
//			} else { // Replica should send the checkpoint and log hashes
//	    		batches = fr.getLogState(requestFGT1.getLogSize() - requestFGT1.getNbrHashesBeforeCkp());
//	    		byte[] logBytes = TOMUtil.getBytes(batches);
//	    		byte[] logHash = TOMUtil.computeHash(logBytes);
//	    		fr.recoverCkpHash();
//				byte[] ckpHash = fr.getCkpStateHash();
//	            return new DefaultApplicationState(null, logHash, lastCheckpointCID, cid, null, ckpHash);
//			}
//				
//		}
		return null;
	}
	
	public void transferApplicationState(SocketChannel sChannel, int cid) {
		fr.transferCkpState(sChannel, lastCkpPath);
		
//		int lastCheckpointCID = getLastCheckpointCID();
//		int lastCID = getLastCID();
//		if (cid >= lastCheckpointCID && cid <= lastCID) {
//			int size = cid - lastCheckpointCID;
//			fr.transferLog(sChannel, size);
//		}
	}

	public void setLastCID(int cid, int checkpointPeriod, int checkpointPortion) {
		super.setLastCID(cid);
		// save the file pointer to retrieve log information later
		if((cid % checkpointPeriod) % checkpointPortion == checkpointPortion -1) {
			int ckpReplicaIndex = (((cid % checkpointPeriod) + 1) / checkpointPortion) -1;
			try {
				logger.info(" --- Replica " + ckpReplicaIndex + " took checkpoint. My current log pointer is " + log.getFilePointer());
				logPointers.put(ckpReplicaIndex, log.getFilePointer());
			} catch (IOException e) {
				logger.error("", e);
			}
		}
	}

	/**
	 * Updates this log, according to the information contained in the
	 * TransferableState object
	 * 
         * @param state
	 * @param transState TransferableState object containing the information which is
	 * used to updated this log
	 */
	public void update(CSTState state) {
		newCheckpoint(state.getSerializedState(), state.getStateHash(), state.getCheckpointCID());
		setLastCheckpointCID(state.getCheckpointCID());
	}

	protected CSTState loadDurableState() {
		FileRecoverer fr = new FileRecoverer(id, DEFAULT_DIR);
		lastCkpPath = fr.getLatestFile(".ckp");
		logPath = fr.getLatestFile(".log");
		byte[] checkpoint = null;
		if(lastCkpPath != null)
			checkpoint = fr.getCkpState(lastCkpPath);
		CommandsInfo[] log = null;
		if(logPath !=null)
			log = fr.getLogState(0, logPath);
		int ckpLastConsensusId = fr.getCkpLastConsensusId();
		int logLastConsensusId = fr.getLogLastConsensusId();
		CSTState cstState = new CSTState(checkpoint, fr.getCkpStateHash(), log, null,
				null, null, ckpLastConsensusId, logLastConsensusId, this.id);
		if(logLastConsensusId > ckpLastConsensusId) {
			super.setLastCID(logLastConsensusId);
		} else
			super.setLastCID(ckpLastConsensusId);
		super.setLastCheckpointCID(ckpLastConsensusId);
		return cstState;
	}
}
