/*
 * 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.geode.internal.cache.eviction;

import java.util.concurrent.atomic.AtomicInteger;

import org.apache.logging.log4j.Logger;

import org.apache.geode.internal.cache.BucketRegion;
import org.apache.geode.internal.cache.versions.RegionVersionVector;
import org.apache.geode.internal.logging.log4j.LogMarker;
import org.apache.geode.logging.internal.log4j.api.LogService;

abstract class AbstractEvictionList implements EvictionList {
  private static final Logger logger = LogService.getLogger();

  /** The last node in the LRU list after which all new nodes are added */
  protected final EvictionNode tail = new GuardNode();

  /** The starting point in the LRU list for searching for the LRU node */
  protected final EvictionNode head = new GuardNode();

  /** Counter for the size of the LRU list */
  private final AtomicInteger size = new AtomicInteger();

  private final EvictionController controller;

  protected AbstractEvictionList(EvictionController controller) {
    this.controller = controller;
    initEmptyList();
  }

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

  private void incrementSize() {
    size.incrementAndGet();
  }

  void decrementSize() {
    // Size could go negative if there is a concurrent clear and
    // cache updates are in progress.
    size.decrementAndGet();
  }

  @Override
  public void closeStats() {
    getStatistics().close();
  }

  @Override
  public EvictionCounters getStatistics() {
    return controller.getCounters();
  }

  @Override
  public void clear(RegionVersionVector regionVersionVector, BucketRegion bucketRegion) {
    if (regionVersionVector != null) {
      return; // when concurrency checks are enabled the clear operation removes entries iteratively
    }

    synchronized (this) {
      if (bucketRegion != null) {
        getStatistics().decrementCounter(bucketRegion.getCounter());
        bucketRegion.resetCounter();
      } else {
        getStatistics().resetCounter();
      }
      initEmptyList();
    }
  }

  private synchronized void initEmptyList() {
    size.set(0);
    head.setNext(tail);
    tail.setPrevious(head);
  }

  /**
   * Adds an lru node to the tail of the list.
   */
  @Override
  public synchronized void appendEntry(final EvictionNode evictionNode) {
    if (evictionNode.next() != null) {
      // already in the list
      return;
    }

    if (logger.isTraceEnabled(LogMarker.LRU_CLOCK_VERBOSE)) {
      logger.trace(LogMarker.LRU_CLOCK_VERBOSE, "adding a Node to lru list: {}", evictionNode);
    }

    evictionNode.setNext(tail);
    tail.previous().setNext(evictionNode);
    evictionNode.setPrevious(tail.previous());
    tail.setPrevious(evictionNode);

    incrementSize();
  }

  @Override
  public synchronized void destroyEntry(EvictionNode evictionNode) {
    if (logger.isTraceEnabled(LogMarker.LRU_CLOCK_VERBOSE)) {
      logger.trace(LogMarker.LRU_CLOCK_VERBOSE, "destroyEntry called for {}", evictionNode);
    }

    if (removeEntry(evictionNode)) {
      getStatistics().incDestroys();
    }

  }

  protected synchronized boolean removeEntry(EvictionNode evictionNode) {
    if (evictionNode.next() == null) {
      // not in the list anymore.
      return false;
    }

    unlinkEntry(evictionNode);
    return true;
  }

  protected synchronized void unlinkEntry(EvictionNode evictionNode) {
    EvictionNode next = evictionNode.next();
    EvictionNode previous = evictionNode.previous();
    next.setPrevious(previous);
    previous.setNext(next);
    evictionNode.setNext(null);
    evictionNode.setPrevious(null);
    decrementSize();
  }

  protected synchronized EvictionNode unlinkTailEntry() {
    EvictionNode evictionNode = tail.previous();
    if (evictionNode == head) {
      return null; // end of eviction list
    }

    unlinkEntry(evictionNode);
    return evictionNode;
  }

  /**
   * Remove and return the head entry in the list
   */
  protected synchronized EvictionNode unlinkHeadEntry() {
    EvictionNode evictionNode = head.next();
    if (evictionNode == tail) {
      return null; // end of list
    }

    unlinkEntry(evictionNode);
    return evictionNode;
  }

  protected boolean isEvictable(EvictionNode evictionNode) {
    if (evictionNode.isEvicted()) {
      if (logger.isTraceEnabled(LogMarker.LRU_CLOCK_VERBOSE)) {
        logger.trace(LogMarker.LRU_CLOCK_VERBOSE,
            "discarding evicted entry");
      }
      return false;
    }

    // If this Entry is part of a transaction, skip it since
    // eviction should not cause commit conflicts
    synchronized (evictionNode) {
      if (evictionNode.isInUseByTransaction()) {
        if (logger.isTraceEnabled(LogMarker.LRU_CLOCK_VERBOSE)) {
          logger.trace(LogMarker.LRU_CLOCK_VERBOSE,
              "removing transactional entry from consideration");
        }
        return false;
      }
    }
    return true;
  }

}
