/*
 * Copyright (C) 2015 AlexMofer
 *
 * 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 am.project.support.job;

import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;

import java.util.ArrayList;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Executor
 * Created by Alex on 2017/9/11.
 */

class JobExecutor extends ThreadPoolExecutor
        implements Handler.Callback {

    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    // We want at least 2 threads and at most 4 threads in the core pool,
    // preferring to have 1 less than the CPU count to avoid saturating
    // the CPU with background work
    private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
    private static final int KEEP_ALIVE_SECONDS = 30;

    private static final ThreadFactory sThreadFactory = new ThreadFactory() {
        private final AtomicInteger mCount = new AtomicInteger(1);

        @Override
        public Thread newThread(@SuppressWarnings("NullableProblems") Runnable r) {
            return new Thread(r, "Job #" + mCount.getAndIncrement());
        }
    };

    private static final BlockingQueue<Runnable> sPoolWorkQueue =
            new PriorityBlockingQueue<>(128);

    private static final BlockingQueue<Runnable> sSinglePoolWorkQueue =
            new PriorityBlockingQueue<>(128);
    private static final ArrayList<MessageTag> MESSAGE_TAGS = new ArrayList<>();
    private static final int MSG_RESULT = 100;
    private static final int MSG_PROGRESS = 101;
    /**
     * An {@link Executor} that can be used to execute jobs in parallel.
     */
    private static Executor JOB_EXECUTOR;
    /**
     * An {@link Executor} that can be used to execute jobs in parallel.
     */
    private static Executor SINGLE_EXECUTOR;
    private final Handler mHandler = new Handler(Looper.getMainLooper(), this);

    private JobExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                        BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
    }

    private static MessageTag getMessageTag() {
        MessageTag tag;
        synchronized (MESSAGE_TAGS) {
            if (MESSAGE_TAGS.isEmpty()) {
                tag = new MessageTag();
            } else {
                tag = MESSAGE_TAGS.remove(MESSAGE_TAGS.size() - 1);
            }
        }
        return tag;
    }

    static Executor getDefault() {
        if (JOB_EXECUTOR == null) {
            JobExecutor jobExecutor = new JobExecutor(
                    CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
                    sPoolWorkQueue, sThreadFactory);
            if (Build.VERSION.SDK_INT >= 9)
                jobExecutor.allowCoreThreadTimeOut(true);
            JOB_EXECUTOR = jobExecutor;
        }
        return JOB_EXECUTOR;
    }

    static Executor getSingle() {
        if (SINGLE_EXECUTOR == null) {
            JobExecutor singleExecutor = new JobExecutor(1, 1,
                    KEEP_ALIVE_SECONDS, TimeUnit.SECONDS, sSinglePoolWorkQueue, sThreadFactory);
            if (Build.VERSION.SDK_INT >= 9)
                singleExecutor.allowCoreThreadTimeOut(true);
            SINGLE_EXECUTOR = singleExecutor;
        }
        return SINGLE_EXECUTOR;
    }

    static Executor getExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
                                TimeUnit unit, BlockingQueue<Runnable> workQueue,
                                ThreadFactory threadFactory, boolean allowCoreThreadTimeOut) {
        JobExecutor singleExecutor = new JobExecutor(corePoolSize, maximumPoolSize,
                keepAliveTime, unit, workQueue, threadFactory);
        if (Build.VERSION.SDK_INT >= 9)
            singleExecutor.allowCoreThreadTimeOut(allowCoreThreadTimeOut);
        return singleExecutor;
    }

    @Override
    protected void beforeExecute(Thread t, Runnable r) {
        super.beforeExecute(t, r);
        if (r instanceof JobHolder) {
            ((JobHolder) r).attachJobExecutor(this);
        }
    }

    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);
        if (r instanceof JobHolder) {
            ((JobHolder) r).detachJobExecutor();
        }
    }

    @Override
    public boolean handleMessage(Message msg) {
        switch (msg.what) {
            case MSG_RESULT:
                postExecute(msg);
                break;
            case MSG_PROGRESS:
                progressUpdate(msg);
                break;
        }
        return true;
    }

    private void postExecute(Message msg) {
        if (msg.obj instanceof MessageTag) {
            MessageTag tag = (MessageTag) msg.obj;
            tag.getJob().onPostExecute();
            tag.clear();
            synchronized (MESSAGE_TAGS) {
                MESSAGE_TAGS.add(tag);
            }
        }
    }

    private void progressUpdate(Message msg) {
        if (msg.obj instanceof MessageTag) {
            MessageTag tag = (MessageTag) msg.obj;
            tag.getJob().onProgressUpdate(tag.getProgress());
            tag.clear();
            synchronized (MESSAGE_TAGS) {
                MESSAGE_TAGS.add(tag);
            }
        }
    }

    void publishResult(Job job) {
        MessageTag tag = getMessageTag();
        tag.setJob(job);
        mHandler.obtainMessage(MSG_RESULT, tag).sendToTarget();
    }

    void publishProgress(Job job, Job.Progress progress) {
        MessageTag tag = getMessageTag();
        tag.setJob(job);
        tag.setProgress(progress);
        mHandler.obtainMessage(MSG_PROGRESS, tag).sendToTarget();
    }

    private static class MessageTag {
        private Job mJob;
        private Job.Progress mProgress;

        private Job getJob() {
            return mJob;
        }

        private void setJob(Job job) {
            mJob = job;
        }

        private Job.Progress getProgress() {
            return mProgress;
        }

        private void setProgress(Job.Progress progress) {
            mProgress = progress;
        }

        private void clear() {
            mJob = null;
            mProgress = null;
        }
    }
}