package org.cache2k.benchmarks.clockProPlus;

/*
 * #%L
 * Benchmarks: Clock-Pro+ and other eviction policies
 * %%
 * Copyright (C) 2018 - 2019 Cong Li, Intel Corporation
 * %%
 * Licensed 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.
 * #L%
 */

import org.apache.commons.collections4.map.HashedMap;

/**
 * @author Jens Wilke
 */
@SuppressWarnings({"Duplicates", "WeakerAccess", "unused"})
public class ClockProA implements ISimpleCache {

  public ClockProA(int size, ClockPro.Tuning tuning) {
    this.size = size;
    this.clockMap = new HashedMap<>();
    this.coldTarget = (int)(this.size * tuning.getColdRatio());
    this.hotTarget = this.size - this.coldTarget;
    this.coldSize = 0;
    this.hotSize = 0;
    this.testSize = 0;
    this.handHot = null;
    this.handCold = null;
    this.handTest = null;
    this.lru = null;
  }

  @Override
  public boolean request(Integer address) {
    if (this.clockMap.containsKey(address)) {
      CacheMetaData data = this.clockMap.get(address);
      if (data.isResident()) {
        data.setReference(true);
        return true;
      }
    }
    this.advanceHandHot();
    this.advanceHandCold();
    if (this.clockMap.containsKey(address)) {
      CacheMetaData data = this.clockMap.get(address);
      this.assertStatement(data.isInStack(),
        "Error: hit a non-resident cold page out of the stack!");
      this.assertStatement(!data.isReferenced(),
        "Error: hit a referenced non-resident cold page");

      this.adjustColdTarget(1);

      this.assertStatement(this.hotSize + this.coldSize == this.size, "Error: cache not full!");
      this.evictColdPage();

      data.setResidentState(true);
      this.testSize--;
      this.coldSize++;

      if (data == this.handTest) {
        this.advanceHandTest();
      }

      this.promoteColdPage(data);
      this.advanceHandCold();

      return false;
    }

    if (this.coldSize + this.hotSize == this.size) {
      this.evictColdPage();
    }

    CacheMetaData data = new CacheMetaData();
    data.setAddress(address);
    data.setResidentState(true);
    this.addToClock(data);
    if (this.coldSize == 0 && this.hotSize < this.hotTarget) {
      data.setLIRState(true);
      this.hotSize++;
    } else {
      data.setLIRState(false);
      if (this.handCold == null) {
        this.handCold = data;
      }
      this.coldSize++;
    }
    this.advanceHandCold();
    this.pruneTestPages();
    return false;
  }

  @Override
  public String toString() {
    return String.format("ClockPro*(%d)", this.size);
  }

  protected void pruneTestPages() {
    while (this.coldSize + this.hotSize + this.testSize > this.size * 2) {
      this.assertStatement(this.handTest.isInStack() && !this.handTest.isLIR()
          && !this.handTest.isResident(),
        "Error: hand_test does not stop at a test page!");

      this.removeNonresidentColdPage(this.handTest);
    }
  }

  protected void evictColdPage() {
    this.assertStatement(!this.handCold.isLIR() && this.handCold.isResident() && !this.handCold.isReferenced(),
      "Error: hand_cold does not stop at a non-referenced resident cold page!");

    CacheMetaData data = this.handCold;
    this.handCold = this.handCold.getNext();
    data.setResidentState(false);
    this.coldSize--;
    this.testSize++;
    if (this.handTest == null) {
      this.handTest = data;
    }
    if (!data.isInStack()) {
      if (data == this.lru) {
        this.lru = data.getPrevious();
      }
      this.removeNonresidentColdPage(data);
    }
  }

  protected void promoteColdPage(CacheMetaData data) {
    data.setLIRState(true);
    this.hotSize++;
    this.coldSize--;

    this.moveToLRU(data);

    while (this.hotSize > this.hotTarget) {
      this.demoteHotPage();
    }
  }

  protected void demoteHotPage() {
    this.assertStatement(!this.handHot.isReferenced(), "Error: hand_hot stops on a referenced page!");

    CacheMetaData data = this.handHot;
    this.handHot = this.handHot.getNext();

    data.setLIRState(false);
    this.hotSize--;
    this.coldSize++;
    data.setInStackStatus(false);
    data.demote(true);
    this.moveToLRU(data);

    this.advanceHandHot();
  }

  protected void advanceHandTest() {
    if (this.testSize == 0) {
      this.handTest = null;
      return;
    }
    while (this.handTest.isLIR() || this.handTest.isResident()) {
      this.handTest = this.handTest.getNext();
    }
  }

  protected void advanceHandHot() {
    if (this.hotSize == 0) {
      return;
    }
    CacheMetaData data = this.handHot;

    while (!data.isLIR() || data.isReferenced()) {
      CacheMetaData nextPosition = data.getNext();
      if (data.isLIR()) {
        data.setReference(false);
        this.lru = data;
      } else {
        if (data.isResident()) {
          if (data.isReferenced()) {
            data.setReference(false);
            if (data.isDemoted()) {
              this.adjustColdTarget(-1);
              data.demote(false);
            }
            this.lru = data;
            if (data.equals(this.handCold)) {
              this.handCold = nextPosition;
            }
          } else {
            data.setInStackStatus(false);
          }
        } else {
          this.removeNonresidentColdPage(data);
        }
        if (data.equals(this.handTest)) {
          this.handTest = nextPosition;
        }
      }
      data = nextPosition;
    }
    this.handHot = data;
  }

  protected void advanceHandCold() {
    if (this.coldSize == 0) {
      return;
    }

    while (this.handCold.isLIR() || !this.handCold.isResident() || this.handCold.isReferenced()) {
      CacheMetaData data = this.handCold;
      this.handCold = this.handCold.getNext();
      if (!data.isLIR()) {
        if (data.isReferenced()) {
          data.setReference(false);
          if (data.isDemoted()) {
            this.adjustColdTarget(-1);
            data.demote(false);
          }
          if (data.isInStack()) {
            this.promoteColdPage(data);
          } else {
            data.setInStackStatus(true);
            this.moveToLRU(data);
          }
        }
      }
    }
  }

  protected void addToClock(CacheMetaData data) {
    if (this.lru == null) {
      data.linkNext(data);
      this.lru = data;
      this.handHot = data;
    } else {
      data.insertAfter(this.lru);
      this.lru = data;
    }

    data.setInStackStatus(true);
    this.clockMap.put(data.getAddress(), data);
  }

  protected void moveToLRU(CacheMetaData data) {
    if (data == this.lru) {
      return;
    }

    data.unlink();
    data.insertAfter(this.lru);

    this.lru = data;
  }

  protected void removeNonresidentColdPage(CacheMetaData data) {
    Integer address = data.getAddress();
    if (data == this.handTest) {
      this.handTest = this.handTest.getNext();
    }
    this.clockMap.remove(address);
    data.unlink();
    this.testSize--;

    this.advanceHandTest();
  }

  protected void assertStatement(boolean result, String message) {
    if (!result) {
      System.err.println(message);
    }
  }

  protected void adjustColdTarget(int delta) {
    this.coldTarget += delta;
    if (this.coldTarget < 1) {
      this.coldTarget = 1;
    }
    if (this.coldTarget > this.size / 2) {
      this.coldTarget = this.size / 2;
    }
    this.hotTarget = this.size - this.coldTarget;
  }

  public void outputStatus() {
    if (this.lru != null) {
      this.lru.outputLinkedList();
    }
    System.out.printf("Hot_pages = %d, cold_pages = %d, hot_target = %d, cold_target = %d, ",
      this.hotSize, this.coldSize, this.hotTarget, this.coldTarget);
    if (this.handHot != null) {
      System.out.printf("Hot: %d, ", this.handHot.getAddress());
    }
    if (this.handCold != null) {
      System.out.printf("Cold: %d, ", this.handCold.getAddress());
    }
    if (this.handTest != null) {
      System.out.printf("Test: %d, ", this.handTest.getAddress());
    }
    System.out.println();
  }

  protected HashedMap<Integer, CacheMetaData> clockMap;
  protected CacheMetaData handHot;
  protected CacheMetaData handCold;
  protected CacheMetaData handTest;
  protected CacheMetaData lru;
  protected int size;
  protected int coldSize;
  protected int hotSize;
  protected int testSize;
  protected int coldTarget;
  protected int hotTarget;

}