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

import java.io.Serializable;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

import org.apache.commons.codec.binary.Base64;

import bftsmart.consensus.messages.ConsensusMessage;
import bftsmart.reconfiguration.ServerViewController;
import bftsmart.reconfiguration.views.View;
import bftsmart.tom.core.messages.TOMMessage;


/**
 * This class stands for a das epoch, as described in
 * Cachin's 'Yet Another Visit to Paxos' (April 2011)
 */
public class Epoch implements Serializable {

    private static final long serialVersionUID = -2891450035863688295L;
    private final transient Consensus consensus; // Consensus where the epoch belongs to
    
    private final int timestamp; // Epochs's timestamp
    private final int me; // Process ID
    private boolean[] writeSetted;
    private boolean[] acceptSetted;
    private byte[][] write; // WRITE values from other processes
    private byte[][] accept; // accepted values from other processes
    
    private boolean alreadyRemoved = false; // indicates if this epoch was removed from its das

    public byte[] propValue = null; // proposed value
    public TOMMessage[] deserializedPropValue = null; //utility var
    public byte[] propValueHash = null; // proposed value hash
    public HashSet<ConsensusMessage> proof; // proof from other processes

    private View lastView = null;

    private ServerViewController controller;

    /**
     * Creates a new instance of Epoch for acceptors
     * @param controller
     * @param parent Consensus to which this epoch belongs
     * @param timestamp Timestamp of the epoch
     */
    public Epoch(ServerViewController controller, Consensus parent, int timestamp) {
        this.consensus = parent;
        this.timestamp = timestamp;
        this.controller = controller;
        this.proof = new HashSet<>();
        //ExecutionManager manager = das.getManager();

        this.lastView = controller.getCurrentView();
        this.me = controller.getStaticConf().getProcessId();

        //int[] acceptors = manager.getAcceptors();
        int n = controller.getCurrentViewN();

        writeSetted = new boolean[n];
        acceptSetted = new boolean[n];

        Arrays.fill(writeSetted, false);
        Arrays.fill(acceptSetted, false);

        if (timestamp == 0) {
            this.write = new byte[n][];
            this.accept = new byte[n][];

            Arrays.fill((Object[]) write, null);
            Arrays.fill((Object[]) accept, null);
        } else {
            Epoch previousEpoch = consensus.getEpoch(timestamp - 1, controller);

            this.write = previousEpoch.getWrite();
            this.accept = previousEpoch.getAccept();
        }
    }

    // If a view change takes place and concurrentely this das is still
    // receiving messages, the write and accept arrays must be updated
    private void updateArrays() {
        
        if (lastView.getId() != controller.getCurrentViewId()) {
            
            int n = controller.getCurrentViewN();
            
            byte[][] write = new byte[n][];
            byte[][] accept = new byte[n][];
            
            boolean[] writeSetted = new boolean[n];
            boolean[] acceptSetted = new boolean[n];

            Arrays.fill(writeSetted, false);
            Arrays.fill(acceptSetted, false);
        
            for (int pid : lastView.getProcesses()) {
                
                if (controller.isCurrentViewMember(pid)) {
                    
                    int currentPos = controller.getCurrentViewPos(pid);
                    int lastPos = lastView.getPos(pid);
                    
                    write[currentPos] = this.write[lastPos];
                    accept[currentPos] = this.accept[lastPos];

                    writeSetted[currentPos] = this.writeSetted[lastPos];
                    acceptSetted[currentPos] = this.acceptSetted[lastPos];

                }
            }
            
            this.write = write;
            this.accept = accept;

            this.writeSetted = writeSetted;
            this.acceptSetted = acceptSetted;

            lastView = controller.getCurrentView();
            
        }
    }
            
    /**
     * Set this epoch as removed from its das instance
     */
    public void setRemoved() {
        this.alreadyRemoved = true;
    }

    /**
     * Informs if this epoch was removed from its das instance
     * @return True if it is removed, false otherwise
     */
    public boolean isRemoved() {
        return this.alreadyRemoved;
    }


    public void addToProof(ConsensusMessage pm) {
        proof.add(pm);
    }
    
    public Set<ConsensusMessage> getProof() {
        return proof;
    }
    /**
     * Retrieves the duration for the timeout
     * @return Duration for the timeout
     */
    /*public long getTimeout() {
        return this.timeout;
    }*/

    /**
     * Retrieves this epoch's timestamp
     * @return This epoch's timestamp
     */
    public int getTimestamp() {
        return timestamp;
    }

    /**
     * Retrieves this epoch's das
     * @return This epoch's das
     */
    public Consensus getConsensus() {
        return consensus;
    }

    /**
     * Informs if there is a WRITE value from a replica
     * @param acceptor The replica ID
     * @return True if there is a WRITE value from a replica, false otherwise
     */
    public boolean isWriteSetted(int acceptor) {
        
        updateArrays();
        
        //******* EDUARDO BEGIN **************//
        int p = this.controller.getCurrentViewPos(acceptor);
        if(p >= 0){
            return write[p] != null;
        }else{
            return false;
        }
        //******* EDUARDO END **************//
    }

    /**
     * Informs if there is a accepted value from a replica
     * @param acceptor The replica ID
     * @return True if there is a accepted value from a replica, false otherwise
     */
    public boolean isAcceptSetted(int acceptor) {
        
        updateArrays();
        
        //******* EDUARDO BEGIN **************//
        int p = this.controller.getCurrentViewPos(acceptor);
        if(p >= 0){
            return accept[p] != null;
        }else{
            return false;
        }
        //******* EDUARDO END **************//
    }

