/*
 * The contents of this file are subject to the terms 
 * of the Common Development and Distribution License 
 * (the "License").  You may not use this file except 
 * in compliance with the License.
 * 
 * You can obtain a copy of the license at 
 * glassfish/bootstrap/legal/CDDLv1.0.txt or 
 * https://glassfish.dev.java.net/public/CDDLv1.0.html. 
 * See the License for the specific language governing 
 * permissions and limitations under the License.
 * 
 * When distributing Covered Code, include this CDDL 
 * HEADER in each file and include the License file at 
 * glassfish/bootstrap/legal/CDDLv1.0.txt.  If applicable, 
 * add the following below this CDDL HEADER, with the 
 * fields enclosed by brackets "[]" replaced with your 
 * own identifying information: Portions Copyright [yyyy] 
 * [name of copyright owner]
 */
// Copyright (c) 1998, 2006, Oracle. All rights reserved.  
package oracle.toplink.essentials.internal.ejb.cmp3.annotations;

import java.util.Hashtable;
import java.lang.reflect.AnnotatedElement;

import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.TableGenerator;
import javax.persistence.UniqueConstraint;
import javax.persistence.SequenceGenerator;

import oracle.toplink.essentials.sequencing.Sequence;
import oracle.toplink.essentials.sessions.DatasourceLogin;
import oracle.toplink.essentials.sequencing.TableSequence;
import oracle.toplink.essentials.sequencing.NativeSequence;
import oracle.toplink.essentials.internal.helper.DatabaseTable;
import oracle.toplink.essentials.internal.helper.DatabaseField;
import oracle.toplink.essentials.exceptions.ValidationException;
import oracle.toplink.essentials.internal.ejb.cmp3.metadata.MetadataHelper;

/**
 * SequencingProcessor operates in two phases.
 * First preProcess methods are called while processing classes and properties. 
 * Those geather the necessary information and perform some validation.
 * Second step is process method that works on all the gathered information,
 * does some additional validation and finally performs the actual
 * sequencing setup on Login and Descriptors.
 * 
 * Here are the rules enforced by the validation for different Annotations.
 * 
 * GeneratedValue {GenerationType strategy() default AUTO; String generator() default "";}
 *   AUTO can't have generator:
 *      AUTO implies DEFAULT_AUTO_GENERATOR is used.
 *   SEQUENCE and IDENTITY
 *      without generator use DEFAULT_SEQUENCE_GENERATOR generator;
 *      with generator that is not defined use data defined in DEFAULT_SEQUENCE_GENERATOR generator.
 *   TABLE
 *      without generator use DEFAULT_TABLE_GENERATOR generator;
 *      with generator that is not defined use data defined in DEFAULT_TABLE_GENERATOR generator.
 *   
 *   By defining DEFAULT_AUTO_GENERATOR TableGenerator or SequenceGenerator 
 *   the user can define sequence corresponding to AUTO and set as default.
 *   In case DEFAULT_AUTO_GENERATOR generator not defined the 
 *   platform-default sequence will be used (TableSequence with all
 *   default settings, see TableSequence class).
 *   
 *   By defining DEFAULT_SEQUENCE_GENERATOR SequenceGenerator 
 *   the user can define sequence corresponding to SEQUENCE or ENTITY without generator.
 *   In case DEFAULT_SEQUENCE_GENERATOR generator not defined 
 *   NativeSequence with all default settings will be used.
 *   
 *   By defining DEFAULT_TABLE_GENERATOR TableGenerator 
 *   the user can define sequence corresponding to TABLE without generator.
 *   In case DEFAULT_TABLE_GENERATOR generator not defined 
 *   TableSequence with all default settings will be used.
 *   
 * SequenceGenerator {String name(); String sequenceName() default ""; int initialValue() default 0;
 *                      int allocationSize() default 50;}
 *    sequenceName is defaulted to nothing - that results in name being used;
 *                      
 * TableGenerator {String name();String table() default "";String catalog() default "";String schema() default "";
 *                 String pkColumnName() default "";String valueColumnName() default "";String pkColumnValue() default "";
 *                 int initialValue() default 0;int allocationSize() default 50;UniqueConstraint[] uniqueConstraints() default {};}
 *    table is defaulted to nothing - that results in TableSequence.defaultTableName being used;
 *    pkColumnName is defaulted to nothing - that results in default value of TableSequence.nameFieldName being used;
 *    valueColumnName is defaulted to nothing - that results in default value of TableSequence.counterFieldName being used;
 *    valueColumnValue is defaulted to nothing - that results in name being used;
 *
 * Validations catches some of the conflicts:
 *      two objects named the same, but are different;
 *      two tables defined in two TableGenerators named the same but have different fields;
 *      strategy in GeneratedValue contradicts to the type of the generator (e.g. TABLE and SequenceGenerator);
 *      pkColumnValue on a TableGenerator equals sequenceName on a SequenceGenerator;
 *      two sequences with the same name but different preallocation sizes;
 *      two GeneratedValues referencing the same generator with conflicting types (e.g. TABLE and SEQUENCE);
 *      
 * Note that DEFAULT_AUTO_GENERATOR, DEFAULT_SEQUENCE_GENERATOR and DEFAULT_TABLE_GENERATOR
 * are String constants defined in this class, not values (so for instance "Generator DEFAULT_AUTO_GENERATOR
 * is used" means "Generator "SEQ_GEN" is used").
 */
public class AnnotationsSequencingProcessor {
    public static final String DEFAULT_AUTO_GENERATOR = "SEQ_GEN";
    public static final String DEFAULT_TABLE_GENERATOR = "SEQ_GEN_TABLE";
    public static final String DEFAULT_SEQUENCE_GENERATOR = "SEQ_GEN_SEQUENCE";
    
    // SequenceGenerators keyed on generator name
    private Hashtable<String, SequenceGenerator> m_sequenceGenerators;
    // TableGenerators keyed on generator name
    private Hashtable<String, TableGenerator> m_tableGenerators;
    // Ids keyed on descriptor metadata
    private Hashtable<AnnotationsDescriptor, GeneratedValue> m_generatedValues;
    // AnnotatedElements keyed on annotations
    private Hashtable<Object, AnnotatedElement> m_annotatedElements;

    public AnnotationsSequencingProcessor() {
        m_tableGenerators = new Hashtable<String, TableGenerator>();
        m_sequenceGenerators = new Hashtable<String, SequenceGenerator>();
        m_generatedValues = new Hashtable<AnnotationsDescriptor, GeneratedValue>();
        m_annotatedElements = new Hashtable<Object, AnnotatedElement>();
    }
	
    /**
     * INTERNAL:
	 * Checks two Id objects for conflicts. Assumes that the objects are not 
     * equal (so may indicate a conflict if obj1==obj2). If conflict is found 
     * then throws conflict exception.
	 */
    protected void checkForConflict(GeneratedValue obj1, GeneratedValue obj2) {
        if (!obj1.generator().equals(obj2.generator())) {
            return;
        } else if (obj1.generator().equals("")) {
            return;
        }
        
        GenerationType type1 = obj1.strategy();
        GenerationType type2 = obj2.strategy();
        
        if (type1.equals(type2)) {
            return;
        }
        
        if( type1.equals(GenerationType.IDENTITY) && type2.equals(GenerationType.SEQUENCE) ||
            type2.equals(GenerationType.IDENTITY) && type1.equals(GenerationType.SEQUENCE)) {
            return;    
        }
        
        throwConflictException(obj1, obj2);
    }
    
    /**
     * INTERNAL:
	 * Checks two SequenceGenerators objects for conflicts. Assumes that the 
     * objects are not equal (so may indicate a conflict if obj1==obj2). If 
     * conflict is found then throws conflict exception.
	 */
    protected void checkForConflict(SequenceGenerator obj1, SequenceGenerator obj2) {
        // this doesn't even check the initial value??
        boolean nameConflict = obj1.name().equals(obj2.name());
        
        boolean sequenceConflict = 
            obj1.sequenceName().equals(obj2.sequenceName()) && 
            obj1.allocationSize() != obj2.allocationSize();
            
        if (nameConflict || sequenceConflict) {
            throwConflictException(obj1, obj2);
        }
    }
    
    /**
	 * INTERNAL:
     * Checks SequenceGenerator, TableGenerator conflicts.
     * If conflict is found then throws conflict exception.
	 */
    protected void checkForConflict(SequenceGenerator obj1, TableGenerator obj2) {
        checkForConflict(obj2, obj1);
    }
    
    /**
     * INTERNAL:
	 * Checks TableGenerator, SequenceGenerator conflicts.
     * If conflict is found then throws conflict exception.
	 */
    protected void checkForConflict(TableGenerator obj1, SequenceGenerator obj2) {
        boolean nameConflict = obj1.name().equals(obj2.name());
        boolean sequenceConflict = obj1.pkColumnValue().equals(obj2.sequenceName());
        
        if (nameConflict || sequenceConflict) {
            throwConflictException(obj1, obj2);
        }
    }
    
    /**
     * INTERNAL:
	 * Checks two TableGenerators objects for conflicts. Assumes that the 
     * objects are not equal (so may indicate a conflict if obj1==obj2).
     * If conflict is found then throws conflict exception.
	 */
    protected void checkForConflict(TableGenerator obj1, TableGenerator obj2) {
        boolean nameConflict = obj1.name().equals(obj2.name());
        
        if (nameConflict) {
            throwConflictException(obj1, obj2);
        }
        
        boolean tableConflict = obj1.table().equals(obj2.table()) && (
            !obj1.pkColumnName().equals(obj2.pkColumnName()) ||
            !obj1.valueColumnName().equals(obj2.valueColumnName()));
            
        if (tableConflict) {
            throwConflictException(obj1, obj2);
        }
        
        boolean sequenceConflict = obj1.pkColumnValue().equals(obj2.pkColumnValue()) && (
            obj1.initialValue() != obj2.initialValue() ||
            obj1.allocationSize() != obj2.allocationSize());
        
        if (sequenceConflict) {
            throwConflictException(obj1, obj2);
        }
    }
    
    /**
	 * INTERNAL: 
	 */
    public static String getFullName(AnnotatedElement annotatedElement, Object annotation) {
        return annotatedElement.toString() + annotation.toString();
    } 
    
    /**
	 * INTERNAL:
	 */
    protected String getFullName(Object annotation) {
        return getFullName(m_annotatedElements.get(annotation), annotation);
    }
    
    /**
     * INTERNAL:
	 */
	public void preProcessGeneratedValue(GeneratedValue generatedValue, AnnotationsAccessor accessor, DatabaseField field, AnnotationsDescriptor dmd) {        
        AnnotatedElement annotatedElement = accessor.getAnnotatedElement();
        
        // If there is a GeneratedValue then process it, otherwise, do nothing.
        // Should never be true if we are dealing with a composite primary key.
        if (generatedValue != null) {
            m_annotatedElements.put(generatedValue, annotatedElement);
        
            String generatorName = generatedValue.generator();
            GenerationType generatorType = generatedValue.strategy();
            if (generatorType == GenerationType.AUTO && ! generatorName.equals("")) {
                throw ValidationException.idAnnotationCannotSpecifyGenerator(getFullName(annotatedElement, generatedValue), generatorType.toString());
            }
        
            // Set the sequence number field on the descriptor.		
            dmd.setSequenceNumberField(field);
        
            for (GeneratedValue other : m_generatedValues.values()) {
                checkForConflict(other, generatedValue);
            }
        
            m_generatedValues.put(dmd, generatedValue);
        }
    }
	
	/**
     * INTERNAL:
	 */
    public void preProcessSequenceGenerator(SequenceGenerator sequenceGenerator, AnnotatedElement annotatedElement) {
        if (sequenceGenerator != null) {
            m_annotatedElements.put(sequenceGenerator, annotatedElement);
            String generatorName = sequenceGenerator.name();

            // DEFAULT_TABLE_GENERATOR can't be used as SequenceGenerator name
            if (generatorName.equals(DEFAULT_TABLE_GENERATOR)) {
                throw ValidationException.reservedName(getFullName(annotatedElement, sequenceGenerator), DEFAULT_TABLE_GENERATOR, TableGenerator.class.getName());
            }
        
            SequenceGenerator otherSequenceGenerator = m_sequenceGenerators.get(generatorName);
            if (otherSequenceGenerator != null) {
                if (otherSequenceGenerator.equals(sequenceGenerator)) {
                    return; // duplication
                }
            }
        
            for (SequenceGenerator other : m_sequenceGenerators.values()) {
                checkForConflict(other, sequenceGenerator);
            }
        
            for (TableGenerator other : m_tableGenerators.values()) {
                checkForConflict(other, sequenceGenerator);
            }
        
            m_sequenceGenerators.put(generatorName, sequenceGenerator);
        }
	}
    
