package org.makumba.providers.query.mql;

import java.util.ArrayList;
import java.util.Date;
import java.util.Hashtable;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.hibernate.hql.antlr.HqlTokenTypes;
import org.makumba.DataDefinition;
import org.makumba.FieldDefinition;
import org.makumba.OQLParseError;
import org.makumba.commons.MakumbaJspAnalyzer;
import org.makumba.commons.NameResolver;
import org.makumba.commons.RegExpUtils;
import org.makumba.commons.RuntimeWrappedException;
import org.makumba.commons.NameResolver.TextList;
import org.makumba.providers.DataDefinitionProvider;
import org.makumba.providers.QueryAnalysis;
import org.makumba.providers.QueryProvider;
import org.makumba.providers.datadefinition.mdd.MakumbaDumpASTVisitor;

import antlr.RecognitionException;
import antlr.collections.AST;

/**
 * The hearth of the MQL query analysis and compilation. The query is first pre-processed and then the initial parsing takes place to produce the initial mql tree.
 * After this, the tree is transformed by the MqlSqlWalker for analysis and finally transformed again for sql query generation.
 * 
 * @author Cristian Bogdan
 * @version $Id: MqlQueryAnalysis.java,v 1.1 Apr 29, 2009 8:54:20 PM manu Exp $
 */
public class MqlQueryAnalysis implements QueryAnalysis {

    public static final String MAKUMBA_PARAM = "param";

    private String query;

    protected DataDefinition insertIn;
    
    private List<String> parameterOrder = new ArrayList<String>();

    private DataDefinition proj;

    private boolean noFrom = false;

    private LinkedHashMap<String, DataDefinition> labels;

    private Hashtable<String, String> aliases;

    private DataDefinition paramInfo;

    private TextList text;
    
    // all the labels that were generated by this analyser, kept to avoid collisions
    protected Vector<String> generatedLabels = new Vector<String>();
    

    static String formatQueryAndInsert(String query, String insertIn) {
        if(insertIn!=null && insertIn.length()>0)
            return  "###"+insertIn+"###"+query;
        else return query;
    }
    
    public MqlQueryAnalysis(String queryAndInsert, boolean optimizeJoins, boolean autoLeftJoin) {
        Date d = new Date();

        if(queryAndInsert.startsWith("###")){
            insertIn= DataDefinitionProvider.getInstance().getDataDefinition(queryAndInsert.substring(3, queryAndInsert.indexOf('#', 3)));
            query= queryAndInsert.substring(queryAndInsert.indexOf('#', 3)+3);
        } else {
            query=queryAndInsert;
        }

        query = preProcess(query);

        if (query.toLowerCase().indexOf("from") == -1) {
            noFrom = true;
            query += " FROM org.makumba.db.makumba.Catalog c";
        }

        HqlParser parser=null;
        try{
            parser = HqlParser.getInstance(query);
            parser.statement();
        }catch(Throwable t){
            doThrow(t, parser!=null?parser.getAST():null);
        }
        doThrow(parser.error, parser.getAST());
        
        // we need to do the transformation first so the second-pass parser will accept the query
        MqlQueryAnalysisProvider.transformOQLParameters(parser.getAST(), parameterOrder);
        MqlQueryAnalysisProvider.transformOQL(parser.getAST());

        
        
        MqlSqlWalker mqlAnalyzer = new MqlSqlWalker(query, insertIn, optimizeJoins, autoLeftJoin, false);
        try {
            mqlAnalyzer.statement(parser.getAST());
        } catch (Throwable e) {
            doThrow(e, parser.getAST());
        }
        doThrow(mqlAnalyzer.error, parser.getAST());
        
        labels = mqlAnalyzer.rootContext.labels;
        aliases = mqlAnalyzer.rootContext.aliases;
        paramInfo = DataDefinitionProvider.getInstance().getVirtualDataDefinition("Parameters for " + query);
        proj = DataDefinitionProvider.getInstance().getVirtualDataDefinition("Projections for " + query);
        mqlAnalyzer.setProjectionTypes(proj);        
        
        for (int i = 0; i < parameterOrder.size(); i++) {
            // first we check whether we have a parameter type for exactly this position
            FieldDefinition fd = mqlAnalyzer.paramInfoByPosition.getFieldDefinition("param" + i);
            
            // failing that, we see if we have a parameter with the same name on another position
            // but we consider that only if the parameter is not multi-type
            if (fd == null && ! mqlAnalyzer.multiTypeParams.contains(parameterOrder.get(i)))
                fd = mqlAnalyzer.paramInfoByName.getFieldDefinition(parameterOrder.get(i));

            if (fd != null)
                paramInfo.addField(DataDefinitionProvider.getInstance().makeFieldWithName("param" + i, fd));
            // FIXME: throw an exception if a type of a parameter cannot be determined
            // most probably the clients of QueryAnalysis do throw one
        }
        // if(mqlAnalyzer.hasSubqueries)
        // System.out.println(mqlDebug);
        
        MqlSqlGenerator mg = new MqlSqlGenerator();
        try {
            mg.statement(mqlAnalyzer.getAST());
        } catch (Throwable e) {
            doThrow(e, mqlAnalyzer.getAST());
        }
        doThrow(mg.error, mqlAnalyzer.getAST());

        text = mg.text;
        
        long diff = new java.util.Date().getTime() - d.getTime();
        java.util.logging.Logger.getLogger("org.makumba.db.query.compilation").fine("MQL to SQL: " + diff + " ms: " + query);

    }

