/**
 * Copyright (C) 2010-14 diirt developers. See COPYRIGHT.TXT
 * All rights reserved. Use is subject to license terms. See LICENSE.TXT
 */
package org.diirt.datasource.integration;

import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import org.diirt.datasource.PVReaderEvent;
import org.diirt.datasource.PVReaderListener;
import org.diirt.datasource.PVWriterEvent;
import org.diirt.datasource.PVWriterListener;
import org.diirt.util.time.Timestamp;
import org.diirt.util.time.TimestampFormat;
import org.diirt.vtype.Alarm;
import org.diirt.vtype.VEnum;
import org.diirt.vtype.VNumber;
import org.diirt.vtype.VTypeToString;
import org.diirt.vtype.VTypeValueEquals;
import org.diirt.vtype.ValueUtil;

/**
 *
 * @author carcassi
 */
public class Log {
    
    private final List<Event> events = Collections.synchronizedList(new ArrayList<Event>());
    private final List<String> errors = Collections.synchronizedList(new ArrayList<String>());
    private final AtomicInteger testCount = new AtomicInteger(0);
    
    public <T> PVReaderListener<T> createReadListener() {
        return new PVReaderListener<T>() {

            @Override
            public void pvChanged(PVReaderEvent<T> event) {
                events.add(new ReadEvent(Timestamp.now(), event.getPvReader().getName(), event, event.getPvReader().isConnected(), event.getPvReader().getValue(), event.getPvReader().lastException()));
            }
        };
    }
    
    public <T> PVWriterListener<T> createWriteListener(final String name) {
        return new PVWriterListener<T>() {

            @Override
            public void pvChanged(PVWriterEvent<T> event) {
                events.add(new WriteEvent(Timestamp.now(), name, event, event.getPvWriter().isWriteConnected(), event.getPvWriter().lastWriteException()));
            }
        };
    }
    
    public List<Event> getEvents() {
        return events;
    }
    
    public boolean isCorrect() {
        return errors.isEmpty();
    }
    
    public List<String> getErrors() {
        return errors;
    }
    
    public int getTestCount() {
        return testCount.get();
    }
    
    public void matchConnections(String pvName, boolean... connectionFlags) {
        int current = 0;
        for (Event event : events) {
            if (event instanceof ReadEvent && event.getPvName().equals(pvName)) {
                ReadEvent readEvent = (ReadEvent) event;
                if (readEvent.getEvent().isConnectionChanged()) {
                    if (current < connectionFlags.length && readEvent.isConnected() != connectionFlags[current]) {
                        errors.add(pvName + ": connection notification " + current + " was " + readEvent.isConnected() + " (expected " + connectionFlags[current] + ")");
                    }
                    current++;
                }
            }
        }
        if (current > connectionFlags.length) {
            errors.add(pvName + ": more connection notifications ("  + current + ") than expected ("  + connectionFlags.length + ")");
        }
        if (current < connectionFlags.length) {
            errors.add(pvName + ": fewer connection notifications ("  + current + ") than expected (" + connectionFlags.length + ")");
        }
        testCount.incrementAndGet();
    }
    
    public void matchWriteConnections(String pvName, boolean... connectionFlags) {
        int current = 0;
        for (Event event : events) {
            if (event instanceof WriteEvent && event.getPvName().equals(pvName)) {
                WriteEvent writeEvent = (WriteEvent) event;
                if (writeEvent.getEvent().isConnectionChanged()) {
                    if (current < connectionFlags.length && writeEvent.isConnected() != connectionFlags[current]) {
                        errors.add(pvName + ": write connection notification " + current + " was " + writeEvent.isConnected() + " (expected " + connectionFlags[current] + ")");
                    }
                    current++;
                }
            }
        }
        if (current > connectionFlags.length) {
            errors.add(pvName + ": more write connection notifications ("  + current + ") than expected ("  + connectionFlags.length + ")");
        }
        if (current < connectionFlags.length) {
            errors.add(pvName + ": fewer write connection notifications ("  + current + ") than expected (" + connectionFlags.length + ")");
        }
        testCount.incrementAndGet();
    }
    
