/*
 * JBoss, Home of Professional Open Source.
 * See the COPYRIGHT.txt file distributed with this work for information
 * regarding copyright ownership.  Some portions may be licensed
 * to Red Hat, Inc. under one or more contributor license agreements.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301 USA.
 */
package org.teiid.translator.jpa;

import static org.teiid.language.SQLConstants.Reserved.*;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.concurrent.atomic.AtomicInteger;

import org.teiid.language.ColumnReference;
import org.teiid.language.Function;
import org.teiid.language.Join;
import org.teiid.language.Join.JoinType;
import org.teiid.language.NamedTable;
import org.teiid.language.SQLConstants.Tokens;
import org.teiid.language.Select;
import org.teiid.language.TableReference;
import org.teiid.language.visitor.HierarchyVisitor;
import org.teiid.language.visitor.SQLStringVisitor;
import org.teiid.metadata.AbstractMetadataRecord;
import org.teiid.metadata.ForeignKey;
import org.teiid.metadata.RuntimeMetadata;
import org.teiid.metadata.Table;
import org.teiid.translator.TranslatorException;
/**
 * This visitor converts the Teiid command into JPQL string 
 */
public class JPQLSelectVisitor extends HierarchyVisitor {
	protected JPA2ExecutionFactory executionFactory;
	protected static final String UNDEFINED = "<undefined>"; //$NON-NLS-1$
    private LinkedList<JoinTable> joins = new LinkedList<JoinTable>();
    protected ArrayList<TranslatorException> exceptions = new ArrayList<TranslatorException>();
    protected LinkedHashMap<String, NamedTable> implicitGroups = new LinkedHashMap<String, NamedTable>();
    protected AtomicInteger aliasCounter = new AtomicInteger(0);
	protected RuntimeMetadata metadata;
    
    public JPQLSelectVisitor(JPA2ExecutionFactory executionFactory, RuntimeMetadata metadata) {
    	super(false);
    	this.executionFactory = executionFactory;
    	this.metadata = metadata;
    }
    	
    public static String getJPQLString(Select obj, JPA2ExecutionFactory executionFactory, RuntimeMetadata metadata)  throws TranslatorException {
    	JPQLSelectVisitor visitor = new JPQLSelectVisitor(executionFactory, metadata);
        
    	visitor.visitNode(obj);
    	
    	if (!visitor.exceptions.isEmpty()) {
    		throw visitor.exceptions.get(0);
    	}  
    	
        return visitor.convertToQuery(obj);
    }
    
    private String convertToQuery(Select obj) {
    	JPQLSelectStringVisitor visitor = new JPQLSelectStringVisitor(this);
    	visitor.visitNode(obj);
    	return visitor.toString();
    }
    
    @Override
    public void visit(Select obj) {
    	visitNodes(obj.getDerivedColumns());
    	visitNodes(obj.getFrom());
        visitNode(obj.getWhere());
        visitNode(obj.getGroupBy());
        visitNode(obj.getHaving());
        visitNode(obj.getOrderBy());
    }    
    
    @Override
    public void visit(ColumnReference obj) {
		AbstractMetadataRecord record = obj.getMetadataObject();
		if (record != null) {
			String name = record.getProperty(JPAMetadataProcessor.KEY_ASSOSIATED_WITH_FOREIGN_TABLE, false); 
			if (name != null) {
				try {
					Table t = this.metadata.getTable(name);
					if (this.implicitGroups.get(name) == null) {
						this.implicitGroups.put(name, new NamedTable(t.getName(), "J_"+this.aliasCounter.getAndIncrement(), t)); //$NON-NLS-1$
					}
				} catch (TranslatorException e) {
					exceptions.add(e);
				}
			}
		}
    }
    
    private boolean alreadyInJoin(String tableName) {
    	for(JoinTable joinTable:this.joins) {
			if ((joinTable.left != null && joinTable.left.getMetadataObject().getFullName().equals(tableName))
					|| (joinTable.right != null && joinTable.right.getMetadataObject().getFullName().equals(tableName))) {
    			return true;
    		}
    	}
    	return false;
    }

