/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2008, Red Hat Middleware LLC, and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This 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 software 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 software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.as.cmp.jdbc.metadata;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jboss.as.cmp.CmpMessages;
import static org.jboss.as.cmp.CmpMessages.MESSAGES;
import org.jboss.as.cmp.jdbc.metadata.parser.ParsedCmpField;
import org.jboss.as.cmp.jdbc.metadata.parser.ParsedRelationshipRole;
import org.jboss.metadata.ejb.spec.RelationRoleMetaData;

/**
 * Immutable class which represents one ejb-relationship-role element found in
 * the ejb-jar.xml file's ejb-relation elements.
 *
 * @author <a href="mailto:dain@daingroup.com">Dain Sundstrom</a>
 * @author <a href="mailto:alex@jboss.org">Alexey Loubyansky</a>
 * @version $Revision: 81030 $
 */
public final class JDBCRelationshipRoleMetaData {
    /**
     * Relation to which this role belongs.
     */
    private final JDBCRelationMetaData relationMetaData;

    /**
     * Role name
     */
    private final String relationshipRoleName;

    /**
     * Should this role have a foreign key constraint?
     */
    private final boolean foreignKeyConstraint;

    /**
     * Should the cascade-delete be batched.
     */
    private final boolean batchCascadeDelete;

    private boolean genIndex;

    /**
     * Type of the cmr field (i.e., collection or set)
     */
    private JDBCReadAheadMetaData readAhead;

    /**
     * Is the multiplicity one? If not, multiplicity is many.
     */
    private final boolean multiplicityOne;

    /**
     * Should this entity be deleted when related entity is deleted.
     */
    private final boolean cascadeDelete;

    /**
     * The entity that has this role.
     */
    private final JDBCEntityMetaData entity;

    /**
     * Name of the entity's cmr field for this role.
     */
    private final String cmrFieldName;

    /**
     * true if this side is navigable
     */
    private final boolean navigable;

    /**
     * Type of the cmr field (i.e., collection or set)
     */
    private final String cmrFieldType;

    /**
     * The other role in this relationship.
     */
    private JDBCRelationshipRoleMetaData relatedRole;

    /**
     * The key fields used by this role by field name.
     */
    private Map<String, JDBCCMPFieldMetaData> keyFields = new HashMap<String, JDBCCMPFieldMetaData>();

    public JDBCRelationshipRoleMetaData(final JDBCRelationMetaData relationMetaData, final JDBCApplicationMetaData application, final RelationRoleMetaData role) {
        this.relationMetaData = relationMetaData;

        relationshipRoleName = role.getEjbRelationshipRoleName();
        multiplicityOne = role.isMultiplicityOne();
        cascadeDelete = role.isCascadedDelete();
        batchCascadeDelete = false;
        foreignKeyConstraint = false;
        readAhead = null;

        String fieldName = role.getCmrField() != null ? role.getCmrField().getCmrFieldName() : null;
        if (fieldName == null) {
            cmrFieldName = generateNonNavigableCMRName(role);
            navigable = false;
        } else {
            cmrFieldName = fieldName;
            navigable = true;
        }
        cmrFieldType = role.getCmrField() != null ? role.getCmrField().getCmrFieldType() : null;
        // get the entity for this role
        entity = application.getBeanByEjbName(role.getRoleSource().getEjbName());
        if (entity == null) {
            throw MESSAGES.entityNotFoundForRelation(role.getRoleSource().getEjbName(), role.getRelation().getEjbRelationName());
        }
    }

    public JDBCRelationshipRoleMetaData(JDBCRelationMetaData relationMetaData, JDBCApplicationMetaData application, JDBCRelationshipRoleMetaData defaultValues) {
        this.relationMetaData = relationMetaData;
        this.entity = application.getBeanByEjbName(defaultValues.getEntity().getName());

        relationshipRoleName = defaultValues.getRelationshipRoleName();
        multiplicityOne = defaultValues.isMultiplicityOne();
        cascadeDelete = defaultValues.isCascadeDelete();

        cmrFieldName = defaultValues.getCMRFieldName();
        navigable = defaultValues.isNavigable();
        cmrFieldType = defaultValues.getCMRFieldType();

        foreignKeyConstraint = defaultValues.hasForeignKeyConstraint();
        readAhead = entity.getReadAhead();

        batchCascadeDelete = defaultValues.isBatchCascadeDelete();
        if (batchCascadeDelete) {
            if (!cascadeDelete)
                throw MESSAGES.cascadeDeleteInJbossXmlButNoEjbJar(relationMetaData.getRelationName(), relationshipRoleName);

            if (relationMetaData.isTableMappingStyle()) {
                throw MESSAGES.batchCascadeDeleteOnlyForFkMapping(relationMetaData.getRelationName());
            }
        }
    }