    public void matchWriteNotifications(String pvName, boolean... sucessfulWrite) {
        int current = 0;
        for (Event event : events) {
            if (event instanceof WriteEvent && event.getPvName().equals(pvName)) {
                WriteEvent writeEvent = (WriteEvent) event;
                if (writeEvent.getEvent().isWriteSucceeded()) {
                    if (current < sucessfulWrite.length && !sucessfulWrite[current]) {
                        errors.add(pvName + ": write notification " + current + " was successful (expected unsuccessful)");
                    }
                    current++;
                } else if (writeEvent.getEvent().isWriteFailed()) {
                    if (current < sucessfulWrite.length && sucessfulWrite[current]) {
                        errors.add(pvName + ": write notification " + current + " was unsuccessful (expected successful)");
                    }
                    current++;
                }
            }
        }
        if (current > sucessfulWrite.length) {
            errors.add(pvName + ": more write notifications ("  + current + ") than expected ("  + sucessfulWrite.length + ")");
        }
        if (current < sucessfulWrite.length) {
            errors.add(pvName + ": fewer write notifications ("  + current + ") than expected (" + sucessfulWrite.length + ")");
        }
        testCount.incrementAndGet();
    }
    
    public void matchValues(String pvName, VTypeMatchMask mask, Object... values) {
        int current = 0;
        for (Event event : events) {
            if (event instanceof ReadEvent && event.getPvName().equals(pvName)) {
                ReadEvent readEvent = (ReadEvent) event;
                if (readEvent.getEvent().isValueChanged()) {
                    Object actualValue = readEvent.getValue();
                    if (current < values.length) {
                        Object expectedValue = values[current];
                        String message = mask.match(expectedValue, actualValue);
                        if (message != null) {
                            errors. add(pvName + ": value notification " + current + " " + message);
                        }
                    }
                    current++;
                }
            }
        }
        if (current > values.length) {
            errors.add(pvName + ": more value notification ("  + current + ") than expected ("  + values.length + ")");
        }
        if (current < values.length) {
            errors.add(pvName + ": fewer value notification ("  + current + ") notification than expected (" + values.length + ")");
        }
        testCount.incrementAndGet();
    }
    
    public void validate(String pvName, Validator validator) {
        List<String> resErrors = validator.validate(valuesForChannel(pvName, Object.class));
        for (String error : resErrors) {
            errors.add(pvName + ": " + error);
        }
        testCount.incrementAndGet();
    }

    public void matchAllValues(String pvName, VTypeMatchMask mask, Object expectedValue) {
        int current = 0;
        for (Event event : events) {
            if (event instanceof ReadEvent && event.getPvName().equals(pvName)) {
                ReadEvent readEvent = (ReadEvent) event;
                if (readEvent.getEvent().isValueChanged()) {
                    Object actualValue = readEvent.getValue();
                    String message = mask.match(expectedValue, actualValue);
                    if (message != null) {
                        errors. add(pvName + ": value notification " + current + " " + message);
                    }
                    current++;
                }
            }
        }
        testCount.incrementAndGet();
    }
    
    public void matchErrors(String pvName, String... messages) {
        int current = 0;
        for (Event event : events) {
            if (event instanceof ReadEvent && event.getPvName().equals(pvName)) {
                ReadEvent readEvent = (ReadEvent) event;
                if (readEvent.getEvent().isExceptionChanged()) {
                    if (current < messages.length && Objects.equals(readEvent.getLastException().getMessage(), messages[current])) {
                        errors.add(pvName + ": error notification " + current + " was " + readEvent.getLastException().getMessage() + " (expected " + messages[current] + ")");
                    }
                    current++;
                }
            }
        }
        if (current > messages.length) {
            errors.add(pvName + ": more error notifications ("  + current + ") than expected ("  + messages.length + ")");
        }
        if (current < messages.length) {
            errors.add(pvName + ": fewer error notifications ("  + current + ") than expected (" + messages.length + ")");
        }
        testCount.incrementAndGet();
    }
    
    public void matchSequentialNumberValues(String pvName, int expectedRepeatedValues) {
        List<VNumber> values = valuesForChannel(pvName, VNumber.class);
        Double currentValue = null;
        int repeatedValues = 0;
        for (VNumber vNumber : values) {
            if (vNumber == null) {
                errors.add(pvName + ": value was null");
            } else {
                if (currentValue == null) {
                    currentValue = vNumber.getValue().doubleValue();
                } else {
                    double nextValue = vNumber.getValue().doubleValue();
                    if (nextValue == currentValue) {
                        repeatedValues++;
                    } else if (nextValue != currentValue + 1) {
                        errors.add(pvName + ": value was not sequential (" + nextValue + " after " + currentValue + ")");
                    }
                    currentValue = nextValue;
                }
            }
        }
        if (repeatedValues != expectedRepeatedValues) {
            errors.add(pvName + ": repeated value occurences mismatch (" + repeatedValues + " but expected " + expectedRepeatedValues + ")");
        }
        testCount.incrementAndGet();
    }

