package org.skywalking.apm.agent.core.queue;

import com.lmax.disruptor.EventHandler;
import com.lmax.disruptor.RingBuffer;
import com.lmax.disruptor.dsl.Disruptor;
import com.lmax.disruptor.util.DaemonThreadFactory;
import org.skywalking.apm.agent.core.conf.Config;
import org.skywalking.apm.agent.core.boot.StatusBootService;
import org.skywalking.apm.agent.core.context.TracerContext;
import org.skywalking.apm.agent.core.context.TracerContextListener;
import org.skywalking.apm.logging.ILog;
import org.skywalking.apm.logging.LogManager;
import org.skywalking.apm.trace.TraceSegment;

import java.util.LinkedList;
import java.util.List;

/**
 * {@link TraceSegmentProcessQueue} is a proxy of {@link Disruptor}, High Performance Inter-Thread MQ.
 * <p>
 * {@see https://github.com/LMAX-Exchange/disruptor}
 * <p>
 * Created by wusheng on 2017/2/17.
 */
public class TraceSegmentProcessQueue extends StatusBootService implements TracerContextListener, EventHandler<TraceSegmentHolder> {
    private static final ILog logger = LogManager.getLogger(TraceSegmentProcessQueue.class);

    private Disruptor<TraceSegmentHolder> disruptor;
    private RingBuffer<TraceSegmentHolder> buffer;
    private TraceSegment[] secondLevelCache;
    private volatile int cacheIndex;

    public TraceSegmentProcessQueue() {
        disruptor = new Disruptor<TraceSegmentHolder>(TraceSegmentHolder.Factory.INSTANCE, Config.Buffer.SIZE, DaemonThreadFactory.INSTANCE);
        secondLevelCache = new TraceSegment[Config.Buffer.SIZE];
        cacheIndex = 0;
        disruptor.handleEventsWith(this);
        buffer = disruptor.getRingBuffer();
    }

    @Override
    protected void bootUpWithStatus() {
        TracerContext.ListenerManager.add(this);
        disruptor.start();
    }

    /**
     * Append the given traceSegment to the queue, wait for sending to Collector.
     *
     * @param traceSegment finished {@link TraceSegment}
     */
    @Override
    public void afterFinished(TraceSegment traceSegment) {
        if (isStarted() && traceSegment.isSampled()) {
            long sequence = this.buffer.next();  // Grab the next sequence
            try {
                TraceSegmentHolder data = this.buffer.get(sequence);
                data.setValue(traceSegment);
            } finally {
                this.buffer.publish(sequence);
            }
        }
    }

    @Override
    public void onEvent(TraceSegmentHolder event, long sequence, boolean endOfBatch) throws Exception {
        TraceSegment traceSegment = event.getValue();
        try {
            if (secondLevelCache[cacheIndex] == null) {
                secondLevelCache[cacheIndex] = traceSegment;
            } else {
                /**
                 * If your application has very high throughput(also called tps/qps),
                 * this log message will be output in very high frequency.
                 * And this is not suppose to happen. Disable log.warn or expend {@link Config.Disruptor.BUFFER_SIZE}
                 */
                logger.warn("TraceSegmentProcessQueue has data conflict. Discard the new TraceSegment.");
            }
            /**
             * increase the {@link #cacheIndex}, if it is out of range, reset it.
             */
            cacheIndex++;
            if (cacheIndex == secondLevelCache.length) {
                cacheIndex = 0;
            }
        } finally {
            event.clear();
        }
    }

    public List<TraceSegment> getCachedTraceSegments() {
        List<TraceSegment> segmentList = new LinkedList<TraceSegment>();
        for (int i = 0; i < secondLevelCache.length; i++) {
            TraceSegment segment = secondLevelCache[i];
            if (segment != null) {
                segmentList.add(segment);
                secondLevelCache[i] = null;
            }
        }
        return segmentList;
    }
}