    public JDBCRelationshipRoleMetaData(JDBCRelationMetaData relationMetaData, JDBCApplicationMetaData application, ParsedRelationshipRole parsedRelationshipRole, JDBCRelationshipRoleMetaData defaultValues) {
        this.relationMetaData = relationMetaData;
        this.entity = application.getBeanByEjbName(defaultValues.getEntity().getName());

        relationshipRoleName = defaultValues.getRelationshipRoleName();
        multiplicityOne = defaultValues.isMultiplicityOne();
        cascadeDelete = defaultValues.isCascadeDelete();

        cmrFieldName = defaultValues.getCMRFieldName();
        navigable = defaultValues.isNavigable();
        cmrFieldType = defaultValues.getCMRFieldType();

        // foreign key constraint?  If not provided, keep default.
        if (parsedRelationshipRole.getForeignKeyConstraint() != null) {
            foreignKeyConstraint = parsedRelationshipRole.getForeignKeyConstraint();
        } else {
            foreignKeyConstraint = defaultValues.hasForeignKeyConstraint();
        }

        // read-ahead
        if (parsedRelationshipRole.getReadAhead() != null) {
            readAhead = new JDBCReadAheadMetaData(parsedRelationshipRole.getReadAhead(), entity.getReadAhead());
        } else {
            readAhead = entity.getReadAhead();
        }
        batchCascadeDelete = parsedRelationshipRole.getBatchCascadeDelete() != null && parsedRelationshipRole.getBatchCascadeDelete();
        if (batchCascadeDelete) {
            if (!cascadeDelete)
                throw CmpMessages.MESSAGES.invalidCascadeDeleteForRelation(relationMetaData.getRelationName(), relationshipRoleName);

            if (relationMetaData.isTableMappingStyle()) {
                throw MESSAGES.batchCascadeDeleteOnlyForFkMapping(relationMetaData.getRelationName());
            }
        }
    }

    public void init(final JDBCRelationshipRoleMetaData relatedRole) {
        this.relatedRole = relatedRole;
        keyFields.putAll(loadKeyFields());
    }

    public void init(final JDBCRelationshipRoleMetaData relatedRole, final ParsedRelationshipRole parsedRole) {
        this.relatedRole = relatedRole;
        keyFields.putAll(loadKeyFields(parsedRole));
    }


    /**
     * Gets the relation to which this role belongs.
     */
    public JDBCRelationMetaData getRelationMetaData() {
        return relationMetaData;
    }

    /**
     * Gets the name of this role.
     */
    public String getRelationshipRoleName() {
        return relationshipRoleName;
    }

    /**
     * Should this role use a foreign key constraint.
     *
     * @return true if the store manager will execute an ALTER TABLE ADD
     *         CONSTRAINT statement to add a foreign key constraint.
     */
    public boolean hasForeignKeyConstraint() {
        return foreignKeyConstraint;
    }

    public boolean isBatchCascadeDelete() {
        return batchCascadeDelete;
    }

    /**
     * Gets the read ahead meta data
     */
    public JDBCReadAheadMetaData getReadAhead() {
        return readAhead;
    }

    public JDBCEntityMetaData getEntity() {
        return entity;
    }

    /**
     * Gets the key fields of this role.
     *
     * @return an unmodifiable collection of JDBCCMPFieldMetaData objects
     */
    public Collection<JDBCCMPFieldMetaData> getKeyFields() {
        return Collections.unmodifiableCollection(keyFields.values());
    }

    public boolean isIndexed() {
        return genIndex;
    }

    private static String generateNonNavigableCMRName(final RelationRoleMetaData role) {
        RelationRoleMetaData relatedRole = role.getRelatedRole();
        return relatedRole.getRoleSource().getEjbName() + "_" + relatedRole.getCmrField().getCmrFieldName();
    }

    /**
     * Checks if the multiplicity is one.
     */
    public boolean isMultiplicityOne() {
        return multiplicityOne;
    }

    /**
     * Checks if the multiplicity is many.
     */
    public boolean isMultiplicityMany() {
        return !multiplicityOne;
    }

    /**
     * Should this entity be deleted when related entity is deleted.
     */
    public boolean isCascadeDelete() {
        return cascadeDelete;
    }

    /**
     * Gets the name of the entity's cmr field for this role.
     */
    public String getCMRFieldName() {
        return cmrFieldName;
    }

    public boolean isNavigable() {
        return navigable;
    }

    /**
     * Gets the type of the cmr field (i.e., collection or set)
     */
    public String getCMRFieldType() {
        return cmrFieldType;
    }

