/*
 * 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.atomic.AtomicLong;

import org.apache.activemq.artemis.api.core.ActiveMQBuffer;
import org.apache.activemq.artemis.core.journal.EncodingSupport;
import org.apache.activemq.artemis.core.persistence.StorageManager;
import org.apache.activemq.artemis.core.server.ActiveMQMessageBundle;
import org.apache.activemq.artemis.core.server.ActiveMQServerLogger;
import org.apache.activemq.artemis.utils.DataConstants;
import org.apache.activemq.artemis.utils.IDGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.invoke.MethodHandles;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * An ID generator that allocates a batch of IDs of size {@link #checkpointSize} and records the ID in the journal only
 * when starting a new batch.
 *
 * @see IDGenerator
 */
public final class BatchingIDGenerator implements IDGenerator {

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

   private final AtomicLong counter;

   private final long checkpointSize;

   private volatile long nextID;

   private boolean started = true;

   private final StorageManager storageManager;

   private List<Long> cleanupRecords = null;

   private final ReadWriteLock lock = new ReentrantReadWriteLock();

   public BatchingIDGenerator(final long start, final long checkpointSize, final StorageManager storageManager) {
      counter = new AtomicLong(start);

      // as soon as you generate the first ID, the nextID should be updated
      nextID = start;

      this.checkpointSize = checkpointSize;

      this.storageManager = storageManager;
   }

   public void stop() {
      lock.writeLock().lock();
      try {
         if (logger.isTraceEnabled()) {
            logger.trace("Stopping generator", new Exception("Trace"));
         }
         persistCurrentID();
         started = false;
      } finally {
         lock.writeLock().unlock();
      }
   }

   public void persistCurrentID() {
      final long recordID = counter.incrementAndGet();
      storeID(recordID, recordID);
   }

   /**
    * A method to cleanup old records after started
    */
   public void cleanup() {
      if (cleanupRecords != null) {
         Iterator<Long> iterRecord = cleanupRecords.iterator();
         while (iterRecord.hasNext()) {
            Long record = iterRecord.next();
            if (iterRecord.hasNext()) {
               // we don't want to remove the last record
               deleteID(record.longValue());
            }
         }
         cleanupRecords.clear(); // help GC
         cleanupRecords = null;
      }
   }

   public void loadState(final long journalID, final ActiveMQBuffer buffer) {
      lock.writeLock().lock();
      try {
         addCleanupRecord(journalID);
         IDCounterEncoding encoding = new IDCounterEncoding();

         encoding.decode(buffer);

         // Keep nextID and counter the same, the next generateID will update the checkpoint
         nextID = encoding.id + 1;

         counter.set(nextID);

         // if we are loading we are restarting it
         started = true;
      } finally {
         lock.writeLock().unlock();
      }
   }

   // for testcases
   public void forceNextID(long nextID) {
      long idJournal = counter.incrementAndGet();
      counter.set(nextID);
      storeID(idJournal, nextID);

   }

   @Override
   public long generateID() {
      lock.readLock().lock();
      try {
         if (!started) {
            if (logger.isDebugEnabled()) {
               logger.debug("BatchIDGenerator is not supposed to be used", new Exception("trace"));
            }
            throw ActiveMQMessageBundle.BUNDLE.idGeneratorStopped();
         }
         long id = counter.getAndIncrement();

         if (id >= nextID) {
            saveCheckPoint(id);
         }
         return id;
      } finally {
         lock.readLock().unlock();
      }
   }

   @Override
   public long getCurrentID() {
      return counter.get();
   }

   private synchronized void saveCheckPoint(final long id) {
      if (id >= nextID) {
         nextID += checkpointSize;

         if (!storageManager.isStarted()) {
            // This could happen after the server is stopped
            // while notifications are being sent and ID generated.
            // If the ID is intended to the journal you would know soon enough
            // so we just ignore this for now
            logger.debug("The journalStorageManager is not loaded. This is probably ok as long as it's a notification being sent after shutdown");
         } else {
            storeID(counter.getAndIncrement(), nextID);
         }
      }
   }

   private void addCleanupRecord(long id) {
      if (cleanupRecords == null) {
         cleanupRecords = new LinkedList<>();
      }

      cleanupRecords.add(id);
   }

   private void storeID(final long journalID, final long id) {
      try {
         storageManager.storeID(journalID, id);
      } catch (Exception e) {
         ActiveMQServerLogger.LOGGER.batchingIdError(e);
      }
   }

   private void deleteID(final long journalID) {
      try {
         storageManager.deleteID(journalID);
      } catch (Exception e) {
         ActiveMQServerLogger.LOGGER.batchingIdError(e);
      }
   }

   public static EncodingSupport createIDEncodingSupport(final long id) {
      return new IDCounterEncoding(id);
   }


   protected static final class IDCounterEncoding implements EncodingSupport {

      private long id;

      @Override
      public String toString() {
         return "IDCounterEncoding [id=" + id + "]";
      }

      private IDCounterEncoding(final long id) {
         this.id = id;
      }

      IDCounterEncoding() {
      }

      @Override
      public void decode(final ActiveMQBuffer buffer) {
         id = buffer.readLong();
      }

      @Override
      public void encode(final ActiveMQBuffer buffer) {
         buffer.writeLong(id);
      }

      @Override
      public int getEncodeSize() {
         return DataConstants.SIZE_LONG;
      }
   }
}
