/*
 * 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.utils.critical;

import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ConcurrentModificationException;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import org.apache.activemq.artemis.core.server.ActiveMQScheduledComponent;
import org.apache.activemq.artemis.utils.ActiveMQThreadFactory;
import org.apache.activemq.artemis.utils.collections.ConcurrentHashSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.invoke.MethodHandles;

public class CriticalAnalyzerImpl implements CriticalAnalyzer {

   private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

   private volatile long timeoutNanoSeconds;

   // one minute by default.. the server will change it for sure
   private volatile long checkTimeNanoSeconds = TimeUnit.SECONDS.toNanos(60);

   private final ActiveMQScheduledComponent scheduledComponent;

   private final AtomicBoolean running = new AtomicBoolean(false);

   public CriticalAnalyzerImpl() {
      // this will make the scheduled component to start its own pool

      /* Important: The scheduled component should have its own thread pool...
       *  otherwise in case of a deadlock, or a starvation of the server the analyzer won't pick up any
       *  issues and won't be able to shutdown the server or halt the VM
       */
      this.scheduledComponent = new ActiveMQScheduledComponent(null, null, checkTimeNanoSeconds, TimeUnit.NANOSECONDS, false) {

         @Override
         public void run() {
            logger.trace("Checking critical analyzer");
            check();
         }

         @Override
         protected ActiveMQThreadFactory getThreadFactory() {
            return new ActiveMQThreadFactory("CriticalAnalyzer", "Critical-Analyzer-", true, getThisClassLoader());
         }

         private ClassLoader getThisClassLoader() {
            return AccessController.doPrivileged((PrivilegedAction<ClassLoader>) () -> CriticalAnalyzerImpl.this.getClass().getClassLoader());

         }

      };

   }

   @Override
   public void clear() {
      actions.clear();
      components.clear();
   }

   private CopyOnWriteArrayList<CriticalAction> actions = new CopyOnWriteArrayList<>();

   private final ConcurrentHashSet<CriticalComponent> components = new ConcurrentHashSet<>();

   @Override
   public int getNumberOfComponents() {
      return components.size();
   }

   @Override
   public boolean isMeasuring() {
      return true;
   }

   @Override
   public void add(CriticalComponent component) {
      components.add(component);
   }

   @Override
   public void remove(CriticalComponent component) {
      components.remove(component);
   }

   @Override
   public CriticalAnalyzer setCheckTime(long timeout, TimeUnit unit) {
      this.checkTimeNanoSeconds = unit.toNanos(timeout);
      this.scheduledComponent.setPeriod(timeout, unit);
      return this;
   }

   @Override
   public long getCheckTimeNanoSeconds() {
      if (checkTimeNanoSeconds == 0) {
         checkTimeNanoSeconds = getTimeout(TimeUnit.NANOSECONDS) / 2;
      }
      return checkTimeNanoSeconds;
   }

   @Override
   public CriticalAnalyzer setTimeout(long timeout, TimeUnit unit) {
      if (checkTimeNanoSeconds <= 0) {
         this.setCheckTime(timeout / 2, unit);
      }
      this.timeoutNanoSeconds = unit.toNanos(timeout);
      return this;
   }

   @Override
   public long getTimeout(TimeUnit unit) {
      if (timeoutNanoSeconds == 0) {
         timeoutNanoSeconds = TimeUnit.MINUTES.toNanos(2);
      }
      return unit.convert(timeoutNanoSeconds, TimeUnit.NANOSECONDS);
   }

   @Override
   public long getTimeoutNanoSeconds() {
      return timeoutNanoSeconds;
   }

   @Override
   public CriticalAnalyzer addAction(CriticalAction action) {
      this.actions.add(action);
      return this;
   }

   @Override
   public void check() {
      boolean retry = true;
      while (retry) {
         try {
            for (CriticalComponent component : components) {

               if (component.checkExpiration(timeoutNanoSeconds, true)) {
                  fireActions(component);
                  // no need to keep running if there's already a component failed
                  return;
               }
            }
            retry = false; // got to the end of the list, no need to retry
         } catch (ConcurrentModificationException dontCare) {
            // lets retry on the loop
         }
      }
   }

   protected void fireActions(CriticalComponent component) {
      for (CriticalAction action : actions) {
         try {
            action.run(component);
         } catch (Throwable e) {
            logger.warn(e.getMessage(), e);
         }
      }
   }

   @Override
   public void start() {
      scheduledComponent.start();
   }

   @Override
   public void stop() {
      scheduledComponent.stop();
   }

   @Override
   public boolean isStarted() {
      return scheduledComponent.isStarted();
   }
}
