#
# encode.rb
#
#   Copyright (c) 1998-2001 Minero Aoki <aamine@dp.u-netsurf.ne.jp>
#
#   This program is free software.
#   You can distribute/modify this program under the terms of
#   the GNU Lesser General Public License version 2 or later.
#

require 'nkf'
require 'strscan'
require 'amstd/bug'
require 'tmail/base64'


module TMail

  module StrategyInterface

    def create_dest( obj )
      case obj
      when nil
        StringOStream.new('', "\n", false)
      when String
        StringOStream.new(obj, "\n", false)
      when File
        FileOStream.new(obj, "\n", false)
      when Stream
        obj
      else
        raise TypeError, 'cannot handle this type of object for dest'
      end
    end
    module_function :create_dest

    def encoded( eol = "\r\n", charset = 'j', dest = nil )
      accept_strategy HFencoder, eol, charset, dest
    end

    def decoded( eol = "\n", charset = 'e', dest = nil )
      accept_strategy HFdecoder, eol, charset, dest
    end

    alias to_s decoded
  
    def accept_strategy( klass, eol, charset, dest0 )
      dest = create_dest( dest0 ||= '' )
      strategy = klass.new( dest, charset )
      accept strategy, dest
      dest0
    end

  end


  ###
  ### decode
  ###

  class HFdecoder

    def initialize( dest, charset = nil )
      @f   = StrategyInterface.create_dest(dest)
      @opt = '-' + (/ejs/ === charset ? charset : 'e')
    end

    class << self
      def decode( str, outcode = 'e' )
        NKF.nkf( "-#{outcode}m", str )
      end
    end


    def terminate
    end

    def header_line( str )
      @f << decode(str)
    end

    def header_name( nm )
      @f << nm << ': '
    end

    def header_body( str )
      @f << decode(str)
    end
      
    def space
      @f << ' '
    end

    alias spc space

    def lwsp( str )
      @f << str
    end
      
    def meta( str )
      @f << str
    end

    def text( str )
      @f << decode(str)
    end

    alias phrase text

    def kv_pair( k, v )
      @f << k << '=' << v
    end

    private

    def decode( str )
      NKF.nkf( @opt + 'm', str )
    end

  end


  ###
  ### encode
  ###

  tspecial     = Regexp.quote( '()<>@,.;:\\"/[]?=' )
  control      = '\x00-\x1f\x7f-\xff'

  TSPECIAL     = /[#{tspecial}]/n
  CONTROL      = /[#{control}]/n
  INSECURE     = /[#{tspecial}#{control} ]/n
  INSECURE_PH  = /[#{tspecial}#{control}]/n

  ENCODED = /=\?[^\s?=]+\?[qb]\?[^\s?=]+\?=/i

  class << self

    def quote( str )
      if INSECURE === str then
        %Q_"#{str.gsub '"', '\\\\"'}"_
      else
        str
      end
    end

    def quote_phrase( str )
      if INSECURE_PH === str then
        %Q_"#{str.gsub '"', '\\\\"'}"_
      else
        str
      end
    end

    def encoded?( str )
      ENCODED === str
    end

  end   # class << TMail


  class HFencoder

    unless defined? BENCODE_DEBUG then
      BENCODE_DEBUG = false
    end


    class << self
      def encode( str )
        e = new()
        e.header_body str
        e.terminate
        e.dest.string
      end
    end


    def initialize( dest = nil, charset = nil, limit = nil )
      @f     = StrategyInterface.create_dest(dest)
      @space = "\t"
      @limit = (limit || 72) - 2
      @opt   = '-' + case charset
                     when 'e', 'j', 's' then charset
                     else                    'j'
                     end
      reset
    end

    def reset
      @text = ''
      @lwsp = ''
      @curlen = 0
    end

    def terminate
      add_lwsp ''
      reset
    end

    def dest
      @f
    end


    #
    # add
    #

    def header_line( line )
      scanadd line
    end

    def header_name( name )
      add_text name.split('-').collect {|i| i.capitalize }.join('-')
      add_text ':'
      add_lwsp ' '
    end

    def header_body( str )
      scanadd NKF.nkf( @opt, str )
    end

    def space
      add_lwsp ' '
    end

    alias spc space

    def lwsp( str )
      add_lwsp str.sub( /[\r\n]+[^\r\n]*\z/, '' )
    end

    def meta( str )
      add_text str
    end

    def text( str )
      scanadd NKF.nkf( @opt, str )
    end

    def phrase( str )
      str = NKF.nkf( @opt, str )
      if CONTROL === str then
        scanadd str
      else
        add_text TMail.quote_phrase( str )
      end
    end

    def kv_pair( k, v )   ### line folding is not used yet
      v = NKF.nkf( @opt, v )

      if not TMail::INSECURE === v then
        add_text k + '=' + v

      elsif not TMail::CONTROL === v then
        add_text k + '=' + TMail.quote(v)
      
      else
        kv = k + '*=' + "iso-2022-jp'ja'" + vencode(v)
        add_text kv
      end
    end

    def vencode( str )
      str.gsub( TMail::INSECURE ) {|s| '%%%02x' % s[0] }
    end


    private


    MPREFIX = '=?iso-2022-jp?B?'
    MSUFFIX = '?='

    ESC_ASCII     = "\e(B"
    ESC_ISO2022JP = "\e$B"


    def scanadd( str, force = false )
      types = ''
      strs = []
      pres = prev = nil

      s = StringScanner.new( str, false )

      until s.empty? do
        if tmp = s.scan( /\A[^\e\t\r\n ]+/ ) then
          types <<  pres = force ? 'j' : 'a'
          strs.push prev = tmp

        elsif tmp = s.scan( /\A[\t\r\n ]+/ ) then
          types <<  pres = 's'
          strs.push prev = tmp

        elsif esc = s.scan( /\A\e../ ) then
          if esc != ESC_ASCII and tmp = s.scan( /\A[^\e]+/ ) then
            types <<  pres = 'j'
            strs.push prev = tmp
          end

        else
          bug! "HFencoder#scan, not match"
        end
      end

      do_encode types, strs
    end

    def do_encode( types, strs )
      #
      # sbuf = (J|A)(J|A|S)*(J|A)
      #
      #   A: ascii only, no need to encode
      #   J: jis, etc. need to encode
      #   S: LWSP
      #
      # (J|A)*J(J|A)* -> W
      # W(SW)*        -> E
      #
      # result = (A|E)(S(A|E))*
      #
      if BENCODE_DEBUG then
        puts "\n-- do_encode ------------"
        puts types.split(//).join(' ')
        p strs
      end

      e = /[ja]*j[ja]*(?:s[ja]*j[ja]*)*/

      while m = e.match(types) do
        pre = m.pre_match
        unless pre.empty? then
          concat_a_s pre, strs[ 0, pre.size ]
        end

        concat_e m[0], strs[ m.begin(0) ... m.end(0) ]

        types = m.post_match
        strs.slice! 0, m.end(0)
      end
      concat_a_s types, strs
    end

    def concat_a_s( types, strs )
      i = 0
      types.each_byte do |t|
        case t
        when ?a then add_text strs[i]
        when ?s then add_lwsp strs[i]
        else
          bug!
        end
        i += 1
      end
    end
    
    def concat_e( types, strs )
      if BENCODE_DEBUG then
        puts '---- concat_e'
        puts "types=#{types.split('').join(' ')}"
        puts "strs =#{strs.inspect}"
      end

      bug! @text unless @text.empty?

      chunk = ''
      strs.each_with_index do |s,i|
        m = 'extract_' + types[i,1]
        until s.empty? do
          unless c = send(m, chunk.size, s) then
            add_with_encode chunk unless chunk.empty?
            flush
            chunk = ''
            fold
            c = send( m, 0, s )
            bug! unless c
          end
          chunk << c
        end
      end
      add_with_encode chunk unless chunk.empty?
    end

    def extract_j( csize, str )
      size = maxbyte( csize, str.size ) - 6
      size = size % 2 == 0 ? size : size - 1
      return nil if size <= 0

      c = str[ 0, size ]
      str[ 0, size ] = ''
      c = ESC_ISO2022JP + c + ESC_ASCII
      c
    end

    def extract_a( csize, str )
      size = maxbyte( csize, str.size )
      return nil if size <= 0

      c = str[ 0, size ]
      str[ 0, size ] = ''
      c
    end

    alias extract_s extract_a

    def maxbyte( csize, ssize )
      rest = restsize - MPREFIX.size - MSUFFIX.size
      rest / 4 * 3 - csize
    end

    #def encsize( len )
    #  amari = (if len % 3 == 0 then 0 else 1 end)
    #  (len / 3 + amari) * 4 + PRESIZE
    #end


    #
    # length-free buffer
    #

    def add_text( str )
      @text << str
#puts '---- text -------------------------------------'
#puts "+ #{str.inspect}"
#puts "txt >>>#{@text.inspect}<<<"
    end

    def add_with_encode( str )
      @text << MPREFIX << Base64.encode(str) << MSUFFIX
    end

    def add_lwsp( lwsp )
#puts '---- lwsp -------------------------------------'
#puts "+ #{lwsp.inspect}"
      if restsize <= 0 then
        fold
      end
      flush
      @lwsp = lwsp
    end

    def flush
#puts '---- flush ----'
#puts "spc >>>#{@lwsp.inspect}<<<"
#puts "txt >>>#{@text.inspect}<<<"
      @f << @lwsp << @text
      @curlen += @lwsp.size + @text.size
      @text = ''
      @lwsp = ''
    end

    def fold
#puts '---- fold ----'
      @f.puts
      @curlen = 0
      @lwsp = @space
    end

    def restsize
      @limit - (@curlen + @lwsp.size + @text.size)
    end

  end   # class HFencoder

end    # module TMail