    /**
     * Retrives the WRITE value from the specified replica
     * @param acceptor The replica ID
     * @return The value from the specified replica
     */
    public byte[] getWrite(int acceptor) {
        
        updateArrays();
        
        //******* EDUARDO BEGIN **************//
        int p = this.controller.getCurrentViewPos(acceptor);
        if(p >= 0){        
            return this.write[p];
        }else{
            return null;
        }
        //******* EDUARDO END **************//
    }

    /**
     * Retrieves all WRITE value from all replicas
     * @return The values from all replicas
     */
    public byte[][] getWrite() {
        return this.write;
    }

    /**
     * Sets the WRITE value from the specified replica
     * @param acceptor The replica ID
     * @param value The valuefrom the specified replica
     */
    public void setWrite(int acceptor, byte[] value) { // TODO: Race condition?
        
        updateArrays();
        
        //******* EDUARDO BEGIN **************//
        int p = this.controller.getCurrentViewPos(acceptor);
        if (p >=0 /*&& !writeSetted[p] && !isFrozen() */) { //it can only be setted once
            write[p] = value;
            writeSetted[p] = true;
        }
        //******* EDUARDO END **************//
    }

    /**
     * Retrieves the accepted value from the specified replica
     * @param acceptor The replica ID
     * @return The value accepted from the specified replica
     */
    public byte[] getAccept(int acceptor) {
        
        updateArrays();
        
        //******* EDUARDO BEGIN **************//
         int p = this.controller.getCurrentViewPos(acceptor);
        if(p >= 0){        
        return accept[p];
        }else{
            return null;
        }
        //******* EDUARDO END **************//
    }

    /**
     * Retrieves all accepted values from all replicas
     * @return The values accepted from all replicas
     */
    public byte[][] getAccept() {
        return accept;
    }

    /**
     * Sets the accepted value from the specified replica
     * @param acceptor The replica ID
     * @param value The value accepted from the specified replica
     */
    public void setAccept(int acceptor, byte[] value) { // TODO: race condition?
        
        updateArrays();
        
        //******* EDUARDO BEGIN **************//
        int p = this.controller.getCurrentViewPos(acceptor);
        if (p >= 0 /*&& !strongSetted[p] && !isFrozen()*/) { //it can only be setted once
            accept[p] = value;
            acceptSetted[p] = true;
        }
        //******* EDUARDO END **************//
    }

    /**
     * Retrieves the amount of replicas from which this process received a WRITE value
     * @param value The value in question
     * @return Amount of replicas from which this process received the specified value
     */
    public int countWrite(byte[] value) {
        return count(writeSetted,write, value);
    }

    /**
     * Retrieves the amount of replicas from which this process accepted a specified value
     * @param value The value in question
     * @return Amount of replicas from which this process accepted the specified value
     */
    public int countAccept(byte[] value) {
        return count(acceptSetted,accept, value);
    }

    /**
     * Counts how many times 'value' occurs in 'array'
     * @param array Array where to count
     * @param value Value to count
     * @return Ammount of times that 'value' was find in 'array'
     */
    private int count(boolean[] arraySetted,byte[][] array, byte[] value) {
        if (value != null) {
            int counter = 0;
            for (int i = 0; i < array.length; i++) {
                if (arraySetted != null && arraySetted[i] && Arrays.equals(value, array[i])) {
                    counter++;
                }
            }
            return counter;
        }
        return 0;
    }

    /*************************** DEBUG METHODS *******************************/
    /**
     * Print epoch information.
     */
    @Override
    public String toString() {
        StringBuffer buffWrite = new StringBuffer(1024);
        StringBuffer buffAccept = new StringBuffer(1024);

        buffWrite.append("\n\t\tWrites=(");
        buffAccept.append("\n\t\tAccepts=(");

        for (int i = 0; i < write.length - 1; i++) {
            buffWrite.append("[" + str(write[i]) + "], ");
            buffAccept.append("[" + str(accept[i]) + "], ");
        }

        buffWrite.append("[" + str(write[write.length - 1]) +"])");
        buffAccept.append("[" + str(accept[accept.length - 1]) + "])");

        return "\n\t\tCID=" + consensus.getId() + " \n\t\tTS=" + getTimestamp() + " " + "\n\t\tPropose=[" + (propValueHash != null ? str(propValueHash) : null) + "] " + buffWrite + " " + buffAccept;
    }

    private String str(byte[] obj) {
        if(obj == null) {
            return "null";
        } else {
            return Base64.encodeBase64String(obj);
        }
    }

    @Override
    public boolean equals(Object o) {
        return this == o;
    }
    
    /**
     * Clear all epoch info.
     */
    public void clear() {

        int n = controller.getCurrentViewN();
        
        writeSetted = new boolean[n];
        acceptSetted = new boolean[n];

        Arrays.fill(writeSetted, false);
        Arrays.fill(acceptSetted, false);

        this.write = new byte[n][];
        this.accept = new byte[n][];

        Arrays.fill((Object[]) write, null);
        Arrays.fill((Object[]) accept, null);
        
        this.proof = new HashSet<ConsensusMessage>();
    }
}
