/* *****************************************************************************
 * Copyright 2018 Dynamic Analysis Group, Università della Svizzera Italiana (USI)
 * Copyright (c) 2018, 2020, Oracle and/or its affiliates. All rights reserved.
 *
 * 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 ch.usi.inf.nodeprof.test.examples;

import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map.Entry;

import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.instrumentation.EventContext;
import com.oracle.truffle.api.instrumentation.Instrumenter;
import com.oracle.truffle.api.instrumentation.TruffleInstrument.Env;
import com.oracle.truffle.api.nodes.Node;

import ch.usi.inf.nodeprof.ProfiledTagEnum;
import ch.usi.inf.nodeprof.analysis.AnalysisFactory;
import ch.usi.inf.nodeprof.handlers.BaseEventHandlerNode;
import ch.usi.inf.nodeprof.handlers.BaseSingleTagEventHandler;
import ch.usi.inf.nodeprof.test.InputChecker;
import ch.usi.inf.nodeprof.test.TestableNodeProfAnalysis;
import ch.usi.inf.nodeprof.test.examples.report.Report;
import ch.usi.inf.nodeprof.test.examples.report.ReportDB;
import ch.usi.inf.nodeprof.test.examples.report.ReportDB.ReportFactory;
import ch.usi.inf.nodeprof.test.examples.report.ReportEntryNode;
import ch.usi.inf.nodeprof.test.examples.report.ReportEntryNodeGen;
import ch.usi.inf.nodeprof.utils.Logger;
import ch.usi.inf.nodeprof.utils.SourceMapping;

/**
 * Analysis which focuses on verifying the inputs of events
 *
 * @See InputChecker
 */
public final class EventLogger extends TestableNodeProfAnalysis {

    public EventLogger(Instrumenter instrumenter, Env env) {
        super("EventLogger", instrumenter, env);
    }

    private final ReportDB db = new ReportDB();

    @Override
    public boolean checkResult(Object result) {
        return db.getRecords().size() == 0;
    }

    @Override
    public void initCallbacks() {
        for (ProfiledTagEnum tag : ProfiledTagEnum.values()) {
            if (tag == ProfiledTagEnum.EXPRESSION) {
                continue;
            }
            this.onCallback(tag, new AnalysisFactory<BaseEventHandlerNode>() {
                public BaseEventHandlerNode create(EventContext context) {
                    return new BaseSingleTagEventHandler(context, tag) {
                        @Override
                        public void enter(VirtualFrame frame) {
                            addDebugEvent("ENTER", getSourceIID(), tag);
                        }

                        @Override
                        public void executePre(VirtualFrame frame, Object[] inputs) {
                            addDebugEvent("PRE", getSourceIID(), tag);
                        }

                        @Child ReportEntryNode reportNode = ReportEntryNodeGen.create(db, new ReportFactory() {
                            public Report create(int iid) {
                                return new EventReport(iid);
                            }
                        });

                        @Override
                        public void executePost(VirtualFrame frame, Object result, Object[] inputs) {
                            addDebugEvent("POST", getSourceIID(), tag);
                            if (!InputChecker.checkInput(tag, this, inputs)) {
                                EventReport report = (EventReport) reportNode.execute(getSourceIID());
                                report.addError(tag, context.getInstrumentedNode().getClass(), inputs);
                            }
                        }

                    };
                }
            });
        }
    }

    @Override
    public void onClear() {
        db.clear();
    }

    @Override
    public void printResult() {
        for (Entry<Integer, Report> entry : db.getRecords().entrySet()) {
            EventReport report = (EventReport) entry.getValue();
            Logger.info(Logger.printSourceSectionWithCode(SourceMapping.getSourceSectionForIID(entry.getKey())).append(report.report()));
        }
    }

    class EventReport extends Report {
        EventReport(int iid) {
            super(iid);
        }

        @TruffleBoundary
        public void addError(ProfiledTagEnum tag, Class<? extends Node> instrumentedNode, Object[] inputs) {
            if (!mapping.containsKey(tag)) {
                mapping.put(tag, new HashSet<EventLogger.EventReport.InputTypes>());
            }
            mapping.get(tag).add(new InputTypes(instrumentedNode, inputs));
        }

        class InputTypes {
            private final Class<? extends Node> nodeClass;
            private final Class<?>[] types;
            private final String sample;

            InputTypes(Class<? extends Node> instrumentedNode, Object[] inputs) {
                this.nodeClass = instrumentedNode;
                if (inputs == null) {
                    types = null;
                    this.sample = "";
                } else {
                    this.types = new Class<?>[inputs.length];
                    String s = "";
                    for (int i = 0; i < inputs.length; i++) {
                        if (inputs[i] == null) {
                            this.types[i] = null;
                        } else {
                            s += inputs[i];
                            this.types[i] = inputs[i].getClass();
                        }
                        if (i != inputs.length - 1) {
                            s += "/";
                        }
                    }
                    this.sample = s;
                }
            }

            @Override
            public String toString() {
                StringBuilder sb = new StringBuilder();
                sb.append("node:").append(nodeClass.getSimpleName()).append(",");
                if (types == null) {
                    return sb.toString();
                }

                for (int i = 0; i < types.length; i++) {
                    Class<?> type = types[i];
                    sb.append(i).append(":");
                    if (type != null) {
                        sb.append(type.getSimpleName());
                    } else {
                        sb.append("null");
                    }
                    if (i != types.length - 1) {
                        sb.append(",");
                    }
                }
                sb.append(", sample:").append(this.sample);
                return sb.toString();
            }

            @Override
            public boolean equals(Object obj) {
                if (obj == null) {
                    return false;
                }
                if (obj instanceof InputTypes) {
                    if (this.nodeClass != ((InputTypes) obj).nodeClass) {
                        return false;
                    }
                    return Arrays.equals(this.types, ((InputTypes) obj).types);
                }
                return false;
            }

            @Override
            public int hashCode() {
                if (types == null) {
                    return 0;
                }
                int result = nodeClass.hashCode();
                for (Class<?> c : types) {
                    if (c == null) {
                        result += 1;
                    } else {
                        result += c.hashCode();
                    }
                }
                return result;
            }
        }

        HashMap<ProfiledTagEnum, HashSet<InputTypes>> mapping = new HashMap<>();

        @Override
        public String report() {
            StringBuilder sb = new StringBuilder();
            for (Entry<ProfiledTagEnum, HashSet<InputTypes>> entry : mapping.entrySet()) {
                for (InputTypes it : entry.getValue()) {
                    sb.append("\n\tIncorrect Input Number/Types for tag " + entry.getKey().name() + ": ");
                    sb.append(it.toString());
                }
            }
            return sb.toString();
        }
    }
}