#
# base.rb
#

require 'rbconfig'


class InstallError < StandardError; end


module HookUtils

  def run_hook(name)
    try_run_hook "#{curr_srcdir()}/#{name}" or
    try_run_hook "#{curr_srcdir()}/#{name}.rb"
  end

  def try_run_hook(fname)
    return false unless File.file?(fname)
    begin
      instance_eval File.read(fname), fname, 1
    rescue
      raise InstallError, "hook #{fname} failed:\n" + $!.message
    end
    true
  end

end


module HookScriptAPI

  def get_config(key)
    @config[key]
  end

  alias config get_config

  def set_config(key, val)
    @config[key] = val
  end

  #
  # srcdir/objdir (works only in the package directory)
  #

  #abstract srcdir_root
  #abstract objdir_root
  #abstract relpath

  def curr_srcdir
    "#{srcdir_root()}/#{relpath()}"
  end

  def curr_objdir
    "#{objdir_root()}/#{relpath()}"
  end

  def srcfile(path)
    "#{curr_srcdir()}/#{path}"
  end

  def srcexist?(path)
    File.exist?(srcfile(path))
  end

  def srcdirectory?(path)
    File.dir?(srcfile(path))
  end
  
  def srcfile?(path)
    File.file? srcfile(path)
  end

  def srcentries(path = '.')
    Dir.open("#{curr_srcdir()}/#{path}") {|d|
      return d.to_a - %w(. ..)
    }
  end

  def srcfiles(path = '.')
    srcentries(path).select {|fname|
      File.file?(File.join(curr_srcdir(), path, fname))
    }
  end

  def srcdirectories(path = '.')
    srcentries(path).select {|fname|
      File.dir?(File.join(curr_srcdir(), path, fname))
    }
  end

end


