// Filename: lru.cxx
// Created by: aignacio (12Dec05)
//
////////////////////////////////////////////////////////////////////
//
// PANDA 3D SOFTWARE
// Copyright (c) 2001 - 2006, Disney Enterprises, Inc.  All rights
// reserved.
// All use of this software is subject to the terms of the Panda 3d
// Software license.  You should have received a copy of this license
// along with this source code; you will also find a current copy of
// the license at http://etc.cmu.edu/panda3d/docs/license/ .
//
// To contact the maintainers of this program write to
// panda3d-general@lists.sourceforge.net .
//
////////////////////////////////////////////////////////////////////

//#include "stdafx.h"

#define LRU_UNIT_TEST 0

#include <stdio.h>
#include <stdlib.h>
//#include <windows.h>

#include "lru.h"


static const int HIGH_PRIORITY_SCALE = 4;
static const int LOW_PRIORITY_RANGE = 25;

////////////////////////////////////////////////////////////////////
//     Function: Lru::Constructor
//       Access: Public
//  Description:
////////////////////////////////////////////////////////////////////
Lru::Lru (int maximum_memory, int maximum_pages, int maximum_page_types)
{
  if(this) {
    int  index;

    memset(&this->_m, 0, sizeof (LruVariables));

    this->_m.maximum_memory = maximum_memory;
    this->_m.maximum_pages  = maximum_pages;
    this->_m.maximum_page_types = maximum_page_types;
    this->_m.available_memory = maximum_memory;
    this->_m.current_frame_identifier = 1;
    this->_m.weight = 0.20f;

    this->set_maximum_frame_bandwidth_utilization(2000000.0f);

    for(index = 0; index < MAXIMUM_LRU_PAGE_TYPES; index++) {
      this->_m.page_in_function_array[index] = default_page_in_function;
      this->_m.page_out_function_array[index] = default_page_out_function;
    }

    if(maximum_pages > 0) {
      this -> _m.lru_page_pool = new LruPage * [maximum_pages];
      this -> _m.lru_page_free_pool = new LruPage * [maximum_pages];
      for(index = 0; index < maximum_pages; index++) {
        LruPage  * lru_page;

        lru_page = new LruPage ( );
        if(lru_page) {
          lru_page->_m.v.pre_allocated = true;
          this->_m.lru_page_pool[index] = lru_page;
        }
        else {
// ERROR
        }
      }
    }

    if(maximum_page_types > 0) {
      this -> _m.page_type_statistics_array =
        new PageTypeStatistics [maximum_page_types];
    }

#if ENABLE_MUTEX
    this -> _m.mutex = new Mutex ("lru");
#endif

  }
}

////////////////////////////////////////////////////////////////////
//     Function: Lru::Destructor
//       Access: Public
//  Description:
////////////////////////////////////////////////////////////////////
Lru::~Lru ( )
{
  int        index;
  LruPage  * lru_page;

  // free pre-allocated LruPages
  if(this->_m.maximum_pages > 0) {
    if(this->_m.lru_page_free_pool) {
      for(index = 0; index < this->_m.maximum_pages; index++) {
        lru_page = this->_m.lru_page_pool[index];
        if(lru_page->_m.v.in_lru) {
          this->remove_page(lru_page);
        }

        delete lru_page;
      }

      delete this -> _m.lru_page_free_pool;
    }
    if(this->_m.lru_page_pool) {
      delete this -> _m.lru_page_pool;
    }
  }

  // free dynamically allocated LruPages
  for(index = 0; index < LPP_TotalPriorities; index++) {
    LruPage  * next_lru_page;

    lru_page = this->_m.lru_page_array[index];
    while(lru_page) {
      next_lru_page = lru_page->_m.next;

      delete  lru_page;

      lru_page = next_lru_page;
    }
  }

  if(this->_m.page_type_statistics_array) {
    delete this -> _m.page_type_statistics_array;
  }

#if ENABLE_MUTEX
  if(this->_m.mutex) {
    delete this -> _m.mutex;
  }
#endif

}

////////////////////////////////////////////////////////////////////
//     Function: LruPage::Constructor
//       Access: Protected
//  Description: Internal function only.
//               Call  Lru::allocate_page instead.
////////////////////////////////////////////////////////////////////
LruPage::LruPage ( )
{
  if(this) {
    memset(&this->_m, 0, sizeof (LruPageVariables));
  }
}

////////////////////////////////////////////////////////////////////
//     Function: LruPage::Destructor
//       Access: Protected
//  Description: Internal function only.
//               Call  Lru::free_page instead.
////////////////////////////////////////////////////////////////////
LruPage::~LruPage ( )
{

}

