#
#   Copyright (C) 2006 Eriko Sato
#
#   This program is free software; you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation; either version 2, or (at your option)
#   any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program; if not, write to the Free Software
#   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#

require "kz/ruby-completion"
require "kz/search-window"

module Kz
  class SandBox
    def initialize(kz)
      app = Kz::App.instance
      @kz = kz
      @binding = binding
    end

    def _binding
      @binding
    end

    def evaluate(statements, file=__FILE__, line=__LINE__)
      eval(statements, @binding, file, line)
    end
  end

  class RubyDialog
    HISTORY_PATH = File.join(Kz::CONFIG_DIR, "ruby-command-history")
    @@history ||= nil
    @@history_spins ||= []

    attr_reader :dialog
    def initialize(kz)
      @kz = kz
      @default_font_size = 14
      init_dialog
      @dialog.show_all
      @entry.grab_focus
      init_sandbox
    end

    def clear_history
      @@history.clear
      update_history_spins_range
      nil
    end

    private
    def _(str)
      Kz.gettext(str)
    end

    def init_sandbox
      @sandbox = SandBox.new(@kz)
      @sandbox.instance_variable_set("@dialog", self)
      @sandbox.evaluate("dialog = @dialog")
    end

    def init_dialog
      @dialog = Gtk::Dialog.new(_("Ruby dialog"))
      @dialog.set_size_request(400, 300)
      @dialog.signal_connect("destroy") do |widget, event|
        Kz.barrier do
          save_history
          @@history_spins.delete(@history_spin)
        end
        false
      end
      init_history
      init_search
      init_output_area
      init_input_area
      init_buttons
      init_redirector
    end

    def init_history
      @@history ||= load_history
    end

    def load_history
      history = []
      return history unless File.exist?(HISTORY_PATH)
      Kz.barrier do
        File.open(HISTORY_PATH) do |f|
          f.each do |line|
            line.strip!
            history << line unless line.empty?
          end
        end
      end
      history
    end

    def save_history
      Kz.barrier do
        File.open(HISTORY_PATH, "w") do |f|
          f.puts(@@history.join("\n"))
        end
      end
    end

    def init_search
      searcher = Object.new
      def searcher.regexp(text)
        /#{Regexp.quote(text)}/i
      end
      @search_window = SearchWindow.new(searcher)
      @search_window.window.set_transient_for(@dialog)
      entry = @search_window.entry
      update_widget_font(entry, nil, "monospace")
      direction = @search_window.direction
      entry.signal_connect("key_press_event") do |widget, event|
        handled = false
        if event.state.control_mask?
          handled = true
          case event.keyval
          when Gdk::Keyval::GDK_s
            search_history(true)
          when Gdk::Keyval::GDK_r
            search_history(false)
          when Gdk::Keyval::GDK_g
            stop_history_search
          else
            handled = false
          end
        end
        handled
      end
      entry.signal_connect("changed") do
        search_history_with_current_input
      end
      direction.signal_connect("toggled") do
        search_history_with_current_input(true)
      end
      entry.signal_connect("activate") do
        stop_history_search
        true
      end
    end

    def init_output_area
      init_text_view
    end

    def init_text_view
      @view = Gtk::TextView.new
      @view.wrap_mode = Gtk::TextTag::WRAP_WORD_CHAR
      @view.editable = false
      @buffer = @view.buffer
      init_mark
      init_tags
      @buffer.signal_connect_after("insert_text") do |widget, iter, text, len|
        start = @buffer.get_iter_at_offset(iter.offset - len)
        @buffer.apply_tag(@all_tag, start, iter)
        false
      end
      sw = add_scrolled_window(@view)
      @dialog.vbox.pack_start(sw, true, true, 0)
    end

    def init_input_area
      init_history_spin_button
      init_input_entry

      input_hbox = Gtk::HBox.new
      input_hbox.pack_start(@history_spin, false, true, 0)
      input_hbox.pack_start(@entry, true, true, 0)
      @dialog.vbox.pack_start(input_hbox, false, true, 0)
    end

    def init_history_spin_button
      adjustment ||= Gtk::Adjustment.new(@@history.size, 0,
                                         @@history.size, 1, 4, 0)
      @history_spin = Gtk::SpinButton.new(adjustment, 1, 0)
      @history_spin.signal_connect_after("value-changed") do |widget, type|
        @last_history_index ||= @@history.size
        @entry_last_text = @entry.text if @last_history_index == @@history.size
        @last_history_index = @history_spin.value.to_i
        update_input_entry
        false
      end
      update_widget_font(@history_spin)
      @@history_spins << @history_spin
    end

    def init_input_entry
      @entry = Gtk::Entry.new
      @entry_last_text = nil
      update_widget_font(@entry, nil, "monospace")
      setup_input_entry_ruby_completion
      @entry.signal_connect("key_press_event") do |widget, event|
        Kz.barrier do
          handle_input(event)
        end
      end
      @entry.signal_connect("activate") do |widget, event|
        Kz.barrier do
          catch(:exit) {activate_input}
        end
        false
      end
      @entry.signal_connect("changed") do |widget, event|
        Kz.barrier do
          update_input_entry_ruby_completion
        end
      end
      @entry
    end

    def update_input_entry
      index = @history_spin.value.to_i
      @entry.text = @@history[index] || @entry_last_text || ""
      @entry.position = -1
    end

    def setup_input_entry_ruby_completion
      @ruby_exp_completion = Gtk::EntryCompletion.new
      @ruby_exp_model = Gtk::ListStore.new(String)
      @ruby_exp_completion.model = @ruby_exp_model
      @ruby_exp_completion.text_column = 0
      @entry.completion = @ruby_exp_completion
    end

    def update_input_entry_ruby_completion
      return if @entry.text.strip.empty?
      result = Kz::RubyCompletion.complete(@entry.text, @sandbox._binding)
      @ruby_exp_model.clear
      result.each do |item|
        iter = @ruby_exp_model.append
        iter[0] = item
      end
    end

    def update_widget_font(widget, size=nil, family=nil)
      size ||= @default_font_size
      desc = widget.style.font_desc.copy
      desc.size = size * Pango::SCALE
      desc.family = family if family
      widget.modify_font(desc)
    end

    def init_mark
      @end_mark = @buffer.create_mark("end", @buffer.end_iter, true)
    end

    def init_tags
      result_tag_prop = {:foreground => "HotPink"}
      @result_tag = @buffer.create_tag("result", result_tag_prop)
      input_tag_prop = {:foreground => "Green"}
      @input_tag = @buffer.create_tag("input", input_tag_prop)
      output_tag_prop = {:foreground => "Blue"}
      @output_tag = @buffer.create_tag("output", output_tag_prop)
      all_tag_prop = {
        :family => "monospace",
        :size_points => @default_font_size
      }
      @all_tag = @buffer.create_tag("all", all_tag_prop)
    end

    def handle_input(event)
      handled = false
      case event.keyval
      when Gdk::Keyval::GDK_m
        @entry.activate if event.state.control_mask?
      when Gdk::Keyval::GDK_p
        handled = previous_history if event.state.control_mask?
      when Gdk::Keyval::GDK_Up
        handled = previous_history
        handled = true
      when Gdk::Keyval::GDK_n
        handled = next_history if event.state.control_mask?
      when Gdk::Keyval::GDK_Down
        handled = next_history
        handled = true
      when Gdk::Keyval::GDK_i
        @ruby_exp_completion.insert_prefix if event.state.control_mask?
      when Gdk::Keyval::GDK_s
        if event.state.control_mask?
          search_history(true)
          handled = true
        end
      when Gdk::Keyval::GDK_r
        if event.state.control_mask?
          search_history(false)
          handled = true
        end
      end
      handled
    end

    def activate_input
      text = @entry.text.strip
      if text.empty?
        @history_spin.value = @@history.size
      else
        eval_print(text)
        update_history(text)
      end
      @entry.text = ""
    end

    def update_history(text)
      if @@history.last != text
        @@history << text
        update_history_spins_range
      end
      @history_spin.value = @@history.size
    end

    def update_history_spins_range
      @@history_spins.each do |spin|
        spin.set_range(0, @@history.size)
      end
    end

    def previous_history
      handled = false
      if @history_spin.value > 0
        @history_spin.value -= 1
        handled = true
      end
      handled
    end

    def next_history
      handled = false
      if @history_spin.value < @@history.size
        @history_spin.value += 1
        handled = true
      end
      handled
    end

    def search_history(forward=false)
      unless @search_window.visible?
        @search_window.show
        adjust_search_window
      end

      if @search_window.forward? == forward
        search_history_with_current_input(true)
      else
        @search_window.forward = forward
      end
    end

    def stop_history_search
      @search_window.hide
      @search_window.entry.text = ""
    end

    def adjust_search_window
      Utils.move_to_bottom_left_outer(@entry, @search_window.window)
    end

    def search_history_with_current_input(search_next=false)
      Kz.barrier do
        return if @search_window.empty?
        change_to_matched_history(@search_window.regexp,
                                  @search_window.forward?,
                                  search_next)
      end
    end

    def change_to_matched_history(reg, forward, search_next=false)
      current_index = @history_spin.value.to_i
      indexes = []
      @@history.each_with_index do |text, i|
        indexes << i if reg =~ text
      end
      if @entry_last_text and reg =~ @entry_last_text
        indexes << @@history.size
      end
      target_index = nil
      indexes.each_with_index do |index, i|
        if index == current_index
          target_index = i
          target_index += (forward ? 1 : -1) if search_next
          break
        elsif index > current_index
          target_index = i + (forward ? 0 : -1)
          break
        end
      end
      target_index = indexes.size - 1 if target_index.nil? and !forward
      if target_index and target_index >= 0 and target_index < indexes.size
        @history_spin.value = indexes[target_index].to_f
        @entry.position = (reg =~ @entry.text || 0)
      end
    end

    def eval_print(text)
      @buffer.insert(@buffer.end_iter, ">> ")
      @buffer.insert(@buffer.end_iter, text, @input_tag)
      @buffer.insert(@buffer.end_iter, "\n")
      result = eval_text(text)
      result = utf8_environment {result.inspect}
      @buffer.insert(@buffer.end_iter, "=> ")
      @buffer.insert(@buffer.end_iter, result, @result_tag)
      @buffer.insert(@buffer.end_iter, "\n")
      @buffer.move_mark(@end_mark, @buffer.end_iter)
      @view.scroll_to_mark(@end_mark, 0, false, 0, 1)
    end

    def add_scrolled_window(widget)
      sw = Gtk::ScrolledWindow.new
      sw.border_width = 5
      sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
      sw.add(widget)
      sw
    end
    
    def init_buttons
      init_font_size_spin_button
      init_exit_button
    end

    def init_font_size_spin_button
      adjustment = Gtk::Adjustment.new(@default_font_size, 8, 72, 1, 4, 0)
      button = Gtk::SpinButton.new(adjustment, 1, 0)
      button.signal_connect("value-changed") do |widget, type|
        @all_tag.size_points = widget.value
        update_widget_font(@entry, widget.value)
        update_widget_font(@history_spin, widget.value)
        update_widget_font(@search_window.entry, widget.value)
        false
      end
      @dialog.action_area.add(button)
      @dialog.action_area.set_child_secondary(button, true)
    end
    
    def init_exit_button
      button = Gtk::Button.new(_("_Close"))
      button.signal_connect("clicked") do |widget, event|
        @dialog.destroy
      end
      @dialog.action_area.add(button)
    end

    def init_redirector
      @buffer_out = Object.new
      @buffer_out.instance_variable_set("@buffer", @buffer)
      @buffer_out.instance_variable_set("@output_tag", @output_tag)
      class << @buffer_out
        def write(str)
          @buffer.insert(@buffer.end_iter, str, @output_tag)
        end
      end
    end

    def eval_text(text)
      redirect do
        @sandbox.evaluate(text)
      end
    rescue SystemExit
      @dialog.destroy
      throw(:exit)
    rescue Exception
      $!
    end

    def redirect
      stdout = $stdout
      $stdout = @buffer_out
      utf8_environment do
        yield
      end
    ensure
      $stdout = stdout
    end

    def utf8_environment
      kcode = $KCODE
      $KCODE = "UTF8"
      yield
    ensure
      $KCODE = kcode if $KCODE == "UTF8"
    end
  end
end
