# protocol.rb
# $Id: protocol.rb,v 1.2 2005/03/07 07:51:32 komatsu Exp $
#
# Copyright (C) 2005 Hiroyuki Komatsu <komatsu@taiyaki.org>
#     All rights reserved.
#     This is free software with ABSOLUTELY NO WARRANTY.
#
# You can redistribute it and/or modify it under the terms of 
# the GNU General Public License version 2.
#

class Command
  attr_reader :name, :args, :nargs, :min_nargs, :description

  def initialize (name, args, description, min_nargs = nil)
    @name = name
    @args = args
    @nargs = args.length
    @min_nargs = (min_nargs or @nargs)
    @description = description
  end
end

class PrimeProtocolCore
  def initialize (prime)
    @prime = prime
  end

  def get_line (io_in)
    return io_in.gets()
  end

  def execute (line)
  end
end

class PrimeProtocol < PrimeProtocolCore
  def initialize (prime, version)
    super(prime)
    @version = version
    @debug   = false
    @command_table = Hash.new
    init_command_table()
  end

  def init_command_table
    add_command(:close,   [], "close the connection")
    add_command(:help,    [], "print the help message")
    add_command(:version, [], "show the version number")
  end

  def add_command (name, args, description, min_nargs = nil)
    command = Command.new(name, args, description, min_nargs)
    @command_table[name] = command
  end

  def reply_successful (result = nil)
    if result.nil? or result.empty? then
      result = ""
    else
      result += "\n"
    end
    return ("ok\n" + result + "\n")
  end

  def reply_unsuccessful (result = nil)
    if result.nil? or result.empty? then
      result = ""
    else
      result += "\n"
    end
    return ("error\n" + result + "Try `help' for protocol information.\n\n")
  end

  def help
#     commands = @command_table.values.sort {|a, b| 
#       (a.nargs <=> b.nargs).nonzero? || a.name.to_s <=> b.name.to_s
#     }
    commands = @command_table.values.sort {|a, b| 
      (a.name.to_s <=> b.name.to_s).nonzero? || a.nargs <=> b.nargs
    }
    help = ""
    commands.each {|c|
      help += format("%-16s %s - %s\n",
                     c.name, c.args.join(" "), c.description)
    }
    help += "Note: Use TAB for delimiters."
    return reply_successful(help)
  end

  def execute (line)
    if line.nil? then
      return false
    end
    chunks = line.gsub(/[\r\n]/, "").split("\t")
    if chunks.length > 0 then
      name = chunks.shift
      args = chunks
      return send_command(name, args)
    end
  end

  def send_command (name, args)
    if name.empty? then
      return reply_unsuccessful("Empty command:")
    end

    command = @command_table[name.intern]
    if command.nil? then
      return reply_unsuccessful("Unknown command: #{name}")
    elsif command.min_nargs <= args.length and args.length <= command.nargs
      begin
        return send(command.name, *args)
      rescue PrimeExceptionNoSession => exception then
        return reply_unsuccessful( exception.message() )
#       rescue StandardError => exception then
#         return reply_unsuccessful( exception.message() )
      end
    else
      error_msg = "Wrong number of arguments (expecting #{command.nargs})"
      return reply_unsuccessful(error_msg)
    end
  end

  def close
    return false
  end

  def version
    return reply_successful(@version)
  end
end