////////////////////////////////////////////////////////////////////
//     Function: LruPage::change_priority
//       Access: Protected
//  Description:
////////////////////////////////////////////////////////////////////
void LruPage::change_priority (int delta)
{
  this->_m.priority_change += delta;
}

////////////////////////////////////////////////////////////////////
//     Function: Lru::register_lru_page_type
//       Access: Public
//  Description: Registers a specific type of page and its
//               required page in and out functions.
////////////////////////////////////////////////////////////////////
bool Lru::register_lru_page_type (int index,
  LruPageTypeFunction page_in_function,
  LruPageTypeFunction page_out_function)
{
  bool  state;

  state = false;
  if(index >= 0 && index < MAXIMUM_LRU_PAGE_TYPES) {
    this->_m.page_in_function_array[index] = page_in_function;
    this->_m.page_out_function_array[index] = page_out_function;
    state = true;
  }

  return state;
}

////////////////////////////////////////////////////////////////////
//     Function: Lru::allocate_page
//       Access: Public
//  Description:
////////////////////////////////////////////////////////////////////
LruPage *Lru::allocate_page (int size)
{
  LruPage  * lru_page;

  lru_page = 0;
  if(size <= this->_m.maximum_memory) {
    if(this->_m.maximum_pages) {
      if(this->_m.total_lru_pages_in_free_pool > 0) {
        lru_page =
          this->_m.lru_page_free_pool [this->_m.total_lru_pages_in_free_pool - 1];
        this->_m.total_lru_pages_in_free_pool--;

        memset (&lru_page -> _m, 0, sizeof (LruPage::LruPageVariables));
        lru_page->_m.v.pre_allocated = true;
      }
      else {
        if(this->_m.total_lru_pages_in_pool < this->_m.maximum_pages) {
          lru_page = this->_m.lru_page_pool[this->_m.total_lru_pages_in_pool];
          this->_m.total_lru_pages_in_pool++;
        }
        else {
          // out of pre-allocated LruPages so dynamically allocate a page
          lru_page = new LruPage ( );
        }
      }
    }
    else {
      lru_page = new LruPage;
    }
    if(lru_page) {
      lru_page->_m.lru = this;
      lru_page->_m.size = size;
      lru_page->_m.first_frame_identifier = this->_m.current_frame_identifier;
      lru_page->_m.last_frame_identifier = this->_m.current_frame_identifier;

      lru_page->_m.v.allocated = true;
      lru_page->_m.identifier = this->_m.identifier;

      lru_page->_m.average_frame_utilization = 1.0f;

      this->_m.total_pages++;
      this->_m.identifier++;
    }
    else {

// ERROR: could not allocate LruPage

    }
  }
  else {

// ERROR: requested page size is larger than maximum memory size

  }

  return lru_page;
}

////////////////////////////////////////////////////////////////////
//     Function: Lru::update_start_update_lru_page
//       Access: Public
//  Description:
////////////////////////////////////////////////////////////////////
void Lru::update_start_update_lru_page (LruPage *lru_page)
{
  if(lru_page) {
    if(this->_m.start_update_lru_page == lru_page) {
      if(lru_page->_m.next) {
        this->_m.start_update_lru_page = lru_page->_m.next;
      }
      else {
        if((this->_m.start_priority_index + 1) >= LPP_TotalPriorities) {
          this->_m.start_priority_index = 0;
        }
        else {
          this->_m.start_priority_index = this->_m.start_priority_index + 1;
        }

        this->_m.start_update_lru_page = 0;
      }
    }
  }
}

////////////////////////////////////////////////////////////////////
//     Function: Lru::free_page
//       Access: Public
//  Description:
////////////////////////////////////////////////////////////////////
void Lru::free_page (LruPage *lru_page)
{
  if(this->_m.total_pages > 0) {
    if(lru_page) {
      LruMutexHolder(this->_m.mutex);

      this->update_start_update_lru_page(lru_page);

      if(lru_page->_m.v.in_cache) {
        this->_m.available_memory += lru_page->_m.size;
      }

      if(lru_page->_m.v.pre_allocated) {
        if(this->_m.maximum_pages) {
          lru_page->_m.v.allocated = false;
          this->_m.lru_page_free_pool [this->_m.total_lru_pages_in_free_pool] =
            lru_page;
          this->_m.total_lru_pages_in_free_pool++;
        }
        else {
// ERROR: this case should not happen
        }
      }
      else {
        delete lru_page;
      }

      this->_m.total_pages--;
    }
  }
  else {

// ERROR: tried to free a page when 0 pages allocated

  }
}