    /**
     * INTERNAL:
	 */
    public void preProcessTableGenerator(TableGenerator tableGenerator, AnnotatedElement annotatedElement) {
        if (tableGenerator != null) {
            m_annotatedElements.put(tableGenerator, annotatedElement);
            String generatorName = tableGenerator.name();
        
            // DEFAULT_SEQUENCE_GENERATOR can't be used as TableGenerator name
            if (generatorName.equals(DEFAULT_SEQUENCE_GENERATOR)) {
                throw ValidationException.reservedName(getFullName(annotatedElement, tableGenerator), DEFAULT_SEQUENCE_GENERATOR, SequenceGenerator.class.getName());
            }
        
            TableGenerator otherTableGenerator = m_tableGenerators.get(generatorName);
            if (otherTableGenerator != null) {
                if (otherTableGenerator.equals(tableGenerator)) {
                    return; // duplication
                }
            }
        
            for (TableGenerator other : m_tableGenerators.values()) {
                checkForConflict(other, tableGenerator);
            }
        
            for (SequenceGenerator other : m_sequenceGenerators.values()) {
                checkForConflict(other, tableGenerator);
            }
        
            m_tableGenerators.put(generatorName, tableGenerator);
        }
	}
    
	/**
     * INTERNAL:
     * Performs sequencing setup on Login and Descriptors
	 */
	public void process(DatasourceLogin login) {
        if (m_generatedValues.isEmpty()) {
            return; // sequence is not used
        }

        // Generators referenced from Id should have correct type
        for (GeneratedValue generatedValue : m_generatedValues.values()) {
            GenerationType type = generatedValue.strategy();
            String generatorName = generatedValue.generator();
            
            if (type.equals(GenerationType.TABLE)) {
                SequenceGenerator sequenceGenerator = m_sequenceGenerators.get(generatorName);
                if (sequenceGenerator != null) {
                    throwConflictException(generatedValue, sequenceGenerator);
                }
            } else if (type.equals(GenerationType.SEQUENCE) || type.equals(GenerationType.IDENTITY)) {
                TableGenerator tableGenerator = m_tableGenerators.get(generatorName);
                if (tableGenerator != null) {
                    throwConflictException(generatedValue, tableGenerator);
                }
            }
        }

        Sequence defaultAutoSequence = null;
        
        NativeSequence defaultNativeSequence = new NativeSequence(DEFAULT_SEQUENCE_GENERATOR);
        TableSequence defaultTableSequence = new TableSequence(DEFAULT_TABLE_GENERATOR);
        // Sequences keyed on generator names.
        Hashtable<String, Sequence> sequences = new Hashtable<String, Sequence>();
        
        //String seqName;
        for (SequenceGenerator sequenceGenerator : m_sequenceGenerators.values()) {
            String sequenceGeneratorName = sequenceGenerator.name();
            String seqName = (sequenceGenerator.sequenceName().equals("")) ? sequenceGeneratorName : sequenceGenerator.sequenceName();              
            NativeSequence sequence = new NativeSequence(seqName, sequenceGenerator.allocationSize());
            sequences.put(sequenceGeneratorName, sequence);
            
            if (sequenceGeneratorName.equals(DEFAULT_AUTO_GENERATOR)) {
                // SequenceGenerator defined with DEFAULT_AUTO_GENERATOR.
                // The sequence it defines will be used as a defaultSequence.
                defaultAutoSequence = sequence;
            } else if (sequenceGeneratorName.equals(DEFAULT_SEQUENCE_GENERATOR)) {
                // SequenceGenerator deinfed with DEFAULT_SEQUENCE_GENERATOR.
                // All sequences of GeneratorType SEQUENCE and IDENTITY 
                // referencing non-defined generators will use a clone of the 
                // sequence defined by this generator.
                defaultNativeSequence = sequence;
            }
        }
        
        for (TableGenerator tableGenerator : m_tableGenerators.values()) {
            String tableGeneratorName = tableGenerator.name();
            String seqName = (tableGenerator.pkColumnValue().equals("")) ? tableGeneratorName : tableGenerator.pkColumnValue();   
            TableSequence sequence = new TableSequence(seqName, tableGenerator.allocationSize());
            sequences.put(tableGeneratorName, sequence);
            
            // Get the database table from the @TableGenerator values.
            // In case tableGenerator.table().equals("") default sequence table name will be extracted from sequence and used,
            // see TableSequence class.
            sequence.setTable(new DatabaseTable(MetadataHelper.getFullyQualifiedTableName(tableGenerator.table(), sequence.getTableName(), tableGenerator.catalog(), tableGenerator.schema())));
            
            // Process the @UniqueConstraints for this table.
            for (UniqueConstraint uniqueConstraint : tableGenerator.uniqueConstraints()) {
                // UniqueConstraint.columnNames()
                String[] columnNames = uniqueConstraint.columnNames();
                sequence.getTable().addUniqueConstraint(columnNames);
            }
            
            if (!tableGenerator.pkColumnName().equals("")) {
                sequence.setNameFieldName(tableGenerator.pkColumnName());
            }
                
            if (!tableGenerator.valueColumnName().equals("")) {
                sequence.setCounterFieldName(tableGenerator.valueColumnName());
            }
            
            if (tableGeneratorName.equals(DEFAULT_AUTO_GENERATOR)) {
                // TableGenerator defined with DEFAULT_AUTO_GENERATOR.
                // The sequence it defines will be used as a defaultSequence.
                defaultAutoSequence = sequence;
            } else if (tableGeneratorName.equals(DEFAULT_TABLE_GENERATOR)) {
                // SequenceGenerator defined with DEFAULT_TABLE_GENERATOR. All 
                // sequences of GenerationType TABLE referencing non-defined 
                // generators will use a clone of the sequence defined by this 
                // generator.
                defaultTableSequence = sequence;
            }
        }

        // Finally loop through descriptors and set sequences as required into 
        // Descriptors and Login
        for (AnnotationsDescriptor dmd : m_generatedValues.keySet()) {
            GeneratedValue generatedValue = m_generatedValues.get(dmd);
            String generatorName = generatedValue.generator();
            Sequence sequence = null;
            
            if (!generatorName.equals("")) {
                sequence = sequences.get(generatorName);
            }
            
            if (sequence == null) {
                if (generatedValue.strategy() == GenerationType.TABLE) {
                    if (generatorName.equals("")) {
                        sequence = defaultTableSequence;
                    } else {
                        sequence = (Sequence)defaultTableSequence.clone();
                        sequence.setName(generatorName);
                    }
                } else if (generatedValue.strategy() == GenerationType.SEQUENCE || generatedValue.strategy() == GenerationType.IDENTITY) {
                    if (generatorName.equals("")) {
                        sequence = defaultNativeSequence;
                    } else {
                        sequence = (Sequence)defaultNativeSequence.clone();
                        sequence.setName(generatorName);
                    }
                }
            }
            
            if (sequence != null) {
                dmd.setSequenceNumberName(sequence.getName());
                login.addSequence(sequence);
            } else if (generatedValue.strategy() == GenerationType.AUTO) {
                if (defaultAutoSequence != null) {
                    dmd.setSequenceNumberName(defaultAutoSequence.getName());
                    login.setDefaultSequence(defaultAutoSequence);
                } else {
                    dmd.setSequenceNumberName(DEFAULT_AUTO_GENERATOR);
                }
            } else {
                assert false;   // should never happen
            }
        }
    }	

	/**
     * INTERNAL:
	 * Throws an exception indicating a conflict between two objects
	 */
    protected void throwConflictException(Object obj1, Object obj2) {
        throw ValidationException.annotationsConflict(getFullName(obj1), getFullName(obj2));
    }  
}
