require 'tempfile'
require 'readline'
require 'getoptlong'
require 'curses'
require 'rrb/rrb'
require 'rrb/completion'

class String
  def trim
    split(/\s+/).find{|s| s != ""}
  end
end

module RRB
  module CUI
    USAGE = <<USG
usage: rrbcui [options] FILES..

Refactoring FILES automatically.

options:
  -w              rewrite FILES [no implementation]
  -d DIFF         output diff [default, DIFF=output.diff]
  -h              print this message and exit
  -r REFACTORING  do REFACTORING
  -o              overwrite output diff file

refactoring:
  rename-local-variable [method old-var new-var]
  rename-instance-variable [class old-var new-var]
  rename-class-variable [class old-var new-var]
  rename-global-variable [old-var new-var]
  rename-constant [old-constant new-constant]
  rename-class [old-class new-class]
  rename-method-all [old-method new-method]
  rename-method [old-methods.. new-method]
  extract-method [file begin end new-method]
  extract-superclass [new-namespace new-class target-classes.. file lineno]
  pullup-method [old-class#old-method new-class file lineno]
  pushdown-method [old-class#old-method new-class file lineno]
USG

    OPTIONS = [
      ['-d', GetoptLong::REQUIRED_ARGUMENT],
      ['-w', GetoptLong::NO_ARGUMENT],
      ['--help', '-h', GetoptLong::NO_ARGUMENT],
      ['-r', GetoptLong::REQUIRED_ARGUMENT],
      ['-o', GetoptLong::NO_ARGUMENT],
    ]

    module_function
    def print_usage
      print USAGE
      exit
    end

    def select_one(prompt, words)
      Readline.basic_word_break_characters = "\t\n\"\\'"
      Readline.completion_proc = Proc.new do |word|
        words.grep(/^#{Regexp.quote(word)}/)
      end
      Readline.readline(prompt).trim
    end

    def select_any(prompt, words)
      Readline.basic_word_break_characters = " \t\n\"\\'"
      Readline.completion_proc = Proc.new do |word|
        words.grep(/^#{Regexp.quote(word)}/)
      end
      Readline.readline(prompt).split(/\s+/)
    end
    

    # this class enables you to show file, scroll, select line
    # and select region
    class Screen
      def initialize(str)
        @str = str.split(/^/)
        @cursor = 0
        @top = 0
        @start = nil
      end

      def new_region(lineno1, lineno2)
        if lineno1 < lineno2
          lineno1+1 .. lineno2+1
        else
          lineno2+1 .. lineno1+1
        end
      end

      def select
        Curses.init_screen
        begin
          Curses.nonl
          Curses.cbreak
          Curses.noecho
          
          loop do
            draw_screen
            
            key = Curses.getch
            case key
            when ?j, ?\C-n, Curses::KEY_DOWN
              cursor_down
            when ?J
              scroll_down
            when ?k, ?\C-p, Curses::KEY_UP
              cursor_up
            when ?K
              scroll_up
            when ?\s, Curses::KEY_NPAGE
              Curses.lines.times{ scroll_down }
            when ?g
              @str.size.times{ cursor_up }
            when ?G
              @str.size.times{ cursor_down }
            when ?q
              return nil
            when ?\C-m
              yield
            end
          end
          
        ensure
          Curses.clear
          Curses.close_screen
        end
      end
      
      def select_region
        select do
          if @start
            return new_region(@start, @cursor)
          else
            @start = @cursor
          end
        end
      end

      def select_line
        select do
          return @cursor + 1
        end
      end
      
      def draw_screen
        Curses.lines.times do |i|
          break if @str[@top+i] == nil
          Curses.setpos(i, 0)
          Curses.standout if i == @start
          Curses.addstr(@str[@top+i])
          Curses.standend
        end
        Curses.setpos(@cursor - @top, 0)
        Curses.refresh
      end
      
      # scroll down the screen
      def scroll_down
        return if @str[@top+Curses.lines+1] == nil
        @top += 1
        @cursor = @top if @cursor < @top
      end
      
      def cursor_down
        return if @cursor >= @str.size - 1
        @cursor += 1
        scroll_down if @cursor - @top >= Curses.lines
      end

      # scroll up the screen
      def scroll_up
        return if @top <= 0
        @top -= 1
        @cursor = @top + Curses.lines - 1 if @cursor > @top + Curses.lines - 1
      end
      
      def cursor_up
        return if @cursor <= 0
        @cursor -= 1
        scroll_up if @cursor < @top
      end
    end

    class DiffOutputer
      def initialize(diff_file)
        @diff_file = diff_file
      end

      def output(script)
        # clear file content
        File.open(@diff_file, "wb"){}
        
        script.files.find_all{|sf| sf.new_script != nil}.each do |sf|
          tmp = Tempfile.new("rrbcui")
          begin
            tmp.print(sf.new_script)
            tmp.close
            system("diff -u #{sf.path} #{tmp.path} >> #{@diff_file}")
          ensure
            tmp.close(true)
          end
        end
      end
    end

    class FileRewriter
      def output(script)
        script.result_rewrite_file
      end
    end
    
    class UI
      def initialize(files, out)
        @script = Script.new_from_filenames(*files)
        @out = out
      end

      def select_one(prompt, words)
        CUI.select_one(prompt, words)
      end

      
      def select_any(prompt, words)
        CUI.select_any(prompt, words)
      end
      
      def input_str(prompt)
        Readline.completion_proc = proc{ [] }
        Readline.readline(prompt).trim
      end
  
      def select_region(scriptfile)
        Screen.new(scriptfile.input).select_region
      end

      def select_line(scriptfile)
        Screen.new(scriptfile.input).select_line
      end
      
      def classes
        @script.refactable_classes
      end

      def files
        @script.files.map{|sf| sf.path}
      end
      
      def check_and_execute(refactoring, *args)
        unless @script.__send__("#{refactoring}?", *args)
          STDERR.print(@script.error_message, "\n")
          exit
        end
        @script.__send__(refactoring, *args)
        @out.output(@script)
      end

      def input_new_namespace
        result = CUI.select_one("What namespace is your new class in: ", classes)
        return Namespace::Toplevel if result == nil
        return Namespace[result]
      end

      def methods
        @script.refactable_methods.map{|method| method.name}
      end
    end

    class RenameLocalVariable < UI
      
      def vars(method)
        @script.refactable_methods.find{|m| m.name == method}.local_vars.to_a
      end

      def run
        method = select_one("Refactored method: ", methods)
        old_var = select_one("Old variable: ", vars(method))
        new_var = input_str("New variable: ")
        check_and_execute("rename_local_var", Method[method], old_var, new_var)
      end
    end
    
    class RenameInstanceVariable < UI
      def ivars(target)
        @script.refactable_classes_instance_vars.each do |classname, ivars|
          return ivars if classname == target.name
        end
        return []
      end
      
      def run
        namespace = Namespace[select_one("Refactared class: ", classes)]
        old_var = select_one("Old variable: ", ivars(namespace))
        new_var = input_str("New variable: ")
        check_and_execute("rename_instance_var", namespace, old_var, new_var)
      end
    end

    class RenameClassVariable < UI
      def cvars(target)
        @script.refactable_classes_class_vars.each do |cname, cvars|
          return cvars if cname == target.name
        end
        return []
      end

      def run
        namespace = Namespace[select_one("Refactared class: ", classes)]
        old_var = select_one("Old variable: ", cvars(namespace))
        new_var = input_str("New variable: ")
        check_and_execute("rename_class_var", namespace, old_var, new_var)
      end
    end

    class RenameGlobalVariable < UI
      def gvars
        @script.refactable_global_vars
      end

      def run
        old_var = select_one("Old variable: ", gvars)
        new_var = input_str("New variable: ")
        check_and_execute("rename_global_var", old_var, new_var)
      end
    end

    class RenameConstant < UI
      def consts
        @script.refactable_consts
      end

      def run
        old_const = select_one("Old contant: ", consts)
        new_const = input_str("New constant: ")
        check_and_execute("rename_constant", old_const, new_const)
      end
    end

    class RenameClass < UI
      def classes
        @scripts.refactable_classes
      end

      def run
        old_class = select_one("Old class: ", classes)
        new_class = input_str("New class: ")
        check_and_execute("rename_constant", old_class, new_class)
      end
    end

    class RenameMethodAll < UI
      def methods
        @script.refactable_methods.map{|m| m.bare_name}.sort.uniq
      end

      def run
        old_method = select_one("Old method: ", methods)
        new_method = input_str("New method: ")
        check_and_execute("rename_method_all", old_method, new_method)
      end
    end
    
    class ExtractMethod < UI
      def run
        path = select_one("What file?: ", files)
        region = select_region(@script.files.find{|sf| sf.path == path})
        new_method = input_str("New method: ")
        check_and_execute("extract_method", path, new_method,
                          region.begin, region.end)
      end
    end

    class ExtractSuperclass < UI
      
      def run
        path = select_one("What file new class is created?: ", files)
        lineno = select_line(@script.files.find{|sf| sf.path == path})
        namespace = input_new_namespace
        new_class = input_str("New class: ")
        targets = select_any("Targets: ", classes).map{|cls| Namespace[cls]}

        check_and_execute("extract_superclass", namespace, new_class,
                          targets, path, lineno)
      end
    end

    class PullupMethod < UI
      def run
        method = select_one("Old method: ", methods)
        new_namespace = input_new_namespace
        path = select_one("What file #{method} is pulled up?: ", files)
        lineno = select_line(@script.files.find{|sf| sf.path == path})
        check_and_execute("pullup_method", Method[method], new_namespace,
                          path, lineno)
      end
    end

    class PushDownMethod < UI
      def run
        method = select_one("Old method: ", methods)
        new_namespace = input_new_namespace
        path = select_one("What file #{method} is pushed down?: ", files)
        lineno = select_line(@script.files.find{|sf| sf.path == path})
        check_and_execute("pushdown_method", Method[method], new_namespace,
                          path, lineno)
      end
    end
    
    REFACTORING_MAP = {
      'rename-local-variable' => RenameLocalVariable,
      'rename-instance-variable' => RenameInstanceVariable,
      'rename-class-variable' => RenameClassVariable,
      'rename-global-variable' => RenameGlobalVariable,
      'rename-constant' => RenameConstant,
      'rename-class' => RenameClass,
      'rename-method-all' => RenameMethodAll,
      'rename-method' => nil,
      'extract-method' => ExtractMethod,
      'extract-superclass' => ExtractSuperclass,
      'pullup-method' => PullupMethod,
      'pushdown-method' => PushDownMethod,
    }

    REFACTORING = REFACTORING_MAP.map{|name, klass| name}
    
    # parse ARGV and do refactoring
    def execute
      print_usage if ARGV.empty?

      diff_file = 'output.diff'
      refactoring = nil
      overwrite_diff = false
      rewrite_source_files = false
      
      parser = GetoptLong.new
      parser.set_options( *OPTIONS )
      parser.each_option do |name, arg|
        print_usage if name == '--help'
        diff_file = arg if name == '-d'
        refactoring = arg if name == '-r'
        overwrite_diff = true if name == '-o'
        rewrite_source_files = true if name == '-w'
      end

      if rewrite_source_files
        out = FileRewriter.new
      else
        if !overwrite_diff && File.exist?(diff_file)
          STDERR.print "ERROR: #{diff_file} exists\n"
          exit 1
        end
        out = DiffOutputer.new(diff_file)
      end
      
      refactoring = select_one("Refactoring: ", REFACTORING) unless refactoring
      ui_class = REFACTORING_MAP.fetch(refactoring){ raise 'No such refactoring' }
      ui_class.new(ARGV, out).run
    end

  end
end

if $0 == __FILE__
  exit if ARGV.empty?
  screen = RRB::CUI::Screen.new(File.read(ARGV[0]))
  p screen.select_region
end
