module VER
  module Methods
    module Control
      module_function

      def enter(text, _old_mode, _new_mode)
        clean_line(text, :insert)
      end

      def leave(text, old_mode, new_mode)
        # clean_line(text, :insert)
      end

      def temporary(buffer, action)
        action.to_method(buffer).call
      end

      def insert_at(text, motion, *count)
        Move.send(motion, text, *count)
        text.minor_mode(:control, :insert)
      end

      def insert_indented_newline_above(text)
        Insert.insert_indented_newline_above(text)
      end

      def insert_indented_newline_below(text)
        Insert.insert_indented_newline_below(text)
      end

      def open_file_under_cursor(text)
        Open.open_file_under_cursor(text)
      end

      def source_buffer(buffer)
        if filename = buffer.filename
          buffer.message("Source #{filename}")
          load(filename.to_s)
        else
          buffer.warn("#{buffer.uri} is no file")
        end
      end

      def cursor_vertical_top(text)
        insert = text.count('1.0', 'insert', :displaylines)
        last   = text.count('1.0', 'end', :displaylines)

        fraction = ((100.0 / last) * insert) / 100

        text.yview_moveto(fraction)
      end

      def cursor_vertical_top_sol(text)
        cursor_vertical_top(text)
        Move.start_of_line(text)
      end

      def cursor_vertical_center(text, index = :insert)
        insert = text.count('1.0', index, :displaylines)
        last   = text.count('1.0', 'end', :displaylines)
        shown  = text.count('@0,0', "@0,#{text.winfo_height}", :displaylines)

        fraction = ((100.0 / last) * (insert - (shown / 2))) / 100

        text.yview_moveto(fraction)
      end

      def cursor_vertical_center_sol(text)
        cursor_vertical_center(text)
        Move.start_of_line(text)
      end

      def cursor_vertical_bottom(text)
        insert = text.count('1.0', 'insert', :displaylines) + 1
        last   = text.count('1.0', 'end', :displaylines)
        shown  = text.count('@0,0', "@0,#{text.winfo_height}", :displaylines)

        fraction = ((100.0 / last) * (insert - shown)) / 100

        text.yview_moveto(fraction)
      end

      def cursor_vertical_bottom_sol(text)
        cursor_vertical_bottom(text)
        start_of_line(text)
      end

      def cursor_horizontal_center(buffer)
        x, y, width, height = *buffer.bbox('insert')
        line_middle = y + (height / 2) # gives less room for error?
        buffer_middle = buffer.winfo_width / 2
        set("@#{buffer_middle},#{y}")
      end

      def chdir(text)
        text.ask 'Change to: ' do |path, action|
          case action
          when :attempt
            path = File.expand_path(path.to_s)

            begin
              Dir.chdir(path)
              text.message format('Changed working directory to %s', path)
              :abort
            rescue Errno::ENOENT => ex
              VER.warn ex
            end
          end
        end
      end

      # Toggle case of the character under the cursor up to +count+ characters
      # forward (+count+ is inclusive the first character).
      # This only works for alphabetic ASCII characters, no other encodings.
      def toggle_case(text, count = text.prefix_count)
        from = 'insert'
        to = "insert + #{count} chars"
        chunk = text.get(from, to)
        chunk.tr!('a-zA-Z', 'A-Za-z')
        text.replace(from, to, chunk)
      end

      # Assigns env variables used in the given command.
      # - $f: The current buffer's filename
      # - $d: The current buffer's directory
      # - $F: A space-separated list of all buffer filenames
      # - $i: A string acquired from the user with a prompt
      # - $c: The current clipboard text
      # - $s: The currently selected text
      #
      # @param [String] command
      #   The string containing the command executed
      def prepare_exec(text, command)
        prepare_exec_f(text) if command =~ /\$f/
        prepare_exec_d(text) if command =~ /\$d/
        prepare_exec_F(text) if command =~ /\$F/
        prepare_exec_i(text) if command =~ /\$i/
        prepare_exec_c(text) if command =~ /\$c/
        prepare_exec_s(text) if command =~ /\$s/
      end

      def prepare_exec_f(_text)
        p f: (ENV['f'] = filename.to_s)
      end

      def prepare_exec_d(_text)
        p d: (ENV['d'] = filename.directory.to_s)
      end

      def prepare_exec_F(_text)
        p F: (ENV['F'] = VER.buffers.map { |_key, buffer| buffer.filename }.join(' '))
      end

      def prepare_exec_i(_text)
        raise NotImplementedError
      end

      def prepare_exec_c(_text)
        p c: (ENV['c'] = clipboard_get)
      end

      def prepare_exec_s(_text)
        content = []

        each_selected_line do |y, fx, tx|
          content << get("#{y}.#{fx}", "#{y}.#{tx}")
        end

        ENV['s'] = content.join("\n")
      end

      ENV_EXPORTERS = %w[
        current_line current_word directory filepath line_index line_number
        scope selected_text
      ]

      # Set up the env for a script, then execute it.
      # For now, I only setup the env, until we figure out a good way to find
      # bundle commands.
      def exec_bundle
        ENV_EXPORTERS.each do |exporter|
          ENV["VER_#{exporter.upcase}"] = ENV["TM_#{exporter.upcase}"] =
                                            send("exec_env_#{exporter}").to_s
        end

        yield if block_given?
      end

      # textual content of the current line
      def exec_env_current_line
        get('insert linestart', 'insert lineend')
      end

      # the word in which the caret is located.
      def exec_env_current_word
        get('insert wordstart', 'insert wordend')
      end

      # the folder of the current document (may not be set).
      def exec_env_directory
        filename.dirname.to_s
      end

      # path (including file name) for the current document (may not be set).
      def exec_env_filepath
        filename.to_s
      end

      # the index in the current line which marks the caret's location.
      # This index is zero-based and takes the utf-8 encoding of the line (e.g.
      # read as TM_CURRENT_LINE) into account.
      def exec_env_line_index
        index('insert').x
      end

      # the carets line position (counting from 1).
      def exec_env_line_number
        index('insert').y
      end

      def exec_env_scope
        tag_names('insert').join(', ')
      end

      # full content of the selection (may not be set).
      # Note that environment variables have a size limitation of roughly 64 KB,
      # so if the user selects more than that, this variable will not reflect
      # the actual selection (commands that need to work with the selection
      # should generally set this to be the standard input).
      def exec_env_selected_text
        content = []

        each_selected_line do |y, fx, tx|
          content << get("#{y}.#{fx}", "#{y}.#{tx}")
        end

        content.join("\n")
      end

      def exec_into_new(text, command = nil)
        if command
          target = text.options.home_conf_dir / 'shell-result.txt'
          prepare_exec(command)
          system(command)
          target.open('w+') { |io| io.write(`#{command}`) }
          VER.find_or_create_buffer(target)
        else
          text.ask 'Command: ' do |answer, action|
            case action
            when :attempt
              begin
                exec_into_new(answer)
                :abort
              rescue StandardError => ex
                VER.warn(ex)
              end
            end
          end
        end
      end

      def exec_into_void(text)
        text.ask 'Command: ' do |command, action|
          case action
          when :attempt
            begin
              system(command)
              text.message("Exit code: #{$CHILD_STATUS}")
              :abort
            rescue StandardError => ex
              VER.warn(ex)
            end
          end
        end
      end

      # Substitute over all lines of the buffer
      def gsub(text, regexp, with)
        total = 0
        Undo.record text do |record|
          text.index('1.0').upto(text.index('end')) do |index|
            lineend = index.lineend
            line = text.get(index, lineend)

            if line.gsub!(regexp, with)
              record.replace(index, lineend, line)
              total += 1
            end
          end
        end

        text.message "Performed gsub on #{total} lines"
      end

      # Substitute on current line
      def sub(text, regexp, with)
        linestart = text.index('insert linestart')
        lineend = linestart.lineend
        line = text.get(linestart, lineend)

        text.replace(linestart, lineend, line) if line.sub!(regexp, with)
      end

      def executor(text, action = nil)
        VER::Executor.new(text, action: action)
      end
      alias ex executor
      module_function :ex

      def wrap_line(text)
        content = text.get('insert linestart', 'insert lineend')
        textwidth = text.options.textwidth
        lines = wrap_lines_of(content, textwidth).join("\n")
        lines.rstrip!

        text.replace('insert linestart', 'insert lineend', lines)
      end

      def smart_evaluate(text)
        if sel = text.tag_ranges(:sel)
          from, to = sel.first
          return Selection.evaluate(text) if from && to
        end

        line_evaluate(text)
      end

      def line_evaluate(text)
        code = text.get('insert linestart', 'insert lineend')
        file = (text.filename || text.uri).to_s
        stdout_capture_evaluate(code, file, binding) do |res, out|
          text.at_insert.lineend.insert(format("\n%s%p", out, res))
        end
      end

      def stdout_capture_evaluate(code, file, binding = self.binding)
        old_stdout = $stdout.dup
        rd, wr = IO.pipe
        $stdout.reopen(wr)
        result = eval(code, binding, file.to_s)
        $stdout.reopen old_stdout; wr.close
        stdout = rd.read

        yield(result, stdout)
      rescue StandardError => exception
        yield(exception, '')
      ensure
        wr.closed? || $stdout.reopen(old_stdout) && wr.close
        rd.closed? || rd.close
      end

      # for some odd reason, vim likes to have arbitrary commands that reduce
      # the argument by one if it's greater 1...
      def join_forward(buffer, count = buffer.prefix_count)
        count = count > 1 ? (count - 1) : count
        buffer.undo_record do |record|
          count.times do
            buffer.insert = buffer.at_insert.lineend
            record.replace('insert', 'insert + 1 chars', ' ')
          end
        end
      end

      def join_forward_nospace(buffer, count = buffer.prefix_count)
        count = count > 1 ? (count - 1) : count
        buffer.undo_record do |record|
          count.times do
            buffer.insert = buffer.at_insert.lineend
            record.replace('insert', 'insert + 1 chars', '')
          end
        end
      end

      def join_backward(text)
        from = 'insert - 1 lines linestart'
        to = 'insert lineend'
        lines = text.get(from, to)
        text.replace(from, to, lines.gsub(/\s*\n\s*/, ' '))
      end

      def indent_line(text, count = text.prefix_count)
        indent = (' ' * text.options[:shiftwidth] * count)
        text.insert('insert linestart', indent)
      end

      def unindent_line(text, count = text.prefix_count)
        indent = ' ' * text.options[:shiftwidth]
        replace_from = 'insert linestart'
        replace_to = "insert linestart + #{indent.size} chars"

        Undo.record text do |record|
          count.times do
            line = text.get('insert linestart', 'insert lineend')

            return unless line.start_with?(indent)

            record.replace(replace_from, replace_to, '')
          end
        end
      end

      def clean_line(text, index, record = text)
        index = text.index(index)
        from = index.linestart
        to = index.lineend
        line = text.get(from, to)
        bare = line.rstrip
        record.replace(from, to, bare) if bare.empty?
      end

      def wrap_lines_of(content, wrap = 80)
        Kernel.raise ArgumentError, '+wrap+ must be > 1' unless wrap > 1
        wrap -= 1

        indent = content[/^\s+/] || ''
        indent_size = indent.size
        lines = [indent.dup]

        content.scan(/\S+/) do |chunk|
          last = lines.last
          last_size = last.size
          chunk_size = chunk.size

          if last_size + chunk_size > wrap
            lines << indent + chunk
          elsif last_size == indent_size
            last << chunk
          elsif chunk =~ /\.$/
            last << ' ' << chunk
            lines << indent.dup
          else
            last << ' ' << chunk
          end
        end

        lines.pop if lines.last == indent
        lines
      end
    end
  end
end
