# backtick_javascript: true
# await: true

require 'await'
require 'pp'
require 'stringio'


module REPLUtils
  module_function

  def ls(object, colorize)
    methods = imethods = object.methods
    ancestors = object.class.ancestors
    constants = []
    ivs = object.instance_variables
    cvs = []

    if [Class, Module].include? object.class
      imethods = object.instance_methods
      ancestors = object.ancestors
      constants = object.constants
      cvs = object.class_variables
    end

    if colorize
      blue      = ->(i) { "\e[1;34m#{i}\e[0m" }
      dark_blue = ->(i) { "\e[34m#{i}\e[0m" }
    else
      blue = dark_blue = ->(i) { i }
    end

    out = ''
    out = "#{blue['class variables']}: #{cvs.map { |i| dark_blue[i] }.sort.join('  ')}\n" + out unless cvs.empty?
    out = "#{blue['instance variables']}: #{ivs.map { |i| dark_blue[i] }.sort.join('  ')}\n" + out unless ivs.empty?
    ancestors.each do |a|
      im = a.instance_methods(false)
      meths = (im & imethods)
      methods -= meths
      imethods -= meths
      next if meths.empty? || [Object, BasicObject, Kernel, PP::ObjectMixin].include?(a)
      out = "#{blue["#{a.name}#methods"]}: #{meths.sort.join('  ')}\n" + out
    end
    methods &= object.methods(false)
    out = "#{blue['self.methods']}: #{methods.sort.join('  ')}\n" + out unless methods.empty?
    out = "#{blue['constants']}: #{constants.map { |i| dark_blue[i] }.sort.join('  ')}\n" + out unless constants.empty?
    out
  end

  def eval_and_print(func, mode, colorize, binding = nil)
    printer = if colorize
                ->(i) do
                  ColorPrinter.default(i)
                rescue => e
                  ColorPrinter.colorize(Opal.inspect(i))
                end
              else
                ->(i) do
                  out = []
                  PP.pp(i, out)
                  out.join
                rescue
                  Opal.inspect(i)
                end
              end

    %x{
      var $_result = binding === nil ? eval(func) : binding.$js_eval(func);

      if (mode == 'silent') return nil;

      if ($_result === null) {
        return "=> null";
      }
      else if (typeof $_result === 'undefined') {
        return "=> undefined";
      }
      else if (typeof $_result.$$class === 'undefined') {
        try {
          var json = JSON.stringify($_result, null, 2);
          if (!colorize) json = #{ColorPrinter.colorize(`json`)}
          return "=> " + $_result.toString() + " => " + json;
        }
        catch {
          return "=> " + $_result.toString();
        }
      }
      else {
        if (mode == 'ls') {
          return #{ls(`$_result`, colorize)};
        }
        else {
          var pretty = #{printer.call(`$_result`)};
          // Is it multiline? If yes, add a linebreak
          if (pretty.match(/\n.*?\n/)) pretty = "\n" + pretty;
          return "=> " + pretty;
        }
      }
    }
  rescue Exception => e # rubocop:disable Lint/RescueException
    e.full_message(highlight: true)
  end

  def eval_and_print_async(func, mode, colorize, binding = nil)
    return eval_and_print(func, mode, colorize, binding)
    # bogus call to make this method async
    nil.__await__ # rubocop:disable Lint/UnreachableCode
  end

  def js_repl
    while (line = gets)
      input = JSON.parse(line)

      out = eval_and_print_async(input[:code], input[:mode], input[:colors]).__await__
      puts out if out
      puts '<<<ready>>>'
    end
  end

  # Slightly based on Pry's implementation
  class ColorPrinter < ::PP
    # Taken from CodeRay
    TOKEN_COLORS = {
      debug: "\e[1;37;44m",

      annotation: "\e[34m",
      attribute_name: "\e[35m",
      attribute_value: "\e[31m",
      binary: {
        self: "\e[31m",
        char: "\e[1;31m",
        delimiter: "\e[1;31m",
      },
      char: {
        self: "\e[35m",
        delimiter: "\e[1;35m"
      },
      class: "\e[1;35;4m",
      class_variable: "\e[36m",
      color: "\e[32m",
      comment: {
        self: "\e[1;30m",
        char: "\e[37m",
        delimiter: "\e[37m",
      },
      constant: "\e[1;34;4m",
      decorator: "\e[35m",
      definition: "\e[1;33m",
      directive: "\e[33m",
      docstring: "\e[31m",
      doctype: "\e[1;34m",
      done: "\e[1;30;2m",
      entity: "\e[31m",
      error: "\e[1;37;41m",
      exception: "\e[1;31m",
      float: "\e[1;35m",
      function: "\e[1;34m",
      global_variable: "\e[1;32m",
      hex: "\e[1;36m",
      id: "\e[1;34m",
      include: "\e[31m",
      integer: "\e[1;34m",
      imaginary: "\e[1;34m",
      important: "\e[1;31m",
      key: {
        self: "\e[35m",
        char: "\e[1;35m",
        delimiter: "\e[1;35m",
      },
      keyword: "\e[32m",
      label: "\e[1;33m",
      local_variable: "\e[33m",
      namespace: "\e[1;35m",
      octal: "\e[1;34m",
      predefined: "\e[36m",
      predefined_constant: "\e[1;36m",
      predefined_type: "\e[1;32m",
      preprocessor: "\e[1;36m",
      pseudo_class: "\e[1;34m",
      regexp: {
        self: "\e[35m",
        delimiter: "\e[1;35m",
        modifier: "\e[35m",
        char: "\e[1;35m",
      },
      reserved: "\e[32m",
      shell: {
        self: "\e[33m",
        char: "\e[1;33m",
        delimiter: "\e[1;33m",
        escape: "\e[1;33m",
      },
      string: {
        self: "\e[31m",
        modifier: "\e[1;31m",
        char: "\e[1;35m",
        delimiter: "\e[1;31m",
        escape: "\e[1;31m",
      },
      symbol: {
        self: "\e[33m",
        delimiter: "\e[1;33m",
      },
      tag: "\e[32m",
      type: "\e[1;34m",
      value: "\e[36m",
      variable: "\e[34m",

      insert: {
        self: "\e[42m",
        insert: "\e[1;32;42m",
        eyecatcher: "\e[102m",
      },
      delete: {
        self: "\e[41m",
        delete: "\e[1;31;41m",
        eyecatcher: "\e[101m",
      },
      change: {
        self: "\e[44m",
        change: "\e[37;44m",
      },
      head: {
        self: "\e[45m",
        filename: "\e[37;45m",
      },

      reset: "\e[0m",
    }

    TOKEN_COLORS[:keyword] = TOKEN_COLORS[:reserved]
    TOKEN_COLORS[:method] = TOKEN_COLORS[:function]
    TOKEN_COLORS[:escape] = TOKEN_COLORS[:delimiter]

    def self.default(obj, width = 79)
      pager = StringIO.new
      pp(obj, pager, width)
      pager.string
    end

    def self.pp(obj, output = $DEFAULT_OUTPUT, max_width = 79)
      queue = ColorPrinter.new(output, max_width, "\n")
      queue.guard_inspect_key { queue.pp(obj) }
      queue.flush
      output << "\n"
    end

    def text(str, max_width = str.length)
      super(ColorPrinter.colorize(str), max_width)
    end

    def self.token(string, *name)
      TOKEN_COLORS.dig(*name) + string + TOKEN_COLORS[:reset]
    end

    NUMBER = '[+-]?(?:0x[0-9a-fA-F]+|[0-9.]+(?:e[+-][0-9]+|i)?)'
    REGEXP = '/.*?/[iesu]*'
    STRING = '".*?"'
    TOKEN_REGEXP = /(\s+|=>|[@$:]?[a-z]\w+|[A-Z]\w+|#{NUMBER}|#{REGEXP}|#{STRING}|#<.*?[> ]|.)/

    def self.tokenize(str)
      str.scan(TOKEN_REGEXP).map(&:first)
    end

    def self.colorize(str)
      tokens = tokenize(str)

      tokens.map do |tok|
        case tok
        when /^[0-9+-]/
          if /[.e]/ =~ tok
            token(tok, :float)
          else
            token(tok, :integer)
          end
        when /^"/
          token(tok, :string, :self)
        when /^:/
          token(tok, :symbol, :self)
        when /^[A-Z]/
          token(tok, :constant)
        when '<', '#', /^#</, '=', '>'
          token(tok, :keyword)
        when /^\/./
          token(tok, :regexp, :self)
        when 'true', 'false', 'nil'
          token(tok, :predefined_constant)
        else
          token(tok, :reset)
        end
      end.join
    end
  end
end
