/*
 * 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.activemq.artemis.core.persistence.impl.journal;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

import org.apache.activemq.artemis.api.core.ActiveMQException;
import org.apache.activemq.artemis.api.core.ActiveMQExceptionType;
import org.apache.activemq.artemis.core.io.IOCallback;
import org.apache.activemq.artemis.core.journal.impl.SimpleWaitIOCallback;
import org.apache.activemq.artemis.core.persistence.OperationContext;
import org.apache.activemq.artemis.core.server.ActiveMQServerLogger;
import org.apache.activemq.artemis.utils.ExecutorFactory;

/**
 * Each instance of OperationContextImpl is associated with an executor (usually an ordered Executor).
 *
 * Tasks are hold until the operations are complete and executed in the natural order as soon as the operations are returned
 * from replication and storage.
 *
 * If there are no pending IO operations, the tasks are just executed at the callers thread without any context switch.
 *
 * So, if you are doing operations that are not dependent on IO (e.g NonPersistentMessages) you wouldn't have any context switch.
 */
public class OperationContextImpl implements OperationContext {

   private static final ThreadLocal<OperationContext> threadLocalContext = new ThreadLocal<>();

   public static void clearContext() {
      OperationContextImpl.threadLocalContext.set(null);
   }

   public static final OperationContext getContext() {
      return getContext(null);
   }

   public static OperationContext getContext(final ExecutorFactory executorFactory) {
      OperationContext token = OperationContextImpl.threadLocalContext.get();
      if (token == null) {
         if (executorFactory == null) {
            return null;
         } else {
            token = new OperationContextImpl(executorFactory.getExecutor());
            OperationContextImpl.threadLocalContext.set(token);
         }
      }
      return token;
   }

   public static void setContext(final OperationContext context) {
      OperationContextImpl.threadLocalContext.set(context);
   }

   private List<TaskHolder> tasks;
   private List<TaskHolder> storeOnlyTasks;

   private long minimalStore = Long.MAX_VALUE;
   private long minimalReplicated = Long.MAX_VALUE;
   private long minimalPage = Long.MAX_VALUE;

   private final AtomicLong storeLineUp = new AtomicLong(0);
   private final AtomicLong replicationLineUp = new AtomicLong(0);
   private final AtomicLong pageLineUp = new AtomicLong(0);

   private long stored = 0;
   private long replicated = 0;
   private long paged = 0;

   private int errorCode = -1;

   private String errorMessage = null;

   private final Executor executor;

   private final AtomicInteger executorsPending = new AtomicInteger(0);

   public OperationContextImpl(final Executor executor) {
      super();
      this.executor = executor;
   }

   @Override
   public void pageSyncLineUp() {
      pageLineUp.incrementAndGet();
   }

   @Override
   public synchronized void pageSyncDone() {
      paged++;
      checkTasks();
   }

   @Override
   public void storeLineUp() {
      storeLineUp.incrementAndGet();
   }

   @Override
   public void replicationLineUp() {
      replicationLineUp.incrementAndGet();
   }

   @Override
   public synchronized void replicationDone() {
      replicated++;
      checkTasks();
   }

   @Override
   public void executeOnCompletion(IOCallback runnable) {
      executeOnCompletion(runnable, false);
   }

   @Override
   public void executeOnCompletion(final IOCallback completion, final boolean storeOnly) {
      if (errorCode != -1) {
         completion.onError(errorCode, errorMessage);
         return;
      }

      boolean executeNow = false;

      synchronized (this) {
         final int UNDEFINED = Integer.MIN_VALUE;
         int storeLined = UNDEFINED;
         int pageLined = UNDEFINED;
         int replicationLined = UNDEFINED;
         if (storeOnly) {
            if (storeOnlyTasks == null) {
               storeOnlyTasks = new LinkedList<>();
            }
         } else {
            if (tasks == null) {
               tasks = new LinkedList<>();
               minimalReplicated = (replicationLined = replicationLineUp.intValue());
               minimalStore = (storeLined = storeLineUp.intValue());
               minimalPage = (pageLined = pageLineUp.intValue());
            }
         }
         //On the next branches each of them is been used
         if (replicationLined == UNDEFINED) {
            replicationLined = replicationLineUp.intValue();
            storeLined = storeLineUp.intValue();
            pageLined = pageLineUp.intValue();
         }
         // On this case, we can just execute the context directly

         if (replicationLined == replicated && storeLined == stored && pageLined == paged) {
            // We want to avoid the executor if everything is complete...
            // However, we can't execute the context if there are executions pending
            // We need to use the executor on this case
            if (executorsPending.get() == 0) {
               // No need to use an executor here or a context switch
               // there are no actions pending.. hence we can just execute the task directly on the same thread
               executeNow = true;
            } else {
               execute(completion);
            }
         } else {
            if (storeOnly) {
               storeOnlyTasks.add(new TaskHolder(completion, storeLined, replicationLined, pageLined));
            } else {
               tasks.add(new TaskHolder(completion, storeLined, replicationLined, pageLined));
            }
         }
      }

      if (executeNow) {
         // Executing outside of any locks
         completion.done();
      }

   }