////////////////////////////////////////////////////////////////////
//     Function: Lru::add_page
//       Access: Public
//  Description: Adds a page to the LRU based on the given priority.
////////////////////////////////////////////////////////////////////
void Lru::add_page (LruPagePriority priority, LruPage *lru_page)
{
  if(lru_page) {
    LruMutexHolder(this->_m.mutex);

    LruPage * first_lru_page;

    lru_page->_m.priority = priority;

    first_lru_page = this->_m.lru_page_array[lru_page->_m.priority];
    if(first_lru_page) {
      first_lru_page->_m.previous = lru_page;
      lru_page->_m.next = first_lru_page;
    }

    this->_m.lru_page_array[lru_page->_m.priority] = lru_page;

    lru_page->_m.v.in_lru = true;
  }
}

////////////////////////////////////////////////////////////////////
//     Function: Lru::add_cached_page
//       Access: Public
//  Description: Adds a page that is already paged in to the LRU
//               based on the given priority.
////////////////////////////////////////////////////////////////////
void Lru::add_cached_page (LruPagePriority priority, LruPage *lru_page)
{
  if(lru_page) {
    LruMutexHolder(this->_m.mutex);

    lru_page->_m.v.in_cache = true;

    if(lru_page->_m.size > this->_m.available_memory) {
      int  memory_required;

      memory_required = lru_page->_m.size - this->_m.available_memory;

      // unload page(s)
      this->page_out_lru(memory_required);
    }

    this->_m.available_memory -= lru_page->_m.size;

    this->add_page(priority, lru_page);
  }
}

////////////////////////////////////////////////////////////////////
//     Function: Lru::remove_page
//       Access: Public
//  Description: Removes a page from the LRU.
////////////////////////////////////////////////////////////////////
void Lru::remove_page (LruPage *lru_page)
{
  if(this) {
    if(this->_m.total_pages > 0) {
      if(lru_page) {
        LruMutexHolder(this->_m.mutex);

        this->update_start_update_lru_page(lru_page);

        if(lru_page->_m.previous) {
          lru_page->_m.previous->_m.next = lru_page->_m.next;
          if(lru_page->_m.next) {
            lru_page->_m.next->_m.previous = lru_page->_m.previous;
          }
        }
        else {
          this->_m.lru_page_array[lru_page->_m.priority] =
            lru_page->_m.next;
          if(lru_page->_m.next) {
            lru_page->_m.next->_m.previous = 0;
          }
        }

        lru_page->_m.next = 0;
        lru_page->_m.previous = 0;

        lru_page->_m.v.in_lru = false;
      }
    }
    else {

// ERROR: tried to remove a page when 0 pages are allocated

    }
  }
  else {

// ERROR: Lru == 0, this should not happen

  }
}

////////////////////////////////////////////////////////////////////
//     Function: Lru::lock_page
//       Access: Public
//  Description:
////////////////////////////////////////////////////////////////////
void Lru::lock_page (LruPage *lru_page)
{
  lru_page->_m.v.lock = true;
}

////////////////////////////////////////////////////////////////////
//     Function: Lru::unlock_page
//       Access: Public
//  Description:
////////////////////////////////////////////////////////////////////
void Lru::unlock_page (LruPage *lru_page)
{
  lru_page->_m.v.lock = false;
}

////////////////////////////////////////////////////////////////////
//     Function: Lru::access_page
//       Access: Public
//  Description: This must always be called before accessing or
//               using a page's memory since it pages in the page
//               if it is currently paged out.
////////////////////////////////////////////////////////////////////
void Lru::access_page (LruPage *lru_page)
{
  if(lru_page) {
    if(lru_page->_m.current_frame_identifier
       == this->_m.current_frame_identifier) {
      lru_page->_m.current_frame_usage++;
      this->_m.total_page_all_access_size += lru_page->_m.size;
    }
    else {
      // first update this frame
      lru_page->_m.last_frame_identifier = lru_page->_m.current_frame_identifier;
      lru_page->_m.current_frame_identifier = this->_m.current_frame_identifier;
      lru_page->_m.last_frame_usage = lru_page->_m.current_frame_usage;
      lru_page->_m.current_frame_usage = 1;
      lru_page->_m.total_frame_page_faults = 0;

      this->_m.total_page_access_size += lru_page->_m.size;
    }

    // check if the page is out
    if(lru_page->_m.v.in_cache == false) {
      bool  state;

      state = true;

      LruMutexHolder(this->_m.mutex);

      // check memory usage
      if(lru_page->_m.size > this->_m.available_memory) {
        int  memory_required;

        memory_required = lru_page->_m.size - this->_m.available_memory;

        // unload page(s)
        state = this->page_out_lru(memory_required);
      }

      // load the page in
      if(state) {
        // PAGE IN CALLBACK
        if(this->_m.page_in_function_array[lru_page->_m.v.type](lru_page)) {
          this->_m.available_memory -= lru_page->_m.size;
          lru_page->_m.v.in_cache = true;

          // CHANGE THE PAGE PRIORITY FROM LPP_PageOut TO LPP_New
          this->remove_page(lru_page);
          this->add_page(LPP_New, lru_page);

          this->_m.total_lifetime_page_ins++;
        }
      }

      lru_page->_m.total_frame_page_faults++;
      lru_page->_m.total_page_faults++;
    }

    lru_page->_m.total_usage++;
    lru_page->_m.update_total_usage++;

    this->_m.total_page_access++;
  }
}

