# encoding: utf-8
require 'readline'

module Rouge::REPL

  def self.run!(options = {:backtrace => true})
    puts "Rouge #{Rouge::VERSION}"

    repl_error = lambda do |e|
      STDOUT.puts "!! #{e.class}: #{e.message}"

      if options[:backtrace]
        STDOUT.puts "#{e.backtrace.join "\n"}"
      end
    end

    context = Rouge::Context.new(Rouge[:user])
    count = 0
    chaining = false
    Readline.completion_proc = Completer.new(context.ns)

    while true

      if !chaining
        prompt = "#{context.ns.name}=> "
        input = Readline.readline(prompt, true)
      else
        prompt = "#{" " * [0, context.ns.name.length - 2].max}#_=> "
        input << "\n" + Readline.readline(prompt, true)
      end

      if input.nil?
        STDOUT.print "\n"
        break
      end

      begin
        form = context.ns.read(input)
      rescue Rouge::Reader::EOFError
        next
      rescue Rouge::Reader::EndOfDataError
        chaining = true
        next
      rescue Rouge::Reader::UnexpectedCharacterError => reader_err
        repl_error.call(reader_err)
      rescue Rouge::Reader::NumberFormatError => reader_err
        repl_error.call(reader_err)
      end

      chaining = false

      begin
        form = Rouge::Compiler.compile(context.ns,
                                       Set[*context.lexical_keys],
                                       form)

        result = context.eval(form)

        Rouge.print(result, STDOUT)
        STDOUT.puts

        count += 1 if count < 10
        count.downto(2) do |i|
          context.set_here :"*#{i}", context[:"*#{i - 1}"]
        end
        context.set_here :"*1", result
      rescue Rouge::Context::ChangeContextException => cce
        context = cce.context
        # Since completion is context sensitive, we need update the proc
        # whenever it changes.
        Readline.completion_proc = Completer.new(context.ns)
        count = 0
      rescue => e
        repl_error.call(e)
      end
    end
  end

  module Completer
    extend self

    # Returns a proc intended to be used with Readline.
    #
    # @param [Rouge::Namespace] current_namespace
    #   the current namespace
    #
    # @return [Proc] the completion proc to be used with Readline. The
    #   returned proc accepts a string and returns an array.
    #
    # @api public
    def new(current_namespace)
      return lambda do |query|
        if query.nil? || query.empty?
          return []
        end

        list = namespace_items(current_namespace)
        list << search(query)

        matches = search_list(list.flatten, query)

        # If there's only one match we check if it's a namespace or a Ruby
        # constant which contains other constants or singleton methods.
        if matches.length == 1
          match = matches[0]
          if Rouge::Namespace.exists?(match)
            if current_namespace.table.include?(match)
              matches << "#{match}/"
            else
              Readline.completion_append_character = "/"
            end
          else
            if locate_module(match.to_s)
              Readline.completion_append_character = ""
            end
          end
        else
          Readline.completion_append_character = ""
        end

        matches
      end
    end

    # Returns a list of constants and singleton method names based on the string
    # query.
    #
    # @param [String] query
    #   the search string to use
    #
    # @return [Array<Symbol,String>] the search results
    #
    # @api public
    def search(query)
      namespace, lookup = query.split('/', 2)
      result =
        case namespace
        # The ruby namespace requires special handling.
        when /^[A-Z]/
          search_ruby(query)
        when /^ruby/
          if lookup && lookup.empty?
            Rouge[:ruby].table.map {|x| "ruby/#{x}" }
          else
            search_ruby(lookup).map {|x| "ruby/#{x}" }
          end
        else
          ns = rg_namespaces[namespace.to_sym]

          if ns
            ns.table.map { |var, _| "#{namespace}/#{var}" }
          else
            # Add the current namepace, rouge.builtin, and ruby tables along with
            # the names of available namespaces in the completion list.
            list = []
            list << Rouge[:"rouge.builtin"].table.keys
            list << :ruby
            list << Rouge[:ruby].table
            list << rg_namespaces.keys
          end
        end

      search_list(result.flatten, query)
    end

    # Applies `locate_module` to the string query and returns a list constants
    # and singleton methods. These results are intended to be filtered in the
    # `search` method.
    #
    # @see Completer.locate_module, Completer.search
    #
    # @example
    #   search_ruby("Rouge") #=> ["Rouge/[]", "Rouge/boot!", ...]
    #   search_ruby("Rouge.") #=> ["Rouge/[]", "Rouge/boot!", ...]
    #
    # @param [String] query
    #   the search string to use
    #
    # @return [Array<Symbol,String>] the search result
    #
    # @api public
    def search_ruby(query)
      namespace = query.split('/', 2).first

      mod = locate_module(namespace)

      if mod == Object
        mod.constants
      else
        ns = mod.name.gsub('::','.')
        result = []
        mod.singleton_methods.each { |sm| result << "#{ns}/#{sm}" }
        mod.constants.each { |c| result << "#{ns}.#{c}" }
        result.flatten
      end
    end

    # Recursively searches for a Ruby module (includes classes) given by the
    # string query. The string should contain a Rouge style namespace name for
    # the module.
    #
    # Optionally, a root module can be supplied as the context for the query.
    # By default this is Object. If no module is found, the method returns nil
    # or the root.
    #
    # Be aware this method *only* returns modules and classes.
    #
    # @example
    #   locate_module("Bil.Bo") #=> Bil::Bo
    #   locate_module("Ji.Tsu", Nin) #=> Nin::Ji::Tsu
    #
    # @param [String] query
    #   the module (or class) to find
    #
    # @param [Module] root
    #   the optional search context
    #
    # @return [Class,Module,nil] the search result
    #
    # @api public
    def locate_module(query, root = Object)
      head, tail = query.split('.', 2)

      return root unless rg_ruby_module?(head)

      lookup = head.to_sym

      if root.is_a?(Module) && root.constants.include?(lookup)
        result = root.const_get(lookup)

        return root unless result.is_a?(Module)

        # `query` may have ended with '.'.
        if tail.nil? || tail.empty?
          result
        else
          locate_module(tail, result)
        end
      else
        root
      end
    end

    # Rouge namespaces. Note we do not include the rouge.builtin and ruby
    # namespaces since we would like built in vars, such as def or let, and top
    # level Ruby constants to be easily accessible with command line
    # completion.
    #
    # @return [Hash] the filtered namespaces
    #
    # @api public
    def rg_namespaces
      Rouge::Namespace.all.reject do |key, _|
        [:"rouge.builtin", :ruby].include?(key)
      end
    end

    # Returns true if the provided namespace is included in `rg_namespaces`.
    #
    # @see Rouge::REPL::Completer.rg_namespaces
    #
    # @param [Rouge::Namespace] x
    #   the namespace to test
    #
    # @return [Boolean]
    #
    # @api public
    def rg_namespace?(x)
      x.is_a?(Rouge::Namespace) && rg_namespaces.keys.include?(x.name)
    end

    private

    # Returns a list of table keys and refer keys based on the given namespace.
    #
    # @param [Rouge::Namespace] ns
    #   the namespace to use
    #
    # @return [Array<Symbol>]
    #
    # @api public
    def namespace_items(ns)
      list = ns.table.keys
      refers = ns.refers.select { |ns| rg_namespace?(ns) }
      list << refers.map { |ns| ns.table.keys }
    end

    # Returns true if the string query matches a Rouge style Ruby module or
    # constant name.
    #
    # @param [String] query
    #   the query string to match.
    #
    # @return [Boolean]
    #
    # @api private
    def rg_ruby_module?(x)
      !!/^(?:[A-Z][A-Za-z_]*\.?)+$/.match(x)
    end

    # Filters a list of items based on a string query.
    #
    # @param [Array] list
    #   the list to filter.
    #
    # @param [String] query
    #   the search string to use.
    #
    # @return [Array]
    #
    # @api private
    def search_list(list, query)
      list.grep(/^#{Regexp.escape(query)}/)
    end
  end
end

# vim: set sw=2 et cc=80: