package org.rug.tracker;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
import org.apache.tinkerpop.gremlin.structure.Graph;
import org.apache.tinkerpop.gremlin.structure.Vertex;
import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph;
import org.rug.data.project.IVersion;
import org.rug.data.smells.ArchitecturalSmell;

import java.io.Serializable;
import java.text.DecimalFormat;
import java.util.*;
import java.util.stream.Collectors;

import static org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__.*;

/**
 * Tracks incrementally the architectural smells and saves them internally.
 */
public class ASmellTracker implements Serializable{

    public static final String NAME = "name";
    public static final String SMELL = "smell";
    public static final String VERSION = "version";
    public static final String VERSION_INDEX = "versionIndex";
    public static final String VERSION_DATE = "versionDate";
    public static final String SMELL_OBJECT = "smellObject";
    public static final String LATEST_VERSION = "latestVersion";
    public static final String EVOLVED_FROM = "evolvedFrom";
    public static final String UNIQUE_SMELL_ID = "uniqueSmellID";
    public static final String REAPPEARED = "reappeared";
    public static final String SIMILARITY = "similarity";
    public static final String CHARACTERISTIC = "characteristic";
    public static final String HAS_CHARACTERISTIC = "hasCharacteristic";
    public static final String COMPONENT = "component";
    public static final String AFFECTS = "affects";
    public static final String SMELL_TYPE = "smellType";
    public static final String AGE = "age";
    public static final String NA = "NA";
    public static final String FIRST_APPEARED = "firstAppeared";
    public static final String LAST_DETECTED = "lastDetected";
    public static final String FIRST_APPEARED_INDEX = "firstAppearedIndex";
    public static final String FIRST_APPEARED_DATE = "firstAppearedDate";
    public static final String LAST_DETECTED_INDEX  = "lastDetectedIndex";
    public static final String LAST_DETECTED_DATE = "lastDetectedDate";
    public static final String SMELL_ID = "smellId";
    public static final String COMPONENT_TYPE = "componentType";
    public static final String TAIL = "tail";
    public static final String COMPONENT_CHARACTERISTIC = "componentCharacteristic";
    public static final String LATEST_VERSION_INDEX = "latestVersionIndex";
    private static final String NON_CONSEC_VERSIONS = "nonConsecVersions";

    private transient Graph trackGraph;
    private transient Graph condensedGraph;
    private transient Map<Long, Vertex> uniqueSmellsMap;
    private transient Map<String, CachedVertex> updatedAffectedElementsCache;
    private transient Vertex tail;
    private long uniqueSmellID;
    private final ISimilarityLinker scorer;
    private final DecimalFormat decimal;

    private final boolean trackNonConsecutiveVersions;
    private final int maxNonConsecutiveVersions;

    private final static Logger logger = LogManager.getLogger(ASmellTracker.class);

    /**
     * Builds an instance of this tracker.
     * @param maxNonConsecutiveVersions how many versions to wait before closing a smell dynasty.
     *
     */
    public ASmellTracker(ISimilarityLinker scorer, int maxNonConsecutiveVersions){
        this.trackGraph = TinkerGraph.open();
        this.tail = trackGraph.traversal().addV(TAIL).next();
        this.condensedGraph = TinkerGraph.open();
        this.uniqueSmellID = 1L;
        this.scorer = scorer;
        this.decimal = new DecimalFormat("0.0#");
        this.trackNonConsecutiveVersions = maxNonConsecutiveVersions > 0;
        this.maxNonConsecutiveVersions = maxNonConsecutiveVersions;
        this.initializeCache();
    }


    /**
     * Builds an instance of this tracker that does not tracks smells through non-consecutive versions.
     * A JaccardSimilarityLinker is used to select the single successor of the given smell.
     */
    public ASmellTracker(){
        this(new JaccardSimilarityLinker(), 0);
    }

