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

import org.apache.meecrowave.Meecrowave;
import org.apache.meecrowave.jpa.api.Jpa;
import org.apache.meecrowave.jpa.api.PersistenceUnitInfoBuilder;
import org.apache.meecrowave.jpa.api.Unit;

import javax.enterprise.context.spi.CreationalContext;
import javax.enterprise.event.Observes;
import javax.enterprise.inject.spi.AfterBeanDiscovery;
import javax.enterprise.inject.spi.AfterDeploymentValidation;
import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.BeanManager;
import javax.enterprise.inject.spi.BeforeBeanDiscovery;
import javax.enterprise.inject.spi.Extension;
import javax.enterprise.inject.spi.ProcessAnnotatedType;
import javax.enterprise.inject.spi.ProcessBean;
import javax.enterprise.inject.spi.WithAnnotations;
import javax.persistence.Embeddable;
import javax.persistence.Entity;
import javax.persistence.MappedSuperclass;
import javax.persistence.SynchronizationType;
import javax.persistence.spi.PersistenceUnitInfo;
import javax.servlet.ServletContext;
import javax.sql.DataSource;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;

import static java.util.Optional.ofNullable;
import static java.util.function.Function.identity;
import static java.util.stream.Collectors.toMap;

/**
 * This extension is responsible to create entitymanagers, entitymanagerfactories
 * and link it to CDI named units.
 * <p>
 * Note: we don't reuse @DataSourceDefinition which is all but defined (pooling, datasource configs are a mess).
 */
public class JpaExtension implements Extension {
    private final EntityManagerContext entityManagerContext = new EntityManagerContext();
    private final List<String> jpaClasses = new ArrayList<>();
    private final Map<UnitKey, EntityManagerBean> entityManagerBeans = new HashMap<>();
    private final Collection<Bean<?>> unitBuilders = new ArrayList<>(2);

    void addInternals(@Observes final BeforeBeanDiscovery bbd, final BeanManager bm) {
        Stream.of(JpaTransactionInterceptor.class, JpaNoTransactionInterceptor.class)
                .forEach(interceptor -> bbd.addAnnotatedType(bm.createAnnotatedType(interceptor)));
    }

    <T> void addJpaToEmConsumers(@Observes @WithAnnotations(Unit.class) final ProcessAnnotatedType<T> pat) {
        if (pat.getAnnotatedType().isAnnotationPresent(Jpa.class)) {
            return;
        }
        pat.setAnnotatedType(new AutoJpaAnnotationType<T>(pat.getAnnotatedType()));
    }

    void collectEntityManagerInjections(@Observes final ProcessBean<?> bean) {
        final Map<UnitKey, EntityManagerBean> beans = bean.getBean().getInjectionPoints().stream()
                .filter(i -> i.getAnnotated().isAnnotationPresent(Unit.class))
                .map(i -> i.getAnnotated().getAnnotation(Unit.class))
                .collect(toMap(u -> new UnitKey(u.name(), u.synchronization()), unit -> new EntityManagerBean(entityManagerContext, unit.name(), unit.synchronization())));
        entityManagerBeans.putAll(beans);
    }

    void collectEntityManagers(@Observes final ProcessBean<?> bean) {
        if (bean.getBean().getTypes().contains(PersistenceUnitInfoBuilder.class)) {
            unitBuilders.add(bean.getBean());
        }
    }

    void collectEntities(@Observes @WithAnnotations({Entity.class, MappedSuperclass.class, Embeddable.class}) final ProcessAnnotatedType<?> jpa) {
        jpaClasses.add(jpa.getAnnotatedType().getJavaClass().getName());
    }

    void addBeans(@Observes final AfterBeanDiscovery afb, final BeanManager bm) {
        afb.addContext(entityManagerContext);
        entityManagerBeans.forEach((n, b) -> afb.addBean(b));
    }