class PrimeProtocolPrime < PrimeProtocol
  def initialize (prime, version)
    super

    add_command(:l, [:PATTERN],
		"look up PATTERN", 0)
    add_command(:lookup, [:PATTERN],
		"look up PATTERN", 0)
    add_command(:lookup_all, [:PATTERN],
		"look up PATTERN", 0)
    add_command(:lookup_compact, [:PATTERN],
		"look up PATTERN and return one or two candidate", 0)
    add_command(:lookup_compact_all, [:PATTERN],
		"look up PATTERN and return one or two candidate", 0)
    add_command(:lookup_direct, [:PATTERN],
		"look up PATTERN for direct typing method", 0)
    add_command(:lookup_direct_all, [:PATTERN],
		"look up PATTERN for direct typing method", 0)
    add_command(:lookup_hybrid, [:PATTERN],
		"look up PATTERN with hybrid matching", 0)
    add_command(:lookup_hybrid_all, [:PATTERN],
		"look up PATTERN with hybrid matching", 0)
    add_command(:lookup_prefix, [:PATTERN],
		"look up PATTERN with prefix matching", 0)
    add_command(:lookup_prefix_ex, [:PATTERN],
		"look up PATTERN with prefix matching", 0)
    add_command(:lookup_exact, [:PATTERN],
		"look up PATTERN with exact matching", 0)
    add_command(:lookup_expansion, [:PATTERN],
                "look up PATTERN from literal dictionaries", 0)
    add_command(:lookup_mixed, [:PATTERN],
                "look up PATTERN.  PATTERN can be mixed with pron and literal",
                0)
    add_command(:learn_word, [:KEY, :VALUE, :PART, :CONTEXT, :SUFFIX, :REST],
		"learn and record a word to the user dictionary", 2)
    add_command(:reset_context, [], "reset context")
    add_command(:set_context, [:CONTEXT], "set context to CONTEXT")
    add_command(:get_env, [:KEY], "get a variable associated with KEY")
    add_command(:get_label, [:PATTERN],
		"get a label string (e.g. hiragana) from a user input.")
    add_command(:preedit_convert_input, [:PATTERN],
                "convert a PATTERN and return a pair of a converted and a pending string.")
    add_command(:refresh, [],
                "refresh the statuses of the conversion engines.")
  end

  ## This returns a string which has the type of a value and the value.
  ## The type and the value are separated by "\t".
  ## For example: "string\tstring_value", "boolean\ttrue", "nil".
  def combine_return_value (value)
    if value.kind_of?(String) then
      output = format("string\t%s", value)
    elsif value.kind_of?(Array) then
      output = format("array\t%s", value.join("\t"))
    elsif value.kind_of?(TrueClass) or value.kind_of?(FalseClass) then
      output = format("boolean\t%s", value ? "true" : "false")
    elsif value == nil then
      output = 'nil'
    else
      output = 'unknown'
    end
    return output
  end

  def learn_word (key, value, part = nil,
                  context = nil, suffix = nil, rest = nil)
    @prime.learn_word(key, value, part, context, suffix, rest)
    return reply_successful()
  end

  def get_env (key)
    output = combine_return_value( @prime.get_env(key) )
    return reply_successful(output)
  end

  def get_label (pattern)
    return reply_successful(@prime.get_label(pattern))
  end

  def preedit_convert_input (pattern)
    return reply_successful(@prime.preedit_convert_input(pattern))
  end

  def set_context (context)
    @prime.set_context(context)
    return reply_successful()
  end
  def reset_context ()
    @prime.set_context(nil)
    return reply_successful()
  end
  
  def l (pattern = "")
    return reply_successful(@prime.lookup(pattern).to_text)
  end
  def lookup (pattern = "")
    return reply_successful(@prime.lookup(pattern).to_text)
  end
  def lookup_all (pattern = "")
    return reply_successful(@prime.lookup_all(pattern).to_text)
  end

  def lookup_compact (pattern = "")
    return reply_successful(@prime.lookup_compact(pattern).to_text)
  end
  def lookup_compact_all (pattern = "")
    return reply_successful(@prime.lookup_compact_all(pattern).to_text)
  end
  def lookup_direct (pattern = "")
    return reply_successful(@prime.lookup_direct(pattern).to_text)
  end
  def lookup_direct_all (pattern = "")
    return reply_successful(@prime.lookup_direct_all(pattern).to_text)
  end
  def lookup_hybrid (pattern = "")
    return reply_successful(@prime.lookup_hybrid(pattern).to_text)
  end
  def lookup_hybrid_all (pattern = "")
    return reply_successful(@prime.lookup_hybrid_all(pattern).to_text)
  end
  def lookup_prefix (pattern = "")
    return reply_successful(@prime.lookup_prefix(pattern).to_text)
  end
  def lookup_prefix_ex (pattern = "")
    return reply_successful(@prime.lookup_prefix_ex(pattern).to_text)
  end
  def lookup_exact (pattern = "")
    return reply_successful(@prime.lookup_exact(pattern).to_text)
  end
  def lookup_expansion (pattern = "")
    return reply_successful(@prime.lookup_expansion(pattern).to_text)
  end
  def lookup_mixed (pattern = "")
    return reply_successful(@prime.lookup_mixed(pattern).to_text)
  end

  def refresh ()
    @prime.refresh()
    return reply_successful()
  end
