/*
 * 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.camel.cdi;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;

import javax.enterprise.context.spi.CreationalContext;
import javax.enterprise.event.Event;
import javax.enterprise.inject.Any;
import javax.enterprise.inject.spi.BeanManager;
import javax.enterprise.inject.spi.InjectionTarget;
import javax.enterprise.util.TypeLiteral;
import javax.inject.Inject;

import org.apache.camel.Consumer;
import org.apache.camel.Endpoint;
import org.apache.camel.Processor;
import org.apache.camel.Producer;
import org.apache.camel.support.DefaultEndpoint;

import static java.util.stream.Collectors.joining;

/**
 * A Camel {@link Endpoint} that bridges the CDI events facility with Camel routes so that CDI events can be seamlessly
 * observed / consumed (respectively produced / fired) from Camel consumers (respectively by Camel producers).
 * <p>
 *
 * The {@code CdiEventEndpoint<T>} bean can be used to observe / consume CDI events whose event type is {@code T}, for
 * example:
 * 
 * <pre>
 * <code>
 * {@literal @}Inject
 *  CdiEventEndpoint{@literal <}String{@literal >} cdiEventEndpoint;
 *
 *  from(cdiEventEndpoint).log("CDI event received: ${body}");
 * </code>
 * </pre>
 *
 * Conversely, the {@code CdiEventEndpoint<T>} bean can be used to produce / fire CDI events whose event type is
 * {@code T}, for example:
 * 
 * <pre>
 * <code>
 * {@literal @}Inject
 *  CdiEventEndpoint{@literal <}String{@literal >} cdiEventEndpoint;
 *
 *  from("direct:event").to(cdiEventEndpoint).log("CDI event sent: ${body}");
 * </code>
 * </pre>
 *
 * The type variable {@code T}, respectively the qualifiers, of a particular {@code CdiEventEndpoint<T>} injection point
 * are automatically translated into the parameterized <i>event type</i>, respectively into the <i>event qualifiers</i>,
 * e.g.:
 * 
 * <pre>
 * <code>
 * {@literal @}Inject
 * {@literal @}FooQualifier
 *  CdiEventEndpoint{@literal <}List{@literal <}String{@literal >}{@literal >} cdiEventEndpoint;
 *
 *  from("direct:event").to(cdiEventEndpoint);
 *
 *  void observeCdiEvents({@literal @}Observes {@literal @}FooQualifier List{@literal <}String{@literal >} event) {
 *      logger.info("CDI event: {}", event);
 *  }
 * </code>
 * </pre>
 */
@javax.enterprise.inject.Vetoed
public final class CdiEventEndpoint<T> extends DefaultEndpoint {

    private final List<CdiEventConsumer<T>> consumers = new ArrayList<>();

    private final Type type;

    private final Set<Annotation> qualifiers;

    private final BeanManager manager;

    CdiEventEndpoint(String endpointUri, Type type, Set<Annotation> qualifiers, BeanManager manager) {
        super(endpointUri, null);
        this.type = type;
        this.qualifiers = qualifiers;
        this.manager = manager;
    }

    static String eventEndpointUri(Type type, Set<Annotation> qualifiers) {
        return "cdi-event://" + authorityFromType(type) + qualifiers.stream()
                .map(CdiSpiHelper::createAnnotationId)
                .collect(joining("%2C", !qualifiers.isEmpty() ? "?qualifiers=" : "", ""));
    }

    private static String authorityFromType(Type type) {
        if (type instanceof Class) {
            return Class.class.cast(type).getName();
        }
        if (type instanceof ParameterizedType) {
            return Stream.of(((ParameterizedType) type).getActualTypeArguments())
                    .map(CdiEventEndpoint::authorityFromType)
                    .collect(joining("%2C", authorityFromType(((ParameterizedType) type).getRawType()) + "%3C", "%3E"));
        }
        if (type instanceof GenericArrayType) {
            return authorityFromType(((GenericArrayType) type).getGenericComponentType()) + "%5B%5D";
        }

        throw new IllegalArgumentException("Cannot create URI authority for event type [" + type + "]");
    }

    Set<Annotation> getQualifiers() {
        return qualifiers;
    }

    Type getType() {
        return type;
    }

    @Override
    public Consumer createConsumer(Processor processor) {
        return new CdiEventConsumer<>(this, processor);
    }

    @Override
    public Producer createProducer() throws IllegalAccessException {
        // FIXME: to be replaced once event firing with dynamic parameterized type
        // is properly supported (see https://issues.jboss.org/browse/CDI-516)
        TypeLiteral<T> literal = new TypeLiteral<T>() {
        };
        for (Field field : TypeLiteral.class.getDeclaredFields()) {
            if (field.getType().equals(Type.class)) {
                field.setAccessible(true);
                field.set(literal, type);
                break;
            }
        }

        InjectionTarget<AnyEvent> target = manager.createInjectionTarget(manager.createAnnotatedType(AnyEvent.class));
        CreationalContext<AnyEvent> ctx = manager.createCreationalContext(null);
        AnyEvent instance = target.produce(ctx);
        target.inject(instance, ctx);
        return new CdiEventProducer<>(
                this, instance.event
                        .select(literal, qualifiers.toArray(new Annotation[0])));
    }

    @Vetoed
    private static class AnyEvent {

        @Any
        @Inject
        private Event<Object> event;
    }

    void addConsumer(CdiEventConsumer<T> consumer) {
        synchronized (consumers) {
            consumers.add(consumer);
        }
    }

    void removeConsumer(CdiEventConsumer<T> consumer) {
        synchronized (consumers) {
            consumers.remove(consumer);
        }
    }

    void notify(T t) {
        synchronized (consumers) {
            consumers.forEach(consumer -> consumer.notify(t));
        }
    }
}