    /**
     * Computes the tracking algorithm on the given system and saves internally the results
     * @param smellsInVersion the architectural smells identified in version
     * @param version the version of the given system
     */
    public void track(List<ArchitecturalSmell> smellsInVersion, IVersion version){
        List<ArchitecturalSmell> nextVersionSmells = new ArrayList<>(smellsInVersion);

        GraphTraversalSource g1 = trackGraph.traversal();

        if (g1.V(tail).outE().hasNext()) {
            List<ArchitecturalSmell> currentVersionSmells;
            if (trackNonConsecutiveVersions) {
                currentVersionSmells = g1.V(tail).out().values(SMELL_OBJECT)
                        .toStream().map(o -> (ArchitecturalSmell) o).collect(Collectors.toList());
            }else {
                currentVersionSmells = g1.V(tail).out().has(VERSION, tail.value(LATEST_VERSION).toString()).values(SMELL_OBJECT)
                        .toStream().map(o -> (ArchitecturalSmell) o).collect(Collectors.toList());
            }

            Set<LinkScoreTriple> bestMatch = scorer.bestMatch(currentVersionSmells, nextVersionSmells);
            logger.debug("Matching complete for {} pairs.", bestMatch.size());
            bestMatch.forEach(t -> {
                // If this fails it means that a successor has already been found, which should never happen!
                Vertex predecessor = g1.V(tail).out().has(SMELL_OBJECT, t.getA()).next();
                Vertex successor = g1.addV(SMELL)
                        .property(VERSION, version.getVersionString())
                        .property(VERSION_INDEX, version.getVersionIndex())
                        .property(VERSION_DATE, version.getVersionDate())
                        .property(SMELL_ID, t.getB().getId())
                        .property(SMELL_OBJECT, t.getB())
                        .property(UNIQUE_SMELL_ID, predecessor.value(UNIQUE_SMELL_ID))
                        .property(NON_CONSEC_VERSIONS, 0)
                        .next();
                g1.V(tail).outE().where(otherV().is(predecessor)).drop().iterate();
                String eLabel = tail.value(LATEST_VERSION).equals(predecessor.value(VERSION)) ? EVOLVED_FROM : REAPPEARED;
                g1.addE(eLabel).property(SIMILARITY, decimal.format(t.getC())).from(successor).to(predecessor).next();
                g1.addE(LATEST_VERSION).from(tail).to(successor).next();
                currentVersionSmells.remove(t.getA());
                nextVersionSmells.remove(t.getB());

                g1.V(predecessor).drop().iterate();
            });
            if (!trackNonConsecutiveVersions) {
                currentVersionSmells.forEach(this::endDynasty);
            }

        }
        nextVersionSmells.forEach(s -> addNewDynasty(s, version));
        tail.property(LATEST_VERSION, version.getVersionString());
        tail.property(LATEST_VERSION_INDEX, version.getVersionIndex());
        logger.debug("Updating condensed graph.");
        updateCondensedGraph();
    }

    /**
     * Begins a new dynasty for the given AS at the given starting version
     * @param s the starter of the dynasty
     * @param version the version
     */
    private void addNewDynasty(ArchitecturalSmell s, IVersion version) {
        GraphTraversalSource g = trackGraph.traversal();
        Vertex successor = g.addV(SMELL)
                .property(VERSION, version.getVersionString())
                .property(VERSION_INDEX, version.getVersionIndex())
                .property(VERSION_DATE, version.getVersionDate())
                .property(SMELL_ID, s.getId())
                .property(SMELL_OBJECT, s)
                .property(UNIQUE_SMELL_ID, uniqueSmellID++)
                .property(NON_CONSEC_VERSIONS, 0)
                .next();
        g.addE(LATEST_VERSION).from(tail).to(successor).next();
    }

    /**
     * Concludes the dynasty of the given smell (last smell in the dynasty)
     * @param smell the smell
     */
    private void endDynasty(ArchitecturalSmell smell) {
        GraphTraversalSource g = trackGraph.traversal();
        Vertex lastHeir = g.V().has(SMELL_OBJECT, smell).next();
        int nonConsecVersions = lastHeir.<Integer>value(NON_CONSEC_VERSIONS) + 1;
        if (nonConsecVersions > maxNonConsecutiveVersions){
            g.V(tail).outE().where(otherV().is(lastHeir)).drop().iterate();
            g.V(lastHeir).drop().iterate();
        } else {
            lastHeir.property(NON_CONSEC_VERSIONS, nonConsecVersions);
        }
    }

    /**
     * Get the scorer used to instantiate this instance.
     * @return The scorer used to link the smells between versions.
     */
    public ISimilarityLinker getScorer() {
        return scorer;
    }

    /**
     * Returns the latest version of update of this tracker.
     * @return a string representing the version or {@link #NA} if no current version is available.
     */
    public String currentVersion(){
        return tail.property(LATEST_VERSION).orElse(NA).toString();
    }

