# prime2.rb: Module for PRIME2 protocol.
# $Id: prime2.rb,v 1.2.2.1 2005/03/08 11:50:39 komatsu Exp $
#
# Copyright (C) 2004 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.

require 'suikyo/suikyo-composer'
require 'prime/session'
require 'prime/session-japanese'
require 'prime/session-english'
require 'prime/session-prime08'

class PrimeExceptionNoSession < Exception
  def initialize (session_name)
    @session_name = session_name
  end

  def message ()
    message = "Session '#{@session_name}' does not exist."
    return message
  end
end

module Prime2
  def initialize_prime2 ()
    @sessions   = {}
    @session_no = 0
  end

  ## 
  ## Session methods
  ##
  def session_start (name = "Japanese")
    case name
    when "Japanese" then
      session_data = PrimeSessionJapanese.new()
    when "English" then
      session_data = PrimeSessionEnglish.new()
    when "Prime08" then
      session_data = PrimeSessionPrime08.new()
    else
      session_data = PrimeSessionJapanese.new()
    end
    @session_no += 1
    @sessions[@session_no.to_s] = session_data

    return @session_no.to_s
  end

  def session_end (session)
    assert_session(session)
    @sessions.delete(session)
    return true
  end

  def session_command (session, command, *arguments)
    assert_session(session)
    return @sessions[session].send(command, *arguments)
  end

  def assert_session (session)
    unless @sessions.has_key?(session) then
      exception = PrimeExceptionNoSession.new(session)
      raise(exception)
    end
  end
end

class PrimeConversionList < Array
  def initialize (*arguments)
    super(*arguments)
    @index_no = -1
  end

  ## This adds a score to each conversion.  The first conversion increases
  ## the base_score and the length of conversions, and the last conversion
  ## increases the base_score.
  def add_score (base_score)
    base_score += ( self.length() - 1 )
    self.each { | conversion |
      conversion.set_score( conversion.score + base_score )
      base_score -= 1
    }
  end

  def set_conversion_index (index_no)
    @index_no = index_no
  end
  def get_conversion ()
    if @index_no == -1 then
      return nil
    else
      return self[@index_no]
    end
  end

  def to_text ()
    text = @index_no.to_s
    self.each { | conversion |
      text += "\n" + conversion.to_text()
    }
    return text
  end

  def to_text_debug ()
    texts = self.sort { | conversion1, conversion2 |
      conversion1.score <=> conversion2.score
    }.map { | conversion | conversion.to_text_debug() }
    return texts.join("\n")
  end

  def PrimeConversionList::merge (*conversions_list)
    uniq_conversions = {}
    conversions_list.each { | conversions |
      if conversions.empty? then
        next
      end

      conversions.each { | conversion |
        literal = conversion.get_literal()
        unless uniq_conversions.has_key?( literal ) then
          uniq_conversions[literal] = conversion
          next
        end

        if conversion.score > uniq_conversions[literal].score then
          uniq_conversions[literal] = conversion
        end
      }
    }

    conversions = uniq_conversions.values.sort { | conversion1, conversion2 |
      conversion2.score <=> conversion1.score
    }
    return PrimeConversionList.new(conversions)
  end
end

