# -*- Mode: ruby; indent-tabs-mode: nil -*-
#
#  $Id: expander.rb,v 1.24 2004/02/06 05:54:25 hisa Exp $
#
#  Copyright (c) 2003 FUJIMOTO Hisakuni <hisa@fobj.com>
#
#  This program is free software.
#  You can distribute/modify this program under the terms of
#  the GNU Lesser General Public License version 2.
#
require 'rexml/document'
require 'tempura/charconv'

module Tempura

  class Expander

    attr_reader :safe_level
    attr_accessor :default_action
    attr_accessor :default_event_key

    def self.pre_expand(rexml_element, bang_p = false)
      elm = bang_p ? rexml_element : rexml_element.deep_clone
      elm.each_element_with_attribute('_nil_') do |child|
        _expand_nil(elm, child)
      end
      elm.each_element do |child|
        pre_expand( child, true )
      end
      return elm
    end

    def pre_expand(rexml_element, bang_p = false)
      self.class.pre_expand(rexml_element, bang_p)
    end

    def self.url_encode(s)
      s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) do
        '%' + $1.unpack('H2' * $1.size).join('%').upcase
      end.tr(' ', '+')
    end

    def url_encode(s)
      self.class.url_encode(s)
    end

    def initialize(safe_level = nil)
      @safe_level = safe_level
      @param_filter = nil
      @action_filter = nil
      @default_action = ""
      @default_event_key = "event"
    end

    def expand(context, rexml_element, charconv, bang_p = false)
      charconv = charconv || Tempura::CharConvDefault
      elm = bang_p ? rexml_element : rexml_element.deep_clone
      return _expand_main( elm, _tempura_context(context), charconv )
    end

    def expand_to_string(context, rexml_element, charconv, bang_p = false)
      charconv = charconv || Tempura::CharConvDefault
      elm = expand( context, rexml_element, charconv, bang_p )
      result = ""
      elm.write( result )
      return charconv.from_u8( result )
    end

    def param_filter(&proc)
      @param_filter = proc
    end

    def action_filter(&proc)
      @action_filter = proc
    end

    def default_param_filter( evt, param, bind )
      param[@default_event_key] = evt
      return param
    end

    def default_action_filter( evt, param, bind )
      return @default_action
    end

    ##########

    protected

    def _handle_expand_event(elm, action, param)
      case elm.name.downcase

      when 'a' then
        ary = param.map { |key,val| "#{url_encode(key)}=#{url_encode(val)}" }
        elm.attributes['href'] = "#{action}?#{ary.join(';')}"

      when 'form' then
        elm.attributes['method'] = 'POST' if elm.attributes['method'].nil?
        elm.attributes['action'] = action
        param.each do |key,val|
          elm.push( _new_input_element(key, val) )
        end

      end
    end

    ####

    def _expand_block(_elm_, _attr_, _bind_, _charconv_)
      _rcv_expr_, _method_, _args_ = _elm_.attributes[_attr_].split('//')
      _elm_.delete_attribute( _attr_ )
      _proc_expr_ = "proc do |#{_args_}|
            _next_elm_ = _elm_.deep_clone
            _expand_main( _next_elm_, binding, _charconv_ )
            _elm_.parent.insert_before( _elm_, _next_elm_ )
           end"
      _proc_ = _tempura_eval( _proc_expr_, binding )
      _rcv_  = _tempura_eval( _rcv_expr_, _bind_ )
      _rcv_.send(_method_, &_proc_)
      _elm_.parent.delete_element( _elm_ )
    end

    def _expand_child_main(elm, attr, bind, charconv, xml_parse_p)
      val = elm.attributes[ attr ]
      val = _tempura_eval( val, bind )
      if val then
        elm.delete_if { true }
        if val.is_a? REXML::Child then
          val = val.root if val.is_a? REXML::Document
          elm.add( val )
        else
          if xml_parse_p then
            str = "<dummy-elm>#{charconv.to_u8(val.to_s)}</dummy-elm>"
            REXML::Document.new(str).root.each do |item|
              elm.add( item )
            end
          else
            val = REXML::Text.new( charconv.to_u8(val.to_s) )
            elm.add( val )
          end
        end
        elm.delete_attribute( attr )
      elsif val.nil? then
        elm.remove
      else
        elm.delete_attribute( attr )
      end
      return (val != false)
    end

    def _expand_child(elm, attr, bind, charconv)
      _expand_child_main( elm, attr, bind, charconv, false )
    end

    def _expand_child_xml(elm, attr, bind, charconv)
      _expand_child_main( elm, attr, bind, charconv, true )
    end

    def _expand_self(elm, attr, bind, charconv)
      if _expand_child_main( elm, attr, bind, charconv, false ) then
        elm.name = "__delete_me__"
      end
    end

    def _expand_self_xml(elm, attr, bind, charconv)
      if _expand_child_main( elm, attr, bind, charconv, true ) then
        elm.name = "__delete_me__"
      end
    end

    def _expand_self_bool(elm, attr, bind, bool)
      val = elm.attributes[ attr ]
      val = _tempura_eval( val, bind )
      del_p = (bool && !val) || (!bool && val)
      elm.delete_if { del_p }
      elm.name = '__delete_me__'
    end

    def _expand_attribute(elm, attr, new_attr, bind, charconv)
      val = elm.attributes[ attr ]
      val = _tempura_eval( val, bind )
      elm.delete_attribute( attr )

      if !new_attr.empty? then
        case val
        when nil then
          elm.delete_attribute( new_attr )
        when false then
          ; # nop
        when REXML::Child then
          elm.add_attribute( new_attr, val.to_s )
        else
          elm.add_attribute( new_attr, charconv.to_u8(val.to_s) )
        end
      end
    end

    def _expand_event(elm, attr, bind, charconv)
      evt, param = _parse_event_value( elm.attributes[attr], bind, charconv )
      elm.delete_attribute( attr )
      param = _array_to_hash( param )
      if elm.attributes['_action_'] then
        action = elm.attributes['_action_']
        elm.delete_attribute('_action_')
      else
        action = (@action_filter && @action_filter.call(evt, param, bind)) || default_action_filter(evt, param, bind)
      end
      param = (@param_filter && @param_filter.call(evt, param, bind)) || default_param_filter(evt, param, bind)
      _handle_expand_event(elm, action, param)
    end

    def self._expand_nil(parent, elm)
      parent = elm.parent if parent.nil?
      child = elm
      index = parent.index( child )
      next_child = parent[ index + 1 ]
      if next_child.is_a? REXML::Text and next_child.to_s == "\n" then
        parent.delete( next_child )
      end
      parent.delete_element( child )
    end

    def _expand_nil(parent, elm)
      self.class._expand_nil(parent, elm)
    end

    ##########

    def _expand_main(elm, bind, charconv)
      attr = elm.attributes
      if attr['_block_'] then
        _expand_block( elm, '_block_', bind, charconv )
      else
        attr.keys.each do |attr|
          case attr
          when '_if_' then
            _expand_self_bool( elm, attr, bind, true )
          when '_unless_' then
            _expand_self_bool( elm, attr, bind, false )
          when '_child_' then
            _expand_child( elm, attr, bind, charconv )
          when '_child_xml_' then
            _expand_child_xml( elm, attr, bind, charconv )
          when '_self_' then
            _expand_self( elm, attr, bind, charconv )
          when '_self_xml_' then
            _expand_self_xml( elm, attr, bind, charconv )
          when /^_attr_(.*)$/ then
            _expand_attribute( elm, attr, $+, bind, charconv )
          when '_event_' then
            _expand_event( elm, attr, bind, charconv )
          when '_nil_' then
            _expand_nil( nil, elm )
          end
        end
        # traverse into children
        if elm.has_elements? then
          elm.each_element { |child| _expand_main( child, bind, charconv ) }
        end
        # remove span tag without attribute
        name = elm.name.downcase
        if (name == 'span' && !elm.has_attributes?) || (name == '__delete_me__') then
          parent = elm.parent
          elm.to_a.each do |child|
            parent.insert_before(elm, child) if parent
          end
          elm.remove
        end
      end
      return elm
    end

    #######

    def _tempura_context( _context_ )
      if _context_.is_a? Binding then
        return _context_
      else
        return _context_.instance_eval { binding }
      end
    end

    def _parse_event_value(attr_value, bind, charconv)
      event, *args = attr_value.split('//')
      event.strip!
      args.map! { |entry|
        key, val = entry.split('==>')
        key.strip!
        val = _tempura_eval( val, bind )
        val = charconv.to_u8(val.to_s) unless val.nil?
        [ key, val ]
      }
      return event, args
    end

    def _array_to_hash(ary)
      param = {}
      ary.each { |k,v| param[k] = v }
      return param
    end

    def _new_input_element(name, value)
      elm = REXML::Element.new('input')
      elm.attributes['type'] = 'hidden'
      elm.attributes['name'] = name
      elm.attributes['value'] = value
      return elm
    end

    # eval wrapper
    def _tempura_eval( _expr_, _bind_ )
      _fname_ = (_expr_.strip)[0,36].inspect
      if @safe_level then
        return Thread.start {
          $SAFE = @safe_level
          eval(_expr_, _bind_, _fname_)
        }.value
      else
        return eval(_expr_, _bind_, _fname_)
      end
    end

  end                           # class Expander

end
