require 'readline'
require 'osc-ruby'

module Qcmd
  class CLI
    include Qcmd::Plaintext

    attr_accessor :prompt

    def self.launch options={}
      new options
    end

    def initialize options={}
      Qcmd.debug "[CLI initialize] launching with options: #{options.inspect}"

      Qcmd.context = Qcmd::Context.new

      if options[:machine_given]
        Qcmd.debug "[CLI initialize] autoconnecting to machine #{ options[:machine] }"

        Qcmd.while_quiet do
          connect_to_machine_by_name(options[:machine])
        end

        if options[:workspace_given]
          Qcmd.debug "[CLI initialize] autoconnecting to workspace #{ options[:machine] }"

          Qcmd.while_quiet do
            connect_to_workspace_by_name(options[:workspace], options[:workspace_passcode])
          end

          if options[:command_given]
            handle_input options[:command]
            print %[sent command "#{ options[:command] }"]
            exit 0
          end
        elsif Qcmd.context.machine.workspaces.size == 1 &&
              !Qcmd.context.machine.workspaces.first.passcode? &&
              !Qcmd.context.workspace_connected?
          connect_to_workspace_by_index(0, nil)
        end
      end

      # add aliases to input completer
      InputCompleter.add_commands aliases.keys

      start
    end

    def machine
      Qcmd.context.machine
    end

    def reset
      Qcmd.context.reset
    end

    def aliases
      @aliases ||= Qcmd::Aliases.defaults.merge(Qcmd::Configuration.config['aliases'])
    end

    def alias_arg_matcher
      /\$(\d+)/
    end

    def add_alias name, expression
      aliases[name] = Parser.generate(expression)
      InputCompleter.add_command name
      Qcmd::Configuration.update('aliases', aliases)

      aliases[name]
    end

    def replace_args alias_expression, original_expression
      Qcmd.debug "[CLI replace_args] populating #{ alias_expression.inspect } with #{ original_expression.inspect }"

      alias_expression.map do |arg|
        if arg.is_a?(Array)
          replace_args(arg, original_expression)
        elsif (arg.is_a?(Symbol) || arg.is_a?(String)) && alias_arg_matcher =~ arg.to_s
          while alias_arg_matcher  =~ arg.to_s
            arg_idx = $1.to_i
            arg_val = original_expression[arg_idx]

            Qcmd.debug "[CLI replace_args] found $#{ arg_idx }, replacing with #{ arg_val.inspect }"

            arg = arg.to_s.sub("$#{ arg_idx }", arg_val.to_s)
          end

          arg
        else
          arg
        end
      end
    end

    def expand_alias key, expression
      Qcmd.debug "[CLI expand_alias] using alias of #{ key } with #{ expression.inspect }"

      new_command = aliases[key]

      # observe alias arity
      argument_placeholders = new_command.scan(alias_arg_matcher).uniq.map {|placeholder|
        placeholder[0].sub(/$\$/, '').to_i
      }

      if argument_placeholders.size > 0
        arguments_expected = argument_placeholders.max

        # because expression is alias + arguments, the expression's size should
        # be at least arguments_expected + 1
        if expression.size <= arguments_expected
          print "This custom command expects at least #{ arguments_expected } arguments."
          return
        end
      end

      new_command = Parser.parse(new_command)
      new_command = replace_args(new_command, expression)

      new_command
    end

    def get_prompt
      clock = Time.now.strftime "%H:%M"
      prefix = []

      if Qcmd.context.machine_connected?
        prefix << "[#{ Qcmd.context.machine.name }]"
      end

      if Qcmd.context.workspace_connected?
        prefix << "[#{ Qcmd.context.workspace.name }]"
      end

      if Qcmd.context.cue_connected?
        prefix << "[#{ Qcmd.context.cue.number } #{ Qcmd.context.cue.name }]"
      end

      ["#{clock} #{prefix.join(' ')}", "> "]
    end

    def connect machine
      if machine.nil?
        print "A valid machine is needed to connect!"
        return
      end

      reset

      Qcmd.context.machine = machine

      # in case this is a reconnection
      Qcmd.context.connect_to_qlab

      # tell QLab to always reply to messages
      response = Qcmd::Action.evaluate('/alwaysReply 1')
      if response.nil? || response.empty?
        print %[Failed to connect to QLab machine "#{ machine.name }"]
      elsif response.status == 'ok'
        print %[Connected to machine "#{ machine.name }"]
      end

      machine.workspaces = Qcmd::Action.evaluate('workspaces').map {|ws| QLab::Workspace.new(ws)}

      if Qcmd.context.machine.workspaces.size == 1 && !Qcmd.context.machine.workspaces.first.passcode?
        connect_to_workspace_by_index(0, nil)
      else
        Handler.print_workspace_list
      end
    end

    def disconnected_machine_warning
      if Qcmd::Network.names.size > 0
        print "Try one of the following:"
        Qcmd::Network.names.each do |name|
          print %[  #{ name }]
        end
      else
        print "There are no QLab machines on this network :("
      end
    end

    def connect_to_machine_by_name machine_name
      if machine = Qcmd::Network.find(machine_name)
        print "Connecting to machine: #{machine_name}"
        connect machine
      else
        print 'Sorry, that machine could not be found'
      end
    end

    def connect_to_machine_by_index machine_idx
      if machine = Qcmd::Network.find_by_index(machine_idx)
        print "Connecting to machine: #{machine.name}"
        connect machine
      else
        print 'Sorry, that machine could not be found'
      end
    end

    def connect_to_workspace_by_index workspace_idx, passcode
      if Qcmd.context.machine_connected?
        if workspace = Qcmd.context.machine.workspaces[workspace_idx]
          connect_to_workspace_by_name workspace.name, passcode
        else
          print "That workspace isn't on the list."
        end
      else
        print %[You can't connect to a workspace until you've connected to a machine. ]
        disconnected_machine_warning
      end
    end

    def connect_to_workspace_by_name workspace_name, passcode
      if Qcmd.context.machine_connected?
        if workspace = Qcmd.context.machine.find_workspace(workspace_name)
          workspace.passcode = passcode
          print "Connecting to workspace: #{workspace_name}"

          use_workspace workspace
        else
          print "That workspace doesn't seem to exist, try one of the following:"
          Qcmd.context.machine.workspaces.each do |ws|
            print %[  "#{ ws.name }"]
          end
        end
      else
        print %[You can't connect to a workspace until you've connected to a machine. ]
        disconnected_machine_warning
      end
    end

    def use_workspace workspace
      Qcmd.debug %[[CLI use_workspace] connecting to workspace: "#{workspace.name}"]

      # set workspace in context. Will unset later if there's a problem.
      Qcmd.context.workspace = workspace

      # send connect message to QLab to make sure subsequent messages target it
      if workspace.passcode?
        ws_action_string = "workspace/#{workspace.id}/connect %04i" % workspace.passcode
      else
        ws_action_string = "workspace/#{workspace.id}/connect"
      end

      reply = Qcmd::Action.evaluate(ws_action_string)

      if reply == 'badpass'
        print 'Failed to connect to workspace, bad passcode or no passcode given.'
        Qcmd.context.disconnect_workspace
      elsif reply == 'ok'
        print %[Connected to "#{Qcmd.context.workspace.name}"]
        Qcmd.context.workspace_connected = true
      end

      # if it worked, load cues automatically
      if Qcmd.context.workspace_connected?
        load_cues

        if Qcmd.context.workspace.cue_lists
          print "Loaded #{pluralize Qcmd.context.workspace.cues.size, 'cue'}"
        end
      end
    end

    def start
      loop do
        # blocks the whole Ruby VM
        prefix, char = get_prompt

        Qcmd.print prefix
        cli_input = Readline.readline(char, true)

        if cli_input.nil? || cli_input.size == 0
          Qcmd.debug "[CLI start] got: #{ cli_input.inspect }"
          next
        end

        # save all commands to log
        Qcmd::History.push(cli_input)

        begin
          if /;/ =~ cli_input
            cli_input.split(';').each do |sub_input|
              handle_input Qcmd::Parser.parse(sub_input)
            end
          else
            handle_input Qcmd::Parser.parse(cli_input)
          end
        rescue => ex
          print "Command parser couldn't handle the last command: #{ ex.message }"
          print ex.backtrace
        end
      end
    end

    # the actual command line interface interactor
    def handle_input args
      command = args[0].to_s

      case command
      when 'exit', 'quit', 'q'
        print 'exiting...'
        exit 0

      when 'connect'
        Qcmd.debug "[CLI handle_input] connect command received args: #{ args.inspect } :: #{ args.map {|a| a.class.to_s}.inspect}"

        machine_ident = args[1]

        if machine_ident.is_a?(Fixnum)
          # machine "index" will be given with a 1-indexed value instead of the
          # stored 0-indexed value.
          connect_to_machine_by_index machine_ident - 1
        else
          connect_to_machine_by_name machine_ident
        end

      when 'disconnect'
        disconnect_what = args[1]

        if disconnect_what == 'workspace'
          Qcmd.context.disconnect_cue
          Qcmd.context.disconnect_workspace

          Handler.print_workspace_list
        elsif disconnect_what == 'cue'
          Qcmd.context.disconnect_cue
        else
          reset
          Qcmd::Network.browse_and_display
        end

      when '..'
        if Qcmd.context.cue_connected?
          Qcmd.context.disconnect_cue
        elsif Qcmd.context.workspace_connected?
          Qcmd.context.disconnect_workspace
        else
          reset
        end

      when 'use'
        Qcmd.debug "[CLI handle_input] use command received args: #{ args.inspect }"

        workspace_name = args[1]
        passcode       = args[2]

        Qcmd.debug "[CLI handle_input] using workspace: #{ workspace_name.inspect }"

        if workspace_name
          if workspace_name.is_a?(Fixnum)
            # decrement given idx
            connect_to_workspace_by_index workspace_name - 1, passcode
          else
            connect_to_workspace_by_name workspace_name, passcode
          end
        else
          print "No workspace name given. The following workspaces are available:"
          Handler.print_workspace_list
        end

      when 'workspaces'
        if !Qcmd.context.machine_connected?
          disconnected_machine_warning
        else
          machine.workspaces = Qcmd::Action.evaluate(args).map {|ws| QLab::Workspace.new(ws)}
          Handler.print_workspace_list
        end

      when 'workspace'
        workspace_command = args[1]

        if !Qcmd.context.workspace_connected?
          handle_failed_workspace_command cli_input
          return
        end

        if workspace_command.nil?
          print_wrapped("no workspace command given. available workspace commands
                         are: #{Qcmd::InputCompleter::ReservedWorkspaceWords.join(', ')}")
        else
          send_workspace_command(workspace_command, *args)
        end

      when 'help'
        help_command = args.shift

        if help_command.nil?
          # print help according to current context
          Qcmd::Commands::Help.print_all_commands
        else
          # print command specific help
        end

      when 'cues'
        if !Qcmd.context.workspace_connected?
          handle_failed_workspace_command cli_input
          return
        end

        # reload cues
        load_cues

        Qcmd.context.workspace.cue_lists.each do |cue_list|
          print
          print centered_text(" Cues: #{ cue_list.name } ", '-')
          printable_cues = []

          add_cues_to_list cue_list, printable_cues, 0

          table ['Number', 'Id', 'Name', 'Type'], printable_cues

          print
        end

      when /^(cue|cue_id)$/
        # id_field = $1

        if !Qcmd.context.workspace_connected?
          handle_failed_workspace_command cli_input
          return
        end

        if args.size < 3
          print "Cue commands should be in the form:"
          print
          print "  > cue NUMBER COMMAND [ARGUMENTS]"
          print
          print "or"
          print
          print "  > cue_id ID COMMAND [ARGUMENTS]"
          print
          print_wrapped("available cue commands are: #{Qcmd::Commands::CUE.join(', ')}")
          print
          return
        end

        cue_action = Qcmd::CueAction.new(args)

        reply = cue_action.evaluate

        if reply.is_a?(QLab::Reply)
          if !reply.status.nil?
            print reply.status
          end
        else
          render_data reply
        end

        # fixate on cue
        if Qcmd.context.workspace.has_cues?
          _cue = Qcmd.context.workspace.cues.find {|cue|
            case cue_action.id_field
            when :cue
              cue.number.to_s == cue_action.identifier.to_s
            when :cue_id
              cue.id.to_s == cue_action.identifier.to_s
            end
          }

          if _cue
            Qcmd.context.cue = _cue
            Qcmd.context.cue_connected = true

            Qcmd.context.cue.sync
          end
        end

      when 'aliases'
        print centered_text(" Available Custom Commands ", '-')
        print

        aliases.each do |(key, val)|
          print key
          print '    ' + word_wrap(val, :indent => '    ', :preserve_whitespace => true).join("\n")
          print
        end

      when 'alias'
        new_alias = add_alias(args[1].to_s, args[2])
        print %[Added alias for "#{ args[1] }": #{ new_alias }]

      else
        if aliases[command]
          Qcmd.debug "[CLI handle_input] using alias #{ command }"

          new_expression = expand_alias(command, args)

          # alias expansion failed, go back to CLI
          return if new_expression.nil?

          Qcmd.debug "[CLI handle_input] expanded to: #{ new_expression.inspect }"

          # recurse!
          if new_expression.size == 1 && new_expression[0].is_a?(Array)
            while new_expression.size == 1 && new_expression[0].is_a?(Array)
              new_expression = new_expression[0]
            end
          end

          if new_expression.all? {|exp| exp.is_a?(Array)}
            new_expression.each {|nested_expression|
              handle_input nested_expression
            }
          else
            handle_input(new_expression)
          end


        elsif Qcmd.context.cue_connected? && Qcmd::InputCompleter::ReservedCueWords.include?(command)
          # prepend the given command with a cue address
          if Qcmd.context.cue.number.nil? || Qcmd.context.cue.number.size == 0
            command = "cue_id/#{ Qcmd.context.cue.id }/#{ command }"
          else
            command = "cue/#{ Qcmd.context.cue.number }/#{ command }"
          end

          args = [command].push(*args[1..-1])

          cue_action = Qcmd::CueAction.new(args)

          reply = cue_action.evaluate

          if reply.is_a?(QLab::Reply)
            if !reply.status.nil?
              print reply.status
            end
          else
            render_data reply
          end

          # send_workspace_command(command, *args)
        elsif Qcmd.context.workspace_connected? && Qcmd::InputCompleter::ReservedWorkspaceWords.include?(command)
          send_workspace_command(command, *args)

        else
          # failure modes?
          if %r[/] =~ command
            # might be legit OSC command, try sending
            reply = Qcmd::Action.evaluate(args)
            if reply.is_a?(QLab::Reply)
              if !reply.status.nil?
                print reply.status
              end
            else
              render_data reply
            end
          else
            if Qcmd.context.cue_connected?
              # cue is connected, but command isn't a valid cue command
              print_wrapped("Unrecognized command: '#{ command }'. Try one of these cue commands: #{ Qcmd::InputCompleter::ReservedCueWords.join(', ') }")
              print 'or disconnect from the cue with ..'
            elsif Qcmd.context.workspace_connected?
              # workspace is connected, but command isn't a valid workspace command
              print_wrapped("Unrecognized command: '#{ command }'. Try one of these workspace commands: #{ Qcmd::InputCompleter::ReservedWorkspaceWords.join(', ') }")
            elsif Qcmd.context.machine_connected?
              send_command(command, *args)
            else
              print 'you must connect to a machine before sending commands'
            end
          end
        end
      end
    end

    def handle_failed_workspace_command command
      print_wrapped(%[The command, "#{ command }" can't be processed yet. you must
                      first connect to a machine and a workspace
                      before issuing other commands.])
    end

    def add_cues_to_list cue, list, level
      cue.cues.each {|_c|
        name = _c.name

        if level > 0
          name += " " + ("-" * level) + "|"
        end

        list << [_c.number, _c.id, name, _c.type]
        add_cues_to_list(_c, list, level + 1) if _c.has_cues?
      }
    end

    ### communication actions
    private

    def render_data data
      if data.is_a?(Array) || data.is_a?(Hash)
        begin
          print JSON.pretty_generate(data)
        rescue JSON::GeneratorError
          Qcmd.debug "[CLI render_data] failed to JSON parse data: #{ data.inspect }"
          print data.to_s
        end
      else
        print data.to_s
      end
    end


    def send_command command, *args
      options = args.extract_options!

      Qcmd.debug "[CLI send_command] building command from command, args, options: #{ command.inspect }, #{ args.inspect }, #{ options.inspect }"

      # make sure command is valid OSC Address
      if %r[^/] =~ command
        address = command
      else
        address = "/#{ command }"
      end

      osc_message = OSC::Message.new address, *args

      Qcmd.debug "[CLI send_command] sending osc message #{ osc_message.address } #{osc_message.has_arguments? ? 'with' : 'without'} args"

      if block_given?
        # use given response handler, pass it response as a QLab Reply
        Qcmd.context.qlab.send osc_message do |response|
          Qcmd.debug "[CLI send_command] converting OSC::Message to QLab::Reply"
          yield QLab::Reply.new(response)
        end
      else
        # rely on default response handler
        Qcmd.context.qlab.send(osc_message)
      end
    end

    def send_workspace_command _command, *args
      command = "workspace/#{ Qcmd.context.workspace.id }/#{ _command }"
      send_command(command, *args)
    end

    ## QLab commands

    def load_cues
      cues = Qcmd::Action.evaluate('/cueLists')
      Qcmd.context.workspace.cue_lists = cues.map {|cue_list| Qcmd::QLab::CueList.new(cue_list)}
    end
  end
end