/*
 * 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.component.disruptor;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;

import com.lmax.disruptor.InsufficientCapacityException;
import org.apache.camel.AsyncEndpoint;
import org.apache.camel.Category;
import org.apache.camel.Component;
import org.apache.camel.Consumer;
import org.apache.camel.Exchange;
import org.apache.camel.MultipleConsumersSupport;
import org.apache.camel.Processor;
import org.apache.camel.Producer;
import org.apache.camel.WaitForTaskToComplete;
import org.apache.camel.api.management.ManagedAttribute;
import org.apache.camel.api.management.ManagedResource;
import org.apache.camel.spi.Metadata;
import org.apache.camel.spi.UriEndpoint;
import org.apache.camel.spi.UriParam;
import org.apache.camel.spi.UriPath;
import org.apache.camel.support.DefaultEndpoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Provides asynchronous SEDA behavior using LMAX Disruptor.
 *
 * This component works much as the standard SEDA Component, but utilizes a Disruptor instead of a BlockingQueue
 * utilized by the standard SEDA.
 */
@ManagedResource(description = "Managed Disruptor Endpoint")
@UriEndpoint(firstVersion = "2.12.0", scheme = "disruptor,disruptor-vm", title = "Disruptor,Disruptor VM",
             syntax = "disruptor:name", category = { Category.ENDPOINT })
public class DisruptorEndpoint extends DefaultEndpoint implements AsyncEndpoint, MultipleConsumersSupport {
    public static final String DISRUPTOR_IGNORE_EXCHANGE = "disruptor.ignoreExchange";
    private static final Logger LOGGER = LoggerFactory.getLogger(DisruptorEndpoint.class);

    private final Set<DisruptorProducer> producers = new CopyOnWriteArraySet<>();
    private final Set<DisruptorConsumer> consumers = new CopyOnWriteArraySet<>();
    private final DisruptorReference disruptorReference;

    @UriPath(description = "Name of queue")
    @Metadata(required = true)
    private String name;
    @UriParam(label = "consumer", defaultValue = "1")
    private int concurrentConsumers;
    @UriParam(label = "consumer")
    private boolean multipleConsumers;
    @UriParam(label = "producer", defaultValue = "IfReplyExpected")
    private WaitForTaskToComplete waitForTaskToComplete = WaitForTaskToComplete.IfReplyExpected;
    @UriParam(label = "producer", defaultValue = "30000", javaType = "java.time.Duration")
    private long timeout = 30000;
    @UriParam(defaultValue = "" + DisruptorComponent.DEFAULT_BUFFER_SIZE)
    private int size;
    @UriParam(label = "producer")
    private boolean blockWhenFull;
    @UriParam(label = "consumer", defaultValue = "Blocking")
    private DisruptorWaitStrategy waitStrategy;
    @UriParam(label = "producer", defaultValue = "Multi")
    private DisruptorProducerType producerType;

    public DisruptorEndpoint(final String endpointUri, final Component component,
                             final DisruptorReference disruptorReference) {
        super(endpointUri, component);
        this.disruptorReference = disruptorReference;
        this.name = disruptorReference.getName();
    }

    @ManagedAttribute(description = "Queue name")
    public String getName() {
        return name;
    }

    @ManagedAttribute(description = "Buffer max capacity")
    public int getBufferSize() {
        return disruptorReference.getBufferSize();
    }

    @ManagedAttribute(description = "Remaining capacity in ring buffer")
    public long getRemainingCapacity() throws DisruptorNotStartedException {
        return getDisruptor().getRemainingCapacity();
    }

    @ManagedAttribute(description = "Amount of pending exchanges waiting for consumption in ring buffer")
    public long getPendingExchangeCount() throws DisruptorNotStartedException {
        return getDisruptor().getPendingExchangeCount();
    }

    @ManagedAttribute(description = "Number of concurrent consumers")
    public int getConcurrentConsumers() {
        return concurrentConsumers;
    }

    /**
     * Number of concurrent threads processing exchanges.
     */
    public void setConcurrentConsumers(int concurrentConsumers) {
        this.concurrentConsumers = concurrentConsumers;
    }

