/*
 * StatsCollector.java
 *
 * Created on July 29, 2005, 4:45 PM
 */

/**
 *
 * @author  Oliviu Ghica
 */
package sidnet.utilityviews.statscollector;

import javax.swing.*; // For JPanel, etc.
import java.awt.*;           // For Graphics, etc.
import java.io.IOException;
import java.lang.reflect.Method;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedList;
import jist.runtime.JistAPI;
import jist.swans.Constants;

import org.apache.log4j.*;
import sidnet.core.interfaces.SimulationTimeRunnable;
import sidnet.core.interfaces.UtilityView;
import sidnet.core.misc.*;

public class StatsCollector extends UtilityView implements SimulationTimeRunnable
{  
    /**
   * 
   */
  private static final long serialVersionUID = 1L;

    public static enum ITEM
    {
        TIME,
        DATA_PACKETS_SENT_COUNT,
        DATA_PACKETS_RECEIVED_COUNT,
        DATA_PACKETS_RECEIVED_PERCENTAGE,
        DATA_PACKETS_RECEIVED_COUNT_CONDITIONAL,
        DATA_PACKETS_RECEIVED_PERCENTAGE_CONDITIONAL,
        DATA_PACKETS_LOST_COUNT,
        DATA_PACKETS_LOST_PERCENTAGE,
        DATA_PACKETS_AVERAGE_DELIVERY_LATENCY,
        DATA_PACKETS_MAXIMUM_DELIVERY_LATENCY,
        DATA_PACKETS_MINIMUM_DELIVERY_LATENCY,
    }
    
    private LinkedList<StatEntry> statEntryList = null;
    
    private JScrollPane jScrollPane;
    
    private static final int vertInitialPosition = 35;
    private static final int vertIncrement = 18;
    
//    private JPanel hostPanel;
    
    public static enum PREDICATE
    {
        LESS_THAN,
        LESS_THAN_OR_EQUAL,
        EQUALS,
        GREATER_THAN_OR_EQUAL,
        GREATER_THAN;
    }
    
    public static final String HEADER_TAG = " <header> ";
    public static final String ROW_TAG = " <row> ";
    public static final String RUN_ID_TAG = "<runId>";
    public static final String EXPERIMENT_ID_TAG = "<experimentId>";
    
    /** self-referencing proxy entity. */
    private Object self;
    
    long StartTimeStamp;
    long CurrentTimeStamp;
    double SimSpeed;        // Measures in x Times the RealTime
    
    long realTimeStart = 0;   //[ms]
    long realTimeCurrent = 0;
    
//    private int areaLength;
//    private int batCapacity;
    public int sdCount;
    public int sampling;
    public String simName;
    
    private String headerLog;
    
    private boolean commitedHeaderLog = false;
    
    Node[] myNode;
    
    private PacketMonitor packetMonitor;
    
    private boolean loggingEnabled;
    private static long loggingInterval;
    
    private LinkedList<StatEntryDeprecated> monitoringList;
    
    /* ***** Common locally computed stats ***** */
    
    public double totalEnergyLeftPercentage = 0;
    public double totalEnergyLostPercentage = 0;
      
    private long packetsCummulativeDeliveryLatency = 0;
    private long packetSentCount = 0;
    private long packetsReceivedCount = 0;
    private long packetsMinimumDeliveryLatency = 0;
    private long packetsMaximumDeliveryLatency = 0;
    
    private long dataPacketsSentCount = 0;
    private long dataPacketsReceivedCount = 0;
    private long dataPacketsReceivedPercentage = 0;
    private long dataPacketsLostCount = 0;
    private long dataPacketsLostPercentage = 0;
    
    private long dataPacketsCummulativeDeliveryLatency = 0;
    private long dataPacketsAverageDeliveryLatency = 0;
    private long dataPacketsMaximumDeliveryLatency = 0;
//    private long dataPacketsMaximumDeliveryLatency99Percentile = 0;
    private long dataPacketsMinimumDeliveryLatency = 0;
//    private long dataPacketsMinimumDeliveryLatency99Percentile = 0;
    private long dataPacketsReceivedCountConditional = 0;
    private long dataPacketsReceivedPercentageConditional = 0;
  
    /* ***************************************** */
    
    
    
    
    private class PacketMonitor
    {
        private LinkedList<Pair> packetMonitoringList;
        