////////////////////////////////////////////////////////////////////
//     Function: Lru::set_maximum_frame_bandwidth_utilization
//       Access: Public
//  Description:
////////////////////////////////////////////////////////////////////
void Lru::set_maximum_frame_bandwidth_utilization
  (float maximum_frame_bandwidth_utilization)
{
  this->_m.maximum_frame_bandwidth_utilization =
    maximum_frame_bandwidth_utilization;

  this->_m.frame_bandwidth_factor = (float) LPP_TotalPriorities
    / this->_m.maximum_frame_bandwidth_utilization;
}

////////////////////////////////////////////////////////////////////
//     Function: Lru::begin_frame
//       Access: Public
//  Description: This must be called before each frame.
////////////////////////////////////////////////////////////////////
void Lru::begin_frame ( )
{
  this->_m.current_frame_identifier++;

  this->_m.total_page_ins_last_frame = this->_m.total_page_ins;
  this->_m.total_page_outs = this->_m.total_page_outs;

  this->_m.total_page_ins = 0;
  this->_m.total_page_outs = 0;
}

////////////////////////////////////////////////////////////////////
//     Function: Lru::update_page_priorities
//       Access: Public
//  Description: This updates the priority of a page that has a
//               change in priority.
////////////////////////////////////////////////////////////////////
void Lru::update_page_priorities (void)
{
  int index;
  LruPage *lru_page;

  for(index = 0; index < this->_m.total_lru_page_priority_changes; index++) {
    int priority;

    lru_page = this->_m.lru_page_priority_change_array[index];

    this->remove_page(lru_page);

    priority = (( int ) lru_page->_m.priority + lru_page->_m.priority_change);
    if(priority < 0) {
      priority = 0;
    }
    if(priority >= LPP_TotalPriorities) {
      priority = LPP_TotalPriorities - 1;
    }

    this->add_page((LruPagePriority) priority, lru_page);
    lru_page->_m.priority_change = 0;
  }
  this->_m.total_lru_page_priority_changes = 0;
}

////////////////////////////////////////////////////////////////////
//     Function: Lru::update_lru_page
//       Access: Public
//  Description: This updates the page's average utilization.
//               Priority LPP_New is considered to be average usage
//               of 1.0 (which means the page is used once per frame
//               on average).  Priorities < LPP_New are for pages
//               used more than once per frame and Priorities >
//               LPP_New are for pages used less than once per frame.
//               If there was a change in priority, then adds it to
//               the array of lru pages with changed priorities
//               which will be updated later.
////////////////////////////////////////////////////////////////////
void Lru::update_lru_page (LruPage *lru_page)
{

#if LRU_UNIT_TEST
  if(false) {
    char  string[256];

    sprintf(string, "  UPDATE %d\n", lru_page->_m.identifier);
    OutputDebugString(string);
  }
#endif

  if(lru_page->_m.v.lock == false && lru_page->_m.v.in_cache) {
    int delta_priority;
    int lifetime_frames;

    delta_priority = 0;

    lifetime_frames = this->_m.current_frame_identifier -
      lru_page->_m.first_frame_identifier;
    if(lifetime_frames >= 1) {
      if(lru_page->_m.update_frame_identifier) {
        int target_priority;
        int integer_update_frames;
        float update_frames;
        float one_over_update_frames;
        float update_average_frame_utilization;

        integer_update_frames = (this->_m.current_frame_identifier -
          lru_page->_m.update_frame_identifier);
        if(integer_update_frames > 0) {
          update_frames = ( float ) integer_update_frames;
          one_over_update_frames = 1.0f / update_frames;

          update_average_frame_utilization =
            (float) (lru_page->_m.update_total_usage)* one_over_update_frames;

          lru_page->_m.average_frame_utilization =
            calculate_exponential_moving_average(
               update_average_frame_utilization, this->_m.weight,
               lru_page->_m.average_frame_utilization);

          target_priority = lru_page->_m.priority;
          if(lru_page->_m.average_frame_utilization >= 1.0f) {
            int integer_average_frame_utilization;

            integer_average_frame_utilization =
              (int) ((lru_page->_m.average_frame_utilization - 1.0f) *
              (float) HIGH_PRIORITY_SCALE);
            if(integer_average_frame_utilization >= LPP_New) {
              integer_average_frame_utilization = LPP_New;
            }
            integer_average_frame_utilization = LPP_New -
              integer_average_frame_utilization;
            target_priority = integer_average_frame_utilization;
          }
          else {
            int integer_average_frame_utilization;

            integer_average_frame_utilization = (int)
               (lru_page->_m.average_frame_utilization *
               (float) LOW_PRIORITY_RANGE);
            integer_average_frame_utilization = LOW_PRIORITY_RANGE -
              integer_average_frame_utilization;
            target_priority = LPP_New + integer_average_frame_utilization;
          }

          delta_priority = target_priority - lru_page->_m.priority;
          lru_page->change_priority(delta_priority);
        }
      }

      lru_page->_m.update_frame_identifier = this->_m.current_frame_identifier;
      lru_page->_m.update_total_usage = 0;
    }

    if(lru_page->_m.priority_change) {
      if(this->_m.total_lru_page_priority_changes
         < FRAME_MAXIMUM_PRIORITY_CHANGES)
      {
        this->_m.lru_page_priority_change_array
          [this->_m.total_lru_page_priority_changes] = lru_page;
        this->_m.total_lru_page_priority_changes++;
      }
    }
  }
}

