package org.studiorailgun.knowledge;

import java.io.File;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.studiorailgun.FileUtils;
import org.studiorailgun.Globals;
import org.studiorailgun.knowledge.anchor.AnchorNodes;

/**
 * A knowledge web
 */
public class KnowledgeWeb {


    /**
     * A unique tag to identify this web
     */
    String tag;

    /**
     * The nodes;
     */
    Map<Integer,Node> nodes;

    /**
     * The relations
     */
    Map<Integer,Relation> relations;

    /**
     * Map of relation type -> list of relations of that type
     */
    transient Map<String,List<Integer>> typeRelationLookup;

    /**
     * Map of node -> relations where that node is the parent
     */
    transient Map<Node,List<Relation>> parentRelations;

    /**
     * Map of node -> relations where that node is the child
     */
    transient Map<Node,List<Relation>> childRelations;

    /**
     * The anchor nodes
     */
    AnchorNodes anchors;

    /**
     * Child webs
     */
    List<String> children;

    /**
     * Constructor
     */
    public KnowledgeWeb(){
        this.nodes = new HashMap<Integer,Node>();
        this.relations = new HashMap<Integer,Relation>();
        this.typeRelationLookup = new HashMap<String,List<Integer>>();
        this.parentRelations = new HashMap<Node,List<Relation>>();
        this.childRelations = new HashMap<Node,List<Relation>>();
    }

    /**
     * Loads the knowledge web starting with the root provided
     * @param startPoint The starting point
     * @return The full web
     */
    public static KnowledgeWeb load(String startPoint){
        Map<String,Integer> crossRelationLookupMap = new HashMap<String,Integer>();
        List<String> webTagList = new LinkedList<String>();
        KnowledgeWeb rVal = KnowledgeWeb.recursivelyLoad(crossRelationLookupMap, webTagList, startPoint);
        if(rVal.getTag() == null){
            throw new UnsupportedOperationException("Web without tag defined! " + startPoint);
        }
        Globals.web = rVal;
        //fix the IDs referenced by the links
        for(Relation relation : rVal.relations.values()){
            String lookupId = "";
            if(relation.parentWeb != null){
                lookupId = "node-" + relation.parentWeb + relation.parent;
            } else {
                lookupId = "node-" + relation.webTag + relation.parent;
            }
            int nodeId = crossRelationLookupMap.get(lookupId);
            relation.parent = nodeId;
            
            if(relation.childWeb != null){
                lookupId = "node-" + relation.childWeb + relation.child;
            } else {
                lookupId = "node-" + relation.webTag + relation.child;
            }
            nodeId = crossRelationLookupMap.get(lookupId);
            if(nodeId == relation.parent){
                throw new Error("Child node id same as parent! " + relation.parent + " " + nodeId + " " + relation.child);
            }
            relation.child = nodeId;
        }
        //populate lookup acceleration structures
        for(Relation relation : rVal.relations.values()){
            //parent->relation map
            Node parent = relation.getParent();
            if(rVal.parentRelations.containsKey(parent)){
                List<Relation> relations = rVal.parentRelations.get(parent);
                relations.add(relation);
            } else {
                List<Relation> relations = new LinkedList<Relation>();
                relations.add(relation);
                rVal.parentRelations.put(parent,relations);
            }

            //child->relation map
            Node child = relation.getChild();
            if(rVal.childRelations.containsKey(child)){
                List<Relation> relations = rVal.childRelations.get(child);
                relations.add(relation);
            } else {
                List<Relation> relations = new LinkedList<Relation>();
                relations.add(relation);
                rVal.childRelations.put(child,relations);
            }
        }
        //error check that all anchors are defined
        Globals.web.getAnchors().errorCheck();
        return rVal;
    }

    /**
     * Recursively loads knowledge webs based on the root
     * @param root The path to the current root knowledge web
     * @return Merged knowledge web
     */
    private static KnowledgeWeb recursivelyLoad(Map<String,Integer> crossRelationLookupMap, List<String> foundTags, String startPoint){
        KnowledgeWeb rVal = FileUtils.loadObjectFromFile(new File(startPoint), KnowledgeWeb.class);;
        if(foundTags.contains(rVal.getTag())){
            throw new UnsupportedOperationException("Trying to load two webs with the same tag! " + rVal.getTag());
        }
        //add nodes to crossrelational lookup map
        HashMap<Integer,Node> newNodeMap = new HashMap<Integer,Node>();
        for(Node node : rVal.nodes.values()){
            String lookupId = "node-" + rVal.getTag() + node.getId();
            crossRelationLookupMap.put(lookupId,Node.idIncrementer);
            node.id = Node.idIncrementer;
            newNodeMap.put(node.id,node);
            Node.idIncrementer++;
        }
        rVal.nodes = newNodeMap;
        //read in relations and assign them new IDs
        HashMap<Integer,Relation> newRelations = new HashMap<Integer,Relation>();
        for(Relation relation : rVal.getRelations()){
            relation.webTag = rVal.getTag();
            relation.id = Relation.idIncrementer;
            newRelations.put(relation.id,relation);
            Relation.idIncrementer++;
        }
        rVal.relations = newRelations;
        //make sure anchors object exists and any values are valid
        if(rVal.anchors == null){
            rVal.anchors = new AnchorNodes();
        } else {
            rVal.anchors.updateReferences(crossRelationLookupMap, rVal.getTag());
        }
        //parse children
        if(rVal.getChildren() != null && rVal.getChildren().size() > 0){
            for(String child : rVal.getChildren()){
                KnowledgeWeb childWeb = KnowledgeWeb.recursivelyLoad(crossRelationLookupMap, foundTags, child);
                KnowledgeWeb.registerChildItems(crossRelationLookupMap, rVal, childWeb);
            }
        }
        return rVal;
    }

