/*****************************************************************
 *   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.jpa.conf;

import java.lang.reflect.AnnotatedElement;

import javax.persistence.AttributeOverride;
import javax.persistence.AttributeOverrides;
import javax.persistence.DiscriminatorColumn;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
import javax.persistence.EntityListeners;
import javax.persistence.IdClass;
import javax.persistence.Inheritance;
import javax.persistence.NamedNativeQueries;
import javax.persistence.NamedNativeQuery;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.PrimaryKeyJoinColumn;
import javax.persistence.PrimaryKeyJoinColumns;
import javax.persistence.SecondaryTable;
import javax.persistence.SecondaryTables;
import javax.persistence.SequenceGenerator;
import javax.persistence.SqlResultSetMapping;
import javax.persistence.Table;
import javax.persistence.TableGenerator;

import org.apache.cayenne.jpa.map.JpaAttributeOverride;
import org.apache.cayenne.jpa.map.JpaAttributes;
import org.apache.cayenne.jpa.map.JpaDiscriminatorColumn;
import org.apache.cayenne.jpa.map.JpaEmbeddable;
import org.apache.cayenne.jpa.map.JpaEntity;
import org.apache.cayenne.jpa.map.JpaEntityListener;
import org.apache.cayenne.jpa.map.JpaEntityListeners;
import org.apache.cayenne.jpa.map.JpaEntityMap;
import org.apache.cayenne.jpa.map.JpaIdClass;
import org.apache.cayenne.jpa.map.JpaInheritance;
import org.apache.cayenne.jpa.map.JpaMappedSuperclass;
import org.apache.cayenne.jpa.map.JpaNamedNativeQuery;
import org.apache.cayenne.jpa.map.JpaNamedQuery;
import org.apache.cayenne.jpa.map.JpaPrimaryKeyJoinColumn;
import org.apache.cayenne.jpa.map.JpaSecondaryTable;
import org.apache.cayenne.jpa.map.JpaSequenceGenerator;
import org.apache.cayenne.jpa.map.JpaSqlResultSetMapping;
import org.apache.cayenne.jpa.map.JpaTable;
import org.apache.cayenne.jpa.map.JpaTableGenerator;
import org.apache.cayenne.util.Util;

/**
 * A factory of class annotation processors.
 * 
 * @author Andrus Adamchik
 */
class ClassAnnotationProcessorFactory extends AnnotationProcessorFactory {

    static final class EntityProcessor implements AnnotationProcessor {

        public void onStartElement(
                AnnotatedElement element,
                AnnotationProcessorStack context) {
            Entity entityAnnotation = element.getAnnotation(Entity.class);

            JpaEntity entity = new JpaEntity();
            entity.setClassName(((Class<?>) element).getName());
            entity.setAttributes(new JpaAttributes());

            if (!Util.isEmptyString(entityAnnotation.name())) {
                entity.setName(entityAnnotation.name());
            }

            context.push(entity);
        }

        public void onFinishElement(
                AnnotatedElement element,
                AnnotationProcessorStack context) {
            JpaEntity entity = (JpaEntity) context.pop();
            JpaEntityMap entityMap = (JpaEntityMap) context.peek();
            entityMap.getEntities().add(entity);
        }
    }

    static final class EmbeddableProcessor implements AnnotationProcessor {

        public void onStartElement(
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            JpaEmbeddable embeddable = new JpaEmbeddable();
            embeddable.setClassName(((Class<?>) element).getName());
            embeddable.setAttributes(new JpaAttributes());
            context.push(embeddable);
        }

        public void onFinishElement(
                AnnotatedElement element,
                AnnotationProcessorStack context) {
            JpaEmbeddable embeddable = (JpaEmbeddable) context.pop();
            JpaEntityMap entityMap = (JpaEntityMap) context.peek();
            entityMap.getEmbeddables().add(embeddable);
        }
    }

    static final class MappedSuperclassProcessor implements AnnotationProcessor {

        public void onStartElement(
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            JpaMappedSuperclass superclass = new JpaMappedSuperclass();
            superclass.setClassName(((Class<?>) element).getName());
            superclass.setAttributes(new JpaAttributes());
            context.push(superclass);
        }

        public void onFinishElement(
                AnnotatedElement element,
                AnnotationProcessorStack context) {
            JpaMappedSuperclass superclass = (JpaMappedSuperclass) context.pop();
            JpaEntityMap entityMap = (JpaEntityMap) context.peek();
            entityMap.getMappedSuperclasses().add(superclass);
        }
    }

