/**
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;

import bftsmart.consensus.messages.ConsensusMessage;
import bftsmart.reconfiguration.ServerViewController;
import bftsmart.statemanagement.ApplicationState;
import bftsmart.tom.core.messages.TOMMessage;
import bftsmart.tom.leaderchange.CertifiedDecision;
import bftsmart.tom.util.BatchBuilder;

import java.util.Arrays;
import java.util.LinkedList;
import java.util.Set;
/**
 * This class represents a state transfered from a replica to another. The state associated with the last
 * checkpoint together with all the batches of messages received do far, comprises the sender's
 * current state
 * 
 * @author Joao Sousa
 */
public class DefaultApplicationState implements ApplicationState {

    private static final long serialVersionUID = 6771081456095596363L;

    protected byte[] state; // State associated with the last checkpoint
    protected byte[] stateHash; // Hash of the state associated with the last checkpoint
    protected int lastCID = -1; // Consensus ID for the last messages batch delivered to the application
    protected boolean hasState; // indicates if the replica really had the requested state

    private CommandsInfo[] messageBatches; // batches received since the last checkpoint.
    private int lastCheckpointCID; // Consensus ID for the last checkpoint
    private byte[] logHash;
    
    private int pid;

    /**
     * Constructs a TansferableState
     * This constructor should be used when there is a valid state to construct the object with
     * @param messageBatches Batches received since the last checkpoint.
     * @param state State associated with the last checkpoint
     * @param stateHash Hash of the state associated with the last checkpoint
     */
    public DefaultApplicationState(CommandsInfo[] messageBatches, int lastCheckpointCID, int lastCID, byte[] state, byte[] stateHash, int pid) {
       
        this.messageBatches = messageBatches; // batches received since the last checkpoint.
        this.lastCheckpointCID = lastCheckpointCID; // Consensus ID for the last checkpoint
        this.lastCID = lastCID; // Consensus ID for the last messages batch delivered to the application
        this.state = state; // State associated with the last checkpoint
        this.stateHash = stateHash;
        this.hasState = true;
        this.pid = pid;
    }

    public DefaultApplicationState(CommandsInfo[] messageBatches, byte[] logHash, int lastCheckpointCID, int lastCID, byte[] state, byte[] stateHash, int pid) {
    	this(messageBatches, lastCheckpointCID, lastCID, state, stateHash, pid);
    	this.logHash = logHash;
    }

    /**
     * Constructs a TansferableState
     * This constructor should be used when there isn't a valid state to construct the object with
     */
    public DefaultApplicationState() {
        this.messageBatches = null; // batches received since the last checkpoint.
        this.lastCheckpointCID = -1; // Consensus ID for the last checkpoint
        this.lastCID = -1;
        this.state = null; // State associated with the last checkpoint
        this.stateHash = null;
        this.hasState = false;
        this.pid = -1;
    }
    
    
    @Override
    public void setSerializedState(byte[] state) {
        this.state = state;
    }

    @Override
    public byte[] getSerializedState() {
        return state;
    }
      
    /**
     * Indicates if the TransferableState object has a valid state
     * @return true if it has a valid state, false otherwise
     */
    @Override
    public boolean hasState() {
        return hasState;
    }


    /**
     * Retrieves the das ID for the last messages batch delivered to the application
     * @return Consensus ID for the last messages batch delivered to the application
     */
    @Override
    public int getLastCID() {
        return lastCID;
    }
    
    /**
     * Retrieves the certified decision for the last das present in this object
     * @param controller
     * @return The certified decision for the last das present in this object
     */
    @Override
    public CertifiedDecision getCertifiedDecision(ServerViewController controller) {
        CommandsInfo ci = getMessageBatch(getLastCID());
        if (ci != null && ci.msgCtx[0].getProof() != null) { // do I have a proof for the das?
            
            Set<ConsensusMessage> proof = ci.msgCtx[0].getProof();
            LinkedList<TOMMessage> requests = new LinkedList<>();
            
            //Recreate all TOMMessages ordered in the das
            for (int i = 0; i < ci.commands.length; i++) {
                
                requests.add(ci.msgCtx[i].recreateTOMMessage(ci.commands[i]));
                
            }
            
            //Serialize the TOMMessages to re-create the proposed value
            BatchBuilder bb = new BatchBuilder(0);
            byte[] value = bb.makeBatch(requests, ci.msgCtx[0].getNumOfNonces(),
                    ci.msgCtx[0].getSeed(), ci.msgCtx[0].getTimestamp(), controller);
            
            //Assemble and return the certified decision
            return new CertifiedDecision(pid, getLastCID(), value, proof);
        }
        else return null; // there was no proof for the das
    }