////////////////////////////////////////////////////////////////////
//     Function: Lru::update_lru_page_old
//       Access: Public
//  Description: This updates the page's average utilization and
//               adds it to the array of pages with changed
//               priorities if there was a change in priority.
//               Old method.
////////////////////////////////////////////////////////////////////
void Lru::update_lru_page_old (LruPage *lru_page)
{

#if LRU_UNIT_TEST
  if(false) {
    char  string[256];

    sprintf(string, "  UPDATE %d\n", lru_page->_m.identifier);
    OutputDebugString(string);
  }
#endif

  if(lru_page->_m.v.lock == false) {
    int  delta_priority;

    delta_priority = 0;
    if(false && lru_page->_m.total_usage > 0) {
      int lifetime_frames;

      lifetime_frames = this->_m.current_frame_identifier -
        lru_page->_m.first_frame_identifier;
      if(lifetime_frames >= 10) {
        float one_over_update_frames;

        if(lru_page->_m.update_frame_identifier) {
          int target_priority;
          int integer_update_frames;
          float update_frames;
          float update_average_frame_utilization;
          float average_frame_bandwidth_utilization;

          integer_update_frames = (this->_m.current_frame_identifier -
            lru_page->_m.update_frame_identifier);
          if(integer_update_frames > 0) {
            update_frames = ( float ) integer_update_frames;
            one_over_update_frames = 1.0f / update_frames;

            update_average_frame_utilization =
              (float) (lru_page->_m.update_total_usage) *
              one_over_update_frames;

            lru_page->_m.average_frame_utilization =
              calculate_exponential_moving_average (
                 update_average_frame_utilization, this->_m.weight,
                 lru_page->_m.average_frame_utilization);

            average_frame_bandwidth_utilization =
              lru_page->_m.average_frame_utilization *
              lru_page->_m.size;

            target_priority = (int) (average_frame_bandwidth_utilization *
              this->_m.frame_bandwidth_factor);

            target_priority = (LPP_TotalPriorities - 1) - target_priority;
            if(target_priority < 0) {
              target_priority = 0;
            }
            if(target_priority >= LPP_TotalPriorities) {
              target_priority = LPP_TotalPriorities - 1;
            }

            delta_priority = target_priority - lru_page->_m.priority;
            lru_page->change_priority(delta_priority);
          }
        }

        lru_page->_m.update_frame_identifier =
          this->_m.current_frame_identifier;

        lru_page->_m.update_total_usage = 0;
      }
    }

    if(delta_priority == 0) {
      if(this->_m.current_frame_identifier
        == lru_page->_m.current_frame_identifier) {
        // page used during this frame twice or more =>
        // increase priority
        if(lru_page->_m.current_frame_usage >= 2) {
          if(lru_page->_m.priority >= LPP_High) {
            lru_page->change_priority(-2);
          }
        }

        if(lru_page->_m.total_frame_page_faults >= 1) {
          // multiple page faults this frame => increase priority
          if(lru_page->_m.total_frame_page_faults >= 2) {
            if(lru_page->_m.priority >= LPP_High) {
              lru_page->change_priority(-2);
            }
          }
          else {
            // single page faults this frame => increase priority
            if(lru_page->_m.priority >= LPP_High) {
              lru_page->change_priority(-1);
            }
          }
        }
      }
      else {
        // page not used this frame
        int  last_access_delta;

        last_access_delta
        = this->_m.current_frame_identifier
          - lru_page->_m.current_frame_identifier;
        if(last_access_delta > 1) {
          if(lru_page->_m.priority < LPP_Low) {
            lru_page->change_priority(+1);
          }
        }
      }
    }

    if(lru_page->_m.priority_change) {
      if(this->_m.total_lru_page_priority_changes
         < FRAME_MAXIMUM_PRIORITY_CHANGES) {
        this->_m.lru_page_priority_change_array
          [this->_m.total_lru_page_priority_changes]= lru_page;
        this->_m.total_lru_page_priority_changes++;
      }
    }
  }
}

