module Padrino
  module Helpers
    module FormHelpers
      ##
      # Constructs a form for object using given or default form_builder
      # 
      # ==== Examples
      # 
      #   form_for :user, '/register' do |f| ... end
      #   form_for @user, '/register', :id => 'register' do |f| ... end
      # 
      def form_for(object, url, settings={}, &block)
        builder_class = configured_form_builder_class(settings[:builder])
        form_html = capture_html(builder_class.new(self, object), &block)
        form_tag(url, settings) { form_html }
      end

      ##
      # Constructs form fields for an object using given or default form_builder
      # Used within an existing form to allow alternate objects within one form
      # 
      # ==== Examples
      # 
      #   fields_for @user.assignment do |assignment| ... end
      #   fields_for :assignment do |assigment| ... end
      # 
      def fields_for(object, settings={}, &block)
        builder_class = configured_form_builder_class(settings[:builder])
        fields_html = capture_html(builder_class.new(self, object), &block)
        concat_content fields_html
      end

      ##
      # Constructs a form without object based on options
      # 
      # ==== Examples
      #   
      #   form_tag '/register' do ... end
      # 
      def form_tag(url, options={}, &block)
        desired_method = options[:method]
        options.delete(:method) if options[:method].to_s !~ /get|post/i
        options.reverse_merge!(:method => 'post', :action => url)
        options[:enctype] = "multipart/form-data" if options.delete(:multipart)
        options["data-remote"] = "true" if options.delete(:remote)
        inner_form_html = hidden_form_method_field(desired_method) + capture_html(&block)
        concat_content content_tag('form', inner_form_html, options)
      end

      ##
      # Returns the hidden method field for 'put' and 'delete' forms
      # Only 'get' and 'post' are allowed within browsers;
      # 'put' and 'delete' are just specified using hidden fields with form action still 'put'.
      # 
      # ==== Examples
      # 
      #   # Generate: <input name="_method" value="delete" />
      #   hidden_form_method_field('delete')
      # 
      def hidden_form_method_field(desired_method)
        return '' if desired_method.blank? || desired_method.to_s =~ /get|post/i
        hidden_field_tag(:_method, :value => desired_method)
      end

      ##
      # Constructs a field_set to group fields with given options
      # 
      # ==== Examples
      #   
      #   field_set_tag("Office", :class => 'office-set')
      # 
      def field_set_tag(*args, &block)
        options = args.extract_options!
        legend_text = args[0].is_a?(String) ? args.first : nil
        legend_html = legend_text.blank? ? '' : content_tag(:legend, legend_text)
        field_set_content = legend_html + capture_html(&block)
        concat_content content_tag('fieldset', field_set_content, options)
      end

      ##
      # Constructs list html for the errors for a given symbol
      # 
      # ==== Options
      #
      # :header_tag:: Used for the header of the error div (default: "h2").
      # :id:: The id of the error div (default: "errorExplanation").
      # :class:: The class of the error div (default: "errorExplanation").
      # :object:: The object (or array of objects) for which to display errors,
      # if you need to escape the instance variable convention.
      # :object_name:: The object name to use in the header, or any text that you prefer.
      # If +:object_name+ is not set, the name of the first object will be used.
      # :header_message:: The message in the header of the error div.  Pass +nil+
      # or an empty string to avoid the header message altogether. (Default: "X errors
      # prohibited this object from being saved").
      # :message:: The explanation message after the header message and before
      # the error list.  Pass +nil+ or an empty string to avoid the explanation message
      # altogether. (Default: "There were problems with the following fields:").
      # 
      # ==== Examples
      # 
      #   error_messages_for :user
      # 
      def error_messages_for(*objects)
        options = objects.extract_options!.symbolize_keys
        objects = objects.collect {|object_name| object_name.is_a?(Symbol) ? instance_variable_get("@#{object_name}") : object_name }.compact
        count   = objects.inject(0) {|sum, object| sum + object.errors.count }

        unless count.zero?
          html = {}
          [:id, :class, :style].each do |key|
            if options.include?(key)
              value = options[key]
              html[key] = value unless value.blank?
            else
              html[key] = 'field-errors' unless key == :style
            end
          end

          options[:object_name] ||= objects.first.class

          I18n.with_options :locale => options[:locale], :scope => [:models, :errors, :template] do |locale|
            header_message = if options.include?(:header_message)
              options[:header_message]
            else
              object_name = options[:object_name].to_s.gsub('_', ' ')
              object_name = I18n.t(object_name, :default => object_name, :scope => :models, :count => 1)
              locale.t :header, :count => count, :model => object_name
            end
            message = options.include?(:message) ? options[:message] : locale.t(:body)
            error_messages = objects.map {|object| object.errors.full_messages.map {|msg| content_tag(:li, msg) } }.join

            contents = ''
            contents << content_tag(options[:header_tag] || :h2, header_message) unless header_message.blank?
            contents << content_tag(:p, message) unless message.blank?
            contents << content_tag(:ul, error_messages)

            content_tag(:div, contents, html)
          end
        else
          ''
        end
      end

      ##
      # Returns a string containing the error message attached to the +method+ on the +object+ if one exists.
      #
      # ==== Options
      # 
      # :tag::      The tag that enclose your error. (Default 'div')
      # :prepend::  Text to add before error.
      # :append::   Text to add after error.
      # 
      # ==== Examples
      # 
      #   # => <span class="error">can't be blank</div>
      #   error_message_on :post, :title
      # 
      #   # => <div class="custom" style="border:1px solid red">can't be blank</div>
      #   error_message_on :post, :title, :tag => :id, :class => :custom, :style => "border:1px solid red"
      # 
      #   # => <div class="error">This title can't be blank (or it won't work)</div>
      #   error_message_on :post, :title, :prepend => "This title", :append => "(or it won't work)"
      # 
      def error_message_on(object, field, options={})
        object = instance_variable_get("@#{object}")
        error  = object.errors[field] rescue nil
        if error
          options.reverse_merge!(:tag => :span, :class => :error)
          tag   = options.delete(:tag)
          error = [options.delete(:prepend), Array(object.errors[field]).first, options.delete(:append)].compact.join(" ")
          content_tag(tag, error, options)
        else
          ''
        end
      end

      ##
      # Constructs a label tag from the given options
      # 
      # ==== Examples
      # 
      #   label_tag :username, :class => 'long-label'
      #   label_tag :username, :class => 'long-label' do ... end
      # 
      def label_tag(name, options={}, &block)
        options.reverse_merge!(:caption => "#{name.to_s.titleize}: ", :for => name)
        caption_text = options.delete(:caption)
        caption_text << "* " if options.delete(:required)
        if block_given? # label with inner content
          label_content = caption_text + capture_html(&block)
          concat_content(content_tag(:label, label_content, options))
        else # regular label
          content_tag(:label, caption_text, options)
        end
      end

      ##
      # Constructs a hidden field input from the given options
      # 
      # ==== Examples
      #   
      #   hidden_field_tag :session_key, :value => "__secret__"
      # 
      def hidden_field_tag(name, options={})
        options.reverse_merge!(:name => name)
        input_tag(:hidden, options)
      end

      ##
      # Constructs a text field input from the given options
      # 
      # ==== Examples
      # 
      #   text_field_tag :username, :class => 'long'
      # 
      def text_field_tag(name, options={})
        options.reverse_merge!(:name => name)
        input_tag(:text, options)
      end

      ##
      # Constructs a text area input from the given options
      # 
      # ==== Examples
      # 
      #   text_area_tag :username, :class => 'long', :value => "Demo?"
      # 
      def text_area_tag(name, options={})
        options.reverse_merge!(:name => name)
        content_tag(:textarea, options.delete(:value).to_s, options)
      end

      ##
      # Constructs a password field input from the given options
      # 
      # ==== Examples
      # 
      #   password_field_tag :password, :class => 'long'
      # 
      def password_field_tag(name, options={})
        options.reverse_merge!(:name => name)
        input_tag(:password, options)
      end

      ##
      # Constructs a check_box from the given options
      # 
      # ==== Examples
      # 
      #   options = [['caption', 'value'], ['Green', 'green1'], ['Blue', 'blue1'], ['Black', "black1"]]
      #   options = ['option', 'red', 'yellow' ]
      #   select_tag(:favorite_color, :options => ['red', 'yellow'], :selected => 'green1')
      #   select_tag(:country, :collection => @countries, :fields => [:name, :code])
      # 
      def select_tag(name, options={})
        options.reverse_merge!(:name => name)
        collection, fields = options.delete(:collection), options.delete(:fields)
        options[:options] = options_from_collection(collection, fields) if collection
        options[:options].unshift('') if options.delete(:include_blank)
        select_options_html = options_for_select(options.delete(:options), options.delete(:selected))
        options.merge!(:name => "#{options[:name]}[]") if options[:multiple]
        content_tag(:select, select_options_html, options)
      end

      ##
      # Constructs a check_box from the given options
      # 
      # ==== Examples
      # 
      #   check_box_tag :remember_me, :value => 'Yes'
      # 
      def check_box_tag(name, options={})
        options.reverse_merge!(:name => name, :value => '1')
        input_tag(:checkbox, options)
      end

      ##
      # Constructs a radio_button from the given options
      # 
      # ==== Examples
      # 
      #   radio_button_tag :remember_me, :value => 'true'
      # 
      def radio_button_tag(name, options={})
        options.reverse_merge!(:name => name)
        input_tag(:radio, options)
      end

      ##
      # Constructs a file field input from the given options
      # 
      # ==== Examples
      # 
      #   file_field_tag :photo, :class => 'long'
      # 
      def file_field_tag(name, options={})
        options.reverse_merge!(:name => name)
        input_tag(:file, options)
      end

      ##
      # Constructs a submit button from the given options
      # 
      # ==== Examples
      # 
      #   submit_tag "Create", :class => 'success'
      # 
      def submit_tag(caption="Submit", options={})
        options.reverse_merge!(:value => caption)
        input_tag(:submit, options)
      end

      ##
      # Constructs a button input from the given options
      # 
      # ==== Examples
      # 
      #   button_tag "Cancel", :class => 'clear'
      # 
      def button_tag(caption, options = {})
        options.reverse_merge!(:value => caption)
        input_tag(:button, options)
      end

      # Constructs a submit button from the given options
      # 
      # ==== Examples
      # 
      #   submit_tag "Create", :class => 'success'
      # 
      def image_submit_tag(source, options={})
        options.reverse_merge!(:src => image_path(source))
        input_tag(:image, options)
      end

      ##
      # Returns an array of option items for a select field based on the given collection
      # fields is an array containing the fields to display from each item in the collection
      # 
      def options_from_collection(collection, fields)
        return '' if collection.blank?
        collection.collect { |item| [ item.send(fields.first), item.send(fields.last) ] }
      end

      #
      # Returns the options tags for a select based on the given option items
      # 
      def options_for_select(option_items, selected_value=nil)
        return '' if option_items.blank?
        option_items.collect do |caption, value|
          value ||= caption
          content_tag(:option, caption, :value => value, :selected => selected_value.to_s =~ /#{value}|#{caption}/)
        end
      end

      private
        ##
        # Returns the FormBuilder class to use based on all available setting sources
        # If explicitly defined, returns that, otherwise returns defaults.
        # 
        #   configured_form_builder_class(nil) => StandardFormBuilder
        # 
        def configured_form_builder_class(explicit_builder=nil)
          default_builder    = self.respond_to?(:options) && self.options.default_builder
          configured_builder = explicit_builder || default_builder || 'StandardFormBuilder'
          configured_builder = "Padrino::Helpers::FormBuilder::#{configured_builder}".constantize if configured_builder.is_a?(String)
          configured_builder
        end
    end # FormHelpers
  end # Helpers
end # Padrino