class PrimeConversion
  attr_reader :score, :segments
  def initialize (segments, score = 0, position = -1)
    @segments = segments  # Array of PrimeSegment
    @position = position  # position of segments, -1 means non-selection.
    @score    = score
  end

  def <=> ( other )
    case other
    when PrimeConversion then
      return @score <=> other.score
    end
    return nil
  end

  def dup ()
    return PrimeConversion.new(@segments.dup, @score, @position)
  end

  def set_score (score)
    @score = score
  end

  def get_segment ()
    return @segments[@position]
  end

  def get_conversion ()
    if @position == -1 then
      return [ get_literal(), "", "" ]
    end

    left  = ""
    right = ""

    @segments[0, @position].each { | segment |
      left += segment.get_literal()
    }
    cursor = @segments[@position].get_literal()
    @segments[(@position + 1)..-1].each { | segment |
      right += segment.get_literal()
    }
    return [left, cursor, right]
  end

  def get_literal()
    literal = ""
    @segments.each { | segment |
      literal_segment = segment.get_literal()
      literal += ( get_separator(literal, literal_segment) + literal_segment )
    }
    return literal
  end
  
  def reconvert ()
    segment = get_segment()
    segment.reconvert()
  end

  def segment_insert (segment)
    ## FIXME: Condiser @position.
    ## FIXME: (2004-12-19) <Hiro>
    @segments.push(segment)
  end

  def segment_expand ()
    if @position == -1 or @position == (@segments.length - 1) then
      return false
    end

    segment      = @segments[@position]
    next_segment = @segments[@position + 1]

    reading_list = next_segment.reading.chars()

    segment.set_reading( segment.reading + reading_list.first() )

    if reading_list.length == 1 then
      @segments[ (@position + 1) , 1 ] = nil
    else
      next_segment.set_reading( reading_list[1..-1].join() )
    end
    return true
  end

  def segment_shrink ()
    if @position == -1 then
      return false
    end

    segment      = @segments[@position]
    reading_list = segment.reading.chars()

    if reading_list.length == 1 then
      return false
    end

    ## Checking if @postion is the end of segments.
    if @position == @segments.length - 1 then
      next_segment = PrimeSegment.new("")  # The first value "" is a reading.
      @segments.push(next_segment)
    else
      next_segment = @segments[@position + 1]
    end

    next_segment.set_reading( reading_list.last() + next_segment.reading )
    segment.set_reading( reading_list[0..-2].join() )
    return true
  end

  def candidate_select (index_no)
    @segments[@position].set_candidate_index(index_no)
  end

  def position_right ()
    @position += 1
    @position %= @segments.length()
  end
  def position_right_edge ()
    @position = @segments.length() - 1
  end
  def position_left ()
    @position -= 1
    if @position < 0 then
      @position = @segments.length() - 1
    end
  end
  def position_left_edge ()
    @position = 0
  end

  def to_text_debug ()
    if @segments.length == 1 then
      return "(#{score})\t" + @segments[0].to_text_data()  # with annotations
    else
      texts = @segments.map { | segment | segment.to_text() }
      return "(#{score})\t" + texts.join("|")
    end
  end

  def to_text ()
    if @segments.length == 1 then
      return @segments[0].to_text_data()  # with annotations
    else
      return get_literal()
    end
  end

  def get_separator (word1, word2)
    if  alphabet?( word1[-1] )  and  alphabet?( word2[0] )  then
      return " "
    else
      return ""
    end
  end

  def alphabet? (char)
    if char.nil? then
      return false
    else
      return ( (char >= ?a and char <= ?z) or (char >= ?A and char <= ?Z) )
    end
  end
end



class PrimeSegment
  attr_accessor :reading
  attr_reader   :base_reading, :pos, :adjunct, :pos_adjunct, :context
  def initialize (reading,
                  base_reading = nil,
                  pos = nil,
                  adjunct = "",
                  pos_adjunct = nil,
                  context = nil)
    @reading = reading

    ## The following data is a query guideline.
    @base_reading = base_reading
    @pos          = pos
    @adjunct      = adjunct
    @pos_adjunct  = pos_adjunct
    @context      = context

    @candidates      = PrimeWordList.new()
    @candidate_index = -1
  end

  def dup ()
    segment = PrimeSegment.new(@reading, @base_reading, @pos,
                               @adjunct, @pos_adjunct, @context)
    segment.set_candidates(@candidates, @candidate_index)
    return segment
  end

  def set_candidates (candidates, candidate_index = -1)
    @candidates      = candidates
    @candidate_index = candidate_index
  end
  def get_candidates ()
    return @candidates
  end
  def get_candidate_index ()
    return @candidate_index
  end
  def set_candidate_index (candidate_index)
    if candidate_index > @candidates.length or candidate_index < 0 then
      @candidate_index = -1
    end
    @candidate_index = candidate_index
  end

  def set_reading (reading)
    @reading = reading
    @candidates = PrimeWordList.new()
    @candidate_index = -1
    @base_reading = nil
    @pos          = nil
    @adjunct      = ""
    @pos_adjunct  = nil
  end

  def get_reading ()
    if @candidate_index == -1 then
      return @reading
    else
      return @candidates[@candidate_index].to_text_pron()
    end
  end
  def get_literal ()
    if @candidate_index == -1 then
      return @reading
    else
      return @candidates[@candidate_index].to_text_literal()
    end
  end

  def get_base_reading ()
    if @candidate_index == -1 then
      return (@base_reading or @reading)
    else
      return @candidates[@candidate_index].pron
    end
  end
  def get_base ()
    if @candidate_index == -1 then
      return (@base_reading or @reading)
    else
      return @candidates[@candidate_index].literal
    end
  end
  def get_pos ()
    if @candidate_index == -1 then
      return (@pos or "")
    else
      return @candidates[@candidate_index].pos
    end
  end
  def get_adjunct ()
    if @candidate_index == -1 then
      if @base_reading.nil? then
        return ""  # @base_reading == @reading
      else
        return @adjunct
      end
    else
      return @candidates[@candidate_index].conjugation
    end
  end
  def get_score ()
    if @candidate_index == -1 then
      ## FIXME: Remove the music number.
      ## FIXME: (2004-12-17) <Hiro>
      return 8000
    else
      return @candidates[@candidate_index].score
    end
  end

  def to_text_candidates ()
    text = @candidate_index.to_s() + "\n"
    text += @candidates.map { | candidate | candidate.to_text2() }.join("\n")
    return text
  end

  def to_text_data ()
    if @candidate_index == -1 then
      text = @reading
    else
      text = @candidates[@candidate_index].to_text2()
    end
    return text
  end

  def to_text ()
    return get_literal()
  end
end
