module Jekyll
  class Regenerator
    attr_reader :site, :metadata, :cache

    def initialize(site)
      @site = site

      # Read metadata from file
      read_metadata

      # Initialize cache to an empty hash
      clear_cache
    end

    # Checks if a renderable object needs to be regenerated
    #
    # Returns a boolean.
    def regenerate?(document)
      case document
      when Post, Page
        document.asset_file? || document.data['regenerate'] || 
          source_modified_or_dest_missing?(
            site.in_source_dir(document.relative_path), document.destination(@site.dest)
          )
      when Document
        !document.write? || document.data['regenerate'] ||
          source_modified_or_dest_missing?(
            document.path, document.destination(@site.dest)
          )
      else
        source_path = document.respond_to?(:path)        ? document.path                    : nil
        dest_path   = document.respond_to?(:destination) ? document.destination(@site.dest) : nil
        source_modified_or_dest_missing?(source_path, dest_path)
      end
    end

    # Add a path to the metadata
    #
    # Returns true, also on failure.
    def add(path)
      return true unless File.exist?(path)

      metadata[path] = {
        "mtime" => File.mtime(path),
        "deps" => []
      }
      cache[path] = true
    end

    # Force a path to regenerate
    #
    # Returns true.
    def force(path)
      cache[path] = true
    end

    # Clear the metadata and cache
    #
    # Returns nothing
    def clear
      @metadata = {}
      clear_cache
    end


    # Clear just the cache
    #
    # Returns nothing
    def clear_cache
      @cache = {}
    end


    # Checks if the source has been modified or the
    # destination is missing
    #
    # returns a boolean
    def source_modified_or_dest_missing?(source_path, dest_path)
      modified?(source_path) || (dest_path and !File.exist?(dest_path))
    end

    # Checks if a path's (or one of its dependencies)
    # mtime has changed
    #
    # Returns a boolean.
    def modified?(path)
      return true if disabled?

      # objects that don't have a path are always regenerated
      return true if path.nil? 

      # Check for path in cache
      if cache.has_key? path
        return cache[path]
      end

      # Check path that exists in metadata
      data = metadata[path]
      if data
        data["deps"].each do |dependency|
          if modified?(dependency)
            return cache[dependency] = cache[path] = true
          end
        end
        if File.exist?(path) && data["mtime"].eql?(File.mtime(path))
          return cache[path] = false
        else
          return add(path)
        end
      end

      # Path does not exist in metadata, add it
      return add(path)
    end

    # Add a dependency of a path
    #
    # Returns nothing.
    def add_dependency(path, dependency)
      return if (metadata[path].nil? || @disabled)

      if !metadata[path]["deps"].include? dependency
        metadata[path]["deps"] << dependency
        add(dependency) unless metadata.include?(dependency)
      end
      regenerate? dependency
    end

    # Write the metadata to disk
    #
    # Returns nothing.
    def write_metadata
      File.open(metadata_file, 'wb') do |f|
        f.write(Marshal.dump(metadata))
      end
    end

    # Produce the absolute path of the metadata file
    #
    # Returns the String path of the file.
    def metadata_file
      site.in_source_dir('.jekyll-metadata')
    end

    # Check if metadata has been disabled
    #
    # Returns a Boolean (true for disabled, false for enabled).
    def disabled?
      @disabled = site.full_rebuild? if @disabled.nil?
      @disabled
    end

    private

    # Read metadata from the metadata file, if no file is found,
    # initialize with an empty hash
    #
    # Returns the read metadata.
    def read_metadata
      @metadata = if !disabled? && File.file?(metadata_file)
        content = File.read(metadata_file)

        begin
          Marshal.load(content)
        rescue TypeError
          SafeYAML.load(content)
        rescue ArgumentError => e
          Jekyll.logger.warn("Failed to load #{metadata_file}: #{e}")
          {}
        end
      else
        {}
      end
    end
  end
end