    @ManagedAttribute(description = "Option to specify whether the caller should wait for the async task to complete or not before continuing")
    public WaitForTaskToComplete getWaitForTaskToComplete() {
        return waitForTaskToComplete;
    }

    /**
     * Option to specify whether the caller should wait for the async task to complete or not before continuing. The
     * following three options are supported: Always, Never or IfReplyExpected. The first two values are
     * self-explanatory. The last value, IfReplyExpected, will only wait if the message is Request Reply based.
     */
    public void setWaitForTaskToComplete(final WaitForTaskToComplete waitForTaskToComplete) {
        this.waitForTaskToComplete = waitForTaskToComplete;
    }

    @ManagedAttribute(description = "Timeout (in milliseconds) before a producer will stop waiting for an asynchronous task to complete")
    public long getTimeout() {
        return timeout;
    }

    /**
     * Timeout (in milliseconds) before a producer will stop waiting for an asynchronous task to complete. You can
     * disable timeout by using 0 or a negative value.
     */
    public void setTimeout(final long timeout) {
        this.timeout = timeout;
    }

    @ManagedAttribute(description = "The maximum capacity of the Disruptors ringbuffer")
    public int getSize() {
        return size;
    }

    /**
     * The maximum capacity of the Disruptors ringbuffer Will be effectively increased to the nearest power of two.
     * Notice: Mind if you use this option, then its the first endpoint being created with the queue name, that
     * determines the size. To make sure all endpoints use same size, then configure the size option on all of them, or
     * the first endpoint being created.
     */
    public void setSize(int size) {
        this.size = size;
    }

    @Override
    @ManagedAttribute(description = "Specifies whether multiple consumers are allowed")
    public boolean isMultipleConsumersSupported() {
        return isMultipleConsumers();
    }

    public boolean isMultipleConsumers() {
        return multipleConsumers;
    }

    /**
     * Specifies whether multiple consumers are allowed. If enabled, you can use Disruptor for Publish-Subscribe
     * messaging. That is, you can send a message to the queue and have each consumer receive a copy of the message.
     * When enabled, this option should be specified on every consumer endpoint.
     */
    public void setMultipleConsumers(boolean multipleConsumers) {
        this.multipleConsumers = multipleConsumers;
    }

    /**
     * Returns the current active consumers on this endpoint
     */
    public Set<DisruptorConsumer> getConsumers() {
        return Collections.unmodifiableSet(consumers);
    }

    /**
     * Returns the current active producers on this endpoint
     */
    public Set<DisruptorProducer> getProducers() {
        return Collections.unmodifiableSet(producers);
    }

    @ManagedAttribute
    public boolean isBlockWhenFull() {
        return blockWhenFull;
    }

    /**
     * Whether a thread that sends messages to a full Disruptor will block until the ringbuffer's capacity is no longer
     * exhausted. By default, the calling thread will block and wait until the message can be accepted. By disabling
     * this option, an exception will be thrown stating that the queue is full.
     */
    public void setBlockWhenFull(boolean blockWhenFull) {
        this.blockWhenFull = blockWhenFull;
    }

    @ManagedAttribute(description = "Defines the strategy used by consumer threads to wait on new exchanges to be published")
    public DisruptorWaitStrategy getWaitStrategy() {
        return waitStrategy;
    }

    /**
     * Defines the strategy used by consumer threads to wait on new exchanges to be published. The options allowed
     * are:Blocking, Sleeping, BusySpin and Yielding.
     */
    public void setWaitStrategy(DisruptorWaitStrategy waitStrategy) {
        this.waitStrategy = waitStrategy;
    }

    @ManagedAttribute(description = " Defines the producers allowed on the Disruptor")
    public DisruptorProducerType getProducerType() {
        return producerType;
    }

    /**
     * Defines the producers allowed on the Disruptor. The options allowed are: Multi to allow multiple producers and
     * Single to enable certain optimizations only allowed when one concurrent producer (on one thread or otherwise
     * synchronized) is active.
     */
    public void setProducerType(DisruptorProducerType producerType) {
        this.producerType = producerType;
    }

