package be.iminds.ilabt.jfed.call_log_output;

import be.iminds.ilabt.jfed.log.ApiCallDetails;
import be.iminds.ilabt.jfed.log.ResultListener;
import be.iminds.ilabt.jfed.log.cache.ApiCallDetailsCache;
import be.iminds.ilabt.jfed.log.cache.ApiCallDetailsRef;
import be.iminds.ilabt.jfed.util.Slf4jHelper;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import org.rendersnake.HtmlCanvas;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlTransient;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;


/**
 * CallReport
 */
public class CallReport extends HtmlLogOutput {
    private final static Logger LOG = LoggerFactory.getLogger(HtmlLogOutput.class);

    private final ApiCallDetailsCache cache;

    @XmlTransient
    protected State curState;

    @XmlElementWrapper(name = "states")
    @XmlElement(name = "state")
    protected final ObservableList<State> states = FXCollections.observableArrayList();

    private final String reportObject;


    protected CallReport(String initialStateName, String reportTitle, String reportObject, ApiCallDetailsCache cache) {
        super(reportTitle);
        this.reportObject = reportObject;
        this.cache = cache;
        curState = new State(initialStateName, new Date(), cache);
        states.add(curState);
    }

    protected CallReport(String reportTitle, String reportObject, ApiCallDetailsCache cache) {
        this("Initial State", reportTitle, reportObject, cache);
    }

    protected CallReport(String reportTitle, String reportObject, Collection<State> states, ApiCallDetailsCache cache) {
        super(reportTitle);
        this.reportObject = reportObject;
        this.states.addAll(states);
        this.cache = cache;
    }

    public synchronized State getCurState() {
        return curState;
    }

    public synchronized State changeState(String newStateName) {
        LOG.debug("changeState(" + newStateName + ")");

        //only change if new state is different
        if (Objects.equals(newStateName, curState.name))
            return curState;

        Date now = new Date();
        State newState = new State(newStateName, now, cache);
        curState.end.set(now);
        states.add(newState);
        curState = newState;
        return newState;
    }

    public String getReportObject() {
        return reportObject;
    }

    public synchronized ObservableList<State> getStates() {
            //while unmodifiableObservableList is cleaner/safer, it does not forward all change events!!!
//        return FXCollections.unmodifiableObservableList(states);
        return states;
        //note: only allowed to do the "unmodifiableObservableList" because JAXB doesn't use this setter
        //if JAXB used this to populate the list, int would cause errors
    }

    public synchronized State getLastState(String stateName) {
        State res = null;
        for (State state : states)
            if (Objects.equals(state.name, stateName))
                res = state;
        return res;
    }

    public synchronized List<ApiCallDetailsRef> getAllStateCalls(String stateName) {
        return states.stream()
                .filter(state -> Objects.equals(state.name, stateName))
                .flatMap(state -> state.getCalls().stream())
                .collect(Collectors.toList());
    }

    @Override
    public synchronized void htmlContent(HtmlCanvas html) throws IOException {
        if (curState != null && curState.end.get() == null)
            curState.end.set(new Date());

        addClassVisibleToggle(html, "logline-DEBUG", "Debug Log Lines");

        int index = 0;
        for (State state : states) {
            List<SerializableApiCallDetails> callsCopy;
            List<LogEntry> logsCopy;
            synchronized (state.calls) {
                callsCopy = state.calls.stream().map(cache::getApiCallDetails).collect(Collectors.toList());
            }
            synchronized (state.logs) {
                logsCopy = new ArrayList<>(state.logs);
            }
            writeGroup(html, index++, state.getState(), state.name + " calls", reportObject + " state \"" + state.name + "\" calls",
                    state.getDurationMs(), state.start, state.end.get(),
                    (String) null/*exception*/,
                    callsCopy,
                    logsCopy);
        }
    }

    public synchronized List<ApiCallDetailsRef> getCalls() {
        List<ApiCallDetailsRef> res = new ArrayList<>();
        for (State state : states) {
            res.addAll(state.getCalls());
        }
        return res;
    }

    public static class State  extends Slf4jHelper.AbstractSlf4JLoggerImpl implements ResultListener {
        private final Date start;

        private final ObjectProperty<Date> end = new SimpleObjectProperty<>();

        private final String name;

        private final ObservableList<LogEntry> logs =
                FXCollections.synchronizedObservableList(FXCollections.observableArrayList());

        private final ObservableList<ApiCallDetailsRef> calls =
                FXCollections.synchronizedObservableList(FXCollections.observableArrayList());

        private final ObjectProperty<TestResultState> testResultState =
                new SimpleObjectProperty<>(TestResultState.SUCCESS);

        private final ApiCallDetailsCache cache;


        State(String name, Date start, ApiCallDetailsCache cache) {
            super(Slf4jHelper.Level.TRACE);
            this.start = start;
            this.name = name;
            this.cache = cache;
        }

        public long getDurationMs() {
            if (end.get() == null || start == null) return 0;
            return end.get().getTime() - start.getTime();
        }

        @Override
        protected void handle(Slf4jHelper.Level level, String message, Throwable t) {
            switch (level) {
                case TRACE: { log(message, LogLineType.DEBUG, t); break; }
                case DEBUG: { log(message, LogLineType.DEBUG, t); break; }
                case INFO: { log(message, LogLineType.NOTE, t); break; }
                case WARN: { log(message, LogLineType.WARN, t); break; }
                case ERROR: { log(message, LogLineType.ERROR, t); break; }
                default:throw new RuntimeException("unsupported Slf4jHelper.Level "+level);
            }
        }

        protected void log(String message, LogLineType logLineType, Throwable e) {
            assert logLineType != null;
            assert message != null;
            log(new LogEntry(logLineType, message, e));
        }

        protected void log(LogEntry logLine) {
            logs.add(logLine);
        }

        @Override
        public void onResult(ApiCallDetails result) {
            logCall(cache.registerApiCallDetails(result));
        }

        public void logCall(ApiCallDetailsRef callDetails) {
            calls.add(callDetails);
        }

        @XmlElement(name = "statestate")
        public TestResultState getState() {
            return testResultState.get();
        }

        public void setState(TestResultState state) {
            this.testResultState.set(state);
        }

        @Override
        public String toString() {
            return "State{" +
                    "" + name + '\'' +
                    '}';
        }

        public List<ApiCallDetailsRef> getCalls() {
            return calls;
        }

        public ObservableList<LogEntry> getLogLines() {
            //while unmodifiableObservableList is cleaner/safer, it does not forward all change events!!!
            //return FXCollections.unmodifiableObservableList(logs);
            return logs;
        }

        public String getName() {
            return name;
        }

        public Date getStateStart() {
            return start;
        }

        @XmlElement(name = "end")
        public Date getEnd() {
            return end.get();
        }

        void setEnd(Date d) {
            end.setValue(d);
        }

        public ObjectProperty<Date> endProperty() {
            return end;
        }

        public ObjectProperty<TestResultState> stateProperty() {
            return testResultState;
        }
    }


}
