#
# field.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 'sync'
require 'delegate'
require 'amstd/to_s'
require 'tmail/parsemail'
require 'tmail/encode'


module TMail

  MSGID = /<[^\@>]+\@[^>\@]+>/

  ZONESTR_TABLE = {
    'jst' =>   9 * 60,
    'eet' =>   2 * 60,
    'bst' =>   1 * 60,
    'met' =>   1 * 60,
    'gmt' =>   0,
    'utc' =>   0,
    'ut'  =>   0,
    'nst' => -(3 * 60 + 30),
    'ast' =>  -4 * 60,
    'edt' =>  -4 * 60,
    'est' =>  -5 * 60,
    'cdt' =>  -5 * 60,
    'cst' =>  -6 * 60,
    'mdt' =>  -6 * 60,
    'mst' =>  -7 * 60,
    'pdt' =>  -7 * 60,
    'pst' =>  -8 * 60,
    'a'   =>  -1 * 60,
    'b'   =>  -2 * 60,
    'c'   =>  -3 * 60,
    'd'   =>  -4 * 60,
    'e'   =>  -5 * 60,
    'f'   =>  -6 * 60,
    'g'   =>  -7 * 60,
    'h'   =>  -8 * 60,
    'i'   =>  -9 * 60,
    # j not use
    'k'   => -10 * 60,
    'l'   => -11 * 60,
    'm'   => -12 * 60,
    'n'   =>   1 * 60,
    'o'   =>   2 * 60,
    'p'   =>   3 * 60,
    'q'   =>   4 * 60,
    'r'   =>   5 * 60,
    's'   =>   6 * 60,
    't'   =>   7 * 60,
    'u'   =>   8 * 60,
    'v'   =>   9 * 60,
    'w'   =>  10 * 60,
    'x'   =>  11 * 60,
    'y'   =>  12 * 60,
    'z'   =>   0 * 60
  }


  class << self

    def msgid?( str )
      MSGID === str
    end

    def zonestr2i( str )
      if m = /([\+\-])(\d\d?)(\d\d)/.match( str ) then
        sec = (m[2].to_i * 60 + m[3].to_i) * 60
        m[1] == '-' ? -sec : sec
      else
        unless min = ZONESTR_TABLE[ str.downcase ] then
          raise MailSyntaxError, "wrong timezone format '#{str}'"
        end
        min * 60
      end
    end

    WDAY = %w( Sun Mon Tue Wed Thu Fri Sat wdaybug )
    MONTH = %w( mbug Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec mbug )

    def time2str( tm )
      # [ruby-list:7928]
      gmt = Time.at(tm.to_i)
      gmt.gmtime
      offset = tm.to_i - Time.local( *gmt.to_a[0,6].reverse ).to_i

      # DO NOT USE strftime: setlocale() breaks it
      sprintf '%s, %s %s %d %02d:%02d:%02d %+.2d%.2d',
              WDAY[tm.wday], tm.mday, MONTH[tm.month],
              tm.year, tm.hour, tm.min, tm.sec,
              *(offset / 60).divmod(60)
    end

  end   # class << self


  class HeaderField

    R     = (1 << 0)  # readable
    W     = (1 << 1)  # writable
    NODUP = (1 << 2)  # dup?
    NOEQL = (1 << 3)  # used by eql? ?

    class << self

      alias newobj new

      def new( fname, fbody, strict = false )
        tmp = fname.downcase
        c = (tmp[0,2] == 'x-' ? StringH : FNAME_TO_CLASS[tmp]) || UnknownH
        c.newobj fname, fbody, strict
      end

      def new_header( port, name, strict = false )
        re = /\A(#{name}):/i
        str = nil
        port.ropen do |f|
          f.each do |line|
            if m = re.match(line)            then str = m.post_match.strip
            elsif str and /\A[\t ]/ === line then str << ' ' << line.strip
            elsif /\A-*\s*\z/ === line       then break
            elsif str                        then break
            end
          end
        end
        new name, str, strict
      end


      def parse_on_rw
        @attrs = nil
      end

      def parse_on_create
        @attrs = nil
      end

      def header_attr( name, type, flags, ali = nil, add = '' )
        name = _name2str( name )
        type = _type2str( type )
        r = (flags & R != 0)
        w = (flags & W != 0)
        c = (flags & NODUP != 0)
        a = (flags & NOEQL != 0)

        unless a then
          if @attrs then
            @attrs.push name
          else
            @attrs = [name]
          end
        end

        if r then
          module_eval %-
            def #{name}
              ensure_parsed
              @#{name}
            end
          -
        end

        if w then
          module_eval %-
            def #{name}=( arg )
              ensure_parsed
              #{add}
              @#{name} = arg
            end
          -
        end

        if ali then
          ali.each do |a|
            na = _name2str( a )
            module_eval %(alias #{na} #{name})
            module_eval %(alias #{na}= #{name}=) if w
          end
        end
      end

      def redefine_eql
        bug! unless @attrs

        eqls = @attrs.collect {|at| sprintf( '%s.eql?(other.%s)', at, at ) }
        module_eval %-
          def eql?( other )
            super and
            #{eqls.join(' and ')}
          end
        -

        eqls = @attrs.collect {|at| sprintf( '%s == other.%s', at, at ) }
        module_eval %-
          def ==( other )
            super and #{eqls.join(' and ')}
          end
        -
      end

      def empty_able
        bug! unless @attrs

        module_eval %-
          def empty?
            ensure_parsed
            if @illegal then
              /\A\s*\z/ === @body
            else
              not ( #{@attrs.join(' or ')} )
            end
          end
        -
      end
          
    end


    def initialize( fname, fbody, strict )
      @name = fname
      fbody.strip!
      @body = fbody
      @strict = strict

      @illegal = false
      @parsed = false
      @parsing = false
      # @sync = Sync.new
    end

    def inspect
      "#<#{type} #@body>"
    end

    def hash
      @name.hash
    end

    def eql?( oth )
      @name.downcase == oth.name.downcase
    end

    alias == eql?


    def illegal?
      @illegal
    end

    def empty?
      ensure_parsed
      @illegal and (/\A\s*\z/ === @body)
    end


    def name
      @name.dup
    end

    def body
      ensure_parsed
      v = HFdecoder.new( '' )
      do_accept v
      v.write_in
    end

    def body=( str )
      @body = str
      clear_parse_status
    end


    include StrategyInterface

    def accept( strategy, dummy = nil )
      ensure_parsed
      strategy.header_name @name
      do_accept strategy
      strategy.terminate
    end


    private

    def ensure_parsed
      begin
        Thread.critical = true   #@sync.lock :EX
        unless @parsed or @parsing then
          @parsing = true
          Thread.critical = false
          parse
          Thread.critical = true
          @parsing = false
          @parsed = true
        end
      ensure
        Thread.critical = false   # @sync.unlock
      end
    end

    def clear_parse_status
      @parsed = false
      @illegal = false
    end

    # abstract parse

    # abstract do_accept

  end



  # string type ---------------------------------------------

  module NoAttrHeader

        def append_features( mod ) super; mod.
    
    header_attr :body, String, R | W    ; mod.
    redefine_eql                          end

    private

    def do_accept( stra )
      stra.text @body
    end

    def parse
      init_real
      @body = TMail::HFdecoder.decode( @body.gsub(/\n|\r\n|\r/, '') )
    end

  end


  class StringH < HeaderField

    parse_on_rw

    include NoAttrHeader

    private

    def init_real
    end

  end


  # struct type -----------------------------------------

  class StructH < HeaderField

    parse_on_rw

    header_attr :comments, Array, R | NODUP | NOEQL


    private

    def init_real
      @comments = []
      init
    end

    def parse
      pre = nil

      begin
        init_real
        Parser.parse( @body, self, nil )
      rescue MailSyntaxError, ScanError
        if not pre and ::TMail.encoded?( @body ) then
          pre = @body
          @body = HFdecoder.decode( pre )
          retry
        elsif pre then
          @body = pre
        end

        @illegal = true
        raise if @strict
      end
    end

  end


  # unknown type ----------------------------------------

  class UnknownH < StructH

    include NoAttrHeader

    private

    def init
    end

  end


  # date type -------------------------------------------

  class DateH < StructH

    parse_on_rw

    header_attr :date, Time, R | W | NODUP

    redefine_eql
    empty_able


    private

    def init
      @date = nil
    end

    def do_accept( strategy )
      strategy.meta ::TMail.time2str( @date )
    end

  end


  # return path type -------------------------------------

  class RetpathH < StructH

    parse_on_rw

    header_attr :route, Array,  R | NODUP, [:routes]
    header_attr :addr,  String, R | W

    redefine_eql
    empty_able


    private

    def init
      @route = []
      @addr = nil
    end

    def do_accept( stra )
      stra.meta '<'
      unless route.empty? then
        stra.meta @route.collect {|i| '@' + i }.join(',')
        stra.meta ':'
      end
      stra.meta @addr
      stra.meta '>'
    end

  end


  # address classes ---------------------------------------------


  class Address

    def initialize( local, domain )
      @local = local
      @domain = domain
      @phrase = nil
      @route = []
    end

    attr :phrase, true

    attr :route
    alias routes route
    attr_writer :route   # internal use only


    def inspect
      "#<#{type} #{address}>"
    end

    def Address.join( arr )
      arr.collect {|i| ::TMail.quote i }.join('.')
    end

    def local
      @local.collect {|i| ::TMail.quote i }.join('.')
    end

    def domain
      if @domain then
        @domain.collect {|i| ::TMail.quote i }.join('.')
      else
        nil
      end
    end

    def address
      s = local
      if d = domain then
        s << '@' << d
      end

      s
    end

    def address=( str )
      tmp = str.split( '@', 2 )
      @local  = tmp[0].split('.')
      @domain = tmp[1].split('.')
    end

    alias spec address
    alias spec= address=
    alias addr address
    alias addr= address=


    def eql?( other )
      self.type === other        and
      addr      ==  other.addr   and
      route     ==  other.route  and
      phrase    ==  other.phrase
    end

    alias == eql?

    def dup
      i = nil
      n = type.new( @local, @domain )
      n.phrase = @phrase.dup if @phrase
      n.route = @routes.collect{|i| i.dup } unless @routes.empty?
      n
    end


    include StrategyInterface

    def accept( strategy, dummy = nil )
      spec_p = !@phrase and @route.empty?

      if @phrase then
        strategy.phrase @phrase
        strategy.space
      end
      tmp = spec_p ? '' : '<'
      unless @route.empty? then
        tmp << @route.collect {|i| '@' + i }.join(',') << ':'
      end
      tmp << address
      tmp << '>' unless spec_p
      strategy.meta tmp
      strategy.lwsp ''
    end

  end



  class AddressGroup < DelegateClass(Array)

    def initialize( name, arg = nil )
      @name = name
      super arg ? arg.dup : []
    end

    attr :name, true
    
    def eql?( other )
      super other and self.name == other.name
    end

    alias == eql?

    def each_address
      each do |mbox|
        if AddressGroup === mbox then
          mbox.each {|i| yield i }
        else
          yield mbox
        end
      end
    end

    include StrategyInterface

    def accept( strategy, dummy = nil )
      strategy.phrase @name
      strategy.meta ':'
      strategy.space
      first = true
      each do |mbox|
        if first then
          first = false
        else
          strategy.meta ','
        end
        strategy.space
        mbox.accept strategy
      end
      strategy.meta ';'
      strategy.lwsp ''
    end

  end


  # saddr type -------------------------------------------

  class SaddrH < StructH
    
    parse_on_rw

    header_attr :addr, Address, R | W

    redefine_eql
    empty_able


    private

    def init
      @addr = nil
    end

    def do_accept( stra )
      @addr.accept stra
      unless @comments.empty? then
        @comments.each do |c|
          stra.space
          stra.meta '('
          stra.text c
          stra.meta ')'
        end
      end
    end

  end

  class SmboxH < SaddrH
  end


  # maddr type -----------------------------------------

  class MaddrH < StructH

    parse_on_rw

    header_attr :addrs, Array, R | NODUP

    redefine_eql
    empty_able


    private

    def init
      @addrs = []
    end

    def do_accept( stra )
      first = true
      @addrs.each do |a|
        if first then
          first = false
        else
          stra.meta ','
          stra.space
        end
        a.accept stra
      end
      unless @comments.empty? then
        @comments.each do |c|
          stra.space
          stra.meta '('
          stra.text c
          stra.meta ')'
        end
      end
    end

  end

  class MmboxH < MaddrH

    #def do_accept( stra )
    #  first = true
    #  last = @addrs[-1]
    #end

  end


  # ref type -------------------------------------------

  class RefH < StructH

    parse_on_rw

    header_attr :refs, Array, R | NODUP

    redefine_eql
    empty_able


    def each_msgid
      refs.each do |i|
        yield i if ::TMail.msgid? i
      end
    end

    def msgids
      ret = []
      each_msgid {|i| ret.push i }
      ret
    end

    def each_phrase
      refs.each do |i|
        yield i unless ::TMail.msgid? i
      end
    end

    def phrases
      ret = []
      each_phrase {|i| ret.push i }
      ret
    end


    private

    def init
      @refs = []
    end

    def do_accept( stra )
      first = true
      refs.each do |i|
        if first then
          first = false
        else
          stra.space
        end
        if ::TMail.msgid? i then
          stra.meta i
        else
          stra.phrase i
        end
      end
    end

  end


  # key type -------------------------------------------

  class KeyH < StructH

    parse_on_rw

    header_attr :keys, Array, R | NODUP

    redefine_eql
    empty_able


    private

    def init
      @keys = []
    end

    def do_accept( stra )
      first = true
      keys.each do |i|
        if first then
          first = false
        else
          stra.meta ','
        end
        stra.meta i
      end
    end

  end


  # received type ---------------------------------------

  class RecvH < StructH

    parse_on_rw

    header_attr :from,  String, R | W
    header_attr :by,    String, R | W
    header_attr :via,   String, R | W
    header_attr :with,  Array,  R | NODUP
    header_attr :_id,   String, R | W,        [:msgid]
    header_attr :_for,  String, R | W,        [:for_]
    header_attr :date,  Time,   R | W | NODUP

    redefine_eql
    empty_able


    private

    def init
      @from = @by = @via = @with = @_id = @_for = nil
      @with = []
      @date = nil
    end

    Tag = %w( from by via with id for )

    def do_accept( stra )
      val = [ @from, @by, @via ] + @with + [ @_id, @_for ]
      val.compact!
      c   = [ @from ? 1 : 0, @by ? 1 : 0, @via ? 1 : 0,
              @with.size, @_id ? 1 : 0, @_for ? 1 : 0 ]

      i = 0
      c.each_with_index do |count, idx|
        label = Tag[idx]
        count.times do
          stra.space unless i == 0
          stra.meta label
          stra.space
          stra.meta val[i]
          i += 1
        end
      end

      stra.meta ';'
      if @date then
        stra.space
        stra.meta ::TMail.time2str( @date )
      end
    end

  end


  # message-id type  ----------------------------------------------------

  class MsgidH < StructH

    parse_on_rw

    def initialize( fname, fbody, strict )
      super
      @msgid = fbody.strip
    end

    header_attr :msgid, String, R | W
    
    redefine_eql
    empty_able


    private

    def init
      @msgid = nil
    end

    def do_accept( stra )
      stra.meta @msgid
    end

  end


  # encrypted type ------------------------------------------------------

  class EncH < StructH

    parse_on_rw

    header_attr :encrypter, String, R | W
    header_attr :keyword,   String, R | W

    redefine_eql
    empty_able


    private

    def init
      @encrypter = nil
      @keyword = nil
    end

    def do_accept( stra )
      if @key then
        stra.meta @encrypter + ','
        stra.space
        stra.meta @keyword
      else
        stra.meta @encrypter
      end
    end

  end


  # version type -----------------------------------------

  class VersionH < StructH

    parse_on_rw

    header_attr :major, :Integer, R | W | NODUP
    header_attr :minor, :Integer, R | W | NODUP

    redefine_eql
    empty_able

    def version
      sprintf( '%d.%d', major, minor )
    end

    
    private

    def init
      @major = nil
      @minor = nil
    end

    def do_accept( stra )
      stra.meta "#{@major}.#{@minor}"
    end

  end


  # content type  ---------------------------------------


  class CTypeH < StructH

    parse_on_create

    header_attr :main,   :String, R | W
    header_attr :sub,    :String, R | W
    header_attr :params, :Hash,   R | NODUP

    redefine_eql
    empty_able

    def []( key )
      params[key]
    end


    private

    def init
      @main = @sub = nil
      @params = {}
    end

    def do_accept( stra )
      stra.meta "#{@main}/#{@sub}"
      @params.each do |k,v|
        stra.meta ';'
        stra.space
        stra.kv_pair k, v
      end
    end

  end


  # encoding type  ----------------------------

  class CEncodingH < StructH

    parse_on_rw

    header_attr :encoding, :String, R | W

    redefine_eql
    empty_able


    private

    def init
      @encoding = nil
    end

    def do_accept( stra )
      stra.meta @encoding
    end

  end


  # disposition type ----------------------------------

  class CDispositionH < StructH

    parse_on_rw

    header_attr :disposition, :String, R | W
    header_attr :params,      :Hash,   R | NODUP

    def []( key )
      params[key]
    end

    redefine_eql
    empty_able
    

    private

    def init
      @disposition = nil
      @params = {}
    end

    def do_accept( stra )
      stra.meta @disposition
      @params.each do |k,v|
        stra.meta ';'
        stra.space
        stra.kv_pair k, v
      end
    end
      
  end


  # ---------------------------------------------------

  class HeaderField   # backward definition

    FNAME_TO_CLASS = {
      'date'                      => DateH,
      'resent-date'               => DateH,
      'received'                  => RecvH,
      'return-path'               => RetpathH,
      'sender'                    => SaddrH,
      'resent-sender'             => SaddrH,
      'to'                        => MaddrH,
      'cc'                        => MaddrH,
      'bcc'                       => MaddrH,
      'from'                      => MmboxH,
      'reply-to'                  => MaddrH,
      'resent-to'                 => MaddrH,
      'resent-cc'                 => MaddrH,
      'resent-bcc'                => MaddrH,
      'resent-from'               => MmboxH,
      'resent-reply-to'           => MaddrH,
      'message-id'                => MsgidH,
      'resent-message-id'         => MsgidH,
      'content-id'                => MsgidH,
      'in-reply-to'               => RefH,
      'references'                => RefH,
      'keywords'                  => KeyH,
      'encrypted'                 => EncH,
      'mime-version'              => VersionH,
      'content-type'              => CTypeH,
      'content-transfer-encoding' => CEncodingH,
      'content-disposition'       => CDispositionH,
      'subject'                   => StringH,
      'comments'                  => StringH,
      'content-description'       => StringH
    }

  end

end   # module TMail