    @Override
    public void visit(NamedTable obj) {
    	if (implicitGroups.isEmpty()) {
    		this.joins.add(new JoinTable(obj, null, JoinType.INNER_JOIN));
    	}
    	else {
	    	for (NamedTable table:this.implicitGroups.values()) {
    			this.joins.add(new JoinTable(obj, table, JoinType.INNER_JOIN));
	    	}
    	}
    }
    
    @Override
    public void visit(Join obj) {
    	try {
			handleJoin(obj);
	    	for (NamedTable table:this.implicitGroups.values()) {
	    		NamedTable parent = findParent(table);
	    		if (parent != null) {
	    			if (!alreadyInJoin(table.getMetadataObject().getFullName())) {
	    				this.joins.add(new JoinTable(parent, table, JoinType.INNER_JOIN));
	    			}
	    		}
	    		else {
	    			exceptions.add(new TranslatorException(JPAPlugin.Util.gs(JPAPlugin.Event.TEIID14004, table.getName())));
	    		}
	    	}			
		} catch (TranslatorException e) {
			exceptions.add(e);
		}
    }

    private NamedTable findParent(NamedTable child) {
    	for (JoinTable jt:this.joins) {
    		if (jt.getParent() != null) {
    			if (isParentOf(jt.getParent(), child)) {
    				return jt.getParent();
    			}
    		}
    		
    		if (jt.getChild() != null) {
    			if (isParentOf(jt.getChild(), child)) {
    				return jt.getChild();
    			}    			
    		}
    	}
    	return null;
    }
    
	private JoinTable handleJoin(Join obj)  throws TranslatorException {
		TableReference left = obj.getLeftItem();
    	TableReference right = obj.getRightItem();
    	
    	if ((left instanceof NamedTable) && (right instanceof NamedTable)) {
    		JoinTable join = handleJoin(obj.getJoinType(), left, right, true);
    		this.joins.add(join);
    		return join;
    	}    	
    	
    	JoinTable leftJoin = null;
    	if (left instanceof Join) {
    		leftJoin = handleJoin((Join)left);
    		if (right instanceof NamedTable) {
    			JoinTable join =  handleJoin(obj.getJoinType(), leftJoin, (NamedTable)right);
        		this.joins.add(join);
        		return join;    			
    		}
    	}
    	
    	JoinTable rightJoin = null;
    	if (right instanceof Join) {
    		rightJoin = handleJoin((Join)right);
    		if (left instanceof NamedTable) {
    			JoinTable join = handleJoin(obj.getJoinType(), (NamedTable)left, rightJoin);
        		this.joins.add(join);
        		return join;      			
    		}
    	}
    	throw new TranslatorException(JPAPlugin.Util.gs(JPAPlugin.Event.TEIID14005));
	}
	
	private JoinTable handleJoin(Join.JoinType joinType, JoinTable left, NamedTable right) throws TranslatorException {
		// first we need to find correct parent for the right 
		JoinTable withParent = handleJoin(joinType, left.getParent(), right, false);
		JoinTable withChild = handleJoin(joinType, left.getChild(), right, false);
		
		NamedTable parent = (withParent.getParent() != null)?withParent.getParent():withChild.getParent();
		if (parent != null) {
			return handleJoin(joinType, parent, right, true);
		}
		throw new TranslatorException(JPAPlugin.Util.gs(JPAPlugin.Event.TEIID14006)); 
	}
	
	private JoinTable handleJoin(Join.JoinType joinType, NamedTable left, JoinTable right) throws TranslatorException {
		// first we need to find correct parent for the left 
		JoinTable withParent = handleJoin(joinType, left, right.getParent(), false);
		JoinTable withChild = handleJoin(joinType, left, right.getChild(), false);
		
		NamedTable parent = (withParent.getParent() != null)?withParent.getParent():withChild.getParent();
		if (parent != null) {
			return handleJoin(joinType, left, parent, true);
		}
		throw new TranslatorException(JPAPlugin.Util.gs(JPAPlugin.Event.TEIID14006));
	}	

