/*
 * Copyright 2014 NAVER Corp.
 *
 * Licensed 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 com.navercorp.pinpoint.profiler.sender;

import java.util.Collection;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.navercorp.pinpoint.common.util.PinpointThreadFactory;

/**
 * @author emeroad
 */
public class AsyncQueueingExecutor<T> implements Runnable {

    private static final AsyncQueueingExecutorListener EMPTY_LISTENER = new EmptyAsyncQueueingExecutorListener();

    private final Logger logger = LoggerFactory.getLogger(this.getClass());
    private final boolean isWarn = logger.isWarnEnabled();

    private final LinkedBlockingQueue<T> queue;
    private final AtomicBoolean isRun = new AtomicBoolean(true);
    private final Thread executeThread;
    private final String executorName;

    private final int maxDrainSize;
    // 주의 single thread용임. ArrayList보다 더 단순한 오퍼레이션을 수행하는 Collection.
    private final Collection<T> drain;

    private AsyncQueueingExecutorListener<T> listener = EMPTY_LISTENER;


    public AsyncQueueingExecutor() {
        this(1024 * 5, "Pinpoint-AsyncQueueingExecutor");
    }

    public AsyncQueueingExecutor(int queueSize, String executorName) {
        if (executorName == null) {
            throw new NullPointerException("executorName must not be null");
        }
        this.queue = new LinkedBlockingQueue<T>(queueSize);
        this.executeThread = this.createExecuteThread(executorName);
        this.executorName = executeThread.getName();

        this.maxDrainSize = 10;
        this.drain = new UnsafeArrayCollection<T>(maxDrainSize);
    }

    private Thread createExecuteThread(String executorName) {
        final ThreadFactory threadFactory = new PinpointThreadFactory(executorName, true);
        Thread thread = threadFactory.newThread(this);
        thread.start();
        return thread;
    }

    @Override
    public void run() {
        logger.info("{} started.", executorName);
        doExecute();
    }

    private void doExecute() {
        drainStartEntry:
        while (isRun()) {
            try {
                Collection<T> dtoList = getDrainQueue();
                int drainSize = takeN(dtoList, this.maxDrainSize);
                if (drainSize > 0) {
                    doExecute(dtoList);
                    continue;
                }

                while (isRun()) {
                    T dto = takeOne();
                    if (dto != null) {
                        doExecute(dto);
                        continue drainStartEntry;
                    }
                }
            } catch (Throwable th) {
                logger.warn("{} doExecute(). Unexpected Error. Cause:{}", executorName, th.getMessage(), th);
            }
        }
        flushQueue();
    }

    private void flushQueue() {
        boolean debugEnabled = logger.isDebugEnabled();
        if (debugEnabled) {
            logger.debug("Loop is stop.");
        }
        while(true) {
            Collection<T> dtoList = getDrainQueue();
           int drainSize = takeN(dtoList, this.maxDrainSize);
            if (drainSize == 0) {
                break;
            }
            if (debugEnabled) {
                logger.debug("flushData size {}", drainSize);
            }
            doExecute(dtoList);
        }
    }

    protected T takeOne() {
        try {
            return queue.poll(1000 * 2, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return null;
        }
    }

    protected int takeN(Collection<T> drain, int maxDrainSize) {
        return queue.drainTo(drain, maxDrainSize);
    }

    public boolean execute(T data) {
        if (data == null) {
            if (isWarn) {
                logger.warn("execute(). data is null");
            }
            return false;
        }
        if (!isRun.get()) {
            if (isWarn) {
                logger.warn("{} is shutdown. discard data:{}", executorName, data);
            }
            return false;
        }
        boolean offer = queue.offer(data);
        if (!offer) {
            if (isWarn) {
                logger.warn("{} Drop data. queue is full. size:{}", executorName, queue.size());
            }
        }
        return offer;
    }

    public void setListener(AsyncQueueingExecutorListener<T> listener) {
        if (listener == null) {
            throw new NullPointerException("listener must not be null");
        }
        this.listener = listener;
    }

    private void doExecute(Collection<T> dtoList) {
        this.listener.execute(dtoList);
    }

    private void doExecute(T dto) {
        this.listener.execute(dto);
    }

    public boolean isEmpty() {
        return queue.isEmpty();
    }

    public boolean isRun() {
        return isRun.get();
    }

    public void stop() {
        isRun.set(false);

        if (!isEmpty()) {
            logger.info("Wait 5 seconds. Flushing queued data.");
        }
        executeThread.interrupt();
        try {
            executeThread.join(5000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            logger.warn("{} stopped incompletely.", executorName);
        }

        logger.info("{} stopped.", executorName);
    }

    Collection<T> getDrainQueue() {
        this.drain.clear();
        return drain;
    }
}

@@ -2011,32 +2011,167 @@
 ll%22);%0A        %7D%0A
+        // before executeThread start%0A        this.maxDrainSize = 10;%0A        this.drain = new UnsafeArrayCollection%3CT%3E(maxDrainSize);%0A
         this.que
@@ -2210,24 +2210,25 @@
 queueSize);%0A
+%0A
         this
@@ -2340,106 +2340,8 @@
 e();
-%0A%0A        this.maxDrainSize = 10;%0A        this.drain = new UnsafeArrayCollection%3CT%3E(maxDrainSize);
 %0A   

fix initialize error