require 'kiss/form/field';

# Part Hash, part Array... it's a Hashay.
class Hashay < Hash
  _attr_accessor :keys, :values
  
  def initialize(*args, &block)
    super
    @_keys = []
    @_values = []
  end
  
  def each_key(&block)
    @_keys.each &block
  end
  
  def each_value(&block)
    @_values.each &block
  end
  alias_method :each, :each_value
  
  def []=(key, value)
    unless has_key?(key)
      @_keys << key
      @_values << value 
    end
    super
  end
  
  def [](key)
    key.is_a?(Numeric) ? @_values[key] : super
  end
  
  include Enumerable
end

class Kiss
  class Form
    _attr_accessor :fields, :params, :submitted, :has_field_errors, :has_required_fields,
      :delegate, :controller, :components, :form, :new_object_index
    dsl_accessor :name, :url, :action, :method, :enctype, :errors, :cancel, :mark_required,
      :id, :class, :style, :html, :error_class, :field_error_class, :objects_save_order,
      :object, :prepend_html, :append_html, :year, :timezone
    
    @@component_types = {
      :text => TextField,
      :hidden => HiddenField,
      :textarea => TextAreaField,
      :password => PasswordField,
      :boolean => BooleanField,
      :file => FileField,
      :select => SelectField,
      :radio => RadioField,
      :checkbox => CheckboxField,
      :multiselect => MultiSelectField,
      :submit => SubmitField
    }
      
    # Create DSL methods for component types
    self.class_eval(
      @@component_types.keys.map do |type|
        "def #{type}(*args, &block); add_component(:#{type}, *args, &block); end; "
      end.join
    )
    
    def add_component(type, name, *args, &block)
      attrs = args.to_attrs
      add_field({
        :type => type,
        :name => name
      }.merge(attrs), &block)
    end
    
    def debug(*args)
      @_delegate.request.debug(args.first, Kernel.caller[0])
    end
    
    def method_missing(method, *args, &block)
      delegate.send method, *args, &block
    end
    
    def unique(*val)
      if val.empty?
        @_unique
      else
        @_unique << val
      end
    end
    
    # Creates a new form object with specified attributes.
    def initialize(*args, &block)
      @_attrs = args.to_attrs
      _instance_variables_set_from_attrs(@_attrs)
    
      @_method ||= 'post'
      
      @_components = []
      @_fields = Hashay.new
      @_object_fields = {}
      @_default_values ||= {}
      @_params = {}
      @_with = nil
      @_field_name_prefix = ''
      @_objects_add_order = []
      @_objects_save_order = []
      @_new_object_index = -1
      @_object_fields = {}
      @_unique = []
      
      @_prepend_html = ''
      @_append_html = ''
      
      (@_attrs[:fields] || []).each do |field|
        # create field here
        add_field(field)
      end
      
    	@_errors = []
    	@_field_errors = {}
    	
      import_instance_variables(@_delegate) if @_delegate
      
      instance_eval(&block) if block_given?
      
      raise "form name required" unless @_name
      raise "form delegate required" unless @_delegate
    end
    
    def context
      @_context ||= begin
        @_timezone ||= (fields['timezone'] && (params['timezone'] || (object && object[:timezone]))) || nil
        {
          :timezone => @_timezone,
          :year => @_year
        }
      end
    end
    
    # Creates and adds a field to the form, according to specified attributes.
    def add_field(attrs = {}, &block)
      attrs = @_with.merge(attrs) if @_with
      name = attrs[:name].to_s
      key = (attrs[:key] || name).to_sym
      
      name = @_field_name_prefix + name unless @_field_name_prefix.empty?
      
      type = attrs[:type] ? attrs[:type].to_sym : :text
      raise "invalid field type '#{type}'" unless @@component_types.has_key?(type)
      
      field = @@component_types[type].new(self, attrs.merge(
        :name => name,
        :key => key,
        :type => type
      ), &block)
      
      field.object = @_object if @_object && !field.object
      obj = field.object
      # must hash @_object_fields by Ruby object id; if by object, weird lookup errors result,
      # even when lookup object has same Ruby object id as the hash key object!
      ruby_obj_id = obj.object_id
      unless @_object_fields[ruby_obj_id]
        @_object_fields[ruby_obj_id] = {}
        @_objects_add_order << object
      end
      if @_object_fields[ruby_obj_id][field.name]
        raise "duplicate form field name '#{attrs[:name]}'#{" on #{obj.class.name} object" if obj}"
      end
      @_object_fields[ruby_obj_id][field.name] = field
      
      @_fields[name] = field
      @_components << field
      
      while true
        other_field = field.other_field
        break unless other_field
        other_field.form = self
        @_other_field = @_form.create_field( { :name => @_name + '.other' }.merge(@_other) )
      end
      
      @_enctype = 'multipart/form-data' if field.type == :file
      
      field
    end
    alias_method :create_field, :add_field
    
    # Creates and adds set of submit buttons to the form, per specified attributes.
    def submit(*args, &block)
      if args.size > 0
        raise 'submit already defined' if @_submit
        
        attrs = {
          :type => :submit,
          :name => 'submit',
          :save => false,
          :cancel => 'Cancel'
        }
        attrs.merge!(args.pop) if args.last.is_a?(Hash)
        attrs[:options] = args if args.size > 0
        attrs = @_with.merge(attrs) if @_with
        @_submit = @@component_types[:submit].new(self, attrs, &block)
      else
        @_submit
      end
    end
    alias_method :add_submit, :submit
    
    def reset
      @_params = {}
    	@_fields.each {|field| field.reset }
    end
    
    def field(name)
      @_fields[name]
    end
    
    def object_field(obj, name)
      @_object_fields[obj.object_id][name.to_s]
    end
    
    # Gets hash of form values.
    def values
      @_values ||= begin
        hash = {}
        @_fields.each {|field| hash[field.name] = field.value }
        hash['submit'] = @_submit.value = params[@_submit.name.to_s]
        hash
      end
    end
    
    # Add error message to be rendered with form.
    # If multiple args given, first arg is field name, and second arg (error message)
    # will render next to specified field.
    def add_error(*args)
      case args.size
      when 0
        raise 'at least one argument required'
      when 1
        arg = args.first
        if arg.is_a?(Hash)
          field = arg.field || arg.field_name
          message = arg.message
        else
          @_errors << arg
          return
        end
      when 2
        field, message = args
      else
        raise 'too many arguments'
      end
      
      field = @_fields[field] if field.is_a?(String)
      field.add_error(message)
    end
    
    # Validates form values against fields' format and required attributes,
    # and returns values unless form has errors.
    def validate
      return nil unless submitted
      
    	@_fields.each {|field| field.validate }
    	@_unique.each {|field_set| validate_uniqueness_of(field_set) }
  	  
  	  has_errors? ? nil : values
    end
    
    def validate_uniqueness_of(column_set)
      column_set_labels_values = column_set.map do |column_name|
        (field = fields[column_name.to_s]) ? 
          [field.label, field.value] : 
          [column_name.to_s.sub(/_id\Z/, '').titlecase, object[column_name]]
      end
      conditions = column_set.zip column_set_labels_values.map {|lv| lv[1] }
      
      dataset = @_object.model.filter(conditions)
      unless (@_object.new? ? dataset : dataset.exclude(@_object.pk_hash)).empty?
        labels = column_set_labels_values.map {|lv| lv[0] }
        message = "There is already another #{@_object.model.name.singularize.gsub('_', ' ')} with the same #{labels.conjoin}."
        return (
          (column_set.length == 1) && 
          (field_name = column_set[0].to_str) &&
          fields[field_name]
        ) ? add_error(field_name, message) : add_error(message)
      end
    end
    
    # Checks whether form was submitted and accepted by user and, if so,
    # whether form validates.
    # If form validates, saves form values to form's Sequel::Model object 
    # and returns true.  Otherwise, returns false.
    def process(*objs)
      return false unless submitted
      
      if accepted
        validate
        return false if has_errors?
      
        save(*objs)
      end
  	  
    	return true
    end
    
    def process_or_render
      self.render unless self.process
    end
    
    private
    
    def extract_objects_from_args(objs)
      objs = args.first if objs.first.is_a?(Array)
      if objs.size == 0
        objects_save_order.push(@_object) if @_object && !objects_save_order.include?(@_object)
        objs = objects_save_order
      end
      objs
    end
    
    public
    
    def set_object_data(obj = @_object)
      @_object_fields[obj.object_id].values.each do |field|
    		# ignore fields whose name starts with underscore
        next if field.name =~ /\A\_/
    		# don't save 'ignore' fields to the database
        next if field.ignore || !field.save
        # ignore file fields
        next if field.type == :file
        
        key = field.key.to_sym
        value = (field.value != nil || obj.class.db_schema[key].allow_null) ?
          field.value : (obj.class.db_schema[key][:default] ||= field.format.default)
        
        if field.digest
          value = Digest.const_get(field.digest.to_sym).hexdigest(value)
        end
        
        obj[key] = value if field.save
      end
    end
    
    def set_objects_data(*args)
      extract_objects_from_args(args).each do |obj|
        next unless obj
        set_object_data(obj)
      end
    end
    
    # Saves form input data associated with specified objects, or
    # all form objects if objects are not specified here.
    def save(*args)
      db.transaction do
        extract_objects_from_args(args).each do |obj|
          set_object_data(obj)
          obj.save
        end
      end
    end
    
    def require_values(*names)
      names.each {|name| fields[name.to_s].require_value }
    end
    
    def with(*args, &block)
      if block_given?
        attrs = args.to_attrs
        old_with = @_with
        @_with = (@_with || {}).merge(attrs)
      
        instance_eval(&block)
        
        @_with = old_with
      end
    end
    
    def object(obj = nil, options = {}, &block)
      if block_given?
        raise 'missing object arg' unless obj
        
        if obj.is_a?(Symbol)
          parent = @_object
          raise 'missing parent object for object symbol reference' unless parent
          obj = parent.send(obj) #||
            #parent.set_associated_object(parent.class.association_reflection(object).associated_class.new)
        end
        raise 'object parameter must reference a model object' unless obj.is_a?(Kiss::Model)
        
        # if new, save early to give object a primary key, needed to save assoc children
        @_objects_save_order << obj if obj.new?
        prefix = options[:prefix]
        
        old_obj = @_object
        old_prefix = @_field_name_prefix
        
        @_object = obj
        @_field_name_prefix = (@_field_name_prefix + prefix.to_s + '.') if prefix
        instance_eval(&block)
        @_object = old_obj
        @_field_name_prefix = old_prefix
        
        # save again to pick up any assoc ids from children
        @_objects_save_order << obj
        obj
      elsif obj
        @_object = obj
      else
        @_object
      end
    end
    
    # Returns true if form has errors.
    def has_errors?
      (@_errors.size > 0 || @_has_field_errors)
    end
    
    # Returns true if user submitted form with non-cancel submit button
    # (non-nil submit param, not equal to cancel submit button value).
    def accepted
      raise 'form missing submit field' unless @_submit
      return params[@_submit.name] != @_submit.cancel
    end
    
    # Renders error HTML block for top of form, and returns as string.
    def errors_html
      return nil unless has_errors?
      
      @_errors << "Please correct the #{@_errors.empty? ? '' : 'other '}errors highlighted below." if @_has_field_errors
      
      if @_errors.size == 1
        content = @_errors[0]
      else
        content = "<ul>" + @_errors.map {|e| "<li>#{e}</li>"}.join + "</ul>"
      end
      
      @_errors.pop if @_has_field_errors
      
      plural = @_errors.size > 1 || @_has_field_errors ? 's' : nil
      %Q(<table class="kiss_error"><tr><td>#{content}</td></tr></table>)
    end
    
    # Renders current action using form's HTML as action render content.
    def render(options = {})
      @_delegate.render options.merge(:content => html)
    end
    
    def form_name_hidden_tag_html
      %Q(<input type=hidden name="form" value="#{@_name}">)
    end
    
    # Renders beginning of form (form open tag and form/field/error styles).
    def html_open
      @_error_class ||= 'kiss_form_error_message'
      @_field_error_class ||= @_error_class
      
      # form tag
      form_attrs = ['id', 'method', 'enctype', 'class', 'style'].map do |attr|
        next if (value = send attr).blank?
        "#{attr}=\"#{send attr}\""
      end
      if @_html
        @_html.each_pair do |k, v|
          form_attrs.push("#{k}=\"#{v}\"")
        end
      end
      form_tag = %Q(<form action="#{@_action}" #{form_attrs.join(' ')}>#{form_name_hidden_tag_html})
      
      # style tag
      styles = []
      styles.push( <<-EOT
table.kiss_form {
  margin-bottom: 6px;
}
.kiss_form td {
  padding: 2px 4px;
  vertical-align: middle;
}
.kiss_form .kiss_error {
  margin-bottom: 4px;
}
.kiss_form .kiss_error td {
  background-color: #ff8;
  padding: 2px 4px;
  line-height: 135%;
  color: #900;
  border: 1px solid #ea4;
}
.kiss_form .kiss_error td ul {
  padding-left: 16px;
  margin: 0;
}
.kiss_form .kiss_help {
  padding: 0px 3px 0 6px;
  font-size: 90%;
  color: #555;
}
.kiss_required {
  color: #a21;
  font-size: 150%;
  line-height: 50%;
  position: relative;
  top: 4px;
}
.kiss_form td.kiss_label {
  text-align: right;
  white-space: nowrap;
}
.kiss_form td.kiss_label.error {
  color: #b10;
}
.kiss_form tr.kiss_prompt td {
  padding: 8px 3px 0px 4px;
}
.kiss_form tr.kiss_submit td.kiss_submit {
  padding: 6px 3px;
}
.kiss_form input[type="text"],
.kiss_form input[type=password],
.kiss_form textarea {
  width: 250px;
  margin-left: 0;
  margin-right: 0;
}
.kiss_form table.kiss_field_columns {
  border-spacing: 0;
  border-collapse: collapse;
  width: 100%;
}
.kiss_form table.kiss_field_columns td {
  padding: 0 16px 2px 0;
  vertical-align: top;
}
.kiss_form .kiss_checkbox .kiss_label {
  vertical-align: top;
  padding-top: 3px;
}
.kiss_form .kiss_radio .kiss_label {
  vertical-align: top;
  padding-top: 4px;
}
.kiss_form .kiss_textarea .kiss_label {
  vertical-align: top;
  padding-top: 3px;
}
EOT
      )
      styles.push( <<-EOT
.kiss_form_error_message {
  padding: 1px 4px;
  border: 1px solid #edc;
  border-top: 0;
  border-bottom: 1px solid #db4;
  background-color: #ff8;
  color: #900;
  font-size: 90%;
  line-height: 135%;
  display: block;
  float: left;
  margin-bottom: 2px;
}
.kiss_form_error_message div {
  color: #c00;
  font-weight: bold;
}
.kiss_form_error_message ul {
  padding-left: 16px;
  margin: 0;
}
tr.kiss_form_error_row td {
  padding-top: 0;
}
tr.kiss_form_error_row .kiss_form_error_message {
  position: relative;
  top: -2px;
  margin-bottom: 0;
}
EOT
      ) if @_error_class == 'kiss_form_error_message'
      style_tag = styles.size == 0 ? '' : "<style>" + styles.join('') + "</style>"
      
      # combine
      return %Q(#{@_prepend_html}#{form_tag}#{style_tag}<div class="kiss_form">#{errors_html})
    end
    
    # Renders end of form (form close tag).
    def html_close
      "</div></form>#{@_append_html}"
    end
    
    # Renders form fields HTML.
    def components_html
      @_components.map do |component|
        component_html(component)
      end.join
    end
    alias_method :fields_html, :components_html
    
    # Renders open of form table.
    def table_html_open
      %Q(<table class="kiss_form" border=0 cellspacing=0>)
    end
    
    def required_legend_html
      (@_has_required_fields && @_mark_required) ?
        %Q( <tr><td></td><td class="kiss_help">Required fields marked by <span class="kiss_required">*</span></td></tr> ) : ''
    end
    
    # Renders close of form table.
    def table_html_close
      '</table>'
    end
    
    # Renders form submit buttons.
    def submit_html
      @_submit ? field_html(@_submit) : ''
    end
    
    # Renders complete form HTML.
    def html
      return [
        html_open,
        table_html_open,
        required_legend_html,
        components_html,
        submit_html,
        table_html_close,
        html_close
      ].flatten.join
    end
    
    # Renders HTML for specified form field.
    def component_html(field)
      field = fields[field.to_s] if (field.is_a?(Symbol) || field.is_a?(String))
      return field.element_html if field.type == :hidden
      
      type = field.type
      prompt = field.prompt
      label = field.label
      errors = field.errors_html
      required = field.required ? %Q(<span class="kiss_required">#{@_mark_required}</span> ) : ''
      
      ([
    	  prompt ? %Q(<tr class="kiss_prompt"><td class="kiss_label">#{required}</td><td>#{prompt.to_s}</td></tr>) : '',
    	  
        %Q(<tr class="kiss_#{type}"><td class="kiss_label#{errors ? ' error' : ''}">),
        !prompt ? (required + (label.blank? ? '' : label.to_s + ':' )) : '',
    	  %Q(</td><td class="kiss_#{type}">),
    		field.element_html, "</td></tr>"
    	] + (errors ? [
    	  '<tr class="kiss_form_error_row"><td class="kiss_required"></td><td>',
    	  errors,
    	  '</td></tr>'
    	] : [])).join
    end
    alias_method :field_html, :component_html
  end
end