# encoding: ascii-8bit

# Copyright 2014 Ball Aerospace & Technologies Corp.
# All Rights Reserved.
#
# This program is free software; you can modify and/or redistribute it
# under the terms of the GNU General Public License
# as published by the Free Software Foundation; version 3 with
# attribution addendums as found in the LICENSE.txt

require 'cosmos'
require 'cosmos/gui/qt'
require 'cosmos/script'

module Cosmos
  # Performs code completion for COSMOS by supporting all the COSMOS keywords
  # as well as the defined targets and packets.
  class Completion < Qt::Completer
    CMD_KEYWORDS = %w(cmd cmd_no_range_check cmd_no_hazardous_check cmd_no_checks
      cmd_raw cmd_raw_no_range_check cmd_raw_no_hazardous_check cmd_raw_no_checks)
    TLM_KEYWORDS = %w(set_tlm set_tlm_raw override_tlm override_tlm_raw normalize_tlm
      tlm tlm_raw tlm_formatted tlm_with_units
      limits_enabled? enable_limits disable_limits
      check check_raw check_tolerance check_tolerance_raw
      wait wait_raw wait_tolerance wait_tolerance_raw wait_check wait_check_raw
      wait_check_tolerance wait_check_tolerance_raw)

    slots 'insertCompletion(const QString&)'

    def initialize(parent)
      # Must be called first
      super(parent)
      # Connect the completion to the passed in widget
      setWidget(parent)
      setCaseSensitivity(Qt::CaseInsensitive)

      @text_widget = parent
      @target = nil
      @command = nil
      connect(self, SIGNAL('activated(const QString&)'), self, SLOT('insertCompletion(const QString&)'))
    end

    def insertCompletion(completion)
      # Delete the characters already entered (the completionPrefix)
      # and then insert the full completion text. This allows the user to enter search text
      # in lower case but it will be completed with upper case text.
      (0...completionPrefix.length).each do
        widget.textCursor.deletePreviousChar
      end
      widget.textCursor.insertText(completion)
      widget.setTextCursor(widget.textCursor)
      # Signal back to the code completion an Enter key so we can continue to process the line
      event = Qt::KeyEvent.new(Qt::Event::KeyPress, Qt::Key_Enter, Qt::NoModifier)
      handle_keypress(event)
      event.dispose
    end

    # Create the popup based on the list of strings
    def create_popup(list)
      cr = widget.cursorRect
      setModel(Qt::StringListModel.new(list, self))
      cr.setWidth(popup.sizeHintForColumn(0) + popup.verticalScrollBar.sizeHint.width())
      complete(cr) # popup it up!
    end

    def handle_tlm(line)
      # Bail if the line is already completed i.e. ends with )
      if line =~ /\"\)/ or line =~ /\)\s*$/ or line =~ /\w\s*\"$/
        popup.close
        return
      end

      # First determine where in the line we are
      tlm_line = line.split(/\"/)[1]
      # If there isn't a first quote i.e. tlm("
      # then we need to handle targets
      if tlm_line.nil?
        handle_targets(line, false)
      else
        # We need a word after the target to have a packet to work with
        num_items = tlm_line.split.length
        if num_items <= 1 and not tlm_line =~ /\w+\s$/
          handle_targets(line, false)
        elsif (num_items <= 1 and tlm_line =~ /\w+\s$/) or
          (num_items == 2 and not tlm_line =~ /\w+\s$/)
          handle_packets(tlm_line)
        elsif (num_items <= 2 and tlm_line =~ /\w+\s$/) or
          (num_items == 3 and not tlm_line =~ /\w+\s+$/)
          handle_items(tlm_line)
        else
          popup.close
        end
      end
    end

    def handle_packets(tlm_line)
      parts = tlm_line.split
      if popup.isVisible and not parts[1].nil?
        setCompletionPrefix(parts[1].upcase)
      else
        target_name = parts[0].strip

        begin
          packets = System.telemetry.packets(target_name)
          packet_names = []
          packets.each {|packet_name, packet| packet_names << "#{packet_name} " unless packet.hidden }
          packet_names.sort!

          # If there is only one packet then just insert the value
          if packet_names.length == 1
            widget.textCursor.insertText(packet_names[0])
            handle_tlm(widget.textCursor.block.text)
          else
            if parts[1].nil?
              setCompletionPrefix("")
            else
              setCompletionPrefix(parts[1].upcase)
            end
            create_list_popup(packet_names)
          end
        rescue
          # Don't do anything
        end
      end
    end

    def handle_items(tlm_line)
      @processing = "TLM_ITEM"
      parts = tlm_line.split
      if popup.isVisible and not parts[2].nil?
        setCompletionPrefix(parts[2].upcase)
      else
        target_name = parts[0].strip
        packet_name = parts[1].strip

        begin
          items = System.telemetry.items(target_name, packet_name)
          item_names = []
          items.each {|item| item_names << "#{item.name}\")"}
          item_names.sort!
          if parts[2].nil?
            setCompletionPrefix("")
          else
            setCompletionPrefix(parts[2].upcase)
          end
          create_list_popup(item_names)
        rescue
          # Don't do anything
        end
      end
    end

    # We need to handle ANY substring of the passed in line: cmd("TARGET CMD with PARAM1 X, PARAM2 Y")
    def handle_cmd(line)
      # Bail if the line is already completed i.e. ends with ")
      if line =~ /\"\)/ or line =~ /\w\"$/
        popup.close
        return
      end

      # First determine where in the line we are
      cmd_line = line.split(/\"/)[1]
      # If there isn't a first quote i.e. cmd("
      # then we need to handle targets
      if cmd_line.nil?
        handle_targets(line, true)
      else
        # We need a word after the target to have a command to work with
        # We also process commands if the line ends with a character followed by a space i.e. "TARGET "
        if cmd_line.split.length <= 1 and not cmd_line =~ /\w+\s$/
          handle_targets(line, true)
        elsif cmd_line.split.length > 1 or cmd_line =~ /\w+\s$/
          # If there is a "with" we handle parameters
          if cmd_line =~ /with\s/
            # If the line already ends in a comma the parameter is complete so don't process it
            if cmd_line =~ /,$/
              popup.close
            else
              handle_parameters(cmd_line, line =~ /cmd_raw/)
            end
          else
            handle_commands(cmd_line)
          end
        else
          popup.close
        end
      end
    end

    def handle_targets(line, use_command_definition = false)
      # Filter the targets if our popup is already up and there is some text to filter by
      if popup.isVisible and line.split('(').length > 1
        setCompletionPrefix(line.split('(')[-1].to_s.upcase)
      else
        if use_command_definition
          target_names = System.commands.target_names
        else
          target_names = System.telemetry.target_names
        end
        target_names_to_delete = []
        target_names.each do |target_name|
          found_non_hidden = false
          begin
            if use_command_definition
              packets = System.commands.packets(target_name)
            else
              packets = System.telemetry.packets(target_name)
            end
            packets.each do |packet_name, packet|
              found_non_hidden = true unless packet.hidden
            end
          rescue
            # Don't do anything
          end
          target_names_to_delete << target_name unless found_non_hidden
        end
        target_names_to_delete.each do |target_name|
          target_names.delete(target_name)
        end

        len = target_names.length
        len.times do |i|
          target_names[i] = "\"#{target_names[i]} "
        end
        target_names.sort!
        if line.split('(').length > 1
          setCompletionPrefix(line.split('(')[-1].to_s.upcase)
        else
          setCompletionPrefix("")
        end
        create_list_popup(target_names)
      end
    end

    def handle_commands(cmd_line)
      parts = cmd_line.split
      if popup.isVisible and not parts[1].nil?
        # Filter on the parts after the first space
        setCompletionPrefix(cmd_line[/\s(.*)/,1].to_s.upcase)
      else
        target_name = parts[0].strip.upcase

        begin
          commands = System.commands.packets(target_name)
          command_strings = []
          commands.each do |command_name, command|
            next if command.hidden
            target = System.targets[target_name]
            params = System.commands.params(target_name, command_name)
            no_params = true
            command_string = nil
            params.each do |param|
              if not target.ignored_parameters.include?(param.name)
                command_string = "#{command.packet_name} with "
                no_params = false
                break
              end
            end
            command_string = "#{command.packet_name}\")" if no_params
            command_strings << command_string
          end
          command_strings.sort!

          # Filter on the parts joined back together to include the potential for "with" after the command
          if parts[1].nil?
            setCompletionPrefix("")
          else
            setCompletionPrefix(cmd_line[/\s(.*)/,1].to_s.upcase)
          end
          create_list_popup(command_strings)
        rescue
          # Don't do anything
        end
      end
    end

    def handle_parameters(cmd_line, raw = false)
      if popup.isVisible and not cmd_line.split("with")[1].nil?
        parameter = cmd_line.split("with")[1].split(',')[-1].lstrip
        setCompletionPrefix(parameter.upcase)
      else
        begin
          parts = cmd_line.split
          target_name = parts[0].strip.upcase
          command_name = parts[1].strip

          target = System.targets[target_name]
          params = System.commands.params(target_name, command_name)
          parameters = []
          params.each do |param|
            if not target.ignored_parameters.include?(param.name)
              if param.states.nil? or param.states.empty? or raw
                param.default = "'X' " if param.default.is_a? String and param.default.strip.length == 0
                if param.default.to_s.is_printable?
                  parameters << (param.name + ' ' + param.default.to_s + ', ')
                else
                  parameters << (param.name + ' 0x' + param.default.to_s.simple_formatted + ', ')
                end
              else
                states = param.states.keys
                states.sort!
                states.each do |state|
                  parameters << (param.name + ' ' + state + ', ')
                end
              end
            end
          end
          parameters.sort!

          # If there is only one parameter then just insert the value
          if parameters.length == 1
            parameters[0] = parameters[0][0..-3] + '")'
            widget.textCursor.insertText(parameters[0])
          else
            unless cmd_line.split("with")[1].nil?
              param = cmd_line.split("with")[1].split(',')[-1].lstrip
              setCompletionPrefix(param.upcase)
            else
              setCompletionPrefix("")
            end
            create_list_popup(parameters)
          end
        rescue
          # Don't do anything
        end
      end
    end

    # Creates a Popup box to help with code completion
    def create_list_popup(list_items)
      return if list_items.nil? or list_items.empty?
      create_popup(list_items)
    end

    # Called by CompletionTextEdit in the keyPressEvent method
    def handle_keypress(event)
      current_line = widget.textCursor.block.text

      if event.key == Qt::Key_Escape
        popup.close
        # Figure out if the last bit of text has a comma in it, indicating the last entry was a parameter
        # Since the user just hit escape we want to strip the command to make the last parameter the final parameter
        if current_line.rstrip[-1,1] == ','
          widget.textCursor.deletePreviousChar
          widget.textCursor.deletePreviousChar
          widget.textCursor.insertText('")')
        elsif current_line.rstrip =~ /with$/
          6.times { widget.textCursor.deletePreviousChar }
          widget.textCursor.insertText('")')
        end
        return
      end

      if event.key == Qt::Key_Backspace
        popup.close
        return
      end

      if event.key == Qt::Key_Return
        # We've already processed the return and moved down a line
        # Thus we need to move up and check the line before
        cursor = widget.textCursor
        cursor.movePosition(Qt::TextCursor::Up)
        widget.setTextCursor(cursor)
        current_line = widget.textCursor.block.text
        indent = current_line.length - current_line.lstrip.length
        cursor.movePosition(Qt::TextCursor::Down)
        widget.setTextCursor(cursor)
        widget.textCursor.insertText(' ' * indent)
        return
      end

      # Only process if we have an open paren which indicates a function
      if current_line =~ /\(/
        split_on_paren = current_line.split('(')[0]
        if split_on_paren
          if CMD_KEYWORDS.include? split_on_paren.split[-1]
            @processing = "CMD"
            handle_cmd(current_line)
          elsif TLM_KEYWORDS.include? split_on_paren.split[-1]
            @processing = "TLM"
            handle_tlm(current_line)
          else
            popup.close
          end
        end
      end

      # Sometimes the popup clears all the values unexpectedly.
      # This also destroys the model which breaks the second to last line of code.
      # If this happens just reprocess the line to cause a new popup to display.
      if model.nil?
        popup.close
        handle_cmd(current_line) if @processing == "CMD"
        handle_tlm(current_line) if @processing == "TLM"
      end

      # Ensure the top item is always selected as a visual cue to the user
      popup.setCurrentIndex(model.index(0,0)) if popup.isVisible
      setCurrentRow(0) if popup.isVisible
    end
  end
end