/*
 * 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.solr.handler;

import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.ExecutorUtil;
import org.apache.solr.util.DefaultSolrThreadFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.invoke.MethodHandles;
import java.util.concurrent.*;

/**
 * Schedule the execution of the {@link org.apache.solr.handler.CdcrReplicator} threads at
 * regular time interval. It relies on a queue of {@link org.apache.solr.handler.CdcrReplicatorState} in
 * order to avoid that one {@link org.apache.solr.handler.CdcrReplicatorState} is used by two threads at the same
 * time.
 */
class CdcrReplicatorScheduler {

  private boolean isStarted = false;

  private ScheduledExecutorService scheduler;
  private ExecutorService replicatorsPool;

  private final CdcrReplicatorManager replicatorManager;
  private final ConcurrentLinkedQueue<CdcrReplicatorState> statesQueue;

  private int poolSize = DEFAULT_POOL_SIZE;
  private int timeSchedule = DEFAULT_TIME_SCHEDULE;
  private int batchSize = DEFAULT_BATCH_SIZE;

  private static final int DEFAULT_POOL_SIZE = 2;
  private static final int DEFAULT_TIME_SCHEDULE = 10;
  private static final int DEFAULT_BATCH_SIZE = 128;

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

  CdcrReplicatorScheduler(final CdcrReplicatorManager replicatorStatesManager, final SolrParams replicatorConfiguration) {
    this.replicatorManager = replicatorStatesManager;
    this.statesQueue = new ConcurrentLinkedQueue<>(replicatorManager.getReplicatorStates());
    if (replicatorConfiguration != null) {
      poolSize = replicatorConfiguration.getInt(CdcrParams.THREAD_POOL_SIZE_PARAM, DEFAULT_POOL_SIZE);
      timeSchedule = replicatorConfiguration.getInt(CdcrParams.SCHEDULE_PARAM, DEFAULT_TIME_SCHEDULE);
      batchSize = replicatorConfiguration.getInt(CdcrParams.BATCH_SIZE_PARAM, DEFAULT_BATCH_SIZE);
    }
  }

  void start() {
    if (!isStarted) {
      scheduler = Executors.newSingleThreadScheduledExecutor(new DefaultSolrThreadFactory("cdcr-scheduler"));
      replicatorsPool = ExecutorUtil.newMDCAwareFixedThreadPool(poolSize, new DefaultSolrThreadFactory("cdcr-replicator"));

      // the scheduler thread is executed every second and submits one replication task
      // per available state in the queue
      scheduler.scheduleWithFixedDelay(() -> {
        int nCandidates = statesQueue.size();
        for (int i = 0; i < nCandidates; i++) {
          // a thread that poll one state from the queue, execute the replication task, and push back
          // the state in the queue when the task is completed
          replicatorsPool.execute(() -> {
            CdcrReplicatorState state = statesQueue.poll();
            assert state != null; // Should never happen
            try {
              if (!state.isBootstrapInProgress()) {
                new CdcrReplicator(state, batchSize).run();
              } else  {
                log.debug("Replicator state is bootstrapping, skipping replication for target collection {}", state.getTargetCollection());
              }
            } finally {
              statesQueue.offer(state);
            }
          });

        }
      }, 0, timeSchedule, TimeUnit.MILLISECONDS);
      isStarted = true;
    }
  }

  void shutdown() {
    if (isStarted) {
      // interrupts are often dangerous in Lucene / Solr code, but the
      // test for this will leak threads without
      replicatorsPool.shutdown();
      try {
        replicatorsPool.awaitTermination(60, TimeUnit.SECONDS);
      } catch (InterruptedException e) {
        log.warn("Thread interrupted while waiting for CDCR replicator threadpool close.");
        Thread.currentThread().interrupt();
      } finally {
        scheduler.shutdownNow();
        isStarted = false;
      }
    }
  }

}