    @Override
    public Producer createProducer() throws Exception {
        if (getProducers().size() == 1 && getDisruptor().getProducerType() == DisruptorProducerType.Single) {
            throw new IllegalStateException(
                    "Endpoint can't support multiple producers when ProducerType SINGLE is configured");
        }
        return new DisruptorProducer(this, getWaitForTaskToComplete(), getTimeout(), isBlockWhenFull());
    }

    @Override
    public Consumer createConsumer(final Processor processor) throws Exception {
        return new DisruptorConsumer(this, processor);
    }

    @Override
    protected void doInit() throws Exception {
        super.doInit();
        // notify reference we are shutting down this endpoint
        disruptorReference.addEndpoint(this);
    }

    @Override
    protected void doStop() throws Exception {
        // notify reference we are shutting down this endpoint
        disruptorReference.removeEndpoint(this);
        super.doStop();
    }

    @Override
    protected void doShutdown() throws Exception {
        // notify component we are shutting down this endpoint
        if (getComponent() != null) {
            getComponent().onShutdownEndpoint(this);
        }

        super.doShutdown();
    }

    @Override
    public DisruptorComponent getComponent() {
        return (DisruptorComponent) super.getComponent();
    }

    void onStarted(final DisruptorConsumer consumer) throws Exception {
        synchronized (this) {
            // validate multiple consumers has been enabled is necessary
            if (!consumers.isEmpty() && !isMultipleConsumersSupported()) {
                throw new IllegalStateException(
                        "Multiple consumers for the same endpoint is not allowed: " + this);
            }
            if (consumers.add(consumer)) {
                LOGGER.debug("Starting consumer {} on endpoint {}", consumer, getEndpointUri());
                getDisruptor().reconfigure();
            } else {
                LOGGER.debug("Tried to start Consumer {} on endpoint {} but it was already started", consumer,
                        getEndpointUri());
            }
        }
    }

    void onStopped(final DisruptorConsumer consumer) throws Exception {
        synchronized (this) {
            if (consumers.remove(consumer)) {
                LOGGER.debug("Stopping consumer {} on endpoint {}", consumer, getEndpointUri());
                getDisruptor().reconfigure();
            } else {
                LOGGER.debug("Tried to stop Consumer {} on endpoint {} but it was already stopped", consumer, getEndpointUri());
            }
        }
    }

    void onStarted(final DisruptorProducer producer) {
        producers.add(producer);
    }

    void onStopped(final DisruptorProducer producer) {
        producers.remove(producer);
    }

    Map<DisruptorConsumer, Collection<LifecycleAwareExchangeEventHandler>> createConsumerEventHandlers() {
        Map<DisruptorConsumer, Collection<LifecycleAwareExchangeEventHandler>> result = new HashMap<>();

        for (final DisruptorConsumer consumer : consumers) {
            result.put(consumer, consumer.createEventHandlers(concurrentConsumers));
        }

        return result;
    }

    /**
     * Called by DisruptorProducers to publish new exchanges on the RingBuffer, blocking when full
     */
    void publish(final Exchange exchange) throws DisruptorNotStartedException {
        disruptorReference.publish(exchange);
    }

    /**
     * Called by DisruptorProducers to publish new exchanges on the RingBuffer, throwing InsufficientCapacityException
     * when full
     *
     * @throws InsufficientCapacityException when the Ringbuffer is full.
     */
    void tryPublish(final Exchange exchange) throws DisruptorNotStartedException, InsufficientCapacityException {
        disruptorReference.tryPublish(exchange);
    }

    DisruptorReference getDisruptor() {
        return disruptorReference;
    }

    @Override
    public boolean equals(Object object) {
        boolean result = super.equals(object);
        return result && getCamelContext().equals(((DisruptorEndpoint) object).getCamelContext());
    }

    @Override
    public int hashCode() {
        return getEndpointUri().hashCode() * 37 + getCamelContext().hashCode();
    }
}
