/*
 * Copyright 2010 Red Hat, Inc. and/or its affiliates.
 *
 * 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 org.drools.eclipse.editors.completion;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.StringTokenizer;

import org.drools.compiler.lang.dsl.DSLMapping;
import org.drools.compiler.lang.dsl.DSLMappingEntry;
import org.drools.compiler.lang.dsl.DSLMappingEntry.Section;

public class DSLTree {

    public static final String separator = "=";
    public static final String tab = "  ";
    
    private Node current = null;
    private Node last = null;
    private Node rootCond = null;
    private Node rootConseq = null;
    private boolean empty = true;
    private ArrayList<String> suggestions = new ArrayList<String>();
    private HashMap<String, String> objToNL = new HashMap<String, String>();
    
    public DSLTree() {
        this.rootCond = new Node("root");
        this.rootConseq = new Node("root");
    }
    
    /**
     * the method will take the dsl file and build a DSLTree using
     * the Node class.
     * @param dslFile
     */
    public void buildTree(String dslFile) {
        buildTree(openDSLFile(dslFile));
    }

    /**
     * the method uses the DSLAdapter to get the contents of the
     * DSL mapping file.
     * @param dslcontents
     */
    public void buildTree(Reader dslcontents) {
        buildTree(createBufferedReader(dslcontents));
    }
    
    private void buildTree(BufferedReader breader) {
        this.rootCond.clearChildren();
        this.rootConseq.clearChildren();
        parseFile(breader);
        try {
            breader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        this.empty = false;
    }
    
    /**
     * method will create a BufferedReader to read the file.
     * @param filename
     * @return
     */
    protected BufferedReader openDSLFile(String filename) {
        try {
            FileReader reader = new FileReader(filename);
            BufferedReader breader = new BufferedReader(reader);
            return breader;
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * Create a buffered reader for the reader created by the DSLAdapater
     * @param reader
     * @return
     */
    protected BufferedReader createBufferedReader(Reader reader) {
        return new BufferedReader(reader);
    }
    
    /**
     * if the DSL mapping hasn't been loaded, the method will return
     * true. If the DSL mapping has been loaded, the method returns
     * false.
     * @return
     */
    public boolean isEmpty() {
        return this.empty;
    }
    
    /**
     * method will use the BufferedReader to read the contents of the file.
     * It calls other methods to parse the line and build the tree.
     * @param reader
     */
    protected void parseFile(BufferedReader reader) {
        String line = null;
        try {
            while ( (line = reader.readLine()) != null) {
                Section section = getSection(line);
                String nl = stripHeadingAndCode(line);
                String objname = this.getObjMetadata(nl);
                nl = this.stripObjMetadata(nl);
                addEntry(section, nl, objname);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    public void buildTree(DSLMapping mapping) {
        for (DSLMappingEntry entry : mapping.getEntries()) {
            Section section = entry.getSection();
            String nl = entry.getMappingKey();
            String objname = entry.getMetaData().getMetaData();
            addEntry(section, nl, objname);
        }
    }
    
    private void addEntry(Section section, String nl, String objname) {
        if (!nl.startsWith("-")) {
            if (objname != null && !"".equals(objname)) {
                this.addObjToNLMap(objname, nl);
            }
            String[] tokenz = nl.split("\\s");
            if (section == DSLMappingEntry.CONDITION || section == DSLMappingEntry.ANY) {
                addTokens(tokenz, rootCond);
            }
            if (section == DSLMappingEntry.CONSEQUENCE || section == DSLMappingEntry.ANY) {
                addTokens(tokenz, rootConseq);
            }
        } else {
            String res = this.objToNL.get(objname);
            StringTokenizer tokenz = new StringTokenizer(nl);
            addTokens(res,tokenz);
        }
    }
    
    public void addObjToNLMap(String objname, String nl) {
        if (!objname.startsWith("-")) {
            this.objToNL.put(objname, nl);
        }
    }
    
    protected Section getSection(String text) {
        if (text.startsWith(DSLMappingEntry.CONDITION.getSymbol())) {
            return DSLMappingEntry.CONDITION;
        } else if (text.startsWith(DSLMappingEntry.CONSEQUENCE.getSymbol())) {
            return DSLMappingEntry.CONSEQUENCE;
        } else if (text.startsWith(DSLMappingEntry.ANY.getSymbol())) {
            return DSLMappingEntry.ANY;
        } else if (text.startsWith(DSLMappingEntry.KEYWORD.getSymbol())) {
            return DSLMappingEntry.KEYWORD;
        }
        return null;
    }
    
    /**
     * method will strip out the when, then, * at the beginning of each 
     * line and the mapped drl expression
     * @param text
     * @return
     */
    protected String stripHeadingAndCode(String text) {
        if (text.startsWith(DSLMappingEntry.CONDITION.getSymbol())) {
            return text.substring(DSLMappingEntry.CONDITION.getSymbol().length() + 2,text.indexOf("="));
        } else if (text.startsWith(DSLMappingEntry.CONSEQUENCE.getSymbol())) {
            return text.substring(DSLMappingEntry.CONSEQUENCE.getSymbol().length() + 2,text.indexOf("="));
        } else if (text.startsWith(DSLMappingEntry.ANY.getSymbol())) {
            return text.substring(DSLMappingEntry.ANY.getSymbol().length() + 2,text.indexOf("="));
        } else if (text.startsWith("#")) {
            return "";
        } else {
            return text;
        }
    }
    
    /**
     * Method will return just the object metadata
     * @param text
     * @return
     */
    protected String getObjMetadata(String text) {
        if (text.startsWith("[")) {
            return text.substring(1,text.lastIndexOf("]"));
        } else {
            return "";
        }
    }

    /**
     * method will strip the metadata from the text string
     * @param text
     * @return
     */
    protected String stripObjMetadata(String text) {
        if (text.startsWith("[")) {
            return text.substring(text.lastIndexOf("]") + 1);
        } else {
            return text;
        }
    }
    
    /**
     * The method is different than addTokens(StringTokenizer). this method
     * expects additional metadata. It expects to get an object name or "*"
     * meaning all. If the metadata is a wildcard all, it will add the
     * tokens to all the top level nodes that are immediate child of root.
     * @param metadata
     * @param tokens
     */
    public void addTokens(String metadata, StringTokenizer tokens) {
        Node mnode = this.rootCond.addToken(metadata);
        Node thenode = mnode;
        while (tokens.hasMoreTokens()) {
            Node newnode = thenode.addToken(tokens.nextToken());
            thenode = newnode;
        }
    }
    
    /**
     * method adds the token to root
     * @param tokens
     */
    public void addTokens(String[] tokens, Node rootNode) {
        Node thenode = rootNode;
        for (int i = 0; i < tokens.length; i++) {
            Node newnode = thenode.addToken(tokens[i]);
            thenode = newnode;
        }
    }
    
    /**
     * the method will tokenize the text and try to find
     * the node that matches and return the children. the method
     * will traverse down the network as far as it can and return
     * the children at that level.
     * @param text
     * @return
     */
    public Node[] getConditionChildren(String text) {
        Node thenode = this.rootCond;
        if (text.length() > 0) {
            StringTokenizer tokenz = new StringTokenizer(text);
            this.last = this.current;
            while (tokenz.hasMoreTokens()) {
                String strtk = tokenz.nextToken();
                Node ch = thenode.getChild(strtk);
                // if a child is found, we set thenode to the child Node
                if (ch != null) {
                    thenode = ch;
                } else {
                    break;
                }
            }
            if (thenode != this.rootCond) {
                this.current = thenode;
            }
        }
        Collection<Node> children = thenode.getChildren();
        Node[] nchild = new Node[children.size()];
        return children.toArray(nchild);
    }

    /**
     * the method will tokenize the text and try to find
     * the node that matches and return the children. the method
     * will traverse down the network as far as it can and return
     * the children at that level.
     * @param text
     * @return
     */
    public Node[] getConsequenceChildren(String text) {
        Node thenode = this.rootConseq;
        if (text.length() >= 0) {
            StringTokenizer tokenz = new StringTokenizer(text);
            this.last = this.current;
            while (tokenz.hasMoreTokens()) {
                String strtk = tokenz.nextToken();
                Node ch = thenode.getChild(strtk);
                // if a child is found, we set thenode to the child Node
                if (ch != null) {
                    thenode = ch;
                } else {
                    break;
                }
            }
            if (thenode != this.rootConseq) {
                this.current = thenode;
            }
        }
        Collection<Node> children = thenode.getChildren();
        Node[] nchild = new Node[children.size()];
        return children.toArray(nchild);
    }

    /**
     * the method expects the caller to pass the object
     * @param obj
     * @param text
     * @return
     */
    public Node[] getChildren(String obj, String text) {
        Node thenode = this.rootCond.getChild(obj);
        if (thenode == null) {
            for (Node child: this.rootCond.getChildren()) {
                String tokenText = child.getToken();
                if (tokenText != null) {
                    int index = tokenText.indexOf("{");
                    if (index != -1) {
                        String substring = tokenText.substring(0, index);
                        if (obj != null && obj.startsWith(substring)) {
                            thenode = child;
                        }
                    }
                }
            }
        }
        if (thenode != null && text.length() > 0) {
            StringTokenizer tokenz = new StringTokenizer(text);
            this.last = this.current;
            while (tokenz.hasMoreTokens()) {
                String strtk = tokenz.nextToken();
                Node ch = thenode.getChild(strtk);
                // if a child is found, we set thenode to the child Node
                if (ch != null) {
                    thenode = ch;
                } else {
                    break;
                }
            }
            if (thenode != this.rootCond) {
                this.current = thenode;
            }
        }
        if (thenode == null) {
            return null;
            // thenode = this.rootCond;
        }
        Collection<Node> children = thenode.getChildren();
        Node[] nchild = new Node[children.size()];
        return children.toArray(nchild);
    }
    
    /**
     * for convienance, the method will return a list of strings
     * that are children of the last node found. If the editor
     * wants to generate the children strings, call the method
     * with true
     * @param text
     * @return
     */
    public ArrayList<String> getConditionChildrenList(String text, boolean addChildren) {
        Node[] c = getConditionChildren(text);
        this.suggestions.clear();
        for (int idx=0; idx < c.length; idx++) {
            this.suggestions.add(c[idx].getToken());
            if (addChildren) {
                this.addChildToList(c[idx], c[idx].getToken(), this.suggestions);
            }
        }
        return this.suggestions;
    }
    
    /**
     * for convienance, the method will return a list of strings
     * that are children of the last node found. If the editor
     * wants to generate the children strings, call the method
     * with true
     * @param text
     * @return
     */
    public ArrayList<String> getConsequenceChildrenList(String text, boolean addChildren) {
        Node[] c = getConsequenceChildren(text);
        this.suggestions.clear();
        for (int idx=0; idx < c.length; idx++) {
            if (addChildren) {
                this.addChildToList(c[idx], c[idx].getToken(), this.suggestions);
            } else {
                this.suggestions.add(c[idx].getToken());
            }
        }
        return this.suggestions;
    }
    
    /**
     * 
     * @param obj
     * @param text
     * @param addChildren
     * @return
     */
    public ArrayList<String> getChildrenList(String obj, String text, boolean addChildren, boolean firstLine) {
        Node[] c = getChildren(obj,text);
        this.suggestions.clear();
        if (c != null) {
            for (int idx=0; idx < c.length; idx++) {
                if (addChildren) {
                    this.addChildToList(c[idx], c[idx].getToken(), this.suggestions);
                } else {
                    this.suggestions.add(c[idx].getToken());
                }
            }
        }
        if (text.trim().length() == 0 || this.suggestions.isEmpty()) {
            // in the event the list is empty, we also add
            // the top level nodes
            for (Node t : rootCond.getChildren()) {
                if ((!firstLine || t.getToken() != null) && !this.suggestions.contains(t.getToken())) {
                    if (addChildren) {
                        this.addChildToList(t, t.getToken(), this.suggestions);
                    } else {
                        this.suggestions.add(t.getToken());
                    }
                }
            }
        }
        return this.suggestions;
    }

    /**
     * method will prepend the parent text to the child and generate
     * the possible combinations in text format.
     * @param n
     * @param prefix
     * @param list
     */
    public void addChildToList(Node n, String prefix, ArrayList<String> list) {
        if (n.getChildren().size() > 0) {
            for (Node child : n.getChildren()) {
                if (prefix != null && "-".equals(child.getToken())) {
                    if (!list.contains(prefix)) {
                        list.add(prefix);
                    }
                    return;
                }
                String text = (prefix == null ? "" : prefix + " ") + child.getToken();
                // list.add(text);
                addChildToList(child,text,list);
            }
        } else {
            if (!list.contains(prefix)) {
                list.add(prefix);
            }
        }
    }
    
    public Node getCurrent() {
        return current;
    }

    public void setCurrent(Node current) {
        this.current = current;
    }

    public Node getLast() {
        return last;
    }

    public void setLast(Node last) {
        this.last = last;
    }

    /**
     * The method will print the DSLTree to System.out in text format.
     */
    public void printTree() {
        System.out.println("ROOT");
        for (Node n : rootCond.getChildren()) {
            printNode(n);
        }
    }
    
    /**
     * method will print the node and then iterate over the children
     * @param n
     */
    protected void printNode(Node n) {
        printTabs(n.getDepth());
        System.out.println("- \"" + n.getToken() + "\"");
        for (Node c : n.getChildren()) {
            printNode(c);
        }
    }
    
    /**
     * Method will print n number of tabs
     * @param count
     */
    protected void printTabs(int count) {
        for (int idx=0; idx < count; idx++) {
            System.out.print(tab);
        }
    }
}
