/*****************************************************************
 *   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 java.lang.reflect.Member;
import java.util.ArrayList;
import java.util.Collection;

import javax.persistence.AssociationOverride;
import javax.persistence.AssociationOverrides;
import javax.persistence.AttributeOverride;
import javax.persistence.AttributeOverrides;
import javax.persistence.Basic;
import javax.persistence.Column;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.JoinColumn;
import javax.persistence.JoinColumns;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.MapKey;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.OrderBy;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

import org.apache.cayenne.jpa.map.JpaAssociationOverride;
import org.apache.cayenne.jpa.map.JpaAttribute;
import org.apache.cayenne.jpa.map.JpaAttributeOverride;
import org.apache.cayenne.jpa.map.JpaBasic;
import org.apache.cayenne.jpa.map.JpaColumn;
import org.apache.cayenne.jpa.map.JpaEmbedded;
import org.apache.cayenne.jpa.map.JpaEmbeddedId;
import org.apache.cayenne.jpa.map.JpaEntity;
import org.apache.cayenne.jpa.map.JpaGeneratedValue;
import org.apache.cayenne.jpa.map.JpaId;
import org.apache.cayenne.jpa.map.JpaJoinColumn;
import org.apache.cayenne.jpa.map.JpaJoinTable;
import org.apache.cayenne.jpa.map.JpaManagedClass;
import org.apache.cayenne.jpa.map.JpaManyToMany;
import org.apache.cayenne.jpa.map.JpaManyToOne;
import org.apache.cayenne.jpa.map.JpaOneToMany;
import org.apache.cayenne.jpa.map.JpaOneToOne;
import org.apache.cayenne.jpa.map.JpaTransient;
import org.apache.cayenne.jpa.map.JpaVersion;

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

    // superclass of the top-level member annotations
    abstract static class L1Processor implements AnnotationProcessor {

        public void onStartElement(
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            Object parent = context.peek();

            if (parent instanceof JpaManagedClass) {
                onManagedClass((JpaManagedClass) parent, element, context);
            }
            else if (parent instanceof JpaAttribute) {
                onAttribute((JpaAttribute) parent, element, context);
            }
            else {
                recordUnsupportedAnnotation(element, context);
            }
        }

        /**
         * Does nothing by default. Any elements that push themselves on stack in the
         * start method, must pop the stack here.
         */
        public void onFinishElement(
                AnnotatedElement element,
                AnnotationProcessorStack context) {
        }

        void onManagedClass(
                JpaManagedClass managedClass,
                AnnotatedElement element,
                AnnotationProcessorStack context) {
            recordUnsupportedAnnotation(element, context);
        }

        void onAttribute(
                JpaAttribute attribute,
                AnnotatedElement element,
                AnnotationProcessorStack context) {
            recordUnsupportedAnnotation(element, context);
        }

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

    // superclass of the second-level member annotations
    abstract static class L2Processor implements AnnotationProcessor {

        public void onStartElement(
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            JpaAttribute attribute = null;

            Object parent = context.peek();
            if (parent instanceof JpaAttribute) {
                attribute = (JpaAttribute) parent;
            }
            else {
                attribute = findOrCreateAttribute(element, parent, context);
            }

            if (parent != null) {
                onAttribute(attribute, element, context);
            }
            else {
                recordUnsupportedAnnotation(element, context);
            }
        }

        JpaAttribute findOrCreateAttribute(
                AnnotatedElement element,
                Object parent,
                AnnotationProcessorStack context) {

            JpaBasic basic = null;

            if (parent instanceof JpaManagedClass) {
                JpaManagedClass managedClass = (JpaManagedClass) parent;
                String name = ((Member) element).getName();
                basic = managedClass.getAttributes().getBasicAttribute(name);
                if (basic == null) {
                    basic = new JpaBasic();

                    // do push/pop as the context does some required injection
                    context.push(basic);
                    context.pop();

                    managedClass.getAttributes().getBasicAttributes().add(basic);
                }
            }

            return basic;
        }

        /**
         * Does nothing by default. Any elements that push themselves on stack in the
         * start method, must pop the stack here.
         */
        public void onFinishElement(
                AnnotatedElement element,
                AnnotationProcessorStack context) {
        }

        void onAttribute(
                JpaAttribute attribute,
                AnnotatedElement element,
                AnnotationProcessorStack context) {
            recordUnsupportedAnnotation(element, context);
        }

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

    static final class AssociationOverrideProcessor extends L1Processor {

        @Override
        void onManagedClass(
                JpaManagedClass managedClass,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            if (managedClass instanceof JpaEntity) {
                JpaAssociationOverride override = new JpaAssociationOverride(element
                        .getAnnotation(AssociationOverride.class));
                ((JpaEntity) managedClass).getAssociationOverrides().add(override);
            }
            else {
                super.onManagedClass(managedClass, element, context);
            }
        }
    }

    static final class AssociationOverridesProcessor extends L1Processor {

        @Override
        void onManagedClass(
                JpaManagedClass managedClass,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            if (managedClass instanceof JpaEntity) {
                AssociationOverrides overrides = element
                        .getAnnotation(AssociationOverrides.class);
                for (AssociationOverride overrideAnnotation : overrides.value()) {
                    JpaAssociationOverride override = new JpaAssociationOverride(
                            overrideAnnotation);
                    ((JpaEntity) managedClass).getAssociationOverrides().add(override);
                }
            }
            else {
                super.onManagedClass(managedClass, element, context);
            }
        }
    }

    static final class AttributeOverrideProcessor extends L1Processor {

        @Override
        void onAttribute(
                JpaAttribute attribute,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            if (attribute instanceof JpaEmbeddedId) {
                JpaAttributeOverride override = new JpaAttributeOverride(element
                        .getAnnotation(AttributeOverride.class));
                ((JpaEmbeddedId) attribute).getAttributeOverrides().add(override);
            }
            else {
                super.onAttribute(attribute, element, context);
            }
        }
    }

    static final class AttributeOverridesProcessor extends L1Processor {

        @Override
        void onAttribute(
                JpaAttribute attribute,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            if (attribute instanceof JpaEmbeddedId) {
                AttributeOverrides overrides = element
                        .getAnnotation(AttributeOverrides.class);

                for (AttributeOverride overrideAnnotation : overrides.value()) {
                    JpaAttributeOverride override = new JpaAttributeOverride(
                            overrideAnnotation);
                    ((JpaEmbeddedId) attribute).getAttributeOverrides().add(override);
                }
            }
            else {
                super.onAttribute(attribute, element, context);
            }
        }
    }

    static final class BasicProcessor extends L1Processor {

        @Override
        void onManagedClass(
                JpaManagedClass managedClass,
                AnnotatedElement element,
                AnnotationProcessorStack context) {
            JpaBasic attribute = new JpaBasic(element.getAnnotation(Basic.class));
            managedClass.getAttributes().getBasicAttributes().add(attribute);
            context.push(attribute);
        }

        @Override
        void onAttribute(
                JpaAttribute attribute,
                AnnotatedElement element,
                AnnotationProcessorStack context) {
            if (!(attribute instanceof JpaBasic)) {
                super.onAttribute(attribute, element, context);
            }
        }

        @Override
        public void onFinishElement(
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            Object pop = context.pop();
            if (!(pop instanceof JpaBasic)) {
                context.push(pop);
            }
        }
    }

    static final class ColumnProcessor extends L2Processor {

        @Override
        void onAttribute(
                JpaAttribute attribute,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            JpaColumn column = new JpaColumn(element.getAnnotation(Column.class));

            if (attribute instanceof JpaBasic) {
                ((JpaBasic) attribute).setColumn(column);
            }
            else if (attribute instanceof JpaVersion) {
                ((JpaVersion) attribute).setColumn(column);
            }
            else if (attribute instanceof JpaId) {
                ((JpaId) attribute).setColumn(column);
            }
            else {
                super.onAttribute(attribute, element, context);
            }
        }
    }

    static final class EmbeddedProcessor extends L1Processor {

        @Override
        void onManagedClass(
                JpaManagedClass managedClass,
                AnnotatedElement element,
                AnnotationProcessorStack context) {
            JpaEmbedded attribute = new JpaEmbedded();
            managedClass.getAttributes().getEmbeddedAttributes().add(attribute);
            context.push(attribute);
        }

        @Override
        void onAttribute(
                JpaAttribute attribute,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            if (!(attribute instanceof JpaEmbedded)) {
                super.onAttribute(attribute, element, context);
            }
        }

        @Override
        public void onFinishElement(
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            Object pop = context.pop();
            if (!(pop instanceof JpaEmbedded)) {
                context.push(pop);
            }
        }
    }

    static final class EmbeddedIdProcessor extends L1Processor {

        @Override
        void onManagedClass(
                JpaManagedClass managedClass,
                AnnotatedElement element,
                AnnotationProcessorStack context) {
            JpaEmbeddedId id = new JpaEmbeddedId();
            managedClass.getAttributes().setEmbeddedId(id);
            context.push(id);
        }

        @Override
        void onAttribute(
                JpaAttribute attribute,
                AnnotatedElement element,
                AnnotationProcessorStack context) {
            if (attribute instanceof JpaEmbeddedId) {
                // was created implictly by another annotation
            }
            else {
                super.onAttribute(attribute, element, context);
            }
        }

        @Override
        public void onFinishElement(
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            Object pop = context.pop();
            if (!(pop instanceof JpaEmbeddedId)) {
                context.push(pop);
            }
        }
    }

    static final class EnumeratedProcessor extends L2Processor {

        @Override
        void onAttribute(
                JpaAttribute attribute,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            if (attribute instanceof JpaBasic) {
                EnumType enumType = element.getAnnotation(Enumerated.class).value();
                ((JpaBasic) attribute).setEnumerated(enumType);
            }
            else {
                super.onAttribute(attribute, element, context);
            }
        }
    }

    static final class GeneratedValueProcessor extends L2Processor {

        @Override
        void onAttribute(
                JpaAttribute attribute,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            // can only attach to id
            if (attribute instanceof JpaId) {
                JpaGeneratedValue generated = new JpaGeneratedValue(element
                        .getAnnotation(GeneratedValue.class));
                ((JpaId) attribute).setGeneratedValue(generated);
            }
            else {
                super.onAttribute(attribute, element, context);
            }
        }
    }

    static final class IdProcessor extends L1Processor {

        @Override
        void onManagedClass(
                JpaManagedClass managedClass,
                AnnotatedElement element,
                AnnotationProcessorStack context) {
            JpaId attribute = new JpaId();
            managedClass.getAttributes().getIds().add(attribute);
            context.push(attribute);
        }

        @Override
        void onAttribute(
                JpaAttribute attribute,
                AnnotatedElement element,
                AnnotationProcessorStack context) {
            if (attribute instanceof JpaId) {
                // id was created implictly by another annotation
            }
            else {
                super.onAttribute(attribute, element, context);
            }
        }

        @Override
        public void onFinishElement(
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            Object pop = context.pop();
            if (!(pop instanceof JpaId)) {
                context.push(pop);
            }
        }
    }

    static final class JoinColumnProcessor extends L2Processor {

        @Override
        void onAttribute(
                JpaAttribute attribute,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            JpaJoinColumn joinColumn = new JpaJoinColumn(element
                    .getAnnotation(JoinColumn.class));

            if (attribute instanceof JpaOneToMany) {
                ((JpaOneToMany) attribute).getJoinColumns().add(joinColumn);
            }
            else if (attribute instanceof JpaOneToOne) {
                ((JpaOneToOne) attribute).getJoinColumns().add(joinColumn);
            }
            else if (attribute instanceof JpaManyToOne) {
                ((JpaManyToOne) attribute).getJoinColumns().add(joinColumn);
            }
            else {
                super.onAttribute(attribute, element, context);
            }
        }
    }

    static final class JoinColumnsProcessor extends L2Processor {

        @Override
        void onAttribute(
                JpaAttribute attribute,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            JoinColumn[] annotations = element.getAnnotation(JoinColumns.class).value();
            Collection<JpaJoinColumn> joinColumns = new ArrayList<JpaJoinColumn>(
                    annotations.length);
            for (int i = 0; i < annotations.length; i++) {
                joinColumns.add(new JpaJoinColumn(annotations[i]));
            }

            if (attribute instanceof JpaOneToMany) {
                ((JpaOneToMany) attribute).getJoinColumns().addAll(joinColumns);
            }
            else if (attribute instanceof JpaOneToOne) {
                ((JpaOneToOne) attribute).getJoinColumns().addAll(joinColumns);
            }
            else if (attribute instanceof JpaManyToOne) {
                ((JpaManyToOne) attribute).getJoinColumns().addAll(joinColumns);
            }
            else {
                super.onAttribute(attribute, element, context);
            }
        }
    }

    static final class JoinTableProcessor extends L2Processor {

        @Override
        void onAttribute(
                JpaAttribute attribute,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            JpaJoinTable joinTable = new JpaJoinTable(element
                    .getAnnotation(JoinTable.class));

            if (attribute instanceof JpaOneToMany) {
                ((JpaOneToMany) attribute).setJoinTable(joinTable);
            }
            else if (attribute instanceof JpaOneToOne) {
                ((JpaOneToOne) attribute).setJoinTable(joinTable);
            }
            else if (attribute instanceof JpaManyToOne) {
                ((JpaManyToOne) attribute).setJoinTable(joinTable);
            }
            else if (attribute instanceof JpaManyToMany) {
                ((JpaManyToOne) attribute).setJoinTable(joinTable);
            }
            else {
                super.onAttribute(attribute, element, context);
            }
        }
    }

    static final class LobProcessor extends L2Processor {

        @Override
        void onAttribute(
                JpaAttribute attribute,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            if (attribute instanceof JpaBasic) {
                ((JpaBasic) attribute).setLob(true);
            }
            else {
                super.onAttribute(attribute, element, context);
            }
        }
    }

    static final class ManyToManyProcessor extends L1Processor {

        @Override
        void onManagedClass(
                JpaManagedClass managedClass,
                AnnotatedElement element,
                AnnotationProcessorStack context) {
            JpaManyToMany attribute = new JpaManyToMany(element
                    .getAnnotation(ManyToMany.class));
            managedClass.getAttributes().getManyToManyRelationships().add(attribute);
            context.push(attribute);
        }

        @Override
        void onAttribute(
                JpaAttribute attribute,
                AnnotatedElement element,
                AnnotationProcessorStack context) {
            if (!(attribute instanceof JpaManyToMany)) {
                super.onAttribute(attribute, element, context);
            }
        }

        @Override
        public void onFinishElement(
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            Object pop = context.pop();
            if (!(pop instanceof JpaManyToMany)) {
                context.push(pop);
            }
        }
    }

    static final class ManyToOneProcessor extends L1Processor {

        @Override
        void onManagedClass(
                JpaManagedClass managedClass,
                AnnotatedElement element,
                AnnotationProcessorStack context) {
            JpaManyToOne attribute = new JpaManyToOne(element
                    .getAnnotation(ManyToOne.class));
            managedClass.getAttributes().getManyToOneRelationships().add(attribute);
            context.push(attribute);
        }

        @Override
        void onAttribute(
                JpaAttribute attribute,
                AnnotatedElement element,
                AnnotationProcessorStack context) {
            if (!(attribute instanceof JpaManyToMany)) {
                super.onAttribute(attribute, element, context);
            }
        }

        @Override
        public void onFinishElement(
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            Object pop = context.pop();
            if (!(pop instanceof JpaManyToOne)) {
                context.push(pop);
            }
        }
    }

    static final class MapKeyProcessor extends L2Processor {

        @Override
        void onAttribute(
                JpaAttribute attribute,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            String key = element.getAnnotation(MapKey.class).name();
            if (attribute instanceof JpaOneToMany) {
                ((JpaOneToMany) attribute).setMapKey(key);
            }
            else if (attribute instanceof JpaManyToMany) {
                ((JpaManyToMany) attribute).setMapKey(key);
            }
            else {
                super.onAttribute(attribute, element, context);
            }
        }
    }

    static final class OneToManyProcessor extends L1Processor {

        @Override
        void onManagedClass(
                JpaManagedClass managedClass,
                AnnotatedElement element,
                AnnotationProcessorStack context) {
            JpaOneToMany attribute = new JpaOneToMany(element
                    .getAnnotation(OneToMany.class));
            managedClass.getAttributes().getOneToManyRelationships().add(attribute);
            context.push(attribute);
        }

        @Override
        void onAttribute(
                JpaAttribute attribute,
                AnnotatedElement element,
                AnnotationProcessorStack context) {
            if (!(attribute instanceof JpaOneToMany)) {
                super.onAttribute(attribute, element, context);
            }
        }

        @Override
        public void onFinishElement(
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            Object pop = context.pop();
            if (!(pop instanceof JpaOneToMany)) {
                context.push(pop);
            }
        }
    }

    static final class OneToOneProcessor extends L1Processor {

        @Override
        void onManagedClass(
                JpaManagedClass managedClass,
                AnnotatedElement element,
                AnnotationProcessorStack context) {
            JpaOneToOne attribute = new JpaOneToOne(element.getAnnotation(OneToOne.class));
            managedClass.getAttributes().getOneToOneRelationships().add(attribute);
            context.push(attribute);
        }

        @Override
        void onAttribute(
                JpaAttribute attribute,
                AnnotatedElement element,
                AnnotationProcessorStack context) {
            if (!(attribute instanceof JpaOneToOne)) {
                super.onAttribute(attribute, element, context);
            }
        }

        @Override
        public void onFinishElement(
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            Object pop = context.pop();
            if (!(pop instanceof JpaOneToOne)) {
                context.push(pop);
            }
        }
    }

    static final class OrderByProcessor extends L2Processor {

        @Override
        void onAttribute(
                JpaAttribute attribute,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            String order = element.getAnnotation(OrderBy.class).value();
            if (attribute instanceof JpaOneToMany) {
                ((JpaOneToMany) attribute).setOrderBy(order);
            }
            else if (attribute instanceof JpaManyToMany) {
                ((JpaManyToMany) attribute).setOrderBy(order);
            }
            else {
                super.onAttribute(attribute, element, context);
            }
        }
    }

    static final class TemporalProcessor extends L2Processor {

        @Override
        void onAttribute(
                JpaAttribute attribute,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            TemporalType value = element.getAnnotation(Temporal.class).value();

            if (attribute instanceof JpaBasic) {
                ((JpaBasic) attribute).setTemporal(value);
            }
            // according to the spec, only @Basic is compatibel with @Temporal, however
            // @Id and @Version also support it in the schema
            else if (attribute instanceof JpaId) {
                ((JpaId) attribute).setTemporal(value);
            }
            else if (attribute instanceof JpaVersion) {
                ((JpaVersion) attribute).setTemporal(value);
            }
            else {
                super.onAttribute(attribute, element, context);
            }
        }
    }

    static final class TransientProcessor extends L1Processor {

        @Override
        void onManagedClass(
                JpaManagedClass managedClass,
                AnnotatedElement element,
                AnnotationProcessorStack context) {
            JpaTransient attribute = new JpaTransient();
            managedClass.getAttributes().getTransientAttributes().add(attribute);
            context.push(attribute);
        }

        @Override
        void onAttribute(
                JpaAttribute attribute,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            if (!(attribute instanceof JpaTransient)) {
                super.onAttribute(attribute, element, context);
            }
        }

        @Override
        public void onFinishElement(
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            Object pop = context.pop();
            if (!(pop instanceof JpaTransient)) {
                context.push(pop);
            }
        }
    }

    static final class VersionProcessor extends L1Processor {

        @Override
        void onManagedClass(
                JpaManagedClass managedClass,
                AnnotatedElement element,
                AnnotationProcessorStack context) {
            JpaVersion attribute = new JpaVersion();
            managedClass.getAttributes().getVersionAttributes().add(attribute);
            context.push(attribute);
        }

        @Override
        void onAttribute(
                JpaAttribute attribute,
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            if (!(attribute instanceof JpaVersion)) {
                super.onAttribute(attribute, element, context);
            }
        }

        @Override
        public void onFinishElement(
                AnnotatedElement element,
                AnnotationProcessorStack context) {

            Object pop = context.pop();
            if (!(pop instanceof JpaVersion)) {
                context.push(pop);
            }
        }
    }
}