    void matchValueEventRate(String pvName, double minRateHz, double maxRateHz) {
        Timestamp initialTime = null;
        Timestamp finalTime = null;
        int nNotifications = 0;
        for (Event event : events) {
            if (pvName.equals(event.getPvName()) && event instanceof ReadEvent) {
                ReadEvent readEvent = (ReadEvent) event;
                if (readEvent.getEvent().isValueChanged()) {
                    Timestamp nextTime = readEvent.getTimestamp();
                    if (initialTime == null) {
                        initialTime = nextTime;
                    } else {
                        finalTime = nextTime;
                    }
                    nNotifications++;
                }
            }
        }
        
        if (initialTime != null && finalTime != null) {
            double seconds = finalTime.durationFrom(initialTime).toSeconds();
            // The period between the first two notification is going to be shorter
            // since we connect independently from the cycle.
            // We'll make sure the rate is between the two extreems: second event
            // is right after, second event is after the correct period
            double minMeasuredRate = (nNotifications - 2) / seconds;
            double maxMeasuredRate = (nNotifications - 1) / seconds;
            if (maxMeasuredRate < minRateHz || minMeasuredRate > maxRateHz) {
                errors.add(pvName + ": event rate mismatch (" + minMeasuredRate + "/" + maxMeasuredRate + " but expected between " + minRateHz + "/" + maxRateHz + ")");
            }
        }
        testCount.incrementAndGet();
    }
    
    private <T> List<T> valuesForChannel(String pvName, Class<T> clazz) {
        List<T> values = new ArrayList<>();
        for (Event event : events) {
            if (pvName.equals(event.getPvName()) && event instanceof ReadEvent) {
                ReadEvent readEvent = (ReadEvent) event;
                if (readEvent.getEvent().isValueChanged()) {
                    try {
                        T value = clazz.cast(readEvent.getValue());
                        values.add(value);
                    } catch(ClassCastException ex) {
                        errors.add(pvName + ": value is not " + clazz.getSimpleName() + " (was " + readEvent.getValue() + ")");
                    }
                }
            }
        }
        return values;
    } 
    
    private List<Alarm> alarmsForChannel(String pvName) {
        List<Alarm> values = new ArrayList<>();
        for (Event event : events) {
            if (pvName.equals(event.getPvName()) && event instanceof ReadEvent) {
                ReadEvent readEvent = (ReadEvent) event;
                if (readEvent.getEvent().isValueChanged()) {
                    values.add(ValueUtil.alarmOf(readEvent.getValue()));
                }
            }
        }
        return values;
    } 
    
    TimestampFormat format = new TimestampFormat("ss.NNNNNNNNN");
    
    public void print(PrintStream out) {
        for (Event event : events) {
            if (event instanceof ReadEvent) {
                ReadEvent readEvent = (ReadEvent) event;
                out.append(format.format(readEvent.getTimestamp()))
                        .append(" R(");
                if (readEvent.getEvent().isConnectionChanged()) {
                    out.append("C");
                }
                if (readEvent.getEvent().isValueChanged()) {
                    out.append("V");
                }
                if (readEvent.getEvent().isExceptionChanged()) {
                    out.append("E");
                }
                out.append(") ").append(readEvent.getPvName());
                if (readEvent.isConnected()) {
                    out.append(" CONN ");
                } else {
                    out.append(" DISC ");
                }
                out.append(Objects.toString(readEvent.getValue()));
                if (readEvent.getLastException() != null) {
                    out.append(" ").append(readEvent.getLastException().getClass().getName())
                            .append(":").append(readEvent.getLastException().getMessage());
                } else {
                    out.append(" NoException");
                }
            }
            if (event instanceof WriteEvent) {
                WriteEvent writeEvent = (WriteEvent) event;
                out.append(format.format(writeEvent.getTimestamp()))
                        .append(" W(");
                if (writeEvent.getEvent().isConnectionChanged()) {
                    out.append("C");
                }
                if (writeEvent.getEvent().isWriteSucceeded()) {
                    out.append("S");
                }
                if (writeEvent.getEvent().isWriteFailed()) {
                    out.append("F");
                }
                if (writeEvent.getEvent().isExceptionChanged()) {
                    out.append("E");
                }
                out.append(") ").append(writeEvent.getPvName());
                if (writeEvent.isConnected()) {
                    out.append(" CONN");
                } else {
                    out.append(" DISC");
                }
                if (writeEvent.getLastException() != null) {
                    out.append(" ").append(writeEvent.getLastException().getClass().getName())
                            .append(":").append(writeEvent.getLastException().getMessage());
                } else {
                    out.append(" NoException");
                }
            }
            out.append("\n");
        }
        out.flush();
    }
}
