module Diakonos

  # The Diakonos::Functions module contains all the methods that can be mapped
  # to keys in Diakonos.  New methods can be added to this module by
  # extensions.

  module Functions

    # Shows the About page, which gives information on Diakonos.
    def about
      about_write
      open_file @about_filename
    end

    # Deletes characters up to, but not including, a given character.
    # Also puts the deleted text into the clipboard.
    def delete_to( char = nil )
      if char.nil?
        set_iline "Type character to delete to..."
        char = @win_main.getch
        set_iline
      end
      if char
        removed_text = buffer_current.delete_to char
        if removed_text
          @clipboard.add_clip removed_text
        else
          set_iline "'#{char}' not found."
        end
      end
    end

    # Deletes characters starting from (but not including) a given character
    # up to (but not including) the current cursor position.
    # Also puts the deleted text into the clipboard.
    def delete_from( char = nil )
      if char.nil?
        set_iline "Type character to delete from..."
        char = @win_main.getch
        set_iline
      end
      if char
        removed_text = buffer_current.delete_from(char)
        if removed_text
          @clipboard.add_clip removed_text
        else
          set_iline "'#{char}' not found."
        end
      end
    end

    # Deletes characters between, but not including, a given pair of
    # characters.  Also puts the deleted text into the clipboard.
    # Brace characters are intelligently matched with their opposite-side
    # counterparts if the left-side brace is given (e.g. '[').
    def delete_to_and_from( inclusive = nil, char = nil )
      if char.nil?
        set_iline "Type character to delete to and from..."
        char = @win_main.getch
        set_iline
      end
      if char
        removed_text = buffer_current.delete_to_and_from(
          char,
          inclusive == :inclusive ? INCLUSIVE : NOT_INCLUSIVE
        )
        if removed_text
          @clipboard.add_clip( [ removed_text ] )
        else
          set_iline "'#{char}' not found."
        end
      end
    end

    # Evaluates (executes) Ruby code.
    def evaluate( code_ = nil )
      if code_.nil?
        if buffer_current.changing_selection
          selected_text = buffer_current.copy_selection[ 0 ]
        end
        code = get_user_input(
          "Ruby code: ",
          history: @rlh_general,
          initial_text: selected_text || "",
          completion_array: ::Diakonos::Functions.public_instance_methods.map { |m| m.to_s }
        )
      else
        code = code_
      end

      if code
        begin
          eval code
        rescue Exception => e
          show_exception(
            e,
            [
              "The code given to evaluate has a syntax error.",
              "The code given to evaluate refers to a Diakonos command which does not exist, or is misspelled.",
              "The code given to evaluate refers to a Diakonos command with missing arguments.",
              "The code given to evaluate refers to a variable or method which does not exist.",
            ]
          )
        end
      end
    end

    # Starts the interactive help system.
    def help( prefill = '' )
      if ! File.exist?( @help_dir ) || Dir[ "#{@help_dir}/*" ].size == 0
        set_iline 'There are no help files installed.'
        return
      end

      open_help_buffer
      matching_docs = nil

      selected = get_user_input(
        "Search terms: ",
        history: @rlh_help,
        initial_text: prefill,
        completion_array: @help_tags
      ) { |input|
        next if input.length < 3 and input[ 0..0 ] != '/'

        matching_docs = matching_help_documents( input )
        with_list_file do |list|
          list.puts matching_docs.join( "\n" )
        end

        open_list_buffer
      }

      close_help_buffer

      case selected
      when /\|/
        open_help_document selected
      when nil
        # Help search aborted; do nothing
      else
        # Not a selected help document
        if matching_docs.nil? or matching_docs.empty?
          matching_docs = matching_help_documents( selected )
        end

        case matching_docs.size
        when 1
          open_help_document matching_docs[ 0 ]
        when 0
          File.open( @error_filename, 'w' ) do |f|
            f.puts "There were no help documents matching your search."
            f.puts "(#{selected.strip})"
          end
          error_file = open_file( @error_filename )

          choice = get_choice(
            "Would you like to send your search terms over the Internet to help improve Diakonos?",
            [ CHOICE_YES, CHOICE_NO ]
          )
          case choice
          when CHOICE_YES
            require 'net/http'
            require 'uri'

            res = Net::HTTP.post_form(
              URI.parse( 'http://dh.pist0s.ca/' ),
              { 'q' => selected }
            )
            # TODO: let them choose "never" and "always"
          end

          close_buffer error_file
        else
          help selected
        end
      end
    end

    # Loads Ruby code from file using Kernel#load.
    def load_script( name_ = nil )
      if name_.nil?
        name = get_user_input( "File to load as script: ", history: @rlh_files )
      else
        name = name_
      end

      if name
        thread = Thread.new( name ) do |f|
          begin
            load( f )
          rescue Exception => e
            show_exception(
              e,
              [
                "The filename given does not exist.",
                "The filename given is not accessible or readable.",
                "The loaded script does not reference Diakonos commands as members of the global Diakonos object.  e.g. cursor_bol instead of $diakonos.cursor_bol",
                "The loaded script has syntax errors.",
                "The loaded script references objects or object members which do not exist."
              ]
            )
          end
          set_iline "Loaded script '#{name}'."
        end

        loop do
          if thread.status != "run"
            break
          else
            sleep 0.1
          end
        end
        thread.join
      end
    end

    def play_macro( name = nil )
      macro, input_history = @macros[ name ]
      if input_history
        @macro_input_history = input_history.deep_clone
        if macro
          @playing_macro = true
          macro.each do |command|
            eval command
          end
          @playing_macro = false
          @macro_input_history = nil
        end
      end
    end

    def print_mapped_function
      @capturing_mapping = true
      set_iline "Type any chain of keystrokes or key chords, or press Enter to stop."
    end

    def print_keychain
      @capturing_keychain = true
      set_iline "Type any chain of keystrokes or key chords, then press Enter..."
    end

    # Quits Diakonos (gracefully).
    def quit
      @quitting = true
      to_all = nil
      save_session
      @buffers.each do |buffer|
        next  if ! buffer.modified?
        switch_to buffer
        closure_choice = close_buffer( buffer, to_all: to_all )
        case closure_choice
        when CHOICE_CANCEL
          @quitting = false
          break
        when CHOICE_YES_TO_ALL, CHOICE_NO_TO_ALL
          to_all = closure_choice
        end
      end
    end

    def repeat_last
      eval @functions_last[ -1 ] if not @functions_last.empty?
    end

    # Send the Diakonos job to background, as if with Ctrl-Z
    def suspend
      Curses::close_screen
      Process.kill( "SIGSTOP", $PID )
      Curses::init_screen
      refresh_all
    end

    # Starts or stops macro recording.
    def toggle_macro_recording( name = nil )
      if @macro_history
        stop_recording_macro
      else
        start_recording_macro name
      end
    end

    # Undoes the latest change made to the current buffer,
    # or reopens the file that was just closed.
    def undo( buffer = buffer_current )
      if @functions_last[-1] == 'close_buffer'
        open_file(
          @buffer_closed.name,
          'cursor' => {
            'row' => @buffer_closed.last_row,
            'col' => @buffer_closed.last_col,
          },
          'display' => {
            'top_line' => @buffer_closed.top_line,
            'left_col' => @buffer_closed.left_column,
          }
        )
      else
        buffer.undo
      end
    end

    # Redoes the latest change undone on the current buffer.
    def unundo( buffer = buffer_current )
      buffer.unundo
    end

  end
end