	private JoinTable handleJoin(Join.JoinType joinType, TableReference left, TableReference right, boolean fixCorrelatedNames) {
		// both sides are named tables
		NamedTable leftTable = (NamedTable)left;
		NamedTable rightTable = (NamedTable)right;
		JoinTable joinTable = new JoinTable(leftTable, rightTable, joinType);
		
		if (fixCorrelatedNames) {
			// fix left table's correleated name
			NamedTable table = this.implicitGroups.get(leftTable.getMetadataObject().getFullName());
			if (table != null) {
				table.setCorrelationName(leftTable.getCorrelationName());
			}
			
			// fix right table's correleated name
			table = this.implicitGroups.get(rightTable.getMetadataObject().getFullName());
			if (table != null) {
				table.setCorrelationName(rightTable.getCorrelationName());
			}   
		}
		return joinTable;
	}    
    
	private boolean isParentOf(NamedTable parent, NamedTable child) {
		for (ForeignKey fk:parent.getMetadataObject().getForeignKeys()){
			if (fk.getPrimaryKey().getParent().equals(child.getMetadataObject())) {
				return true;
			}
		}
		return false;
	}
    
    static class JoinTable {
    	NamedTable parent;
    	NamedTable child;
    	NamedTable left;
    	NamedTable right;
    	String childAttributeName;
    	String parentAttributeName;
    	JoinType joinType;
    	
    	JoinTable(NamedTable customer, NamedTable address, JoinType type) {
    		this.left = customer;
    		this.right = address;
    		this.joinType = type;
    		if (address == null) {
    			this.parent = customer;
    			this.parentAttributeName = customer.getName();
    		}
    		else {
	    		for (ForeignKey fk:customer.getMetadataObject().getForeignKeys()){
	    			if (fk.getReferenceKey().getParent().equals(address.getMetadataObject())) {
	    				this.parent = customer;
	    				this.child = address;
	    				this.childAttributeName = fk.getSourceName();
	    				this.parentAttributeName = customer.getName();
	    			}
	    		}
	    		if (this.parent == null) {
	        		for (ForeignKey fk:address.getMetadataObject().getForeignKeys()){
	        			if (fk.getReferenceKey().getParent().equals(customer.getMetadataObject())) {
	            			this.parent = address;
	            			this.child = customer;
	        				this.childAttributeName = fk.getSourceName();
	        				this.parentAttributeName = customer.getName();
	        			}
	        		}    			
	    		}
    		}
    	}
    	
    	NamedTable getParent() {
    		return this.parent;
    	}
    	
    	NamedTable getChild() {
    		return this.child;
    	}
    	
    	NamedTable getLeft() {
    		return this.left;
    	}
    	
    	NamedTable getRight() {
    		return this.right;
    	}    	
    	
    	String childAttributeName() {
    		return this.childAttributeName;
    	}
    	
    	String parentAttributeName() {
    		return this.parentAttributeName;
    	}
    	
    	public boolean isLeftParent() {
    		return this.left == this.parent;
    	}
    }
    
    
    static class JPQLSelectStringVisitor extends SQLStringVisitor {
    	private JPQLSelectVisitor visitor;
    	
    	public JPQLSelectStringVisitor(JPQLSelectVisitor visitor) {
    		this.visitor = visitor;
    	}
    	
