/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.ode.ql.jcc;

import java.util.ArrayList;
import java.util.Collection;

import org.apache.ode.ql.tree.Builder;
import org.apache.ode.ql.tree.nodes.Conjunction;
import org.apache.ode.ql.tree.nodes.Disjunction;
import org.apache.ode.ql.tree.nodes.Equality;
import org.apache.ode.ql.tree.nodes.GE;
import org.apache.ode.ql.tree.nodes.Greater;
import org.apache.ode.ql.tree.nodes.Identifier;
import org.apache.ode.ql.tree.nodes.In;
import org.apache.ode.ql.tree.nodes.LE;
import org.apache.ode.ql.tree.nodes.Less;
import org.apache.ode.ql.tree.nodes.Like;
import org.apache.ode.ql.tree.nodes.Limit;
import org.apache.ode.ql.tree.nodes.LogicNode;
import org.apache.ode.ql.tree.nodes.OrderBy;
import org.apache.ode.ql.tree.nodes.OrderByElement;
import org.apache.ode.ql.tree.nodes.OrderByType;
import org.apache.ode.ql.tree.nodes.Query;
import org.apache.ode.ql.tree.nodes.Value;

public class TreeBuilder extends Builder<String> {

    public org.apache.ode.ql.tree.nodes.Node build(String query) {
      try {
        org.apache.ode.ql.jcc.ASTStart start = new Parser(query).start();
        return build(start);
      }catch(ParseException ex) {
        //TODO create common exception which will indicate parsing exception
        throw new RuntimeException(ex.getMessage(), ex);
      }
    }
    private org.apache.ode.ql.tree.nodes.Node build(Node node) {
        if(node instanceof ASTAnd) {
            return createConjunction(node);
        }
        if(node instanceof ASTOr) {
            return createDisjunction(node);
        }
        if(node instanceof ASTLess) {
            return createLess(node);
        }
        if(node instanceof ASTGreater) {
            return createGreater(node);
        }
        if(node instanceof ASTLE) {
            return createLE(node);
        }
        if(node instanceof ASTIn) {
            return createIn(node);
        }
        if(node instanceof ASTGE) {
            return createGE(node);
        }
        if(node instanceof ASTEqual) {
            return createEquality(node);
        }
        if(node instanceof ASTLike) {
            return createLike(node);
        }
        if(node instanceof ASTStart) {
            return createSelection((ASTStart)node);
        }
        throw new IllegalArgumentException("Unsupported node type "+node.getClass());
    }

    private Query createSelection(ASTStart node) {
        Collection<Object> childs = new ArrayList<Object>(node.jjtGetNumChildren());
        OrderBy orderBy = null;
        Limit limit = null;
        for(int index = 0;index < node.jjtGetNumChildren();index++) {
          Node childNode = node.jjtGetChild(index);
            if(childNode instanceof ASTOrderBy) {
              orderBy = createOrderBy(childNode);
            } else if(childNode instanceof ASTLimit) {
              limit = createLimit(childNode);
            } else {
              Object child = build(childNode);
              childs.add(child);
            }
        }
        return new Query(childs, orderBy, limit);
    }
    private OrderBy createOrderBy(Node node) {
        Collection<OrderByElement> orders = new ArrayList<OrderByElement>(node.jjtGetNumChildren());
        for(int i = 0;i < node.jjtGetNumChildren();i++) {
          orders.add(createOrderByElement((ASTOrderByField)node.jjtGetChild(i)));
        }
        return new OrderBy(orders);
    }
    private OrderByElement createOrderByElement(ASTOrderByField node) {
        int childsNum = node.jjtGetNumChildren();
        OrderByType type;
        if(childsNum==1) {
            type = OrderByType.ASC;
        } if(childsNum==2) {
            ASTOrderType astType = (ASTOrderType)extractChildNode(node, 1);
            type = OrderByType.valueOf(astType.getValue().toUpperCase());
        } else {
            //TODO
            throw new IllegalArgumentException();
        }
        Identifier id = createIdentifier(node, 0);
        return new OrderByElement(id, type);
    }
    private Conjunction createConjunction(Node node) {
        Collection<LogicNode> childs = extractLogicNodes(node);
        return new Conjunction(childs);
    }

    private Disjunction createDisjunction(Node node) {
        Collection<LogicNode> childs = extractLogicNodes(node);
        return new Disjunction(childs);
    }

    private LE createLE(Node node) {
        checkChildsNumber(node, 2);
        return new LE(createIdentifier(node, 0), createValue(node, 1));
    }

    private In createIn(Node node) {
        checkChildsNumber(node, 2);
        Node inValuesNode = extractChildNode(node, 1, ASTInValues.class);
        Collection<Value> values = new ArrayList<Value>(inValuesNode.jjtGetNumChildren());
        for(int index = 0;index < inValuesNode.jjtGetNumChildren();index++) {
            values.add(createValue(inValuesNode, index));
        }
        return new In(createIdentifier(node, 0), values);
    }

    private GE createGE(Node node) {
        checkChildsNumber(node, 2);
        return new GE(createIdentifier(node, 0), createValue(node, 1));
    }
    private Less createLess(Node node) {
        checkChildsNumber(node, 2);
        return new Less(createIdentifier(node, 0), createValue(node, 1));
    }
    private Greater createGreater(Node node) {
        checkChildsNumber(node, 2);
        return new Greater(createIdentifier(node, 0), createValue(node, 1));
    }
    private Equality createEquality(Node node) {
        checkChildsNumber(node, 2);
        return new Equality(createIdentifier(node, 0), createValue(node, 1));
    }
    private Like createLike(Node node) {
        checkChildsNumber(node, 2);
        return new Like(createIdentifier(node, 0), createValue(node, 1));
    }

    private Value createValue(Node parentNode, int index) {
        return new Value<String>(extractValue(parentNode, index).value);
    }
    private Limit createLimit(Node node) {
        return new Limit(((ASTLimit)node).getNumber());
    }
    private Identifier createIdentifier(Node parentNode, int index) {
        Node node = extractChildNode(parentNode, index);
        if(node instanceof ASTField) {
            return new org.apache.ode.ql.tree.nodes.Field(((ASTField)node).name);
        }
        if(node instanceof ASTProperty) {
            return new org.apache.ode.ql.tree.nodes.Property(((ASTProperty)node).getName());
        }
        //TODO
        throw new IllegalArgumentException("");
    }
    private ASTValue extractValue(Node parentNode, int index) {
        return (ASTValue)extractChildNode(parentNode, index, ASTValue.class);
    }

    @SuppressWarnings("unchecked")
    private Node extractChildNode(Node parentNode, int index, Class expected) {
        Node node = extractChildNode(parentNode, index);
        if(!(expected.isAssignableFrom(node.getClass()))) {
            //TODO
            throw new IllegalArgumentException("");
        }
        return node;
    }
    private Node extractChildNode(Node parentNode, int index) {
        if(parentNode.jjtGetNumChildren()<=index) {
            //TODO
            throw new IllegalArgumentException("");
        }
        return parentNode.jjtGetChild(index);
    }

    private Collection<LogicNode> extractLogicNodes(Node parentNode) {
        Collection<LogicNode> childs = new ArrayList<LogicNode>(parentNode.jjtGetNumChildren());
        for(int index = 0;index < parentNode.jjtGetNumChildren();index++) {
            childs.add((LogicNode)build(parentNode.jjtGetChild(index)));
        }
        return childs;
    }

    /*
    private static boolean checkInheritance(Object obj, Class clazz) {
        return clazz.isAssignableFrom(clazz.getClass());
    }
    */
    private static void checkChildsNumber(Node node, int expected) {
        int actual = node.jjtGetNumChildren();
        if(actual!=expected) {
            throw new IllegalArgumentException("Expected childs cound("+actual+") differes from expected "+expected);
        }
    }
}