    /**
     * A superclass of all processors that for class annotations that do not define the
     * type of the mapping.
     */
    abstract static class AbstractChildProcessor implements AnnotationProcessor {

        public void onStartElement(
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            Object parent = context.peek();

            if (parent instanceof JpaEntity) {
                onEntity((JpaEntity) parent, element, context);
            }
            else if (parent instanceof JpaMappedSuperclass) {
                onMappedSuperclass((JpaMappedSuperclass) parent, element, context);
            }
            else if (parent instanceof JpaEntityMap) {
                onEntityMap((JpaEntityMap) parent, element, context);
            }
            else if (parent instanceof JpaEmbeddable) {
                onEmbeddable((JpaEmbeddable) parent, element, context);
            }
            else {
                recordUnsupportedAnnotation(element, context);
            }
        }

        public void onFinishElement(
                AnnotatedElement element,
                AnnotationProcessorStack context) {
            // noop
        }

        void onEntity(
                JpaEntity entity,
                AnnotatedElement element,
                AnnotationProcessorStack context) {
            recordUnsupportedAnnotation(element, context);
        }

        void onMappedSuperclass(
                JpaMappedSuperclass superclass,
                AnnotatedElement element,
                AnnotationProcessorStack context) {
            recordUnsupportedAnnotation(element, context);
        }

        void onEmbeddable(
                JpaEmbeddable embeddable,
                AnnotatedElement element,
                AnnotationProcessorStack context) {
            recordUnsupportedAnnotation(element, context);
        }

        // not sure if this is compatible with the spec, but such annotations are
        // possible...
        void onEntityMap(
                JpaEntityMap entityMap,
                AnnotatedElement element,
                AnnotationProcessorStack context) {
            recordUnsupportedAnnotation(element, context);
        }

        void recordUnsupportedAnnotation(
                AnnotatedElement element,
                AnnotationProcessorStack context) {
            context.recordConflict(element, AnnotationProcessorFactory
                    .annotationClass(getClass()), "Unsupported in this place");
        }
    }

    static final class AttributeOverrideProcessor extends AbstractChildProcessor {

        @Override
        void onEntity(
                JpaEntity entity,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            AttributeOverride annotation = element.getAnnotation(AttributeOverride.class);
            entity.getAttributeOverrides().add(new JpaAttributeOverride(annotation));
        }
    }

    static final class AttributeOverridesProcessor extends AbstractChildProcessor {

        @Override
        void onEntity(
                JpaEntity entity,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            AttributeOverrides annotation = element
                    .getAnnotation(AttributeOverrides.class);
            for (int i = 0; i < annotation.value().length; i++) {
                entity.getAttributeOverrides().add(
                        new JpaAttributeOverride(annotation.value()[i]));
            }
        }
    }

    static final class DiscriminatorColumnProcessor extends AbstractChildProcessor {

        @Override
        void onEntity(
                JpaEntity entity,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            DiscriminatorColumn annotation = element
                    .getAnnotation(DiscriminatorColumn.class);
            entity.setDiscriminatorColumn(new JpaDiscriminatorColumn(annotation));
        }
    }

    static final class DiscriminatorValueProcessor extends AbstractChildProcessor {

        @Override
        void onEntity(
                JpaEntity entity,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            DiscriminatorValue annotation = element
                    .getAnnotation(DiscriminatorValue.class);
            entity.setDiscriminatorValue(annotation.value());
        }
    }

    static final class EntityListenersProcessor extends AbstractChildProcessor {

        private EntityListenerAnnotationLoader listenerLoader;

        @Override
        void onEntity(
                JpaEntity entity,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            EntityListeners annotation = element.getAnnotation(EntityListeners.class);

            if (annotation.value().length > 0) {
                if (listenerLoader == null) {
                    listenerLoader = new EntityListenerAnnotationLoader();
                }

                JpaEntityListeners listenerHolder = entity.getEntityListeners();
                if (listenerHolder == null) {
                    listenerHolder = new JpaEntityListeners();
                    entity.setEntityListeners(listenerHolder);
                }

                for (int i = 0; i < annotation.value().length; i++) {
                    JpaEntityListener listener = listenerLoader
                            .getEntityListener(annotation.value()[i]);
                    if (listener != null) {
                        listenerHolder.getEntityListeners().add(listener);
                    }
                }
            }
        }