////////////////////////////////////////////////////////////////////
//     Function: Lru::update_entire_lru
//       Access: Public
//  Description: This updates all the pages in the Lru.
//               Lru::partial_lru_update should be called instead
//               due to performance reasons.
////////////////////////////////////////////////////////////////////
void Lru::update_entire_lru ( )
{
  if(this->_m.total_pages > 0) {
    int index;
    LruPage *lru_page;

    LruMutexHolder(this->_m.mutex);

    for(index = 0; index < LPP_TotalPriorities; index++) {

      LruPage  * next_lru_page;

      lru_page = this->_m.lru_page_array[index];
      while(lru_page) {
        next_lru_page = lru_page->_m.next;

        this->update_lru_page(lru_page);

        lru_page = next_lru_page;
      }
    }

    this->update_page_priorities( );
  }
}

////////////////////////////////////////////////////////////////////
//     Function: Lru::partial_lru_update
//       Access: Public
//  Description: This only updates a number of pages up to the
//               specified maximum_updates.
////////////////////////////////////////////////////////////////////
void Lru::partial_lru_update (int maximum_updates)
{
  int total_page_updates;

  if (maximum_updates <= 0) {
    // enforce a minimum number of updates
    maximum_updates = 1;
  }

  total_page_updates = 0;
  if(this->_m.total_pages > 0) {
    int index;
    int start_priority;
    LruPage *lru_page;

    LruMutexHolder(this->_m.mutex);

    start_priority = this->_m.start_priority_index;

    {
      for(index = start_priority;  index < LPP_TotalPriorities; index++) {

        LruPage *next_lru_page;

        if(index == start_priority) {
          if(this->_m.start_update_lru_page) {
            lru_page = this->_m.start_update_lru_page;
          }
          else {
            lru_page = this->_m.lru_page_array[index];
          }
        }
        else {
          lru_page = this->_m.lru_page_array[index];
        }
        while(lru_page) {
          next_lru_page = lru_page->_m.next;

          this->update_lru_page(lru_page);

          total_page_updates++;
          if(total_page_updates >= maximum_updates) {
            if(next_lru_page) {
              this->_m.start_priority_index = index;
              this->_m.start_update_lru_page = next_lru_page;
            }
            else {
              if((index + 1) >= LPP_TotalPriorities) {
                this->_m.start_priority_index = 0;
              }
              else {
                this->_m.start_priority_index = index + 1;
              }

              this->_m.start_update_lru_page = 0;
            }

            break;
          }

          lru_page = next_lru_page;
        }

        if(total_page_updates >= maximum_updates) {
          break;
        }
      }
    }

    if(total_page_updates < maximum_updates) {
      for(index = 0;  index <= start_priority;  index++) {
        LruPage *next_lru_page;

        lru_page = this->_m.lru_page_array[index];
        while(lru_page) {
          next_lru_page = lru_page->_m.next;

          this->update_lru_page(lru_page);

          total_page_updates++;
          if(total_page_updates >= maximum_updates) {
            if(next_lru_page) {
              this->_m.start_priority_index = index;
              this->_m.start_update_lru_page = next_lru_page;
            }
            else {
              if((index + 1) >= LPP_TotalPriorities) {
                this->_m.start_priority_index = 0;
              }
              else {
                this->_m.start_priority_index = index + 1;
              }

              this->_m.start_update_lru_page = 0;
            }

            break;
          }

          lru_page = next_lru_page;
        }

        if(total_page_updates >= maximum_updates) {
          break;
        }
      }
    }

    if(total_page_updates < maximum_updates) {
      this->_m.start_priority_index  = 0;
      this->_m.start_update_lru_page = 0;
    }

    this->update_page_priorities( );
  }
}