        public PacketMonitor()
        {
            packetMonitoringList = new LinkedList();
        }
        
        public long removeSentTimeStamp(long packetId)
        {
            for (Pair pair: packetMonitoringList)
                if (pair.packetId == packetId)
                {   
                    packetMonitoringList.remove(pair);
                    return pair.sentTimeStamp;
                }
            return -1;
        }
        
        public void add(long packetId, long sentTimeStamp)
        {
            packetMonitoringList.add(new Pair(packetId, sentTimeStamp));
        }
        
        private class Pair
        {
            long packetId;
            long sentTimeStamp;
            
            public Pair(long packetId, long sentTimeStamp)
            {
                this.packetId = packetId;
                this.sentTimeStamp = sentTimeStamp;
            }
        }
    }
    
    /* Log services */
    static Logger log;
    
    
    
    private class StatEntryDeprecated
    {
        String itemName;
        Object method;
        PREDICATE predicate;
        long threshold;
        private boolean conditional;

        public StatEntryDeprecated(String itemName, Object method)
        {
            this.itemName = itemName;
            this.method = method;
            this.conditional = false;
        }
        public StatEntryDeprecated(String itemName, Object method, PREDICATE predicate, long threshold)
        {
            this.itemName  = itemName;
            this.method    = method;
            this.predicate = predicate;
            this.threshold = threshold;
            this.conditional = true;
        }
        
        public String getItemName()
        {
            return itemName;
        }
        public Object getMethod()
        {
            return method;
        }
        public PREDICATE getPredicate()
        {
            return predicate;
        }
        public long getThreshold()
        {
            return threshold;
        }
        public boolean isConditional()
        {
            return conditional;
        }
    }
    
    
    public StatsCollector(Node[] myNode, int areaLength, int batCapacity, long loggingInterval, long runId, long experimentId, String experimentsTargetDirectory, String filenameOptionalPrefix)
    {
        loggingEnabled = true;
        configureLogger( runId, experimentId, experimentsTargetDirectory, filenameOptionalPrefix);
        
        StartTimeStamp  = 0;
        CurrentTimeStamp= 0;
        
        this.myNode = myNode;
//        this.areaLength = areaLength;
//        this.batCapacity = batCapacity;
        
        packetMonitor = new PacketMonitor();
        StatsCollector.loggingInterval = loggingInterval;
        
        this.self = JistAPI.proxy(this, SimulationTimeRunnable.class);
        
        monitoringList = new LinkedList<StatEntryDeprecated>();
      
        headerLog = "StatsCollector Log Time: " + getDateTime() + "\n\n";
        
        ((SimulationTimeRunnable)self).run();
    }
    
    
    
    /**
     * Creates a new instance of StatsCollector
     */
    public StatsCollector(Node[] myNode, int areaLength, int batCapacity, long loggingInterval) 
    {
        // no logging for this mode
        loggingEnabled = false;
        //configureLogger(-1, -1, ".\\", "");
        
        StartTimeStamp  = 0;
        CurrentTimeStamp= 0;
        
        this.myNode = myNode;
//        this.areaLength = areaLength;
//        this.batCapacity = batCapacity;
        
        packetMonitor = new PacketMonitor();
        StatsCollector.loggingInterval = loggingInterval;
        
        this.self = JistAPI.proxy(this, SimulationTimeRunnable.class);
        
        monitoringList = new LinkedList<StatEntryDeprecated>();
      
        headerLog = "StatsCollector Log Time: " + getDateTime() + "\n\n";
        
        ((SimulationTimeRunnable)self).run();
    }
    
    private void configureLogger( long runId, long experimentId, String experimentsTargetDirectory, String filanameOptionalPrefix)
    {
         /* logging services */
        String fileName = "run" + runId + "-exp" + experimentId + "-";
        if (filanameOptionalPrefix != null)
            fileName += filanameOptionalPrefix;

        fileName += "-log"+getDateTime() + ".log";
        
        FileAppender appender; 
        try
        {
            PatternLayout layout = new PatternLayout("%m %n");
            System.out.println("experimentsTargetDirectory = " + experimentsTargetDirectory);
            if (experimentsTargetDirectory!= null)
                appender = new FileAppender(layout, experimentsTargetDirectory + fileName, true);
            else
                appender = new FileAppender(layout, fileName, true);
        }
        catch(IOException e)
        {
            e.printStackTrace();
            throw new RuntimeException("Unable to load loggin property");
        }

        log = Logger.getLogger(fileName);
        log.addAppender(appender);
        log.setLevel((Level)Level.INFO);      
        log.setAdditivity(false);
    }
    