    void initBeans(@Observes final AfterDeploymentValidation adv, final BeanManager bm) {
        if (entityManagerBeans.isEmpty()) {
            return;
        }

        // only not portable part is this config read, could be optional
        final ServletContext sc = ServletContext.class.cast(bm.getReference(bm.resolve(bm.getBeans(ServletContext.class)), ServletContext.class, bm.createCreationalContext(null)));
        final Meecrowave.Builder config = Meecrowave.Builder.class.cast(sc.getAttribute("meecrowave.configuration"));
        final Map<String, String> props = new HashMap<>();
        if (config != null) {
            ofNullable(config.getProperties()).ifPresent(p -> p.stringPropertyNames().stream()
                    .filter(k -> k.startsWith("jpa.property."))
                    .forEach(k -> props.put(k.substring("jpa.property.".length()), p.getProperty(k))));
        }

        final Map<String, PersistenceUnitInfo> infoIndex = unitBuilders.stream()
                .map(bean -> {
                    final CreationalContext<?> cc = bm.createCreationalContext(null);
                    try {
                        final Bean<?> resolvedBean = bm.resolve(bm.getBeans(
                                PersistenceUnitInfoBuilder.class,
                                bean.getQualifiers().toArray(new Annotation[bean.getQualifiers().size()])));
                        final PersistenceUnitInfoBuilder builder = PersistenceUnitInfoBuilder.class.cast(
                                bm.getReference(resolvedBean, PersistenceUnitInfoBuilder.class, cc));
                        if (builder.getManagedClasses().isEmpty()) {
                            builder.setManagedClassNames(jpaClasses).setExcludeUnlistedClasses(true);
                        }
                        props.forEach(builder::addProperty);
                        return builder.toInfo();
                    } finally {
                        cc.release();
                    }
                }).collect(toMap(PersistenceUnitInfo::getPersistenceUnitName, identity()));

        entityManagerBeans.forEach((k, e) -> {
            PersistenceUnitInfo info = infoIndex.get(k.unitName);
            if (info == null) {
                info = tryCreateDefaultPersistenceUnit(k.unitName, bm, props);
            }
            if (info == null) { // not valid
                adv.addDeploymentProblem(new IllegalArgumentException("Didn't find any PersistenceUnitInfoBuilder for " + k));
            } else {
                e.init(info, bm);
            }
        });
    }

    private PersistenceUnitInfo tryCreateDefaultPersistenceUnit(final String unitName, final BeanManager bm, final Map<String, String> props) {
        final Set<Bean<?>> beans = bm.getBeans(DataSource.class);
        final Bean<?> bean = bm.resolve(beans);
        if (bean == null || !bm.isNormalScope(bean.getScope())) {
            return null;
        }

        final DataSource ds = DataSource.class.cast(bm.getReference(bean, DataSource.class, bm.createCreationalContext(null)));

        final PersistenceUnitInfoBuilder builder = new PersistenceUnitInfoBuilder()
                .setManagedClassNames(jpaClasses)
                .setExcludeUnlistedClasses(true)
                .setUnitName(unitName)
                .setDataSource(ds);

        props.forEach(builder::addProperty);

        return builder.toInfo();
    }

    EntityManagerContext getEntityManagerContext() {
        return entityManagerContext;
    }

    private static class UnitKey {
        private final String unitName;
        private final SynchronizationType synchronizationType;
        private final int hash;

        private UnitKey(final String unitName, final SynchronizationType synchronizationType) {
            this.unitName = unitName;
            this.synchronizationType = synchronizationType;
            this.hash = 31 * unitName.hashCode() + synchronizationType.hashCode();
        }

        @Override
        public boolean equals(final Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }

            final UnitKey unitKey = UnitKey.class.cast(o);
            return unitName.equals(unitKey.unitName) && synchronizationType == unitKey.synchronizationType;

        }

        @Override
        public int hashCode() {
            return hash;
        }
    }
}
