require "digest"

require "./entry"
require "./types"

# Base class for an entry in the LRU cache.
# There are two ways to use it:
#   1. Use it as it is by instantiating with the appropriate `SaveT` and
#     `ReturnT`. Note that in this case, `SaveT` and `ReturnT` must be the
#     same type. That is, the input value will be stored as it is without
#     any transformation.
#   2. You can also subclass it and provide custom implementations for
#     `to_save_t` and `to_return_t`. This allows you to transform and store
#     the input value to a different type. See `SortedEntriesCacheEntry` as
#     an example.
private class CacheEntry(SaveT, ReturnT)
  getter key : String, atime : Time

  @value : SaveT

  def initialize(@key : String, value : ReturnT)
    @atime = @ctime = Time.utc
    @value = self.class.to_save_t value
  end

  def value
    @atime = Time.utc
    self.class.to_return_t @value
  end

  def self.to_save_t(value : ReturnT)
    value
  end

  def self.to_return_t(value : SaveT)
    value
  end

  def instance_size
    instance_sizeof(CacheEntry(SaveT, ReturnT)) + # sizeof itself
      instance_sizeof(String) + @key.bytesize +   # allocated memory for @key
      @value.instance_size
  end
end

class SortedEntriesCacheEntry < CacheEntry(Array(String), Array(Entry))
  def self.to_save_t(value : Array(Entry))
    value.map &.id
  end

  def self.to_return_t(value : Array(String))
    ids_to_entries value
  end

  private def self.ids_to_entries(ids : Array(String))
    e_map = Library.default.deep_entries.to_h { |entry| {entry.id, entry} }
    entries = [] of Entry
    begin
      ids.each do |id|
        entries << e_map[id]
      end
      return entries if ids.size == entries.size
    rescue
    end
  end

  def instance_size
    instance_sizeof(SortedEntriesCacheEntry) +  # sizeof itself
      instance_sizeof(String) + @key.bytesize + # allocated memory for @key
      @value.size * (instance_sizeof(String) + sizeof(String)) +
      @value.sum(&.bytesize) # elements in Array(String)
  end

  def self.gen_key(book_id : String, username : String,
                   entries : Array(Entry), opt : SortOptions?)
    entries_sig = Digest::SHA1.hexdigest (entries.map &.id).to_s
    user_context = opt && opt.method == SortMethod::Progress ? username : ""
    sig = Digest::SHA1.hexdigest (book_id + entries_sig + user_context +
                                  (opt ? opt.to_tuple.to_s : "nil"))
    "#{sig}:sorted_entries"
  end
end

class String
  def instance_size
    instance_sizeof(String) + bytesize
  end
end

struct Tuple(*T)
  def instance_size
    sizeof(T) + # total size of non-reference types
      self.sum do |e|
        next 0 unless e.is_a? Reference
        if e.responds_to? :instance_size
          e.instance_size
        else
          instance_sizeof(typeof(e))
        end
      end
  end
end

alias CacheableType = Array(Entry) | String | Tuple(String, Int32)
alias CacheEntryType = SortedEntriesCacheEntry |
                       CacheEntry(String, String) |
                       CacheEntry(Tuple(String, Int32), Tuple(String, Int32))

def generate_cache_entry(key : String, value : CacheableType)
  if value.is_a? Array(Entry)
    SortedEntriesCacheEntry.new key, value
  else
    CacheEntry(typeof(value), typeof(value)).new key, value
  end
end

# LRU Cache
class LRUCache
  @@limit : Int128 = Int128.new 0
  @@should_log = true
  # key => entry
  @@cache = {} of String => CacheEntryType

  def self.enabled
    Config.current.cache_enabled
  end

  def self.init
    cache_size = Config.current.cache_size_mbs
    @@limit = Int128.new cache_size * 1024 * 1024 if enabled
    @@should_log = Config.current.cache_log_enabled
  end

  def self.get(key : String)
    return unless enabled
    entry = @@cache[key]?
    if @@should_log
      Logger.debug "LRUCache #{entry.nil? ? "miss" : "hit"} #{key}"
    end
    return entry.value unless entry.nil?
  end

  def self.set(cache_entry : CacheEntryType)
    return unless enabled
    key = cache_entry.key
    @@cache[key] = cache_entry
    Logger.debug "LRUCache cached #{key}" if @@should_log
    remove_least_recent_access
  end

  def self.invalidate(key : String)
    return unless enabled
    @@cache.delete key
  end

  def self.print
    return unless @@should_log
    sum = @@cache.sum { |_, entry| entry.instance_size }
    Logger.debug "---- LRU Cache ----"
    Logger.debug "Size: #{sum} Bytes"
    Logger.debug "List:"
    @@cache.each do |k, v|
      Logger.debug "#{k} | #{v.atime} | #{v.instance_size}"
    end
    Logger.debug "-------------------"
  end

  private def self.is_cache_full
    sum = @@cache.sum { |_, entry| entry.instance_size }
    sum > @@limit
  end

  private def self.remove_least_recent_access
    if @@should_log && is_cache_full
      Logger.debug "Removing entries from LRUCache"
    end
    while is_cache_full && @@cache.size > 0
      min_tuple = @@cache.min_by { |_, entry| entry.atime }
      min_key = min_tuple[0]
      min_entry = min_tuple[1]

      Logger.debug "  \
        Target: #{min_key}, \
        Last Access Time: #{min_entry.atime}" if @@should_log
      invalidate min_key
    end
  end
end