        @Override
        void onMappedSuperclass(
                JpaMappedSuperclass superclass,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            EntityListeners annotation = element.getAnnotation(EntityListeners.class);

            if (annotation.value().length > 0) {
                if (listenerLoader == null) {
                    listenerLoader = new EntityListenerAnnotationLoader();
                }

                JpaEntityListeners listenerHolder = superclass.getEntityListeners();
                if(listenerHolder  == null) {
                    listenerHolder = new JpaEntityListeners();
                    superclass.setEntityListeners(listenerHolder);
                }

                for (int i = 0; i < annotation.value().length; i++) {
                    JpaEntityListener listener = listenerLoader
                            .getEntityListener(annotation.value()[i]);
                    if (listener != null) {
                        listenerHolder.getEntityListeners().add(listener);
                    }
                }
            }
        }
    }

    static final class ExcludeDefaultListenersProcessor extends AbstractChildProcessor {

        @Override
        void onEntity(
                JpaEntity entity,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            entity.setExcludeDefaultListeners(true);
        }

        @Override
        void onMappedSuperclass(
                JpaMappedSuperclass superclass,
                AnnotatedElement element,
                AnnotationProcessorStack context) {
            superclass.setExcludeDefaultListeners(true);
        }
    }

    static final class ExcludeSuperclassListenersProcessor extends AbstractChildProcessor {

        @Override
        void onEntity(
                JpaEntity entity,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            entity.setExcludeSuperclassListeners(true);
        }

        @Override
        void onMappedSuperclass(
                JpaMappedSuperclass superclass,
                AnnotatedElement element,
                AnnotationProcessorStack context) {
            superclass.setExcludeSuperclassListeners(true);
        }
    }

    static final class IdClassProcessor extends AbstractChildProcessor {

        @Override
        void onEntity(
                JpaEntity entity,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            IdClass annotation = element.getAnnotation(IdClass.class);
            JpaIdClass idClass = new JpaIdClass();
            idClass.setClassName(annotation.value().getName());
            entity.setIdClass(idClass);
        }

        @Override
        void onMappedSuperclass(
                JpaMappedSuperclass superclass,
                AnnotatedElement element,
                AnnotationProcessorStack context) {
            IdClass annotation = element.getAnnotation(IdClass.class);
            JpaIdClass idClass = new JpaIdClass();
            idClass.setClassName(annotation.value().getName());
            superclass.setIdClass(idClass);
        }
    }

    static final class InheritanceProcessor extends AbstractChildProcessor {

        @Override
        void onEntity(
                JpaEntity entity,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            Inheritance annotation = element.getAnnotation(Inheritance.class);
            entity.setInheritance(new JpaInheritance(annotation));
        }
    }

    static final class NamedNativeQueriesProcessor extends AbstractChildProcessor {

        @Override
        void onEntity(
                JpaEntity entity,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            NamedNativeQueries annotation = element
                    .getAnnotation(NamedNativeQueries.class);
            for (int i = 0; i < annotation.value().length; i++) {
                entity.getNamedNativeQueries().add(
                        new JpaNamedNativeQuery(annotation.value()[i]));
            }
        }

        @Override
        void onEntityMap(
                JpaEntityMap entityMap,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            NamedNativeQueries annotation = element
                    .getAnnotation(NamedNativeQueries.class);
            for (int i = 0; i < annotation.value().length; i++) {
                entityMap.getNamedNativeQueries().add(
                        new JpaNamedNativeQuery(annotation.value()[i]));
            }
        }
    }

    static final class NamedNativeQueryProcessor extends AbstractChildProcessor {

        @Override
        void onEntity(
                JpaEntity entity,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            NamedNativeQuery annotation = element.getAnnotation(NamedNativeQuery.class);
            entity.getNamedNativeQueries().add(new JpaNamedNativeQuery(annotation));
        }

        @Override
        void onEntityMap(
                JpaEntityMap entityMap,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            NamedNativeQuery annotation = element.getAnnotation(NamedNativeQuery.class);
            entityMap.getNamedNativeQueries().add(new JpaNamedNativeQuery(annotation));
        }
    }

    static final class NamedQueriesProcessor extends AbstractChildProcessor {

        @Override
        void onEntity(
                JpaEntity entity,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            NamedQueries annotation = element.getAnnotation(NamedQueries.class);
            for (int i = 0; i < annotation.value().length; i++) {
                entity.getNamedQueries().add(new JpaNamedQuery(annotation.value()[i]));
            }
        }

