/*
 * 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.ActiveMQServerLogger;
import org.apache.activemq.artemis.utils.DataConstants;
import org.apache.activemq.artemis.utils.IDGenerator;
import org.jboss.logging.Logger;

/**
 * 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 = Logger.getLogger(BatchingIDGenerator.class);

   private final AtomicLong counter;

   private final long checkpointSize;

   private volatile long nextID;

   private final StorageManager storageManager;

   private List<Long> cleanupRecords = null;

   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 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) {
      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;

      counter.set(nextID);
   }

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

   }

   public long generateID() {
      long id = counter.getAndIncrement();

      if (id >= nextID) {
         saveCheckPoint(id);
      }
      return id;
   }

   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 gerated.
            // 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);
   }

   // Inner classes -------------------------------------------------

   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() {
      }

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

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

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