    public void addToHeaderLog(String key, Object value)
    {
        headerLog += key + ": " + value + "\n";
    }
    
    public void addToHeaderLog(String log)
    {
        headerLog += log;
    }
    
    public void commitHeaderLog()
    {
        if(!commitedHeaderLog)
        {
            commitedHeaderLog = true;       
            //System.out.println(headerLog);
            log.info(headerLog);
        }
        String str= "\n\n" + HEADER_TAG;
        for(StatEntryDeprecated stat:monitoringList)
            str += "\t" + stat.getItemName();
        
        for (StatEntry statEntry: statEntryList)
            str += "\t" + statEntry.getHeader();
        
        log.info(str);
    }
    
    public static String getDateTime() {
        DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd--HH-mm-ss");
        Date date = new Date();
        return dateFormat.format(date);
    }
    
    /* @override */
    public void configureGUI(JPanel hostPanel)
    {
        this.setOpaque(true);
        this.setBackground(Color.BLACK);
        this.setVisible(true);
       
       
        jScrollPane = new JScrollPane(this);
        jScrollPane.setOpaque(false);
        jScrollPane.setBackground(Color.gray);
        hostPanel.add(jScrollPane);
        
//        this.hostPanel = hostPanel;
        
         if (jScrollPane != null)
        {
            jScrollPane.setBounds(0,0, hostPanel.getWidth(), hostPanel.getHeight());
            jScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
            jScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
        }
    }
    
    /* @override */
    public void repaintGUI()
    {
        update(JistAPI.getTime()/Constants.MILLI_SECOND, false);
        this.repaint();
    }
    
    public void markDataPacketSent(long packetId)
    {
        packetMonitor.add(packetId, JistAPI.getTime());
        markDataPacketSent();
    }
        
    private void markDataPacketSent()
    {
         dataPacketsSentCount++;
         dataPacketsReceivedPercentage = (long)Math.ceil((double)dataPacketsReceivedCount * 100 / dataPacketsSentCount);
    }
    
    private void markPacketSent()
    {
         packetSentCount++;
    }
    
    public void markPacketSent(long packetId)
    {
        packetMonitor.add(packetId, JistAPI.getTime());
        markPacketSent();
    }
    
    public void markPacketReceived(long packetId)
    {
        long sentTimeStamp = packetMonitor.removeSentTimeStamp(packetId);
        if (sentTimeStamp != -1)
        {
            long latency =  (JistAPI.getTime() - sentTimeStamp)/Constants.MILLI_SECOND;
            packetsCummulativeDeliveryLatency += latency;
            if (packetsMinimumDeliveryLatency == -1 || packetsMinimumDeliveryLatency != -1 && packetsMinimumDeliveryLatency > latency)
                packetsMinimumDeliveryLatency = latency;
            if (packetsMaximumDeliveryLatency < latency)
                packetsMaximumDeliveryLatency = latency;
        }
        packetsReceivedCount++;
    }
    
    public void markDataPacketReceived(long packetId)
    {
        long sentTimeStamp = packetMonitor.removeSentTimeStamp(packetId);
        long latency = -1;
        if (sentTimeStamp != -1)
        {
            latency = (JistAPI.getTime() - sentTimeStamp)/Constants.MILLI_SECOND;
            dataPacketsCummulativeDeliveryLatency += latency;
            if (dataPacketsMinimumDeliveryLatency == -1 || dataPacketsMinimumDeliveryLatency != -1 && dataPacketsMinimumDeliveryLatency > latency)
                dataPacketsMinimumDeliveryLatency = latency;
            if (dataPacketsMaximumDeliveryLatency < latency)
                dataPacketsMaximumDeliveryLatency = latency;
       }
       dataPacketsReceivedCount++;
       dataPacketsReceivedPercentage = (long)Math.ceil((double)dataPacketsReceivedCount * 100 / dataPacketsSentCount);
       
        
       if (latency != -1 && passesConditionalEvaluation(ITEM.DATA_PACKETS_RECEIVED_PERCENTAGE_CONDITIONAL, latency * Constants.MILLI_SECOND))
       {
           dataPacketsReceivedCountConditional ++;
           dataPacketsReceivedPercentageConditional = (long)Math.ceil((double)dataPacketsReceivedCountConditional * 100 / dataPacketsSentCount);
       }
       if (latency != -1 && passesConditionalEvaluation(ITEM.DATA_PACKETS_RECEIVED_COUNT_CONDITIONAL, latency * Constants.MILLI_SECOND))
       {
           dataPacketsReceivedCountConditional ++;
           dataPacketsReceivedPercentageConditional = (long)Math.ceil((double)dataPacketsReceivedCountConditional * 100 / dataPacketsSentCount);
       }
    }

    
    public void run()
    {
        update(JistAPI.getTime() / Constants.MILLI_SECOND, true);
        repaint(); // this does also the update
        if (loggingEnabled)
            commitLog();
        
        JistAPI.sleepBlock(loggingInterval);
        ((SimulationTimeRunnable)self).run();
    }
    