    	@Override
    	public void visit(Select obj) {
    		
    		buffer.append(SELECT).append(Tokens.SPACE);
    		
            if (obj.isDistinct()) {
                buffer.append(DISTINCT).append(Tokens.SPACE);
            }
                   
            append(obj.getDerivedColumns());
            
            if (obj.getFrom() != null && !obj.getFrom().isEmpty()) {
            	buffer.append(Tokens.SPACE).append(FROM).append(Tokens.SPACE);
            	append(obj.getFrom());
            }
            
            if (obj.getWhere() != null) {
                buffer.append(Tokens.SPACE)
                      .append(WHERE)
                      .append(Tokens.SPACE);
                append(obj.getWhere());
            }
            if (obj.getGroupBy() != null) {
                buffer.append(Tokens.SPACE);
                append(obj.getGroupBy());
            }
            if (obj.getHaving() != null) {
                buffer.append(Tokens.SPACE)
                      .append(HAVING)
                      .append(Tokens.SPACE);
                append(obj.getHaving());
            }
            if (obj.getOrderBy() != null) {
                buffer.append(Tokens.SPACE);
                append(obj.getOrderBy());
            }
        }    	
    	
    	@Override
        public void visit(ColumnReference column) {
    		AbstractMetadataRecord record = column.getMetadataObject();
    		if (record != null) {
    			String name = record.getProperty(JPAMetadataProcessor.KEY_ASSOSIATED_WITH_FOREIGN_TABLE, false); 
    			if (name == null) {
					buffer.append(column.getTable().getCorrelationName()).append(Tokens.DOT).append(column.getMetadataObject().getName());
    			}
    			else {
    				buffer.append(this.visitor.implicitGroups.get(name).getCorrelationName()).append(Tokens.DOT).append(column.getMetadataObject().getName());
    			}
    		}   
    		else {
    			buffer.append(column.getName());
    		}
    	}
    	
    	@Override
        public void visit(Join obj) {
    		addFromClause();
        }
    	
    	@Override
        public void visit(Function func) {
        	if (visitor.executionFactory.getFunctionModifiers().containsKey(func.getName())) {
                visitor.executionFactory.getFunctionModifiers().get(func.getName()).translate(func);
        	}
        	super.visit(func);
        }    
    	
    	@Override
        public void visit(NamedTable obj) {
    		addFromClause();
    	}
    	
    	private void addFromClause() {
    		boolean first = true;
    		for (JoinTable joinTable:this.visitor.joins) {
    			if (!joinTable.isLeftParent() && joinTable.joinType == JoinType.LEFT_OUTER_JOIN) {
    				joinTable.joinType = JoinType.RIGHT_OUTER_JOIN;
    			}
    			if (first) {
    				buffer.append(joinTable.getParent().getName());
    		        buffer.append(Tokens.SPACE);
    		        buffer.append(AS).append(Tokens.SPACE);
    		    	buffer.append(joinTable.getParent().getCorrelationName());    		    	
    		    	first = false;
    			}
    			if (joinTable.getChild() != null) {
    				buffer.append(Tokens.SPACE);
    		        switch(joinTable.joinType) {
    		        case CROSS_JOIN:
    		            buffer.append(CROSS);
    		            break;
    		        case FULL_OUTER_JOIN:
    		            buffer.append(FULL)
    		                  .append(Tokens.SPACE)
    		                  .append(OUTER);
    		            break;
    		        case INNER_JOIN:
    		            buffer.append(INNER);
    		            break;
    		        case LEFT_OUTER_JOIN:
    		            buffer.append(LEFT)
    		                  .append(Tokens.SPACE)
    		                  .append(OUTER);
    		            break;
    		        case RIGHT_OUTER_JOIN:
    		            buffer.append(RIGHT)
    		                  .append(Tokens.SPACE)
    		                  .append(OUTER);
    		            break;
    		        default: buffer.append(UNDEFINED);
    		        } 
    		        buffer.append(Tokens.SPACE).append(JOIN).append(Tokens.SPACE);
    				buffer.append(joinTable.getParent().getCorrelationName()).append(Tokens.DOT).append(joinTable.childAttributeName());
    		        buffer.append(Tokens.SPACE);
    		        buffer.append(AS).append(Tokens.SPACE);
    		    	buffer.append(joinTable.getChild().getCorrelationName());
    			}
    		}
    	}    	
    }
}