end

class PrimeProtocolPrime2 < PrimeProtocolPrime
  def initialize (prime, version)
    super(prime, version)

    add_command(:session_start, [:LANGUAGE],
                "start a session and return the session id.", 0)
    add_command(:session_end, [:SESSION],
                "close the session specified with the session id.")
    add_command(:session_get_env, [:SESSION, :VARIABLE],
                "return a value of the variable.")
#     add_command(:session_language_set, [:SESSION, :LANGUAGE],
#                 "Set the target language (Japanese or English).")
#     add_command(:session_language_get, [:SESSION],
#                 "Get the current target language.")
    add_command(:register_word, [:SESSION, :READING, :LITERAL, :POS],
                "Register a word.")
    add_command(:edit_insert, [:SESSION, :STRING],
                "insert this string into the preediting string.")
    add_command(:edit_delete, [:SESSION],
                "delete a character from the preediting string.")
    add_command(:edit_backspace, [:SESSION],
                "delete a character backward from the preediting string.")
    add_command(:edit_erase, [:SESSION],
                "erase the preediting string.")
    add_command(:edit_undo, [:SESSION],
                "undo the preediting string.")
    add_command(:edit_cursor_right, [:SESSION],
                "move the cursor right")
    add_command(:edit_cursor_left, [:SESSION],
                "move the cursor left")
    add_command(:edit_cursor_right_edge, [:SESSION],
                "move the cursor the end of the preediting string.")
    add_command(:edit_cursor_left_edge, [:SESSION],
                "move the cursor the beginning of the preediting string.")
    add_command(:edit_get_preedition, [:SESSION],
                "return a list fo the preediting string [left, cursor, right]")
    add_command(:edit_get_query_string, [:SESSION],
                "return a query string for lookup functions. (temporal)")

    add_command(:edit_set_mode, [:SESSION, :MODE],
                "set display mode of the preedition.\n" +
                "        MODE = [default, katakana, half_katakana, \n" + 
                "                wide_ascii, raw]")
    add_command(:edit_commit, [:SESSION],
                "commit the current preediting string")

    add_command(:context_reset, [:SESSION],
                "reset the context.")
    add_command(:context_set_previous_word, [:SESSION, :WORD],
                "set the word to the context")

    add_command(:conv_convert, [:SESSION, :METHOD],
                "convert the preedition string.", 1)
    add_command(:conv_predict, [:SESSION, :METHOD],
                "predict candidate words with the method", 1)
    add_command(:conv_select,  [:SESSION, :INDEX],
                "select an indexed word to a conversion.")
    add_command(:conv_commit,  [:SESSION],
                "commit the current conversion.")

    add_command(:modify_start, [:SESSION],
                "Start a modification of the selected conversion.")
    add_command(:segment_select, [:SESSION, :INDEX],
                "set an indexed word to the current segment.")
    add_command(:segment_reconvert, [:SESSION],
                "convert the current segment again.")
    add_command(:segment_commit, [:SESSION],
                "fix up the selected word in the current segment.")
    add_command(:modify_get_candidates, [:SESSION],
                "")
    add_command(:modify_get_conversion, [:SESSION],
                "")
    add_command(:modify_cursor_left, [:SESSION],
                "")
    add_command(:modify_cursor_right, [:SESSION],
                "")
    add_command(:modify_cursor_left_edge, [:SESSION],
                "")
    add_command(:modify_cursor_right_edge, [:SESSION],
                "")
    add_command(:modify_cursor_expand, [:SESSION],
                "")
    add_command(:modify_cursor_shrink, [:SESSION],
                "")

  end

  ## 
  ## Session methods
  ##
  def session_start (language = nil)
    session = @prime.session_start(language)
    return reply_successful(session)
  end

  def session_end (session)
    @prime.session_end(session)
    return reply_successful()
  end

  def session_get_env (session, variable)
    value = @prime.session_command(session, :session_get_env, variable)
    return reply_successful( combine_return_value(value) )
  end

  ## 
  ## Composition methods
  ##
  def edit_insert (session, string)
    @prime.session_command(session, :edit_insert, string)
    return edit_get_preedition(session)
  end
  def edit_delete (session)
    @prime.session_command(session, :edit_delete)
    return edit_get_preedition(session)
  end
  def edit_backspace (session)
    @prime.session_command(session, :edit_backspace)
    return edit_get_preedition(session)
  end
  def edit_erase (session)
    @prime.session_command(session, :edit_erase)
    return edit_get_preedition(session)
  end
  def edit_undo (session)
    @prime.session_command(session, :edit_undo)
    return edit_get_preedition(session)
  end
  def edit_cursor_right (session)
    @prime.session_command(session, :edit_cursor_right)
    return edit_get_preedition(session)
  end
  def edit_cursor_left (session)
    @prime.session_command(session, :edit_cursor_left)
    return edit_get_preedition(session)
  end
  def edit_cursor_right_edge (session)
    @prime.session_command(session, :edit_cursor_right_edge)
    return edit_get_preedition(session)
  end
  def edit_cursor_left_edge (session)
    @prime.session_command(session, :edit_cursor_left_edge)
    return edit_get_preedition(session)
  end
  def edit_get_preedition (session)
    preedition = @prime.session_command(session, :edit_get_preedition).join("\t")
    return reply_successful(preedition)
  end
  def edit_get_query_string (session)
    query_string = @prime.session_command(session, :edit_get_query_string)
    return reply_successful( query_string)
  end

  def edit_set_mode (session, mode)
    if @prime.session_command(session, :edit_set_mode, mode) then
      return edit_get_preedition(session)
    else
      error_message = "Unknown mode.  Valid modes are: \n" +
        "[default, katakana, half_katakana, wide_ascii, raw]"
      return reply_unsuccessful(error_message)
    end
  end

  def edit_commit (session)
    commited_string = @prime.session_command(session, :edit_commit)
    return reply_successful(commited_string)
  end

  ##
  ## Context methods
  ##
  def context_reset (session)
    @prime.session_command(session, :context_reset)
    return reply_successful()
  end

  def context_set_previous_word (session, word)
    @prime.session_command(session, :context_set_previous_word)
    return reply_successful()
  end

  ##
  ## Conversion methods
  ##
  def conv_convert (session)
    conversions = @prime.session_command(session, :conv_convert)
    return reply_successful( conversions.to_text() )
  end

  def conv_predict (session, method = nil)
    conversions = @prime.session_command(session, :conv_predict)
    return reply_successful( conversions.to_text() )
  end

  def conv_select (session, index)
    selected_candidate = @prime.session_command(session, :conv_select, index.to_i)
    return reply_successful(selected_candidate.to_text)
  end

  def conv_commit (session)
    commited_string = @prime.session_command(session, :conv_commit)
    return reply_successful(commited_string)
  end

  ##
  ## Conversion methods
  ##
  def modify_start (session)
    @prime.session_command(session, :modify_start)
    return modify_get_conversion(session)
  end

  def segment_select (session, index_no)
    @prime.session_command(session, :modify_select, index_no.to_i)
    return modify_get_conversion(session)
  end

  def segment_reconvert (session)
    @prime.session_command(session, :modify_reconvert)
    return modify_get_candidates(session)
  end

  def modify_commit (session)
  end

  def modify_get_candidates (session)
    segment = @prime.session_command(session, :modify_get_segment).to_text_candidates()
    return reply_successful(segment)
  end

  def modify_get_conversion (session)
    conversion = @prime.session_command(session, :modify_get_conversion).join("\t")
    return reply_successful(conversion)
  end

  def modify_cursor_left (session)
    @prime.session_command(session, :modify_cursor_left)
    return modify_get_conversion(session)
  end
  def modify_cursor_right (session)
    @prime.session_command(session, :modify_cursor_right)
    return modify_get_conversion(session)
  end
  def modify_cursor_left_edge (session)
    @prime.session_command(session, :modify_cursor_left_edge)
    return modify_get_conversion(session)
  end
  def modify_cursor_right_edge (session)
    @prime.session_command(session, :modify_cursor_right_edge)
    return modify_get_conversion(session)
  end
  def modify_cursor_expand (session)
    @prime.session_command(session, :modify_cursor_expand)
    return modify_get_conversion(session)
  end
  def modify_cursor_shrink (session)
    @prime.session_command(session, :modify_cursor_shrink)
    return modify_get_conversion(session)
  end
