class Merb::Cache
  cattr_accessor :cached_pages
  self.cached_pages = {}
end

module Merb::Cache::ControllerClassMethods
  # Mixed in Merb::Controller. Provides class methods related to page caching
  # Page caching is mostly action caching with file backend using its own output directory of .html files

  # Register the action for page caching
  #
  # ==== Parameters
  # action<Symbol>:: The name of the action to register
  # from_now<~minutes>::
  #   The number of minutes (from now) the cache should persist
  #
  # ==== Examples
  #   cache_page :mostly_static
  #   cache_page :barely_dynamic, 10
  def cache_page(action, from_now = nil)
    cache_pages([action, from_now])
  end

  # Register actions for page caching (before and after filters)
  #
  # ==== Parameter
  # pages<Symbol,Array[Symbol,~minutes]>:: See #cache_page
  #
  # ==== Example
  #   cache_pages :mostly_static, [:barely_dynamic, 10]
  def cache_pages(*pages)
    if pages.any? && !Merb::Cache.cached_pages.key?(controller_name)
      before(:cache_page_before)
      after(:cache_page_after)
    end
    pages.each do |action, from_now| 
      _pages = Merb::Cache.cached_pages[controller_name] ||= {}
      _pages[action] = [from_now, 0]
    end
    true
  end
end

module Merb::Cache::ControllerInstanceMethods
  # Mixed in Merb::Controller. Provides methods related to page caching

  DEFAULT_PAGE_EXTENSION = 'html'

  # Checks whether a cache entry exists
  #
  # ==== Parameter
  # options<String,Hash>:: The options that will be passed to #key_for
  #
  # ==== Returns
  # true if the cache entry exists, false otherwise
  #
  # ==== Example
  #   cached_page?(:action => 'show', :params => [params[:page]])
  #   cached_page?(:action => 'show', :extension => 'js')
  def cached_page?(options)
    key = Merb::Controller._cache.key_for(options, controller_name, true)
    extension = options[:extension] || DEFAULT_PAGE_EXTENSION
    File.file?(Merb::Controller._cache.config[:cache_html_directory] / "#{key}.#{extension}")
  end

  # Expires the page identified by the key computed after the parameters
  #
  # ==== Parameter
  # options<String,Hash>:: The options that will be passed to #expire_key_for
  #
  # ==== Examples
  #   expire_page(:action => 'show', :controller => 'news')
  #   expire_page(:action => 'show', :match => true)
  #   expire_page(:action => 'show', :extension => 'js')
  def expire_page(options)
    config_dir = Merb::Controller._cache.config[:cache_html_directory]
    Merb::Controller._cache.expire_key_for(options, controller_name, true) do |key, match|
      if match
        files = Dir.glob(config_dir / "#{key}*")
      else
        extension = options[:extension] || DEFAULT_PAGE_EXTENSION
        files = config_dir / "#{key}.#{extension}"
      end
      FileUtils.rm_rf(files)
    end
    true
  end

  # Expires all the pages stored in config[:cache_html_directory]
  def expire_all_pages
    FileUtils.rm_rf(Dir.glob(Merb::Controller._cache.config[:cache_html_directory] / "*"))
  end

  # You can call this method if you need to prevent caching the page
  # after it has been rendered.
  def abort_cache_page
    @capture_page = false
  end

  private

  # Called by the before and after filters. Stores or recalls a cache entry.
  # The name used for the cache file is based on request.path
  # If the name ends with "/" then it is removed
  # If the name is "/" then it will be replaced by "index"
  #
  # ==== Parameters
  # data<String>:: the data to put in cache
  #
  # ==== Examples
  #   All the file are written to config[:cache_html_directory]
  #   If request.path is "/", the name will be "/index.html"
  #   If request.path is "/news/show/1", the name will be "/news/show/1.html"
  #   If request.path is "/news/show/", the name will be "/news/show.html"
  #   If request.path is "/news/styles.css", the name will be "/news/styles.css"
  def _cache_page(data = nil)
    return if Merb::Controller._cache.config[:disable_page_caching]
    controller = controller_name
    action = action_name.to_sym
    pages = Merb::Controller._cache.cached_pages[controller]
    return unless pages && pages.key?(action)
    path = request.path.chomp("/")
    path = "index" if path.empty?
    ext = "." + (params[:format].empty? ? DEFAULT_PAGE_EXTENSION : params[:format])
    ext = nil if File.extname(path) == ext
    cache_file = Merb::Controller._cache.config[:cache_html_directory] / "#{path}#{ext}"
    if data
      cache_directory = File.dirname(cache_file)
      FileUtils.mkdir_p(cache_directory)
      _expire_in = pages[action][0]
      pages[action][1] = _expire_in.minutes.from_now unless _expire_in.nil?
      cache_write_page(cache_file, data)
      Merb.logger.info("cache: set (#{path})")
    else
      @capture_page = false
      if File.file?(cache_file)
        _data = cache_read_page(cache_file)
        _expire_in, _expire_at = pages[action]
        if _expire_in.nil? || Time.now < _expire_at
          Merb.logger.info("cache: hit (#{path})")
          throw(:halt, _data)
        end
        FileUtils.rm_f(cache_file)
      end
      @capture_page = true
    end
    true
  end

  # Read data from a file using exclusive lock
  #
  # ==== Parameters
  # cache_file<String>:: the full path to the file
  #
  # ==== Returns
  # data<String>:: the data that has been read from the file
  def cache_read_page(cache_file)
    _data = nil
    File.open(cache_file, "r") do |cache_data|
      cache_data.flock(File::LOCK_EX)
      _data = cache_data.read
      cache_data.flock(File::LOCK_UN)
    end
    _data
  end

  # Write data to a file using exclusive lock
  #
  # ==== Parameters
  # cache_file<String>:: the full path to the file
  # data<String>:: the data that will be written to the file
  def cache_write_page(cache_file, data)
    File.open(cache_file, "w+") do |cache_data|
      cache_data.flock(File::LOCK_EX)
      cache_data.write(data)
      cache_data.flock(File::LOCK_UN)
    end
    true
  end

  # before filter
  def cache_page_before
    # recalls a cached entry or set @capture_page to true in order
    # to grab the response in the after filter
    _cache_page
  end

  # after filter
  def cache_page_after
    # takes the body of the response
    # if the cache entry expired, if it doesn't exist or status is 200
    _cache_page(body) if @capture_page && status == 200
  end
end