////////////////////////////////////////////////////////////////////
//     Function: Lru::unlock_all_pages
//       Access: Public
//  Description:
////////////////////////////////////////////////////////////////////
void Lru::unlock_all_pages (void)
{
  if(this->_m.total_pages > 0) {
    int  index;

    for(index = 0;  index < LPP_TotalPriorities; index++) {
      LruPage *lru_page;
      LruPage *next_lru_page;

      lru_page = this->_m.lru_page_array[index];
      while(lru_page) {
        next_lru_page = lru_page->_m.next;

        lru_page->_m.v.lock = false;

        lru_page = next_lru_page;
      }
    }
  }
}

////////////////////////////////////////////////////////////////////
//     Function: Lru::page_out_lru
//       Access: Public
//  Description: Pages out the lowest priority pages until the
//               memory_required is satisfied.  This will unlock
//               all pages if needed.
////////////////////////////////////////////////////////////////////
bool Lru::page_out_lru (int memory_required)
{
  bool state;
  int attempts;

  state = false;
  attempts = 0;
  if(this->_m.total_pages > 0) {
    LruMutexHolder(this->_m.mutex);

    do {
      int index;
      int minimum_frame_identifier;
      
      minimum_frame_identifier = this->_m.current_frame_identifier - 1;

      // page out lower priority pages first
      for(index = LPP_PageOut - 1; index >= 0; index--) {
        LruPage *lru_page;
        LruPage *next_lru_page;

        lru_page = this->_m.lru_page_array[index];
        while(lru_page) {
          next_lru_page = lru_page->_m.next;

          if(attempts == 0 && (lru_page->_m.current_frame_identifier >= minimum_frame_identifier)) {
            // avoid swapping out pages used in the current and last frame on the first attempt
          }
          else {
            if(lru_page->_m.v.lock == false && lru_page->_m.v.in_cache) {
              memory_required -= lru_page->_m.size;
              this->_m.available_memory += lru_page->_m.size;
              lru_page->_m.v.in_cache = false;

              // PAGE OUT CALLBACK
              this->_m.page_out_function_array[lru_page->_m.v.type](lru_page);
              this->_m.total_lifetime_page_outs++;

              // MOVE THE PAGE TO THE LPP_PageOut PRIORITY
              this->remove_page(lru_page);
              this->add_page(LPP_PageOut, lru_page);

              if(memory_required <= 0) {
                break;
              }
            }
          }
          
          lru_page = next_lru_page;
        }

        if(memory_required <= 0) {
          break;
        }
      }

      if(memory_required > 0) {
        state = false;
      }
      else {
        state = true;
      }

      attempts++;
    } while(state == false && attempts < 2);
  }

  return state;
}

////////////////////////////////////////////////////////////////////
//     Function: Lru::count_priority_level_pages
//       Access: Public
//  Description: Debug function. Counts the number of pages for each
//               priority level.
////////////////////////////////////////////////////////////////////
void Lru::count_priority_level_pages (void)
{
  int  index;

  LruMutexHolder(this->_m.mutex);

  for(index = 0; index < LPP_TotalPriorities; index++) {
    int total_pages;
    LruPage *lru_page;
    LruPage *next_lru_page;

    total_pages = 0;
    lru_page = this->_m.lru_page_array[index];
    while(lru_page) {
      next_lru_page = lru_page->_m.next;

      total_pages++;

      lru_page = next_lru_page;
    }

    this->_m.lru_page_count_array[index] = total_pages;
  }
}

////////////////////////////////////////////////////////////////////
//     Function: Lru::calculate_lru_statistics
//       Access: Public
//  Description: Debug function.
////////////////////////////////////////////////////////////////////
void Lru::calculate_lru_statistics (void)
{
  LruMutexHolder(this->_m.mutex);

  if(this->_m.maximum_page_types > 0) {
    int  index;

    memset(this->_m.page_type_statistics_array, 0,
           sizeof (PageTypeStatistics) * this->_m.maximum_page_types);
    for(index = 0;  index < LPP_TotalPriorities;  index++) {
      LruPage *lru_page;
      LruPage *next_lru_page;
      PageTypeStatistics *page_type_statistics;

      lru_page = this->_m.lru_page_array[index];
      while(lru_page) {
        int  type;

        next_lru_page = lru_page->_m.next;

        type = lru_page->_m.v.type;
        page_type_statistics = &this->_m.page_type_statistics_array[type];
        page_type_statistics->total_pages++;

        if(lru_page->_m.v.in_cache) {
          page_type_statistics->total_pages_in++;
          page_type_statistics->total_memory_in += lru_page->_m.size;
        }
        else {
          page_type_statistics->total_pages_out++;
          page_type_statistics->total_memory_out += lru_page->_m.size;
        }

        lru_page = next_lru_page;
      }
    }
  }
}

