#
# Author:: Adam Jacob (<adam@opscode.com>)
# Author:: Daniel DeLeo (<dan@kallistec.com>)
# Copyright:: Copyright (c) 2009 Opscode, Inc.
# Copyright:: Copyright (c) 2009 Daniel DeLeo
# License:: Apache License, Version 2.0
#
# 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.
#

require 'set'
require 'fileutils'
require 'chef/log'
require 'chef/config'
require 'chef/client'
require 'chef/mixin/convert_to_class_name'
require 'singleton'
require 'moneta'

class Chef
  class ChecksumCache
    include Chef::Mixin::ConvertToClassName
    include ::Singleton

    attr_reader :moneta

    def initialize(*args)
      self.reset!(*args)
    end

    def reset!(backend=nil, options=nil)
      backend ||= Chef::Config[:cache_type]
      options ||= Chef::Config[:cache_options]

      begin
        require "moneta/#{convert_to_snake_case(backend, 'Moneta')}"
      rescue LoadError => e
        Chef::Log.fatal("Could not load Moneta back end #{backend.inspect}")
        raise e
      end

      @moneta = Moneta.const_get(backend).new(options)
    end

    def self.reset_cache_validity
      @valid_cached_checksums = nil
    end

    Chef::Client.when_run_starts do |run_status|
      reset_cache_validity
    end

    def self.valid_cached_checksums
      @valid_cached_checksums ||= Set.new
    end

    def self.validate_checksum(checksum_key)
      valid_cached_checksums << checksum_key
    end

    def self.all_cached_checksums
      all_checksums_with_filenames = {}

      Dir[File.join(Chef::Config[:cache_options][:path], '*')].each do |cksum_file|
        all_checksums_with_filenames[File.basename(cksum_file)] = cksum_file
      end
      all_checksums_with_filenames
    end

    def self.cleanup_checksum_cache
      Chef::Log.debug("Cleaning the checksum cache")
      if (Chef::Config[:cache_type].to_s == "BasicFile")
        all_cached_checksums.each do |cache_key, cksum_cache_file|
          unless valid_cached_checksums.include?(cache_key)
            remove_unused_checksum(cksum_cache_file)
          end
        end
      end
    end

    Chef::Client.when_run_completes_successfully do |run_status|
      cleanup_checksum_cache
    end

    def self.remove_unused_checksum(checksum_file)
      Chef::Log.debug("Removing unused checksum cache file #{checksum_file}")
      FileUtils.rm(checksum_file)
    end

    def self.checksum_for_file(*args)
      instance.checksum_for_file(*args)
    end

    def validate_checksum(*args)
      self.class.validate_checksum(*args)
    end

    def checksum_for_file(file, key=nil)
      key ||= generate_key(file)
      fstat = File.stat(file)
      lookup_checksum(key, fstat) || generate_checksum(key, file, fstat)
    end

    def lookup_checksum(key, fstat)
      cached = fetch(key)
      if cached && file_unchanged?(cached, fstat)
        validate_checksum(key)
        cached["checksum"]
      else
        nil
      end
    end

    def generate_checksum(key, file, fstat)
      checksum = checksum_file(file, Digest::SHA256.new)
      moneta.store(key, {"mtime" => fstat.mtime.to_f, "checksum" => checksum})
      validate_checksum(key)
      checksum
    end

    def generate_key(file, group="chef")
      "#{group}-file-#{file.gsub(/(#{File::SEPARATOR}|\.)/, '-')}"
    end

    def self.generate_md5_checksum_for_file(*args)
      instance.generate_md5_checksum_for_file(*args)
    end

    def generate_md5_checksum_for_file(file)
      checksum_file(file, Digest::MD5.new)
    end

    def generate_md5_checksum(io)
      checksum_io(io, Digest::MD5.new)
    end

    private

    def fetch(key)
      @moneta.fetch(key)
    rescue ArgumentError => e
      Log.warn "Error loading cached checksum for key #{key.inspect}"
      Log.warn(e)
      repair_checksum_cache
      nil
    end

    def repair_checksum_cache
      Chef::Log.info("Removing invalid checksum cache files")
      Dir["#{Chef::Config[:cache_options][:path]}/*"].each do |file_path|
        File.unlink(file_path) unless File.size?(file_path)
      end
    end

    def file_unchanged?(cached, fstat)
      cached["mtime"].to_f == fstat.mtime.to_f
    end

    def checksum_file(file, digest)
      File.open(file, 'rb') { |f| checksum_io(f, digest) }
    end

    def checksum_io(io, digest)
      while chunk = io.read(1024 * 8)
        digest.update(chunk)
      end
      digest.hexdigest
    end

  end
end

module Moneta
  module Defaults
    def default
      nil
    end
  end
end