    /**
     * Gets the related role's jdbc meta data.
     */
    public JDBCRelationshipRoleMetaData getRelatedRole() {
        return relationMetaData.getOtherRelationshipRole(this);
    }

    /**
     * Loads the key fields for this role based on the primary keys of the
     * this entity.
     */
    private Map<String, JDBCCMPFieldMetaData> loadKeyFields() {
        // with foreign key mapping, foreign key fields are no added if
        // - it is the many side of one-to-many relationship
        // - it is the one side of one-to-one relationship and related side is not navigable
        if (relationMetaData.isForeignKeyMappingStyle()) {
            if (isMultiplicityMany())
                return Collections.emptyMap();
            else if (getRelatedRole().isMultiplicityOne() && !getRelatedRole().isNavigable())
                return Collections.emptyMap();
        }

        // get all of the pk fields
        List<JDBCCMPFieldMetaData> pkFields = new ArrayList<JDBCCMPFieldMetaData>();
        for (JDBCCMPFieldMetaData cmpField : entity.getCMPFields()) {
            if (cmpField.isPrimaryKeyMember()) {
                pkFields.add(cmpField);
            }
        }

        // generate a new key field for each pk field
        Map<String, JDBCCMPFieldMetaData> fields = new HashMap<String, JDBCCMPFieldMetaData>(pkFields.size());
        for (JDBCCMPFieldMetaData cmpField : pkFields) {
            String columnName;
            if (relationMetaData.isTableMappingStyle()) {
                if (entity.equals(relatedRole.getEntity()))
                    columnName = getCMRFieldName();
                else
                    columnName = entity.getName();
            } else {
                columnName = relatedRole.getCMRFieldName();
            }

            if (pkFields.size() > 1) {
                columnName += "_" + cmpField.getFieldName();
            }

            genIndex = (genIndex) || cmpField.isIndexed();

            cmpField = new JDBCCMPFieldMetaData(
                    entity,
                    cmpField,
                    columnName,
                    false,
                    relationMetaData.isTableMappingStyle(),
                    relationMetaData.isReadOnly(),
                    relationMetaData.getReadTimeOut(),
                    relationMetaData.isTableMappingStyle());
            fields.put(cmpField.getFieldName(), cmpField);
        }
        return Collections.unmodifiableMap(fields);
    }

    private Map<String, JDBCCMPFieldMetaData> loadKeyFields(final ParsedRelationshipRole parsedRole) {

        // no field overrides, we're done
        final List<ParsedCmpField> keyFields = parsedRole.getKeyFields();
        if (keyFields == null) {
            return loadKeyFields();
        }

        if(keyFields.isEmpty()) {
            return Collections.EMPTY_MAP;
        }

        if (relationMetaData.isForeignKeyMappingStyle() && isMultiplicityMany()) {
            throw CmpMessages.MESSAGES.relationshipRoleCanNotHaveKeyFields(relationshipRoleName);
        }

        // load the default field values
        Map<String, JDBCCMPFieldMetaData> defaultFields = getPrimaryKeyFields();

        // load overrides
        Map<String, JDBCCMPFieldMetaData> fields = new HashMap<String, JDBCCMPFieldMetaData>(defaultFields.size());
        for (ParsedCmpField keyField : keyFields) {
            String fieldName = keyField.getFieldName();

            JDBCCMPFieldMetaData cmpField = defaultFields.remove(fieldName);
            if (cmpField == null) {
                throw MESSAGES.cmpFieldNotFoundForRole(relationshipRoleName, entity.getName(), fieldName);
            }
            genIndex = keyField.getGenIndex() != null && keyField.getGenIndex();


            cmpField = new JDBCCMPFieldMetaData(
                    entity,
                    keyField,
                    cmpField,
                    false,
                    relationMetaData.isTableMappingStyle(),
                    relationMetaData.isReadOnly(),
                    relationMetaData.getReadTimeOut(),
                    relationMetaData.isTableMappingStyle());
            fields.put(cmpField.getFieldName(), cmpField);
        }

        // all fields must be overridden
        if (!defaultFields.isEmpty()) {
            throw MESSAGES.mappingsNotProvidedForAllFieldsForRole(defaultFields.keySet(), relationshipRoleName);
        }
        return Collections.unmodifiableMap(fields);
    }

    /**
     * Returns the primary key fields of the entity mapped by field name.
     */
    private Map<String, JDBCCMPFieldMetaData> getPrimaryKeyFields() {
        Map<String, JDBCCMPFieldMetaData> pkFields = new HashMap<String, JDBCCMPFieldMetaData>();
        for (JDBCCMPFieldMetaData cmpField : entity.getCMPFields()) {
            if (cmpField.isPrimaryKeyMember())
                pkFields.put(cmpField.getFieldName(), cmpField);
        }
        return pkFields;
    }
}