    /**
     * Retrieves the state associated with the last checkpoint
     * @return State associated with the last checkpoint
     */
    public byte[] getState() {
        return state;
    }

    /**
     * Retrieves the hash of the state associated with the last checkpoint
     * @return Hash of the state associated with the last checkpoint
     */
    @Override
    public byte[] getStateHash() {
        return stateHash;
    }

    /**
     * Sets the state associated with the last checkpoint
     * @param state State associated with the last checkpoint
     */
    public void setState(byte[] state) {
        this.state = state;
    }
    
    /**
     * Retrieves all batches of messages
     * @return Batch of messages
     */
    public CommandsInfo[] getMessageBatches() {
        return messageBatches;
    }

    public void setMessageBatches(CommandsInfo[] messageBatches) {
    	this.messageBatches = messageBatches;
    }

    /**
     * Retrieves the specified batch of messages
     * @param cid Consensus ID associated with the batch to be fetched
     * @return The batch of messages associated with the batch correspondent das ID
     */
    public CommandsInfo getMessageBatch(int cid) {
        if (messageBatches != null && cid >= lastCheckpointCID && cid <= lastCID) {
            return messageBatches[cid - lastCheckpointCID - 1];
        }
        else return null;
    }

    /**
     * Retrieves the das ID for the last checkpoint
     * @return Consensus ID for the last checkpoint, or -1 if no checkpoint was yet executed
     */
    public int getLastCheckpointCID() {

        return lastCheckpointCID;
    }


    @Override
    public boolean equals(Object obj) {
        if (obj instanceof DefaultApplicationState) {
            DefaultApplicationState tState = (DefaultApplicationState) obj;

            if ((this.messageBatches != null && tState.messageBatches == null) ||
                    (this.messageBatches == null && tState.messageBatches != null)) {
                //logger.info("[DefaultApplicationState] returing FALSE1!");
                return false;
            }

            if (this.messageBatches != null && tState.messageBatches != null) {

                if (this.messageBatches.length != tState.messageBatches.length) {
                    //logger.info("[DefaultApplicationState] returing FALSE2!");
                    return false;
                }
                
                for (int i = 0; i < this.messageBatches.length; i++) {
                    
                    if (this.messageBatches[i] == null && tState.messageBatches[i] != null) {
                        //logger.info("[DefaultApplicationState] returing FALSE3!");
                        return false;
                    }

                    if (this.messageBatches[i] != null && tState.messageBatches[i] == null) {
                        //logger.info("[DefaultApplicationState] returing FALSE4!");
                        return false;
                    }
                    
                    if (!(this.messageBatches[i] == null && tState.messageBatches[i] == null) &&
                        (!this.messageBatches[i].equals(tState.messageBatches[i]))) {
                        //logger.info("[DefaultApplicationState] returing FALSE5!" + (this.messageBatches[i] == null) + " " + (tState.messageBatches[i] == null));
                        return false;
                    }
                }
            }
            return (Arrays.equals(this.stateHash, tState.stateHash) &&
                    tState.lastCheckpointCID == this.lastCheckpointCID &&
                    tState.lastCID == this.lastCID && tState.hasState == this.hasState);
        }
        //logger.info("[DefaultApplicationState] returing FALSE!");
        return false;
    }

    @Override
    public int hashCode() {
        int hash = 1;
        hash = hash * 31 + this.lastCheckpointCID;
        hash = hash * 31 + this.lastCID;
        hash = hash * 31 + (this.hasState ? 1 : 0);
        if (this.stateHash != null) {
            for (int i = 0; i < this.stateHash.length; i++) hash = hash * 31 + (int) this.stateHash[i];
        } else {
            hash = hash * 31 + 0;
        }
        if (this.messageBatches != null) {
            for (int i = 0; i < this.messageBatches.length; i++) {
                if (this.messageBatches[i] != null) {
                    hash = hash * 31 + this.messageBatches[i].hashCode();
                } else {
                    hash = hash * 31 + 0;
                }
            }
        } else {
            hash = hash * 31 + 0;
        }
        return hash;
    }

}