    /**
     * Updates the condensed graph from the current state of trackGraph.
     */
    private void updateCondensedGraph(){
        GraphTraversalSource gt = trackGraph.traversal();
        GraphTraversalSource gs = condensedGraph.traversal();

        Set<Vertex> smellsInVersion = gt.V().hasLabel(TAIL).out().toSet();

        logger.debug("Updating {} smells and affected components into the condensed graph.", smellsInVersion.size());

        for(var smellVertex : smellsInVersion){
            Long smellUID = smellVertex.value(UNIQUE_SMELL_ID);
            var smellVersionString = smellVertex.value(VERSION);
            var smellVersionIndex = smellVertex.value(VERSION_INDEX);
            var smellVersionDate = smellVertex.value(VERSION_DATE);
            ArchitecturalSmell smellObject = smellVertex.value(SMELL_OBJECT);
            String affectedComponentType = smellObject.getLevel().toString();

            var condensedSmell = uniqueSmellsMap.computeIfAbsent(smellUID, (sUIDKey) -> gs.addV(SMELL)
                            .property(UNIQUE_SMELL_ID, smellUID)
                            .property(FIRST_APPEARED, smellVersionString)
                            .property(FIRST_APPEARED_INDEX, smellVersionIndex)
                            .property(FIRST_APPEARED_DATE, smellVersionDate).next());

            if (!condensedSmell.property(SMELL_TYPE).isPresent()){
                condensedSmell.property(SMELL_TYPE, smellObject.getType().toString());
            }
            Vertex characteristics = gs.addV(CHARACTERISTIC).next();
            smellObject.getCharacteristicsMap().forEach(characteristics::property);

            gs.addE(HAS_CHARACTERISTIC).from(condensedSmell).to(characteristics)
                    .property(VERSION, smellVersionString)
                    .property(VERSION_INDEX, smellVersionIndex)
                    .property(VERSION_DATE, smellVersionDate)
                    .property(SMELL_ID, smellObject.getId()).next();
            
            for(var affectedComp : smellObject.getAffectedElements()) {
                String name = affectedComp.value(NAME);
                var component = updatedAffectedElementsCache.computeIfAbsent(name, (key) -> new CachedVertex(gs.addV(COMPONENT)
                        .property(NAME, name)
                        .property(COMPONENT_TYPE, affectedComponentType).next()));
                if (!component.updated){
                    var cce = gs.V(component.vertex).outE(HAS_CHARACTERISTIC)
                            .has(VERSION, smellVersionString.toString())
                            .tryNext();
                    if (cce.isEmpty()) {
                        var componentCharacteristic = gs.addV(COMPONENT_CHARACTERISTIC).next();
                        affectedComp.keys().stream().filter(k -> !k.equals(NAME)).forEach(k ->
                                componentCharacteristic.property(k, affectedComp.value(k))
                        );
                        gs.addE(HAS_CHARACTERISTIC).from(component.vertex).to(componentCharacteristic)
                                .property(VERSION, smellVersionString)
                                .property(VERSION_INDEX, smellVersionIndex)
                                .property(VERSION_DATE, smellVersionDate)
                                .next();
                    }
                    component.updated = true;
                }
                gs.addE(AFFECTS).from(condensedSmell).to(component.vertex)
                        .property(VERSION, smellVersionString)
                        .property(VERSION_INDEX, smellVersionIndex)
                        .property(VERSION_DATE, smellVersionDate)
                        .next();
            }

            long age = condensedSmell.<Long>property(AGE).orElse(0L);
            condensedSmell.property(AGE, ++age);
            condensedSmell.property(LAST_DETECTED, smellVersionString);
            condensedSmell.property(LAST_DETECTED_INDEX, smellVersionIndex);
            condensedSmell.property(LAST_DETECTED_DATE, smellVersionDate);
        }
        updatedAffectedElementsCache.clear(); // reset cache for next version
    }

    /**
     * Retrieves the condensed graph.
     * @return the graph representing the tracked smells including their characteristics and components affected
     * with their own characteristics.
     */
    public Graph getCondensedGraph(){
        return condensedGraph;
    }

    /**
     * Get the graph object used to perform the tracking.
     * @return the track graph.
     */
    public Graph getTrackGraph(){
        return trackGraph;
    }

    /**
     * Returns the number of smells linked in the current iteration.
     * @return the number of smells linked.
     */
    public long smellsLinked(){
        return this.getScorer().bestMatch().size();
    }

    public void setTrackGraph(Graph trackGraph) {
        this.trackGraph = trackGraph;
    }

    public void setCondensedGraph(Graph condensedGraph) {
        this.condensedGraph = condensedGraph;
    }

    public void setTail(Vertex tail) {
        this.tail = tail;
    }

    /**
     * Return the map that contains all the unique smell objects from the condensed graph.
     * @return a map where the keys are the UNIQUE_SMELL_ID and the values are the smell vertices with the
     * corresponding UNIQUE_SMELL_ID.
     */
    public Map<Long, Vertex> getUniqueSmellsMap() {
        return uniqueSmellsMap;
    }

    public void initializeCache() {
        this.uniqueSmellsMap = new HashMap<>(5000);
        this.updatedAffectedElementsCache = new HashMap<>(5000);
    }

    /**
     * Encapsulates a vertex to quickly check whether it was updated or not.
     */
    private static class CachedVertex {
        boolean updated;
        Vertex vertex;

        public CachedVertex(Vertex vertex) {
            this.vertex = vertex;
            updated = false;
        }

        @Override
        public boolean equals(Object o) {
            return this.vertex.equals(o);
        }

        @Override
        public int hashCode() {
            return vertex.hashCode();
        }
    }
}