    public synchronized void update(long currentTime, boolean triggerError)
    {
        if (CurrentTimeStamp > currentTime)
        {
            if (triggerError)
            {
                System.err.println("<StatsCollector>[ERROR] Out of order time stamps in Update() method. CurrentTimeStamp = " + CurrentTimeStamp + " and received timestamp = " + currentTime);
                new Exception().printStackTrace();
                System.exit(1);
            }
        }
        
        CurrentTimeStamp = currentTime;
        
        if (realTimeStart == 0)
            realTimeStart = System.currentTimeMillis();
        else
            realTimeCurrent = System.currentTimeMillis() - realTimeStart;    
        
        updateCommonStats();
    }
    
    // Final update stats. To be executed towards the end of a simulation
    public void commitLog()
    {
        updateCommonStats();
        
        String row = ROW_TAG;
        if (monitoringList != null)
            for (StatEntryDeprecated statEntry:monitoringList)
            {
                row += resolve(statEntry.getMethod()) + "\t";
            }
        if (statEntryList != null)
            for (StatEntry statEntry:statEntryList)
            {
                row += statEntry.getValueAsString() + "\t";
            }
        log.info(row);
    }
    
    public Object resolve(Object method)
    {
        Object returnable = null;
        if (method instanceof Method)
        {
            // TODO
            returnable = null;
        }
        else
        {
            switch((ITEM)method)
            {
                case TIME                          : returnable = JistAPI.getTime()/Constants.HOUR;     break;
                case DATA_PACKETS_SENT_COUNT       : returnable = dataPacketsSentCount;        break;
                case DATA_PACKETS_RECEIVED_COUNT   : returnable = dataPacketsReceivedCount;    break;
                case DATA_PACKETS_RECEIVED_COUNT_CONDITIONAL: returnable = dataPacketsReceivedCountConditional;           break;
                case DATA_PACKETS_RECEIVED_PERCENTAGE_CONDITIONAL: returnable = dataPacketsReceivedPercentageConditional; break;
                case DATA_PACKETS_LOST_COUNT       : returnable = dataPacketsLostCount;        break;
                case DATA_PACKETS_LOST_PERCENTAGE  : returnable = dataPacketsLostPercentage;   break;
                case DATA_PACKETS_RECEIVED_PERCENTAGE  : returnable = dataPacketsReceivedPercentage;   break;
                case DATA_PACKETS_AVERAGE_DELIVERY_LATENCY:              returnable = dataPacketsAverageDeliveryLatency;             break;
                case DATA_PACKETS_MAXIMUM_DELIVERY_LATENCY:              returnable = dataPacketsMaximumDeliveryLatency;             break;
                case DATA_PACKETS_MINIMUM_DELIVERY_LATENCY:              returnable = dataPacketsMinimumDeliveryLatency;             break;
            }
        }
        
        return returnable;
    }
    
    
    
    
    
    public boolean passesConditionalEvaluation(ITEM item, long currentValue)
    {
        boolean result = false;
        for (StatEntryDeprecated statEntry:monitoringList)
        {
            if (statEntry.isConditional() && statEntry.getMethod() == item)
            {
                switch(statEntry.getPredicate())
                {
                    case LESS_THAN:             result = currentValue <  statEntry.getThreshold(); break;
                    case LESS_THAN_OR_EQUAL:    result = currentValue <= statEntry.getThreshold(); break;
                    case EQUALS:                result = currentValue == statEntry.getThreshold(); break;
                    case GREATER_THAN_OR_EQUAL: result = currentValue >= statEntry.getThreshold(); break;
                    case GREATER_THAN:          result = currentValue >  statEntry.getThreshold();break;
                }
            }
        }
        return result;
    }
    