end

# ----

class PrimeProtocolSKK < PrimeProtocolCore
  def initialize (prime, portnum)
    super(prime)
    @portnum = portnum
  end

  def get_line (io_in)
    line = ""
    loop {
      char = io_in.getc()
      if char.nil? then
        return nil
      end
      if char.chr == " " or char.chr == "\n" then
        return line
      else
        line += char.chr
      end
    }
  end

  def lookup (pattern)
    return @prime.lookup_japanese(pattern)
  end

  def execute (line)
    line.chomp!()

    case line[0,1]
    when "0" then
      return false
    when "1" then
      pattern = line.chomp[1..-1]
      results = lookup(pattern)
      if results.empty? then
        return "4" + pattern + "\n"
      else
        result_line = results.map {|result|
          result.to_text_literal()
        }.join('/')
        return "1/" + result_line + "/\n"
      end
    when "2" then
      # FIXME
      return "prime-#{PRIME_VERSION} "
    when "3" then
      # FIXME
      return "localhost:#{@protnum} "
    else
      return ""
    end
  end
end


# POBox Protocol
# ----
# Close          "0"                     "<none>"
# GetWords       "1<query>"              "1/<word1>/<word2>/.../\n" or
#                                        "0\n" (error) or
#                                        "4\n" (no word)
# GetVersion     "2"                     "<major>.<minor> "
# GetHostName    "3"                     "<hostname>:<port> "
# SetContext     "4<context>"            "1"
# RegisterWord   "5<word>\t<pattern>"    "1"
# DeleteWord     "6<word>"               "1"
# SaveDict       "7"                     "1"
# SelectWord     "8<number>"             "1"

class PrimeProtocolPOBox < PrimeProtocolSKK
  def lookup (pattern)
    return @prime.lookup(pattern)
  end

  def execute (line)
    ## The detail of POBox protocol is here; 
    ## <http://pitecan.com/OpenPOBox/server/protocol.html>

    line.chomp!()

    case line[0,1]
    when "0" then
      return false
    when "1" then
      pattern = line.chomp[1..-1]
      results = lookup(pattern)
      if results.empty? then
        return "4" + pattern + "\n"
      else
        result_line = results.map {|result|
          result.to_text_literal()
        }.join("\t")
        return "1\t" + result_line + "\n"
      end
    when "2" then
      # FIXME
      return "prime-#{PRIME_VERSION} "
    when "3" then
      # FIXME
      return "localhost:#{@protnum} "
    when "4" then
      context = line.chomp[1..-1]
      if context == "" then
        @prime.set_context(nil)
      else
        @prime.set_context(context)
      end
      return "1 "
    when "5" then
      # Not implemented yet.
      return "1 "
    when "6" then
      # Not implemented yet.
      return "1 "
    when "7" then
      # Not implemented yet.
      return "1 "
    when "8" then
      # Not implemented yet.
      return "1 "
    else
      super
    end
  end
end