    protected void doThrow(Throwable t, AST debugTree) {
        if (t == null)
            return;
        if (t instanceof RuntimeException) {
            t.printStackTrace();
            throw (RuntimeException) t;
        }
        String errorLocation = "";
        String errorLocationNumber="";
        if (t instanceof RecognitionException) {
            RecognitionException re = (RecognitionException) t;
            if (re.getColumn() > 0) {
                errorLocationNumber= " column "+re.getColumn()+" of ";
                StringBuffer sb = new StringBuffer();
                sb.append("\r\n");

                for (int i = 0; i < re.getColumn(); i++) {
                    sb.append(' ');
                }
                sb.append('^');
                errorLocation = sb.toString();
            }
        }
        throw new OQLParseError("\r\nin "+errorLocationNumber+" query:\r\n" + query + errorLocation+errorLocation+errorLocation, t);
    }


    
    public String writeInSQLQuery(NameResolver nr) {
        // TODO: we can cache these SQL results by the key of the NameResolver
        // still we should first check if this is needed, maybe the generated SQL (or processing of it)
        // is cached already somewhere else
        String sql = text.toString(nr);
        if (noFrom)
            return sql.substring(0, sql.toLowerCase().indexOf("from")).trim();
        return sql;
    }

    public String getQuery() {
        return query;
    }

    public DataDefinition getLabelType(String labelName) {
        String s1 = (String) aliases.get(labelName);
        if (s1 != null)
            labelName = s1;
        return (DataDefinition) labels.get(labelName);
    }

    public Map<String, DataDefinition> getLabelTypes() {
        return labels;
    }

    public DataDefinition getParameterTypes() {
        return paramInfo;
    }

    public DataDefinition getProjectionType() {
        return proj;
    }
    
    // FIXME this is ugly, there is for sure a way to get the FROM from the tree
    private String getFrom() {

        String[] splitAtFrom = query.split("\\s[f|F][r|R][o|O][m|M]\\s");
        String[] splitAtWhere = splitAtFrom[1].split("\\s[w|W][h|H][e|E][r|R][e|E]\\s");

        return splitAtWhere[0];
    }

    // FIXME needed for relation miner, but should maybe be moved, it's dependent on the analysis per se
    public String getFieldOfExpr(String expr) {
        if (expr.indexOf(".") > -1)
            return expr.substring(expr.lastIndexOf(".") + 1);
        else
            return expr;
    }

    // FIXME needed for relation miner, but should be refactored or made more robust
    public DataDefinition getTypeOfExprField(String expr) {

        if (expr.indexOf(".") == -1) {
            return getLabelType(expr);
        } else {
            DataDefinition result;
            int lastDot = expr.lastIndexOf(".");
            String beforeLastDot = expr.substring(0, lastDot);
            if (beforeLastDot.indexOf(".") == -1) {
                result = getLabelType(beforeLastDot);
            } else {
                // compute dummy query for determining pointed type

                // FIXME will this work if the FROM contains subqueries?
                String dummyQuery = "SELECT " + beforeLastDot + " AS projection FROM " + getFrom();
                result = QueryProvider.getQueryAnalzyer(MakumbaJspAnalyzer.QL_OQL).getQueryAnalysis(dummyQuery).getProjectionType().getFieldDefinition(
                    "projection").getPointedType();
            }
            return result;

        }
    }
    
    public int parameterAt(int index) {
        String s = parameterOrder.get(index);
        if (!s.startsWith(MAKUMBA_PARAM))
            throw new IllegalArgumentException("parameter at " + index + " is not a $n parameter");
        s = s.substring(MAKUMBA_PARAM.length());
        return Integer.parseInt(s) + 1;
    }

    public int parameterNumber() {
        return parameterOrder.size();
    }

    public static String showAst(AST ast) {
        return ast.toStringTree();
    }

    static boolean isNil(AST a) {
        return a.getType() == HqlTokenTypes.IDENT && a.getText().toUpperCase().equals("NIL");
    }

    static void setNullTest(AST a) {
        if (a.getType() == HqlTokenTypes.EQ) {
            a.setType(HqlTokenTypes.IS_NULL);
            a.setText("is null");
        } else {
            a.setType(HqlTokenTypes.IS_NOT_NULL);
            a.setText("is not null");
        }
    }

    public static final String regExpInSET = "in" + RegExpUtils.minOneWhitespace + "set" + RegExpUtils.whitespace
            + "\\(";

    public static final Pattern patternInSet = Pattern.compile(regExpInSET);

    public static String preProcess(String query) {
        // replace -> (subset separators) with __
        query = query.replaceAll("->", "__");

        // replace IN SET with IN.
        Matcher m = patternInSet.matcher(query.toLowerCase()); // find all occurrences of lower-case "in set"
        while (m.find()) {
            int start = m.start();
            int beginSet = m.group().indexOf("set"); // find location of "set" keyword
            // System.out.println(query);
            // composing query by concatenating the part before "set", 3 space and the part after "set"
            query = query.substring(0, start + beginSet) + "   " + query.substring(start + beginSet + 3);
            // System.out.println(query);
            // System.out.println();
        }
        query = query.replaceAll("IN SET", "IN    ");
        return query;
    }

    private int labelCounter = 0;
    
    public String createLabel() {
        String l = "_x_gen_" + labelCounter++;
        generatedLabels.add(l);
        return l;
    }
}