        @Override
        void onEntityMap(
                JpaEntityMap entityMap,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            NamedQueries annotation = element.getAnnotation(NamedQueries.class);
            for (int i = 0; i < annotation.value().length; i++) {
                entityMap.getNamedQueries().add(new JpaNamedQuery(annotation.value()[i]));
            }
        }
    }

    static final class NamedQueryProcessor extends AbstractChildProcessor {

        @Override
        void onEntity(
                JpaEntity entity,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            NamedQuery annotation = element.getAnnotation(NamedQuery.class);
            entity.getNamedQueries().add(new JpaNamedQuery(annotation));
        }

        @Override
        void onEntityMap(
                JpaEntityMap entityMap,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            NamedQuery annotation = element.getAnnotation(NamedQuery.class);
            entityMap.getNamedQueries().add(new JpaNamedQuery(annotation));
        }
    }

    static final class PrimaryKeyJoinColumnProcessor extends AbstractChildProcessor {

        @Override
        void onEntity(
                JpaEntity entity,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            PrimaryKeyJoinColumn annotation = element
                    .getAnnotation(PrimaryKeyJoinColumn.class);
            entity
                    .getPrimaryKeyJoinColumns()
                    .add(new JpaPrimaryKeyJoinColumn(annotation));
        }
    }

    static final class PrimaryKeyJoinColumnsProcessor extends AbstractChildProcessor {

        @Override
        void onEntity(
                JpaEntity entity,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            PrimaryKeyJoinColumns annotation = element
                    .getAnnotation(PrimaryKeyJoinColumns.class);
            for (int i = 0; i < annotation.value().length; i++) {
                entity.getPrimaryKeyJoinColumns().add(
                        new JpaPrimaryKeyJoinColumn(annotation.value()[i]));
            }
        }
    }

    static final class SecondaryTableProcessor extends AbstractChildProcessor {

        @Override
        void onEntity(
                JpaEntity entity,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            SecondaryTable annotation = element.getAnnotation(SecondaryTable.class);
            entity.getSecondaryTables().add(new JpaSecondaryTable(annotation));
        }
    }

    static final class SecondaryTablesProcessor extends AbstractChildProcessor {

        @Override
        void onEntity(
                JpaEntity entity,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            SecondaryTables annotation = element.getAnnotation(SecondaryTables.class);
            for (int i = 0; i < annotation.value().length; i++) {
                entity.getSecondaryTables().add(
                        new JpaSecondaryTable(annotation.value()[i]));
            }
        }
    }

    static final class SequenceGeneratorProcessor extends AbstractChildProcessor {

        @Override
        void onEntity(
                JpaEntity entity,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            SequenceGenerator annotation = element.getAnnotation(SequenceGenerator.class);
            entity.setSequenceGenerator(new JpaSequenceGenerator(annotation));
        }

        @Override
        void onEntityMap(
                JpaEntityMap entityMap,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            SequenceGenerator annotation = element.getAnnotation(SequenceGenerator.class);
            entityMap.getSequenceGenerators().add(new JpaSequenceGenerator(annotation));
        }
    }

    static final class SqlResultSetMappingProcessor extends AbstractChildProcessor {

        @Override
        void onEntity(
                JpaEntity entity,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            SqlResultSetMapping annotation = element
                    .getAnnotation(SqlResultSetMapping.class);
            entity.getSqlResultSetMappings().add(new JpaSqlResultSetMapping(annotation));
        }

        @Override
        void onEntityMap(
                JpaEntityMap entityMap,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            SqlResultSetMapping annotation = element
                    .getAnnotation(SqlResultSetMapping.class);
            entityMap.getSqlResultSetMappings().add(
                    new JpaSqlResultSetMapping(annotation));
        }
    }

    static final class TableGeneratorProcessor extends AbstractChildProcessor {

        @Override
        void onEntity(
                JpaEntity entity,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            TableGenerator annotation = element.getAnnotation(TableGenerator.class);
            entity.setTableGenerator(new JpaTableGenerator(annotation));
        }

        @Override
        void onEntityMap(
                JpaEntityMap entityMap,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            TableGenerator annotation = element.getAnnotation(TableGenerator.class);
            entityMap.getTableGenerators().add(new JpaTableGenerator(annotation));
        }
    }

    static final class TableProcessor extends AbstractChildProcessor {

        @Override
        void onEntity(
                JpaEntity entity,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            Table annotation = element.getAnnotation(Table.class);
            entity.setTable(new JpaTable(annotation));
        }
    }

}
