#---
# Excerpted from "Everyday Scripting in Ruby"
# We make no guarantees that this code is fit for any purpose. 
# Visit http://www.pragmaticprogrammer.com/titles/bmsft for more book information.
#---
# The following is borrowed from setup.rb

class File; end

def File.binread(fname)
  open(fname, 'rb') {|f|
    return f.read
  }
end

# for corrupted windows stat(2)
def File.dir?(path)
  File.directory?((path[-1,1] == '/') ? path : path + '/')
end


module Gem
  
  module Deployment

    # The following is borrowed from setup.rb
    module FileOperations

      def mkdir_p(dirname, prefix = nil)
        dirname = prefix + File.expand_path(dirname) if prefix
        $stderr.puts "mkdir -p #{dirname}" if verbose?
        return if no_harm?

        # does not check '/'... it's too abnormal case
        dirs = File.expand_path(dirname).split(%r<(?=/)>)
        if /\A[a-z]:\z/i =~ dirs[0]
          disk = dirs.shift
          dirs[0] = disk + dirs[0]
        end
        dirs.each_index do |idx|
          path = dirs[0..idx].join('')
          Dir.mkdir path unless File.dir?(path)
        end
      end

      def rm_f(fname)
        $stderr.puts "rm -f #{fname}" if verbose?
        return if no_harm?

        if File.exist?(fname) or File.symlink?(fname)
          File.chmod 0777, fname
          File.unlink fname
        end
      end

      def rm_rf(dn)
        $stderr.puts "rm -rf #{dn}" if verbose?
        return if no_harm?

        Dir.chdir dn
        Dir.foreach('.') do |fn|
          next if fn == '.'
          next if fn == '..'
          if File.dir?(fn)
            verbose_off {
              rm_rf fn
            }
          else
            verbose_off {
              rm_f fn
            }
          end
        end
        Dir.chdir '..'
        Dir.rmdir dn
      end

      def no_harm?
        false
      end

      def verbose?
        false
      end

      def verbose_off(&block)
        block.call
      end

    end
  
    class Manager
    
      DEPLOYMENTS_DIR = "."
      DEPLOYMENTS_DB = "deployments.yaml"
      
      attr_reader :deployments
    
      def initialize(dir = DEPLOYMENTS_DIR, db = DEPLOYMENTS_DB)
        require 'yaml'
        require 'digest/sha1'
        @db_file = File.expand_path(File.join(dir, db))
        if File.exist?(@db_file)
          @deployments = YAML.load(File.binread(@db_file))
          @deployments.each {|deployment| deployment.manager = self}
        else
          @deployments = []
        end
      end
      
      def new_deployment(target_dir = nil)
        unless target_dir
          require 'rbconfig'
          target_dir = Config::CONFIG['sitelibdir']
        end
        target_dir = File.expand_path(target_dir)
        deployment = self[target_dir]
        unless deployment
          deployment = ActiveDeployment.new(target_dir)
          deployment.manager = self
          @deployments << deployment
        end
        deployment
      end
    
      def persist
        File.open(@db_file, "wb") {|f| f.puts @deployments.to_yaml}
      end
    
      def [](target_dir)
        target_dir = File.expand_path(target_dir)
        @deployments.each {|deployment| return deployment if deployment.target_directory == target_dir}
        nil
      end
    end
  
    class ActiveDeployment
      attr_reader :target_directory, :deployed_gems
      attr_accessor :manager
    
      def initialize(target_directory)
        @target_directory = target_directory
        @deployed_gems = []
      end

      def to_yaml_properties
        ['@target_directory', '@deployed_gems']
      end

      def prepare
        @deployed_gems.each {|gem| gem.prepare}
      end
    
      def deploy
        @deployed_gems.each {|gem| gem.deploy}
        if fully_deployed?
          @manager.persist
        else
          raise "ERROR: Did not fully deploy"
        end
      end

      def fully_deployed?
        @deployed_gems.each {|gem| return false unless gem.deployed?}
        return true
      end
    
      def add_all_gems
        Gem.source_index.each do |name, spec|
          @deployed_gems << DeployedGem.new(spec, @target_directory)
        end
        self
      end
    
      def add_gem(gem_to_add)
        @deployed_gems.each {|dg| return if gem_to_add.full_name == dg.gem_name}
        @deployed_gems << DeployedGem.new(gem_to_add, @target_directory)
        return self if gem_to_add.dependencies.size == 0
        # must fulfill dependencies
        sats = {}
        Gem.source_index.each do |name, gem|
          gem_to_add.dependencies.each do |dependency|
            (sats[dependency] ||= []) << gem if gem.satisfies_requirement?(dependency)
          end
        end
        sats.each_value {|list| add_gem list.sort.last}
        self
      end
    end

    class DeployedGem
      attr_reader :specification, :gem_name, :gem_path, :deployed_files
      
      def initialize(spec, target_directory)
        @gem_name = spec.full_name
        @gem_path = spec.full_gem_path
        @lib_paths = spec.require_paths
        @target_directory = target_directory
        @deployed_files = []
      end

      def to_yaml_properties
        ['@gem_name', '@gem_path', '@lib_paths', '@target_directory', '@deployed_files']
      end
      
      def prepare
        paths = @lib_paths.collect {|lib_path| File.expand_path(File.join(gem_path, lib_path))}
        paths.each do |path|
          Dir.glob("#{path}/**/*").each do |file|
            unless File.directory?(file)
              @deployed_files << DeployedFile.new(file, File.join(@target_directory, file[(path.size+1)..-1]))
            end
          end
        end
        @deployed_files.each {|df| df.prepare}
      end
      
      def deploy
        @deployed_files.each {|file| file.deploy}
      end
      
      def deployed?
        @deployed_files.each {|file| return false unless file.deployed?}
        return true
      end
    end
    
    class DeployedFile
      
      include FileOperations
      
      attr_reader :source_path, :destination_path, :checksum
      
      def initialize(source_path, destination_path)
        @source_path = source_path
        @destination_path = destination_path
      end
      
      def to_yaml_properties
        ['@source_path', '@destination_path', '@checksum']
      end
      
      def prepare
        @checksum ||= Digest::SHA1.new(File.binread(@source_path)).hexdigest
      end
      
      def deploy
        return if deployed?
        File.open(@source_path, "rb") do |source| 
          mkdir_p(File.dirname(@destination_path))
          File.open(@destination_path, "wb") do |destination|
            destination.write(source.read)
          end
        end
      end

      def deployed?
        return false unless File.exist?(@destination_path)
        return false if File.size(@source_path) != File.size(@destination_path)
        new_checksum = nil
        begin
          new_checksum = Digest::SHA1.new(File.binread(@destination_path)).hexdigest
        rescue
          puts $!
          puts $!.backtrace.join("\n")
        end
        new_checksum == @checksum
      end
    end
    
  end
end