   @Override
   public synchronized void done() {
      stored++;
      checkTasks();
   }

   private void checkTasks() {

      if (storeOnlyTasks != null) {
         Iterator<TaskHolder> iter = storeOnlyTasks.iterator();
         while (iter.hasNext()) {
            TaskHolder holder = iter.next();
            if (stored >= holder.storeLined) {
               // If set, we use an executor to avoid the server being single threaded
               execute(holder.task);

               iter.remove();
            }
         }
      }

      if (stored >= minimalStore && replicated >= minimalReplicated && paged >= minimalPage) {
         Iterator<TaskHolder> iter = tasks.iterator();
         while (iter.hasNext()) {
            TaskHolder holder = iter.next();
            if (stored >= holder.storeLined && replicated >= holder.replicationLined && paged >= holder.pageLined) {
               // If set, we use an executor to avoid the server being single threaded
               execute(holder.task);

               iter.remove();
            } else {
               // End of list here. No other task will be completed after this
               break;
            }
         }
      }
   }

   /**
    * @param task
    */
   private void execute(final IOCallback task) {
      executorsPending.incrementAndGet();
      try {
         executor.execute(new Runnable() {
            @Override
            public void run() {
               try {
                  // If any IO is done inside the callback, it needs to be done on a new context
                  OperationContextImpl.clearContext();
                  task.done();
               } finally {
                  executorsPending.decrementAndGet();
               }
            }
         });
      } catch (Throwable e) {
         ActiveMQServerLogger.LOGGER.errorExecutingAIOCallback(e);
         executorsPending.decrementAndGet();
         task.onError(ActiveMQExceptionType.INTERNAL_ERROR.getCode(), "It wasn't possible to complete IO operation - " + e.getMessage());
      }
   }

   /*
    * (non-Javadoc)
    * @see org.apache.activemq.artemis.core.replication.ReplicationToken#complete()
    */
   public void complete() {
   }

   @Override
   public synchronized void onError(final int errorCode, final String errorMessage) {
      this.errorCode = errorCode;
      this.errorMessage = errorMessage;

      if (tasks != null) {
         Iterator<TaskHolder> iter = tasks.iterator();
         while (iter.hasNext()) {
            TaskHolder holder = iter.next();
            holder.task.onError(errorCode, errorMessage);
            iter.remove();
         }
      }
   }

   static final class TaskHolder {

      @Override
      public String toString() {
         return "TaskHolder [storeLined=" + storeLined +
            ", replicationLined=" +
            replicationLined +
            ", pageLined=" +
            pageLined +
            ", task=" +
            task +
            "]";
      }

      final int storeLined;
      final int replicationLined;
      final int pageLined;

      final IOCallback task;

      TaskHolder(final IOCallback task, int storeLined, int replicationLined, int pageLined) {
         this.storeLined = storeLined;
         this.replicationLined = replicationLined;
         this.pageLined = pageLined;
         this.task = task;
      }
   }

   @Override
   public void waitCompletion() throws Exception {
      waitCompletion(0);
   }

   @Override
   public boolean waitCompletion(final long timeout) throws InterruptedException, ActiveMQException {
      SimpleWaitIOCallback waitCallback = new SimpleWaitIOCallback();
      executeOnCompletion(waitCallback);
      complete();
      if (timeout == 0) {
         waitCallback.waitCompletion();
         return true;
      } else {
         return waitCallback.waitCompletion(timeout);
      }
   }

   @Override
   public String toString() {
      StringBuffer buffer = new StringBuffer();
      if (tasks != null) {
         for (TaskHolder hold : tasks) {
            buffer.append("Task = " + hold + "\n");
         }
      }

      return "OperationContextImpl [" + hashCode() + "] [minimalStore=" + minimalStore +
         ", storeLineUp=" +
         storeLineUp +
         ", stored=" +
         stored +
         ", minimalReplicated=" +
         minimalReplicated +
         ", replicationLineUp=" +
         replicationLineUp +
         ", replicated=" +
         replicated +
         ", paged=" +
         paged +
         ", minimalPage=" +
         minimalPage +
         ", pageLineUp=" +
         pageLineUp +
         ", errorCode=" +
         errorCode +
         ", errorMessage=" +
         errorMessage +
         ", executorsPending=" +
         executorsPending +
         ", executor=" + this.executor +
         "]" + buffer.toString();
   }
}