class Installer

  FILETYPES = %w( bin lib ext data )

  include HookScriptAPI
  include HookUtils
  include FileOperations

  def initialize(config, opt, srcroot, objroot)
    @config = config
    @options = opt
    @srcdir = File.expand_path(srcroot)
    @objdir = File.expand_path(objroot)
    @currdir = '.'
  end

  def inspect
    "#<#{self.class} #{File.basename(@srcdir)}>"
  end

  #
  # Hook Script API bases
  #

  def srcdir_root
    @srcdir
  end

  def objdir_root
    @objdir
  end

  def relpath
    @currdir
  end

  #
  # configs/options
  #

  def no_harm?
    @options['no-harm']
  end

  def verbose?
    @options['verbose']
  end

  def verbose_off
    begin
      save, @options['verbose'] = @options['verbose'], false
      yield
    ensure
      @options['verbose'] = save
    end
  end

  #
  # TASK config
  #

  def exec_config
    exec_task_traverse 'config'
  end

  def config_dir_bin(rel)
  end

  def config_dir_lib(rel)
  end

  def config_dir_ext(rel)
    extconf if extdir?(curr_srcdir())
  end

  def extconf
    opt = @options['config-opt'].join(' ')
    command "#{config('ruby-prog')} #{curr_srcdir()}/extconf.rb #{opt}"
  end

  def config_dir_data(rel)
  end

  #
  # TASK setup
  #

  def exec_setup
    exec_task_traverse 'setup'
  end

  def setup_dir_bin(rel)
    all_files_in(curr_srcdir()).each do |fname|
      adjust_shebang "#{curr_srcdir()}/#{fname}"
    end
  end

  # modify: #!/usr/bin/ruby
  # modify: #! /usr/bin/ruby
  # modify: #!ruby
  # not modify: #!/usr/bin/env ruby
  SHEBANG_RE = /\A\#!\s*\S*ruby\S*/

  def adjust_shebang(path)
    return if no_harm?

    tmpfile = File.basename(path) + '.tmp'
    begin
      File.open(path, 'rb') {|r|
        File.open(tmpfile, 'wb') {|w|
          first = r.gets
          return unless SHEBANG_RE =~ first

          $stderr.puts "adjusting shebang: #{File.basename path}" if verbose?
          w.print first.sub(SHEBANG_RE, '#!' + config('ruby-path'))
          w.write r.read
        }
      }
      move_file tmpfile, File.basename(path)
    ensure
      File.unlink tmpfile if File.exist?(tmpfile)
    end
  end

  def setup_dir_lib(rel)
  end

  def setup_dir_ext(rel)
    make if extdir?(curr_srcdir())
  end

  def setup_dir_data(rel)
  end

  #
  # TASK install
  #

  def exec_install
    exec_task_traverse 'install'
  end

  def install_dir_bin(rel)
    install_files collect_filenames_auto(), config('bin-dir') + '/' + rel, 0755
  end

  def install_dir_lib(rel)
    install_files ruby_scripts(), config('rb-dir') + '/' + rel, 0644
  end

  def install_dir_ext(rel)
    return unless extdir?(curr_srcdir())
    install_files ruby_extentions('.'),
                  config('so-dir') + '/' + File.dirname(rel),
                  0555
  end

  def install_dir_data(rel)
    install_files collect_filenames_auto(), config('data-dir') + '/' + rel, 0644
  end

  def install_files(list, dest, mode)
    mkdir_p dest, @options['install-prefix']
    list.each do |fname|
      install fname, dest, mode, @options['install-prefix']
    end
  end

  def ruby_scripts
    collect_filenames_auto().select {|n| /\.rb\z/ =~ n }
  end
  
  # picked up many entries from cvs-1.11.1/src/ignore.c
  reject_patterns = %w( 
    core RCSLOG tags TAGS .make.state
    .nse_depinfo #* .#* cvslog.* ,* .del-* *.olb
    *~ *.old *.bak *.BAK *.orig *.rej _$* *$

    *.org *.in .*
  )
  mapping = {
    '.' => '\.',
    '$' => '\$',
    '#' => '\#',
    '*' => '.*'
  }
  REJECT_PATTERNS = Regexp.new('\A(?:' +
                               reject_patterns.map {|pat|
                                 pat.gsub(/[\.\$\#\*]/) {|ch| mapping[ch] }
                               }.join('|') +
                               ')\z')

  def collect_filenames_auto
    mapdir((existfiles() - hookfiles()).reject {|fname|
             REJECT_PATTERNS =~ fname
           })
  end

  def existfiles
    all_files_in(curr_srcdir()) | all_files_in('.')
  end

  def hookfiles
    %w( pre-%s post-%s pre-%s.rb post-%s.rb ).map {|fmt|
      %w( config setup install clean ).map {|t| sprintf(fmt, t) }
    }.flatten
  end

  def mapdir(filelist)
    filelist.map {|fname|
      if File.exist?(fname)   # objdir
        fname
      else                    # srcdir
        File.join(curr_srcdir(), fname)
      end
    }
  end

  def ruby_extentions(dir)
    _ruby_extentions(dir) or
        raise InstallError, "no ruby extention exists: 'ruby #{$0} setup' first"
  end

  DLEXT = /\.#{ ::Config::CONFIG['DLEXT'] }\z/

  def _ruby_extentions(dir)
    Dir.open(dir) {|d|
      return d.select {|fname| DLEXT =~ fname }
    }
  end

  #
  # TASK clean
  #

  def exec_clean
    exec_task_traverse 'clean'
    rm_f 'config.save'
    rm_f 'InstalledFiles'
  end

  def clean_dir_bin(rel)
  end

  def clean_dir_lib(rel)
  end

  def clean_dir_ext(rel)
    return unless extdir?(curr_srcdir())
    make 'clean' if File.file?('Makefile')
  end

  def clean_dir_data(rel)
  end

  #
  # TASK distclean
  #

  def exec_distclean
    exec_task_traverse 'distclean'
    rm_f 'config.save'
    rm_f 'InstalledFiles'
  end

  def distclean_dir_bin(rel)
  end

  def distclean_dir_lib(rel)
  end

  def distclean_dir_ext(rel)
    return unless extdir?(curr_srcdir())
    make 'distclean' if File.file?('Makefile')
  end

  #
  # lib
  #

  def exec_task_traverse(task)
    run_hook "pre-#{task}"
    FILETYPES.each do |type|
      if config('without-ext') == 'yes' and type == 'ext'
        $stderr.puts 'skipping ext/* by user option' if verbose?
        next
      end
      traverse task, type, "#{task}_dir_#{type}"
    end
    run_hook "post-#{task}"
  end

  def traverse(task, rel, mid)
    dive_into(rel) {
      run_hook "pre-#{task}"
      __send__ mid, rel.sub(%r[\A.*?(?:/|\z)], '')
      all_dirs_in(curr_srcdir()).each do |d|
        traverse task, "#{rel}/#{d}", mid
      end
      run_hook "post-#{task}"
    }
  end

  def dive_into(rel)
    return unless File.dir?("#{@srcdir}/#{rel}")

    dir = File.basename(rel)
    Dir.mkdir dir unless File.dir?(dir)
    prevdir = Dir.pwd
    Dir.chdir dir
    $stderr.puts '---> ' + rel if verbose?
    @currdir = rel
    yield
    Dir.chdir prevdir
    $stderr.puts '<--- ' + rel if verbose?
    @currdir = File.dirname(rel)
  end

end
