/*****************************************************************
 *   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.cayenne.access.jdbc;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import org.apache.cayenne.CayenneRuntimeException;
import org.apache.cayenne.dba.TypesMapping;
import org.apache.cayenne.ejbql.EJBQLBaseVisitor;
import org.apache.cayenne.ejbql.EJBQLExpression;
import org.apache.cayenne.map.DbAttribute;
import org.apache.cayenne.map.DbEntity;
import org.apache.cayenne.map.DbJoin;
import org.apache.cayenne.map.DbRelationship;
import org.apache.cayenne.map.ObjAttribute;
import org.apache.cayenne.map.ObjRelationship;
import org.apache.cayenne.reflect.ArcProperty;
import org.apache.cayenne.reflect.AttributeProperty;
import org.apache.cayenne.reflect.ClassDescriptor;
import org.apache.cayenne.reflect.PropertyVisitor;
import org.apache.cayenne.reflect.ToManyProperty;
import org.apache.cayenne.reflect.ToOneProperty;

/**
 * @since 3.0
 * @author Andrus Adamchik
 */
class EJBQLIdentifierColumnsTranslator extends EJBQLBaseVisitor {

    private EJBQLTranslationContext context;
    private Set<String> columns;

    EJBQLIdentifierColumnsTranslator(EJBQLTranslationContext context) {
        this.context = context;
    }

    @Override
    public boolean visitIdentifier(EJBQLExpression expression) {

        Map<String, String> xfields = null;
        if (context.isAppendingResultColumns()) {
            xfields = context.nextEntityResult().getDbFields(context.getEntityResolver());
        }

        // assign whatever we have to a final ivar so that it can be accessed within
        // the inner class
        final Map<String, String> fields = xfields;
        final String idVar = expression.getText();

        // append all table columns ... the trick is to follow the algorithm for
        // describing the fields in the expression compiler, so that we could assign
        // columns labels from FieldResults in the order we encounter them here...
        // TODO: andrus 2008/02/17 - this is a bit of a hack, think of a better solution

        ClassDescriptor descriptor = context.getEntityDescriptor(idVar);

        PropertyVisitor visitor = new PropertyVisitor() {

            public boolean visitAttribute(AttributeProperty property) {
                ObjAttribute oa = property.getAttribute();
                Iterator<?> dbPathIterator = oa.getDbPathIterator();

                EJBQLJoinAppender joinAppender = null;
                String marker = null;
                EJBQLTableId lhsId = new EJBQLTableId(idVar);

                while (dbPathIterator.hasNext()) {
                    Object pathPart = dbPathIterator.next();

                    if (pathPart == null) {
                        throw new CayenneRuntimeException(
                                "ObjAttribute has no component: " + oa.getName());
                    }
                    else if (pathPart instanceof DbRelationship) {

                        if (marker == null) {
                            marker = EJBQLJoinAppender.makeJoinTailMarker(idVar);
                            joinAppender = context
                                    .getTranslatorFactory()
                                    .getJoinAppender(context);
                        }

                        DbRelationship dr = (DbRelationship) pathPart;

                        EJBQLTableId rhsId = new EJBQLTableId(lhsId, dr.getName());
                        joinAppender.appendOuterJoin(marker, lhsId, rhsId);
                        lhsId = rhsId;
                    }
                    else if (pathPart instanceof DbAttribute) {
                        appendColumn(idVar, oa, (DbAttribute) pathPart, fields, oa
                                .getType());
                    }
                }
                return true;
            }

            public boolean visitToMany(ToManyProperty property) {
                visitRelationship(property);
                return true;
            }

            public boolean visitToOne(ToOneProperty property) {
                visitRelationship(property);
                return true;
            }

            private void visitRelationship(ArcProperty property) {
                ObjRelationship rel = property.getRelationship();
                DbRelationship dbRel = rel.getDbRelationships().get(0);

                for (DbJoin join : dbRel.getJoins()) {
                    DbAttribute src = join.getSource();
                    appendColumn(idVar, null, src, fields);
                }
            }
        };

        // EJBQL queries are polymorphic by definition - there is no distinction between
        // inheritance/no-inheritance fetch
        descriptor.visitAllProperties(visitor);

        // append id columns ... (some may have been appended already via relationships)
        DbEntity table = descriptor.getEntity().getDbEntity();
        for (DbAttribute pk : table.getPrimaryKeys()) {
            appendColumn(idVar, null, pk, fields);
        }

        // append inheritance discriminator columns...
        Iterator<DbAttribute> discriminatorColumns = descriptor.getDiscriminatorColumns();
        while (discriminatorColumns.hasNext()) {
            appendColumn(idVar, null, discriminatorColumns.next(), fields);
        }

        return false;
    }

    private void appendColumn(
            String identifier,
            ObjAttribute property,
            DbAttribute column,
            Map<String, String> fields) {
        appendColumn(identifier, property, column, fields, null);
    }

    private void appendColumn(
            String identifier,
            ObjAttribute property,
            DbAttribute column,
            Map<String, String> fields,
            String javaType) {

        DbEntity table = (DbEntity) column.getEntity();
        String alias = context.getTableAlias(identifier, table.getFullyQualifiedName());
        String columnName = alias + "." + column.getName();

        Set<String> columns = getColumns();

        if (columns.add(columnName)) {

            context.append(columns.size() > 1 ? ", " : " ");

            if (context.isAppendingResultColumns()) {
                context.append("#result('");
            }

            context.append(columnName);

            if (context.isAppendingResultColumns()) {
                if (javaType == null) {
                    javaType = TypesMapping.getJavaBySqlType(column.getType());
                }

                String columnLabel = fields.get(property != null ? property
                        .getDbAttributePath() : column.getName());

                // TODO: andrus 6/27/2007 - the last parameter is an unofficial "jdbcType"
                // pending CAY-813 implementation, switch to #column directive
                context
                        .append("' '")
                        .append(javaType)
                        .append("' '")
                        .append(columnLabel)
                        .append("' '")
                        .append(columnLabel)
                        .append("' " + column.getType())
                        .append(")");
            }
        }
    }

    private Set<String> getColumns() {

        if (columns == null) {
            columns = new HashSet<String>();
        }

        return columns;
    }
}