////////////////////////////////////////////////////////////////////
//     Function: calculate_exponential_moving_average
//       Access: Public
//  Description:
////////////////////////////////////////////////////////////////////
float calculate_exponential_moving_average(float value,
  float weight, float average)
{
  return ((value - average) * weight) + average;
}

////////////////////////////////////////////////////////////////////
//     Function: default_page_in_function
//       Access: Public
//  Description:
////////////////////////////////////////////////////////////////////
bool default_page_in_function(LruPage *lru_page)
{

#if LRU_UNIT_TEST
  char  string[256];

  sprintf(string, "  PAGE IN %d\n", lru_page->_m.identifier);
  OutputDebugString(string);
#endif

  return true;
}

////////////////////////////////////////////////////////////////////
//     Function: default_page_out_function
//       Access: Public
//  Description:
////////////////////////////////////////////////////////////////////
bool default_page_out_function(LruPage *lru_page)
{

#if LRU_UNIT_TEST
  char  string[256];

  sprintf(string, "  PAGE OUT %d\n", lru_page->_m.identifier);
  OutputDebugString(string);
#endif

  return true;
}

#if LRU_UNIT_TEST

////////////////////////////////////////////////////////////////////
//     Function: test_ema
//       Access:
//  Description: Unit test function for ema.
////////////////////////////////////////////////////////////////////
void test_ema(void)
{
  int    index;
  float  usage;
  float  weight;
  float  average;

  weight  = 0.2f;
  average = 1.0f;
  for(index = 0; index < 50; index++) {
    if(index < 25) {
      usage = (float) (index & 0x01);
    }
    else {
      usage = 0.0f;
    }
    average =
      calculate_exponential_moving_average(usage, weight, average);

    char  string[256];
    sprintf(string, "%d  %f\n", index, average);
    OutputDebugString(string);
  }
}

////////////////////////////////////////////////////////////////////
//     Function: test_lru
//       Access:
//  Description: Unit test function for Lru.
////////////////////////////////////////////////////////////////////
void test_lru(void)
{
  int maximum_memory;
  int maximum_pages;
  int maximum_page_types;
  Lru *lru;

  test_ema( );

  maximum_memory = 3000000;
  maximum_pages = 3;
  maximum_page_types = 4;
  lru = new Lru (maximum_memory, maximum_pages, maximum_page_types);
  if(lru) {
    lru->_m.minimum_memory = 1000000;

    LruPage *lru_page_0;
    LruPage *lru_page_1;
    LruPage *lru_page_2;
    LruPage *lru_page_3;
    LruPage *lru_page_4;
    LruPage *lru_page_5;

    lru_page_0 = lru->allocate_page(1000000);
    if(lru_page_0) {
      lru->add_page(LPP_PageOut, lru_page_0);
    }

    lru_page_1 = lru->allocate_page(1000000);
    if(lru_page_1) {
      lru->add_page(LPP_PageOut, lru_page_1);
    }

    lru_page_2 = lru->allocate_page(1000000);
    if(lru_page_2) {
      lru->add_page(LPP_PageOut, lru_page_2);
    }

    lru_page_3 = lru->allocate_page(1000000);
    if(lru_page_3) {
      lru->add_page(LPP_PageOut, lru_page_3);
    }

    lru_page_4 = lru->allocate_page(1000000);
    if(lru_page_4) {
      lru->add_page(LPP_PageOut, lru_page_4);
    }

    lru_page_5 = lru->allocate_page(1000000);
    if(lru_page_5) {
      lru->add_page(LPP_PageOut, lru_page_5);
    }

    int index;
    int total_frames;

    total_frames = 300;
    for(index = 0;  index < total_frames;  index++) {
      char  string[256];

      sprintf(string, "FRAME %d\n", index);
      OutputDebugString(string);

      lru->begin_frame( );

      if(index <= 5) {
        lru->access_page(lru_page_0);
      }

      lru->access_page(lru_page_1);
      lru->access_page(lru_page_1);

      if(index & 0x01) {
        lru->access_page(lru_page_2);
      }

      if((index % 10) == 0) {
        lru->access_page(lru_page_3);
      }

      if(index >= 100) {
        lru->access_page(lru_page_4);
      }

      if(index >= 200) {
        lru->access_page(lru_page_5);
      }

      if(false) {
        lru->update_entire_lru( );
      }
      else {
        int  maximum_updates;

        maximum_updates = 3;
        lru->partial_lru_update(maximum_updates);
      }
    }

    if(!true) {
      lru->remove_page(lru_page_2);
      lru->free_page(lru_page_2);

      lru->remove_page(lru_page_3);
      lru->free_page(lru_page_3);

      lru->remove_page(lru_page_1);
      lru->free_page(lru_page_1);
    }

    delete lru;
  }
}

#endif