    public void resetPacketLatencies()
    {
        dataPacketsCummulativeDeliveryLatency = 0;
        dataPacketsMinimumDeliveryLatency = -1;
        dataPacketsMaximumDeliveryLatency = 0;
        
        packetsCummulativeDeliveryLatency = 0;
        packetsMinimumDeliveryLatency = -1;
        packetsMaximumDeliveryLatency = 0;
    }
    
    public void updateCommonStats()
    {
        if (JistAPI.getTime() == 0)
            return;
        
       
        if (statEntryList != null)
                for (StatEntry statEntry: statEntryList)
                {
                    statEntry.update(myNode);
                }        
    }
    
    public void monitor(StatEntry statEntry)
    {
        if (statEntryList == null)
            statEntryList = new LinkedList<StatEntry>();
        statEntryList.add(statEntry);
    }
    
    public void monitor(String itemName, Object method)
    {
        monitoringList.add(new StatEntryDeprecated(itemName, method));
        this.setPreferredSize(new Dimension(this.getWidth(), vertInitialPosition + vertIncrement * monitoringList.size()));
    }
    
    public void monitor(String itemName, Object method, PREDICATE condition, long threshold)
    {
        monitoringList.add(new StatEntryDeprecated(itemName, method, condition, threshold));
        this.setPreferredSize(new Dimension(this.getWidth(), vertInitialPosition + vertIncrement * monitoringList.size()));
    }
    
  
    public double getFurthestNeighborDistanceNCS()
    {
        double max=0;
        double dist;
        for (int i=0; i < myNode.length; i++)
        {
            dist = myNode[i].neighboursList.getFurthestNodeDistanceNCS(myNode[i].getNCS_Location2D());
            if (max < dist)
                max = dist;
        }
        return max;
    }

    /* @override */
     public void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2d = (Graphics2D)g;
        String s;
  
        g2d.setColor(Color.white);  
        s = "Elapsed Time: ";
        
        if (CurrentTimeStamp < 1000)
            s = s+0+" m . "+0+" s . "+(int)CurrentTimeStamp+" ms";
        if (CurrentTimeStamp >= 1000 && CurrentTimeStamp < 60*1000)
            s = s+0+" h . "+0+" m . "+(int)CurrentTimeStamp/1000+" s . "+(int)(CurrentTimeStamp-((int)CurrentTimeStamp/1000)*1000)+ " ms";
        if (CurrentTimeStamp >= 60*1000 && CurrentTimeStamp < 60*60*1000)
            s = s+0+" h . "+(int)CurrentTimeStamp/(60*1000)+" m . "+(int)(CurrentTimeStamp-((int)CurrentTimeStamp/(60*1000))*60*1000)/1000 + " s";
        if (CurrentTimeStamp >= 60*60*1000 )
            s = s+(int)CurrentTimeStamp/(60*60*1000)+" h. "+(int)(CurrentTimeStamp-((int)CurrentTimeStamp/(60*60*1000))*60*60*1000)/(60*1000)+" m";
        g2d.drawString(s,10,15);
        
        int vertPosition = vertInitialPosition;
        
        for (StatEntryDeprecated statEntry: monitoringList)
            if (!(statEntry.getMethod() instanceof Method) && statEntry.getMethod() != ITEM.TIME)
            {
                s = statEntry.getItemName() + ": " + resolve(statEntry.getMethod());
                g2d.drawString(s, 10, vertPosition);
                vertPosition += vertIncrement;
            }
        
        if (statEntryList != null)
            for (StatEntry statEntry:statEntryList)
            {
                s = statEntry.getHeader() + ": " + statEntry.getValueAsString();
                g2d.drawString(s, 10, vertPosition);
                vertPosition += vertIncrement;
            }
     
        if (realTimeCurrent >= 1)
        {
            s = "Simulation Speed : "+(double)((int)(CurrentTimeStamp / realTimeCurrent * 10))/10+"x";
            g2d.drawString(s,10,vertPosition);
        }
     }
    
    protected void clear(Graphics g) {
        //super.paintComponent(g);
    }    
    
 
    
}