    /**
     * Adds the children to the cross relational lookup map
     * @param parent The parent web
     * @param child The child web
     */
    private static void registerChildItems(Map<String,Integer> crossRelationLookupMap, KnowledgeWeb parent, KnowledgeWeb child){
        //read in nodes and assign them new IDs
        for(Node node : child.getNodes()){
            parent.nodes.put(node.getId(),node);
        }

        //read in relations and assign them new IDs
        for(Relation relation : child.getRelations()){
            parent.relations.put(relation.getId(),relation);
        }
        
        //hoist anchors from children
        AnchorNodes.copyToParent(parent.anchors, child.anchors);
    }

    /**
     * Initializes the linked datastructures between items
     */
    public void initLinks(){
        //set id incrementers
        for(Entry<Integer,Node> entry : this.nodes.entrySet()){
            if(entry.getValue().getId() >= Node.idIncrementer){
                Node.idIncrementer = entry.getValue().getId() + 1;
            }
            if(entry.getValue().getId() != entry.getKey()){
                throw new Error("Node id doesn't match map value! " + entry.getValue().getId() + " " + entry.getValue().getName() + " " + entry.getKey());
            }
        }
        for(Entry<Integer,Relation> entry : this.relations.entrySet()){
            Relation relation = entry.getValue();
            if(relation.getId() >= Relation.idIncrementer){
                Relation.idIncrementer = relation.getId() + 1;
            }
            if(this.typeRelationLookup.containsKey(relation.getName())){
                this.typeRelationLookup.get(relation.getName()).add(relation.getId());
            } else {
                LinkedList<Integer> list = new LinkedList<Integer>();
                list.add(relation.getId());
                this.typeRelationLookup.put(relation.getName(),list);
            }
            if(relation.getId() != entry.getKey()){
                throw new Error("Relation id doesn't match map value! " + relation.getId() + " " + relation.getName() + " " + entry.getKey());
            }
        }
    }

    /**
     * Gets the collection of all nodes
     * @return The collection
     */
    public Collection<Node> getNodes() {
        return nodes.values();
    }

    /**
     * Gets the collection of all relations
     * @return The collection of all relations
     */
    public Collection<Relation> getRelations() {
        return relations.values();
    }

    /**
     * Gets a node by its id
     * @param id The id
     * @return The node
     */
    public Node getNode(int id){
        return nodes.get(id);
    }

    /**
     * Gets a singleton relationship with a given parent
     * @param relationType The relationship type
     * @param parent The parent
     * @return The relation if it exists, null otherwise
     */
    public Relation getSingletonRelation(String relationType, Node parent){
        List<Integer> relations = this.typeRelationLookup.get(relationType);
        if(relations != null){
            for(int relationId : relations){
                if(relationId == parent.getId()){
                    return this.relations.get(relationId);
                }
            }
        }
        return null;
    }
    
    /**
     * Gets the child webs of this web
     * @return The list of child webs
     */
    public List<String> getChildren(){
        return children;
    }

    public String getTag(){
        return tag;
    }

    /**
     * Gets the relations where the provided node is the parent of the relationship
     * @param node The node
     * @return The list of relations
     */
    public List<Relation> getRelationsOfParentNode(Node node){
        if(this.parentRelations.containsKey(node)){
            return this.parentRelations.get(node);
        } else {
            return new LinkedList<Relation>();
        }
    }

    /**
     * Gets the relations where the provided node is the parent of the relationship
     * @param node The node
     * @return The list of relations
     */
    public List<Relation> getRelationsOfChildNode(Node node){
        if(this.childRelations.containsKey(node)){
            return this.childRelations.get(node);
        } else {
            return new LinkedList<Relation>();
        }
    }

    /**
     * Gets the anchor nodes for the web
     * @return The anchor nodes
     */
    public AnchorNodes getAnchors(){
        return anchors;
    }

}
