module Exodus
  class Migration
    include MongoMapper::Document
    extend Exodus::TextFormatter
    set_collection_name 'migrations'

    UP = 'up'
    DOWN = 'down'
    @migrations = []

    timestamps!

    key :description,                 String
    key :status_complete,             Integer, :default => 1

    has_one :status, :class_name => "Exodus::MigrationStatus", :autosave => true

    class << self
      attr_accessor :migration_number

      # Overides #inherited to have an easy and reliable way to find all migrations
      # Migrations need to have embedded callbacks on depending on the MM's version
      def inherited(klass)
        klass.embedded_callbacks_on if defined?(MongoMapper::Plugins::EmbeddedCallbacks::ClassMethods) #MongoMapper version compatibility
        klass.migration_number = 0
        @migrations << [klass]
        super(klass)
      end

      # Sorts all migrations by migration number
      def sort_all
        @migrations.sort_by {|migration,args| migration.migration_number }
      end

      # Using a list of migrations
      # Formats and overrides migrations without arguments using ones that have given arguments
      # Removes duplicates
      # migrations: list of migrations => [[MyMigration, {:my_args => 'some_args'}]]
      def load_all(migrations)
        if migrations
          migrations.each do |migration, args|
            if migration
              formated_migration = format(migration, args || {})
              migration, args = formated_migration

              unless @migrations.include?(formated_migration)
                @migrations.delete_if {|loaded_migration, loaded_args| migration == loaded_migration && (loaded_args.nil? || loaded_args.empty?) }
                @migrations << formated_migration
              end
            end
          end
        end

        @migrations
      end

      # Using a list of migrations formats them and removes duplicates
      # migrations: list of migrations => [[MyMigration, {:my_args => 'some_args'}]]
      def load_custom(migrations)
        migrations ||= []
        migrations.map {|migration_str, args| format(migration_str, args) }.uniq
      end

      # Formats a given migration making sure the first argument is a class
      # and the second one -if it exists- is a none empty hash
      def format(migration, args = {})
        migration_klass = migration.is_a?(String) ? migration.constantize : migration
        args.is_a?(Hash) && args.empty? ? [migration_klass] : [migration_klass, args]
      end

      # Prints in the console all migrations class with their name and description
      def list
        last_info = [["Migration n#:", "Name:", "Description:"]]

        last_info |= Migration.sort_all.map do|migration, args|
          [migration.migration_number, migration.name, migration.new.description]
        end

        super_print(last_info, 70)
      end

      # Prints in the console all migrations that has been ran at least once with their name and description
      def db_status
        status_info = [["Migration n#:", "Name:", "Direction:", "Current Status:", "Arguments:", "Last completion Date:", "Current Message:"]]

        status_info |= Migration.all.map do|migration|
          [migration.class.migration_number, migration.class.name, *migration.status.to_a_string]
        end

        super_print(status_info)
      end

      def rerunnable_safe=(safe)
        migrations = Migration.instance_variable_get("@migrations")
        deletion = migrations.delete([self])
        Migration.instance_variable_set("@migrations", migrations) if deletion

        @rerunnable_safe = safe
      end

      def rerunnable_safe?
        @rerunnable_safe == true
      end
    end

    # Makes sure status get instanciated on migration's instanciation
    def initialize(args = {})
      self.build_status(args[:status])
      super(args)
    end

    # Runs the migration following the direction
    # sets the status, the execution time and the last succesful_completion date
    def run(direction)
      self.status.direction = direction

      # reset the status if the job is rerunnable and has already be completed
      self.status.reset! if self.class.rerunnable_safe? && completed?(direction)
      self.status.execution_time = time_it { self.send(direction) }
      self.status.last_succesful_completion = Time.now
    end

    # Sets an error to migration status
    def failure=(exception)
      self.status.error = MigrationError.new(
        :error_message => exception.message,
        :error_class => exception.class,
        :error_backtrace => exception.backtrace)
    end

    # Checks if a migration can be run
    def is_runnable?(direction)
      self.class.rerunnable_safe? ||
      (direction == UP && status.current_status < status_complete) ||
      (direction == DOWN && status.current_status > 0)
    end

    # Checks if a migration as been completed
    def completed?(direction)
      return false if self.status.execution_time == 0
      (direction == UP && self.status.current_status == self.status_complete) ||
      (direction == DOWN && self.status.current_status == 0)
    end

    def characteristic
      "#{self.class}: #{self.status.arguments}"
    end

    def eql?(other_migration)
      self.class == other_migration.class && self.status.arguments == other_migration.status.arguments
    end

    def hash
      self.class.hash ^ self.status.arguments.hash
    end

    protected

    # Executes a given block if the status has not being processed
    # Then update the status
    def step(step_message = nil, step_status = 1)
      unless status.status_processed?(status.direction, step_status)
        self.status.message = step_message
        puts "\t #{step_message}"

        yield if block_given?
        self.status.current_status += status.direction_to_i
      end
    end

    # Prints a given message with the current time
    def tick(msg)
      puts "#{Time.now}: #{msg}"
    end

    # Executes a block and returns the time it took to be executed
    def time_it
      puts "Running #{self.class}[#{self.status.arguments}](#{self.status.direction})"

      start = Time.now
      yield if block_given?
      end_time = Time.now - start

      puts "Tasks #{self.class} executed in #{end_time} seconds. \n\n"
      end_time
    end

    # contains the code that will be executed when run(up) will be called
    def up
      raise StandardError, 'Needs to be implemented in child class.'
    end

    # contains the code that will be executed when run(down) will be called
    def down
      raise StandardError, 'Needs to be implemented in child class.'
    end
  end
end

Dir[File.dirname(__FILE__) + "/*.rb"].sort.each { |file|  require file;}


module Exodus
  module TextFormatter

    # Prints a paragraphes
    def super_print(paragraphes, space_number = 50, title = true)
      puts format_paragraph(space_number, title, *paragraphes)
    end

    private

    # Transforms an array of paragraphes to a String using lines and columns
    # Each paragraph is actually an Array of string, where each string is a sentence of a given column
    # if the sentence contains to much caractere the sentence will be splitted (using whitespaces) and written on several lines
    # e,g considering paragraphes = [["id", "type"]["id_1", "test"]]
    # format_paragraph will print:
    # id      type
    # id_1    test
    def format_paragraph(space_number, title, *paragraphes)
      column_size = paragraphes.max_by{|paragraph| paragraph.size}.size
      @full_text = Hash[*column_size.times.map {|i| [i,[]]}.flatten(1)]

      paragraphes.each_with_index do |sentences, paragraph_number|
        sentences.each_with_index do |sentence, column|
          sentence = sentence.to_s
          words = sentence.gsub('=>', ' => ').split(' ') || ''

          if sentence.size > space_number && (words).size > 1
            new_sentence = ""
            words.each_with_index do |word, nb_word|
              if new_sentence.size + word.size  < space_number
                new_sentence << word << ' '
              else
                insert_line(column, new_sentence) unless new_sentence.empty?
                new_sentence = word << ' '
              end
            end

            insert_line(column, new_sentence) unless new_sentence == @full_text[column].last
          else
            insert_line(column, sentence)
          end
        end

        @full_text.each {|column, lines| (@max_lines - lines.size).times { lines << '' }}
        space = paragraph_number == 0 && title ? "/nbspace" : ""
        @full_text.each {|column, lines| lines << space}
      end

      stringify_paragraph
    end

    # Creates a String from a Hash of the following format {column_number => [lines]}
    # "/nbspace" is used to define a border
    def stringify_paragraph
      ordered_lines = {}

      spaces = @full_text.map {|column, lines| lines.max_by {|sentence| sentence.size}.size}
      @full_text.each_with_index do |(column, lines), i|
        lines.each_with_index do |line, line_number|
          if line == "/nbspace"
            (ordered_lines[line_number] ||= "") << line.gsub("/nbspace", "-" * (spaces[i] + 4))
          else
            (ordered_lines[line_number] ||= "") << line.to_s.ljust(spaces[i] + 4)
          end
        end
      end

      ordered_lines.values.join("\n")
    end

    # Inserts a line at the correcponding column and re-sets the number of maximum line if the maximum has been increased
    def insert_line(column, line)
      @max_lines ||= 0
      @full_text[column] << line
      @max_lines = @full_text[column].size if @max_lines < @full_text[column].size
    end
  end
end


# reference: http://github.com/ahe/2dc_jqgrid

module Gridify
  class Grid

    # ----------------------
    # generate the grid javascript for a view
    # options:
    #     :script => true generates <script> tag (true)
    #     :ready  => true generates jquery ready function (true)
    def to_javascript(options={})
      options = {:script => true, :ready => true}.merge(options)

      s = ''
      if options[:script]
        s << %Q^
        <script type="text/javascript">
        var lastsel_#{dom_id};
        ^
      end

      s << js_helpers

      if options[:ready]
        s << %Q^
        $(document).ready(function(){
        ^
      end

      s << jqgrid_javascript(options)

      if options[:ready]
        s << %Q^
        });
        ^
      end
      if options[:script]
        s << %Q^
        </script>
        ^
      end
      s
    end

    def to_json
      jqgrid_properties.to_json_with_js
    end

    # alias :to_s, :to_javascript
    def to_s(options={})
      to_javascript(options)
    end

    # ------------------
    protected
    # ------------------

    # //{ url: '/notes/{id}', mtype: 'PUT', reloadAfterSubmit: false, closeAfterEdit: true },  // edit settings
    # //{ url: '/notes', mtype: 'POST', reloadAfterSubmit: false, closeAfterEdit: true },  // add settings
    # //{ url: '/notes/{id}', mtype: 'DELETE', reloadAfterSubmit: false },  // delete settings

    # get the button options
    def edit_button_options
      # 'url' => '/notes/{id}', 'mtype' => 'PUT'
      #         {afterSubmit:function(r,data){return #{options[:error_handler_return_value]}(r,data,'edit');}},

      # note, closeAfterEdit will not close if response returns a non-empty string (even if "success" message)
      merge_options_defaults(edit_button || inline_edit,
                             'reloadAfterSubmit' => true,
                             'closeAfterEdit' => true,
                             'afterSubmit' => "javascript: function(r,data){return #{error_handler_return_value}(r,data,'edit');}"
      )
    end

    def add_button_options
      # 'url' => '/notes', 'mtype' => 'POST'
      merge_options_defaults(add_button,
                             'reloadAfterSubmit' => true,
                             'closeAfterAdd' => true,
                             'afterSubmit' => "javascript: function(r,data){return #{error_handler_return_value}(r,data,'add');}"
      )
    end

    def delete_button_options
      # 'url' => '/notes/{id}', 'mtype' => 'DELETE'
      merge_options_defaults(delete_button,
                             'reloadAfterSubmit' => true,
                             'afterSubmit' => "javascript: function(r,data){return #{error_handler_return_value}(r,data,'delete');}"
      )
    end

    def search_button_options
      if search_multiple
        merge_options_defaults(search_multiple,
                               'multipleSearch' => true,
                               'multipleGroup'  => search_group.present? && search_group ? true : false,
                               'resize'         => search_resize.present? && search_resize ? true : false,
                               'closeOnEscape'  => search_close_esc.present? && search_close_esc ? true : false,
                               'showQuery'      => search_show_query.present? && search_show_query ? true : false)
      else
        merge_options_defaults(search_button)
      end
    end

    def view_button_options
      merge_options_defaults(view_button)
    end

    # generate the jqGrid initial values in json
    #   maps our attributes to jqGrid options; omit values when same as jqGrid defaults
    def jqgrid_properties
      vals = {}
      vals[:ajaxGridOptions] = ajax_grid_options if ajax_grid_options

      # data and request options
      vals[:url] = url if url
      vals[:editurl] = url if editable
      vals[:restful] = true if restful
      vals[:inline_edit] = inline_edit if inline_edit.present?
      vals[:postData] = {:grid => name, :datatype => data_type} #identify which grid making the request
      vals[:colNames] = colNames if colNames.present?
      vals[:colModel] = column_model if colModel.present?
      vals[:datatype] = data_type if data_type
      if data_format.present?
        case data_type
          when :xml
            vals[:xmlReader] = data_format
          when :json
            vals[:jsonReader] = data_format
        end
      end

      vals[:loadonce] = load_once if load_once

      vals[:sortname] = sort_by if sort_by
      vals[:sortorder] = sort_order if sort_order && sort_by
      vals[:rowNum] = rows_per_page if rows_per_page
      vals[:rowTotal] = total_rows if total_rows
      vals[:page] = current_page if current_page

      # grid options
      vals[:height] = height if height
      vals[:gridview] = grid_view # faster views, NOTE theres cases when this needs to be disabled

      case width_fit
        when :fitted
          #vals[:autowidth]    = false #default
          #vals[:shrinkToFit]  = true #default
          vals[:forceFit] = true
          vals[:width] = width if width

        when :scroll
          #vals[:autowidth]    = false #default
          vals[:shrinkToFit] = false
          #vals[:forceFit]     = #ignored by jqGrid
          vals[:width] = width if width

        else #when :fluid
          vals[:autowidth] = true
          #vals[:shrinkToFit]  = true #default
          vals[:forceFit] = true
          #vals[:width]        = is ignored
          vals[:resizeStop] = 'javascript: gridify_fluid_recalc_width'
      end

      vals[:sortable] = true if arranger_type.include?(:sortable)

      # header layer
      vals[:caption] = title if title
      vals[:hidegrid] = false unless collapsible
      vals[:hiddengrid] = true if collapsed

      # row formatting
      vals[:altrows] = true if alt_rows
      vals[:altclass] = alt_rows if alt_rows.is_a?(String)

      vals[:rownumbers] = true if row_numbers
      vals[:rownumWidth] = row_numbers if row_numbers.is_a?(Numeric)

      if inline_edit
        vals[:scrollrows] = true
        vals[:multiselect] = true if multi_select
        vals[:onSelectRow] = "javascript: function(id, status) { if(id && id!==lastsel_#{dom_id}) { jQuery('##{dom_id}').jqGrid('restoreRow', lastsel_#{dom_id}); jQuery('##{dom_id}').jqGrid('editRow', id, true, #{inline_edit_handler}, #{error_handler}); lastsel_#{dom_id}=id}}"
      elsif select_rows #.present?
        vals[:scrollrows] = true
        vals[:onSelectRow] = select_rows
        vals[:multiselect] = true if multi_select
      else
        vals[:hoverrows] = false
        vals[:beforeSelectRow] = "javascript: function(){ false; }"
      end

      # pager layer
      if pager
        vals[:pager] = "##{pager}"
        vals[:viewrecords] = true # display total records in the query (eg "1 - 10 of 25")
        vals[:rowList] = paging_choices
        if paging_controls.is_a?(Hash)
          # allow override of jqGrid pager options
          vals.merge!(paging_controls)
        elsif !paging_controls
          vals[:rowList] = []
          vals[:pgbuttons] = false
          vals[:pginput] = false
          vals[:recordtext] = "{2} records"
        end
      end

      if tree_grid
        vals[:treeGrid] = tree_grid
        vals[:gridview] = true
        vals[:sortable] = false
      end

      #subgrid
      if sub_grid
        vals[:subGrid] = sub_grid
        vals[:subGridUrl] = sub_grid_url
        vals[:subGridModel] = sub_grid_model
        vals[:gridview] = true
      end

      #events
      #vals[:serializeGridData] = serialize_grid_data if serialize_grid_data

      vals[:loadonce] = load_once if load_once

      # allow override of native jqGrid options
      vals.merge(jqgrid_options)

    end

    # -----------------
    def jqgrid_javascript(options={})
      s = ''

      if table_to_grid
        s << %Q^
        tableToGrid("##{dom_id}", #{to_json});
        ^
        s << %Q^
        grid_#{dom_id} = jQuery("##{dom_id}")
        ^
      else
        s << %Q^
        grid_#{dom_id} = jQuery("##{dom_id}").jqGrid(#{to_json})
        ^
      end

      s << ';'

      # tag the grid as fluid so we can find it on resize events
      if width_fit == :fluid
        s << %Q^
        jQuery("##{dom_id}").addClass("fluid");
        ^
      end

      # override tableToGrid colmodel options as needed (sortable)
      #s << %Q^ .jqGrid('setColProp','Title',{sortable: false})^

      # resize method
      if resizable
        s << %Q^
        jQuery("##{dom_id}").jqGrid('gridResize', #{resizable.to_json});
        ^
      end

      # pager buttons (navGrid)
      if pager
        nav_params = {
            'edit' => edit_button.present?,
            'add' => add_button.present?,
            'del' => delete_button.present?,
            'search' => search_button.present? || search_multiple.present?,
            'view' => view_button.present?,
            'refresh' => refresh_button.present?
        }.merge(jqgrid_nav_options||{})

        s << %Q^
        jQuery("##{dom_id}").jqGrid('navGrid', '##{pager}',
               #{nav_params.to_json},
               #{edit_button_options.to_json_with_js},
               #{add_button_options.to_json_with_js},
               #{delete_button_options.to_json_with_js},
               #{search_button_options.to_json_with_js},
               #{view_button_options.to_json_with_js}
               );
        ^
      end

      if arranger_type.include?(:hide_show)
        s << %Q^
        jQuery("##{dom_id}").jqGrid('navButtonAdd','##{pager}',{
               caption: "Columns",
               title: "Hide/Show Columns",
               onClickButton : function (){ jQuery("##{dom_id}").jqGrid('setColumns',
                 #{arranger_options(:hide_show).to_json_with_js} );
               }
        });
        ^
      end
      if arranger_type.include?(:chooser)
        # hackey way to build the string but gets it done
        chooser_code = %Q^ function (){ jQuery('##{dom_id}').jqGrid('columnChooser', {
                          done : function (perm) {
                            if (perm)  {
                              this.jqGrid('remapColumns', perm, true);
                              var gwdth = this.jqGrid('getGridParam','width');
                              this.jqGrid('setGridWidth',gwdth);
                            }
                          } })}^
        chooser_opts = {
            'caption' => 'Columns',
            'title' => 'Arrange Columns',
            'onClickButton' => 'chooser_code'
        }.merge(arranger_options(:chooser))
        s << %Q^
        jQuery("##{dom_id}").jqGrid('navButtonAdd','##{pager}', #{chooser_opts.to_json.gsub('"chooser_code"', chooser_code)} );
        ^
      end

      if search_toolbar
        # I wish we could put this in the header rather than the pager
        s << %Q^
        jQuery("##{dom_id}").jqGrid('navButtonAdd',"##{pager}", { caption:"Toggle", title:"Toggle Search Toolbar", buttonicon: 'ui-icon-pin-s', onClickButton: function(){ grid_#{dom_id}[0].toggleToolbar() } });
        jQuery("##{dom_id}").jqGrid('navButtonAdd',"##{pager}", { caption:"Clear", title:"Clear Search", buttonicon: 'ui-icon-refresh', onClickButton: function(){ grid_#{dom_id}[0].clearToolbar() } });
        jQuery("##{dom_id}").jqGrid('filterToolbar');
        ^
      end

      if sortable_rows
        # I wish we could put this in the header rather than the pager
        s << %Q^
        jQuery("##{dom_id}").jqGrid('sortableRows');
        ^
      end

      # TODO: built in event handlers, eg
      # loadError
      # onSelectRow, onDblClickRow, onRightClickRow etc

      #unless search_toolbar == :visible
      #  s << %Q^
      #  grid_#{dom_id}[0].toggleToolbar();
      #  ^
      #end

      # # keep page controls centered (jqgrid bug) [eg appears when :width_fit => :scroll]
      # s << %Q^ $("##{pager}_left").css("width", "auto"); ^

      s
    end

    # ------------------
    def js_helpers
      # just move this into appliaction.js?

      # gridify_fluid_recalc_width: allow grid resize on window resize events
      # recalculate width of grid based on parent container; handles multiple grids
      # ref: http://www.trirand.com/blog/?page_id=393/feature-request/Resizable%20grid/

      # afterSubmit: display error message in response

      %Q^
      function gridify_fluid_recalc_width(){
        if (grids = jQuery('.fluid.ui-jqgrid-btable:visible')) {
          grids.each(function(index) {
            gridId = jQuery(this).attr('id');
            gridParentWidth = jQuery('#gbox_' + gridId).parent().width();
            jQuery('#' + gridId).jqGrid('setGridWidth', gridParentWidth);
          });
        }
      };

      jQuery(window).live('resize', gridify_fluid_recalc_width);
      function gridify_action_error_handler(r, data, action){
        if (r.responseText != '') {
          return [false, r.responseText];
        } else  {
          return true;
        }
      }
      ^
    end

    # if(r.responseText != "") {
    #   $('#{error_container}').html(r.responseText);
    #   $('#{error_container}').slideDown();
    #   //window.setTimeout(function() { // Hide error div after 6 seconds
    #   // $('#{error_container}').slideUp();
    #   //}, 6000);
    #   return false;
    # }
    # return true;

    # lets options be true or a hash, merges into defaults and returns a hash
    def merge_options_defaults(options, defaults={})
      if options
        defaults.merge(options==true ? {} : options)
      else
        {}
      end
    end

  end
end

class Hash
  # replace embedded '"javascript: foo"' with 'foo'
  def to_json_with_js
    self.to_json.gsub(/\"javascript: ([^"]*)\"/) { |string| string[1..-2].gsub('javascript:', '') }
  end
end

module JqgridJson
  include ActionView::Helpers::JavaScriptHelper
  include HandleAttributes

  def to_subgrid_json(attributes)
    json = %Q({)
    if self.empty?
      json << "}"
    else
      json <<= %Q("rows":[)
      each do |elem|
        elem.id ||= index(elem)
        json << %Q({"id":"#{elem.id}","cell":[)
        couples = elem.attributes.symbolize_keys
        attributes.each do |atr|
          value = get_atr_value(elem, atr, couples)
          value = escape_javascript(value) if value and value.is_a? String
          json << %Q("#{value}",)
        end
        json.chop! << "]},"
      end
      json.chop! << "]}"
    end
  end
end

class Array
  include JqgridJson
end


require 'jquery/gridify/assertions'
require 'jquery/gridify/grid_options'
require 'jquery/gridify/grid_view'
require 'jquery/gridify/grid_column'
require 'jquery/gridify/grid_finder'

# NOTE: for readablity/clarity, native jqGrid options should be string; Ruby ones as syms

module Gridify
  class Grid
    include Assertions

    # todo: change this so klass is optional, decouple from active record
    def initialize(klass, *args, &block)
      #debugger
      options = args.extract_options! # => args.last.is_a?(Hash) ? args.pop : {}
      assert_exclusive_keys(options, :only, :except)

      @resource = klass.to_s.tableize.pluralize
      @name = args.first || :grid

      # non-persistent options
      # generate model unless explicitly say no
      build_model = options.delete(:build_model) == false ? false : true
      only = options.delete(:only)
      except = options.delete(:except)
      col_presets = options.delete(:colModel)
      col_include = options[:colInclude]
      row_actions = options[:rowActions]

      # assign options
      update options

      # build columns from ActiveRecord model (klass)
      if klass.present? && build_model
        @model = build_columns(klass, only, except, col_presets, col_include, row_actions)
      end

      instance_eval(&block) if block
      #(note instance_eval cannot access things outside its scope; otherwise do this:
      #yield self if block_given?
    end

    def update(options)
      options.each { |atr, val| send("#{atr}=", val) }
      # exception "invalid option..."
    end

    def column(name, options={})
      name = name.to_s
      klass = resource.classify.constantize
      # TODO: set edit options based on ar_column
      # TODO: edit
      # TODO: handle file input types
      # TODO: custom input types
      if col = columns_hash[name]
        # update an existing column
        col.update options

      elsif ar = klass.columns.detect { |c| c.name==name }
        #debugger
        # create column from database schema
        edit = editable &&
            # only edit accessible attributes
        (klass.accessible_attributes.nil? || klass.accessible_attributes.include?(ar.name))
        args = {
            :ar_column => ar,
            :name => ar.name,
            :value_type => ar.type,
            :searchable => searchable,
            :sortable => sortable,
            :editable => edit
        }.merge(options)
        colModel << GridColumn.new(args)

      else
        # create column from scratch
        args = {
            :name => name,
            :value_type => :string,
            :searchable => searchable,
            :sortable => sortable,
            :editable => edit
        }.merge(options)
        colModel << GridColumn.new(args)
      end
    end

    def column_names
      colModel.collect { |col| col.name.titleize }
    end

    def column_model
      colModel.collect { |col| col.properties }
    end

    # normally we need to keep columns an ordered array, sometimes its convenient to have a hash
    def columns_hash
      colModel.inject({}) { |h, col| h[col.name] = col; h }
    end

    protected

    # # isnt there something in rails to do this already?
    # def parse_options( keys, options )
    #   ops = keys.inject({}) do |h, k|
    #     val = options.delete(k)
    #     h[k] = val unless val.nil?
    #     h
    #   end
    #   ops || {}
    # end

    # generate list of columns based on AR model
    # option:  :only or :except
    #          :col_options  hash of hash of preset values for columns (eg from cookie) { :title => {:width => 98}}
    def build_columns(klass, only, except, presets, include, actions)
      #debugger
      # stringify
      only = Array(only).map { |s| s.to_s }
      except = Array(except).map { |s| s.to_s }
      presets ||= []

      if presets.length > 0
        hashed_defs = {}

        klass.columns.collect do |ar|
          #debugger
          next if only.present? && !only.include?(ar.name)
          next if except.present? && except.include?(ar.name)
          hashed_defs[ar.name] = strct2args(klass, ar)
        end.compact

        if include
          include.each do |sub_model|
            my_model = sub_model.to_s
            if klass.inheritable_attributes[:reflections][sub_model].options[:class_name]
              my_class = klass.inheritable_attributes[:reflections][sub_model].options[:class_name].to_s
              model = klass.inheritable_attributes[:reflections][sub_model].options[:class_name].to_s
            else
              my_class = my_model
            end
            Object.const_get(my_class.capitalize).columns.collect do |ar|
              #debugger
              next if only.present? && !only.include?("#{my_model}.#{ar.name}")
              next if except.present? && except.include?("#{my_model}.#{ar.name}")
              hashed_defs["#{my_model}.#{ar.name}"] = strct2args(klass, ar)
            end
          end
        end

        if actions
          hashed_defs["row_actions"] = actions_args()
        end

        # Take sequence from colModel
        self.colModel = []
        presets.each do |col|
          # create column with default args merged with options given for this column
          self.colModel << GridColumn.new(hashed_defs[col[:name]].merge(col))
        end
      else
        # Take sequence from database definition
        self.colModel = klass.columns.collect do |ar|
          #debugger
          next if only.present? && !only.include?(ar.name)
          next if except.present? && except.include?(ar.name)
          args = strct2args(klass, ar)
          # create column with default args merged with options given for this column
          GridColumn.new(args)
        end.compact

        if include
          include.each do |sub_model|
            my_model = sub_model.to_s
            if klass.inheritable_attributes[:reflections][sub_model].options[:class_name]
              my_class = klass.inheritable_attributes[:reflections][sub_model].options[:class_name].to_s
              model = klass.inheritable_attributes[:reflections][sub_model].options[:class_name].to_s
            else
              my_class = my_model
            end
            Object.const_get(my_class.capitalize).columns.collect do |ar|
              #debugger
              next if only.present? && !only.include?("#{my_model}.#{ar.name}")
              next if except.present? && except.include?("#{my_model}.#{ar.name}")
              args = strct2args(klass, ar, "#{my_model}.")
              # create column with default args merged with options given for this column
              self.colModel << GridColumn.new(args)
            end
          end
        end
      end
    end

    private

    def strct2args(my_class, record, prefix="")
      is_key = (record.name=='id')
      edit = editable && !is_key &&
          # only edit accessible attributes
      (my_class.accessible_attributes.nil? || my_class.accessible_attributes.include?(record.name))
      args = {
          :ar_column => record,
          :name => "#{prefix}#{record.name}",
          :value_type => record.type,
          :key => is_key,
          #:hidden => is_key,
          :searchable => searchable,
          :sortable => sortable,
          :editable => edit
      }
    end

    def actions_args
      args = {
          #:ar_column => record,
          :name => "row_actions",
          #:value_type => record.type,
          #:hidden => is_key,
          :searchable => false,
          :sortable => false,
          :editable => false
      }
    end
  end
end


module ActionView
  module Helpers

    def jqgrid_javascripts(locale)
      js = capture { javascript_include_tag "i18n/jqgrid/locale-#{locale}.min" }
      js << capture { javascript_tag "jQuery.jgrid.no_legacy_api = true;" }
      js << capture { javascript_include_tag 'jquery.jqGrid.min' }
    end

    def jqgrid(title, id, action, columns = {},
        options = {}, edit_options = "", add_options = "", del_options = "", search_options = "",
        custom1_button = nil, custom2_button = nil, custom3_button = nil, custom4_button = nil, custom5_button = nil)

      # Default options
      options =
          {
              :rows_per_page => '10',
              :sort_column => '',
              :sort_order => '',
              :height => '150',
              :gridview => 'false',
              :error_handler => 'null',
              :inline_edit_handler => 'null',
              :add => 'false',
              :delete => 'false',
              :search => 'true',
              :edit => 'false',
              :inline_edit => 'false',
              :autowidth => 'false',
              :hidegrid => 'false',
              :rownumbers => 'false'
          }.merge(options)

      # Stringify options values
      options.inject({}) do |options, (key, value)|
        options[key] = (key != :subgrid) ? value.to_s : value
        options
      end

      options[:error_handler_return_value] = (options[:error_handler] == 'null') ? 'true;' : options[:error_handler]
      edit_button = (options[:edit] == 'true' && options[:inline_edit] == 'false').to_s

      # Generate columns data
      col_names, col_model = gen_columns(columns)

      # Enable multi-selection (checkboxes)
      multiselect = "multiselect: false,"
      if options[:multi_selection]
        multiselect = "multiselect: true,"
        multihandler = %Q/
          jQuery("##{id}_select_button").click( function() {
            var s; s = jQuery("##{id}").jqGrid('getGridParam', 'selarrrow');
            #{options[:selection_handler]}(s);
            return false;
          });/
      end

      # Enable master-details
      masterdetails = ""
      if options[:master_details]
        masterdetails = %Q/
          onSelectRow: function(ids) {
            if(ids == null) {
              ids=0;
              if(jQuery("##{id}_details").jqGrid('getGridParam', 'records') >0 )
              {
                jQuery("##{id}_details").jqGrid('setGridParam', {url:"#{options[:details_url]}?q=1&id="+ids,page:1})
                jQuery("##{id}_details").jqGrid('setCaption', "#{options[:details_caption]}: "+ids)
                jQuery("##{id}_details").trigger('reloadGrid');
              }
            }
            else
            {
              jQuery("##{id}_details").jqGrid('setGridParam', {url:"#{options[:details_url]}?q=1&id="+ids,page:1})
              jQuery("##{id}_details").jqGrid('setCaption', "#{options[:details_caption]} : "+ids)
              jQuery("##{id}_details").trigger('reloadGrid');
            }
          },/
      end

      # Enable selection link, button
      # The javascript function created by the user (options[:selection_handler]) will be called with the selected row id as a parameter
      selection_link = ""
      if options[:direct_selection].blank? && options[:selection_handler].present? && options[:multi_selection].blank?
        selection_link = %Q/
        jQuery("##{id}_select_button").click( function(){
          var id = jQuery("##{id}").jqGrid('getGridParam', 'selrow');
          if (id) {
            #{options[:selection_handler]}(id);
          } else {
            alert("Please select a row");
          }
          return false;
        });/
      end

      # Enable direct selection (when a row in the table is clicked)
      # The javascript function created by the user (options[:selection_handler]) will be called with the selected row id as a parameter
      direct_link = ""
      if options[:direct_selection] && options[:selection_handler].present? && options[:multi_selection].blank?
        direct_link = %Q/
        onSelectRow: function(id){
          if(id){
            #{options[:selection_handler]}(id);
          }
        },/
      end

      # Enable grid_loaded callback
      # When data are loaded into the grid, call the Javascript function options[:grid_loaded] (defined by the user)
      grid_loaded = ""
      if options[:grid_loaded].present?
        grid_loaded = %Q/
        loadComplete: function(){
          #{options[:grid_loaded]}();
        },
        /
      end

      # Enable inline editing
      # When a row is selected, all fields are transformed to input types
      editable = ""
      if options[:edit] && options[:inline_edit] == 'true'
        editable = %Q/
        onSelectRow: function(id){
          if(id && id!==lastsel){
            jQuery('##{id}').jqGrid('restoreRow', lastsel);
            jQuery('##{id}').jqGrid('editRow', id, true, #{options[:inline_edit_handler]}, #{options[:error_handler]});
            lastsel=id;
          }
        },/
      end

      # Enable subgrids
      subgrid = ""
      subgrid_enabled = "subGrid:false,"

      if options[:subgrid].present?

        subgrid_enabled = "subGrid:true,"

        options[:subgrid] =
            {
                :rows_per_page => '10',
                :sort_column => 'id',
                :sort_order => 'asc',
                :add => 'false',
                :edit => 'false',
                :delete => 'false',
                :search => 'false'
            }.merge(options[:subgrid])

        # Stringify options values
        options[:subgrid].inject({}) do |suboptions, (key, value)|
          suboptions[key] = value.to_s
          suboptions
        end

        subgrid_inline_edit = ""
        if options[:subgrid][:inline_edit] == true
          options[:subgrid][:edit] = 'false'
          subgrid_inline_edit = %Q/
          onSelectRow: function(id){
            if(id && id!==lastsel){
              jQuery('#'+subgrid_table_id).jqGrid('restoreRow', lastsel);
              jQuery('#'+subgrid_table_id).jqGrid('editRow', id,true);
              lastsel=id;
            }
          },
          /
        end

        if options[:subgrid][:direct_selection] && options[:subgrid][:selection_handler].present?
          subgrid_direct_link = %Q/
          onSelectRow: function(id){
            if(id){
              #{options[:subgrid][:selection_handler]}(id);
            }
          },
          /
        end

        sub_col_names, sub_col_model = gen_columns(options[:subgrid][:columns])

        subgrid = %Q(
        subGridRowExpanded: function(subgrid_id, row_id) {
            var subgrid_table_id, pager_id;
            subgrid_table_id = subgrid_id+"_t";
            pager_id = "p_"+subgrid_table_id;
            $("#"+subgrid_id).html("

");
            jQuery("#"+subgrid_table_id).jqGrid({
              url:"#{options[:subgrid][:url]}?q=2&id="+row_id,
              editurl:'#{options[:subgrid][:edit_url]}?parent_id='+row_id,
              datatype: "json",
              colNames: #{sub_col_names},
              colModel: #{sub_col_model},
              rowNum:#{options[:subgrid][:rows_per_page]},
              pager: pager_id,
              sortname: '#{options[:subgrid][:sort_column]}',
              sortorder: '#{options[:subgrid][:sort_order]}',
              viewrecords: true,
              //toolbar : [true,"top"],
              #{subgrid_inline_edit}
        #{subgrid_direct_link}
              height: '100%'
            });
            jQuery("#"+subgrid_table_id).jqGrid('navGrid', "#"+pager_id,{edit:#{options[:subgrid][:edit]},add:#{options[:subgrid][:add]},del:#{options[:subgrid][:delete]},search:false})
            jQuery("#"+subgrid_table_id).jqGrid('navButtonAdd', "#"+pager_id,{caption:"Search",title:"Toggle Search",buttonimg:'/images/jqgrid/search.png',
              onClickButton:function(){
                if(jQuery("#t_"+subgrid_table_id).css("display")=="none") {
                  jQuery("#t_"+subgrid_table_id).css("display","");
                } else {
                  jQuery("#t_"+subgrid_table_id).css("display","none");
                }
              }
            });
            jQuery("#t_"+subgrid_table_id).height(25).hide().jqGrid('filterGrid', ""+subgrid_table_id,{gridModel:true,gridToolbar:true});
          },
          subGridRowColapsed: function(subgrid_id, row_id) {
          },
        )
      end

      # Generate required Javascript & html to create the jqgrid
      %Q(

      )
    end

    private

    def gen_columns(columns)
      # Generate columns data
      col_names = "[" # Labels
      col_model = "[" # Options
      columns.each do |c|
        col_names << "'#{c[:label]}',"
        col_model << "{name:'#{c[:field]}', index:'#{c[:field]}'#{get_attributes(c)}},"
      end
      col_names.chop! << "]"
      col_model.chop! << "]"
      [col_names, col_model]
    end

    # Generate a list of attributes for related column (align:'right', sortable:true, resizable:false, ...)
    def get_attributes(column)
      options = ","
      column.except(:field, :label).each do |couple|
        if couple[0] == :editoptions
          options << "editoptions:#{get_sub_options(couple[1])},"
        elsif couple[0] == :formoptions
          options << "formoptions:#{get_sub_options(couple[1])},"
        elsif couple[0] == :searchoptions
          options << "searchoptions:#{get_sub_options(couple[1])},"
        elsif couple[0] == :editrules
          options << "editrules:#{get_sub_options(couple[1])},"
        else
          if couple[1].class == String
            options << "#{couple[0]}:'#{couple[1]}',"
          else
            options << "#{couple[0]}:#{couple[1]},"
          end
        end
      end
      options.chop!
    end

    # Generate options for editable fields (value, data, width, maxvalue, cols, rows, ...)
    def get_sub_options(editoptions)
      options = "{"
      editoptions.each do |couple|
        if couple[0] == :value # :value => [[1, "Rails"], [2, "Ruby"], [3, "jQuery"]]
          options << %Q/value:"/
          couple[1].each do |v|
            options << "#{v[0]}:#{v[1]};"
          end
          options.chop! << %Q/",/
        elsif couple[0] == :data # :data => [Category.all, :id, :title])
          options << %Q/value:"/
          couple[1].first.each do |obj|
            options << "%s:%s;" % [obj.send(couple[1].second), obj.send(couple[1].third)]
          end
          options.chop! << %Q/",/
        else # :size => 30, :rows => 5, :maxlength => 20, ...
          if couple[1].instance_of?(Fixnum) || couple[1] == 'true' || couple[1] == 'false' || couple[1] == true || couple[1] == false
            options << %Q/#{couple[0]}:#{couple[1]},/
          else
            options << %Q/#{couple[0]}:"#{couple[1]}",/
          end
        end
      end
      options.chop! << "}"
    end
  end
end

require 'handle_attributes'

module JqgridJson
  include ActionView::Helpers::JavaScriptHelper
  include HandleAttributes

  def to_jqgrid_json(attributes, current_page, per_page, total)
    json = %Q({"page":"#{current_page}","total":#{total/per_page.to_i+1},"records":"#{total}")
    if total > 0
      json << %Q(,"rows":[)
      each do |elem|
        elem.id ||= index(elem)
        json << %Q({"id":"#{elem.id}","cell":[)
        couples = elem.attributes.symbolize_keys
        attributes.each do |atr|
          value = get_atr_value(elem, atr, couples)
          value = escape_javascript(value) if value and value.is_a? String
          json << %Q("#{value}",)
        end
        json.chop! << "]},"
      end
      json.chop! << "]}"
    else
      json << "}"
    end
  end
end

class Array
  include JqgridJson
end


module Gridify
  class Grid

    attr_accessor :search_rules, # current search/filter rules, as hash
                  :search_rules_op # :and, :or

    # finds records based on request params
    # e.g. params from jqGrid
    #   :_search      do search (true/false)  ["false"]
    #   :sidx         sort index (column to search on)  [""]
    #   :sord         sort direction (desc/asc)  ["asc"]
    #   :nd           ?
    #   :rows         number of items to get   ["20"]
    #   :page         page number (starts at 1) ["1"]

    def update_from_params(params)
      params.symbolize_keys!
      params_to_rules params
      self.data_type = params[:datatype] if params[:datatype]
      self.sort_by = params[:sidx] if params[:sidx]
      self.sort_order = params[:sord] if params[:sord]
      self.current_page = params[:page].to_i if params[:page]
      self.rows_per_page = params[:rows].to_i if params[:rows]
      self.total_rows = params[:total_rows].to_i if params[:total_rows]
      if tree_grid
        self.nodeid = params[:nodeid].to_i if params[:nodeid]
        self.n_level = params[:n_level].to_i if params[:n_level]
        self.n_left = params[:n_left].to_i if params[:n_left]
        self.n_right = params[:n_right].to_i if params[:n_right]
      end
    end

    # return find args (scope) for current settings
    def current_scope
      #debugger
      find_args = {}
      find_args[:include] = colInclude if colInclude
      if sort_by.present? && col = columns_hash[sort_by]
        if (sort_by.include? ".")
          # Workaround for :include and nested attributes
          field = sort_by.split('.', 2)
          if field.length == 2
            self.sort_by = field[0].pluralize + "." + field[1]
          end
        end
        if case_sensitive || !([:string, :text].include?(col.value_type))
          find_args[:order] = "#{sort_by} #{sort_order}"
        else
          find_args[:order] = "upper(#{sort_by}) #{sort_order}"
        end
      end

      if total_rows.present? && total_rows > 0
        find_args[:limit] = total_rows
        offset = (current_page.to_i-1) * rows_per_page if current_page.present?
        find_args[:offset] = offset if offset && offset > 0
      elsif rows_per_page.present? && rows_per_page > 0
        find_args[:limit] = rows_per_page
        offset = (current_page.to_i-1) * rows_per_page if current_page.present?
        find_args[:offset] = offset if offset && offset > 0
      end

      cond = rules_to_conditions
      find_args[:conditions] = cond unless cond.blank?
      find_args
    end

    def find(params)
      #debugger
      update_from_params params
      klass = resource.classify.constantize
      records = klass.send(finder, :all, current_scope)
    end

    def encode_records(records, userdata=nil, total_count=nil)
      #debugger
      klass = resource.classify.constantize
      total_count ||= klass.count
      total_pages = total_count / rows_per_page + 1
      #TODO: :only => [attributes], :methods => [virtual attributes]
      case data_type
        when :xml
          if colInclude
            xml = records.to_xml(:include => colInclude, :skip_types => true, :dasherize => false) do |xml|
              if rows_per_page > 0
                xml.page current_page
                xml.total total_pages
                xml.records total_count
              end
            end
          else
            xml = records.to_xml(:skip_types => true, :dasherize => false) do |xml|
              if rows_per_page > 0
                xml.page current_page
                xml.total total_pages
                xml.records total_count
              end
            end
          end
        when :json
          #debugger
          data = {resource => records}
          if rows_per_page > 0
            data.merge!(
                :page => current_page,
                :total => total_pages,
                :records => total_count
            )
          end
          data.merge!(:userdata => userdata) if userdata
          save = ActiveRecord::Base.include_root_in_json
          ActiveRecord::Base.include_root_in_json = false
          if colInclude
            json = build_json(data);
          else
            json = data.to_json
          end
          ActiveRecord::Base.include_root_in_json = save
          json
        #others...
        else #nop ?
          records.to_s
      end
    end

    def find_and_encode(params)
      encode_records(find(params))
    end

    # grid doesnt nest attributes inside the resource
    # could change this behavior in jqGrid, see grid.postext.js ?
    #   http://www.trirand.com/jqgridwiki/doku.php?id=wiki:post_data_module
    #
    def member_params(params)
      params.inject({}) { |h, (name, value)| h[name] = value if columns_hash[name]; h }
    end

    protected

    OPS = {'eq' => '=', 'lt' => '<', 'le' => '<=', 'gt' => '>', 'ge' => '>=', 'ne' => '!='}
    #['eq','ne','lt','le','gt','ge',
    # 'bw','bn','in','ni','ew','en','cn','nc']
    #['equal','not equal', 'less', 'less or equal','greater','greater or equal',
    # 'begins with','does not begin with','is in','is not in','ends with','does not end with','contains','does not contain']

    OPS_PATTERN = {
        'bw' => '?%',
        'bn' => '?%',
        # 'in'
        # 'ni'
        'ew' => '%?',
        'en' => '%?',
        'cn' => '%?%',
        'nc' => '%?%',
    }

    STRING_OPS = {
        'bw' => 'LIKE',
        'bn' => 'NOT LIKE',
        'ew' => 'LIKE',
        'en' => 'NOT LIKE',
        'cn' => 'LIKE',
        'nc' => 'NOT LIKE',
    }

    # params[:filters] => {"groupOp"=>"AND",
    #              "rules"=>[{"data"=>"b", "op"=>"ge", "field"=>"title"}, {"data"=>"f", "op"=>"le", "field"=>"title"}] }
    def params_to_rules(params)
      #debugger
      if params[:_search]=='true' || params[:_search]==true
        if params[:filters]
          # advanced search
          filters = ActiveSupport::JSON.decode(params[:filters])
          self.search_rules = filters['rules']
          self.search_rules_op = filters['groupOp']
        elsif params[:searchField]
          # simple search
          self.search_rules = [{"field" => params[:searchField], "op" => params[:searchOper], "data" => params[:searchString]}]
        else
          # toolbar search
          self.search_rules = []
          self.search_rules_op = :and
          colModel.each do |col|
            name = col.name
            data = params[name.to_sym]
            self.search_rules << {"field" => name, "op" => "cn", "data" => data} if data
          end
        end
      end
      search_rules
    end

    def rules_to_conditions
      # note: ignoring case_sensitive as an option, ActiveRecord find is insensitive by default (have to model the db to let it be case sensitive?)
      return nil if search_rules.blank?
      cond = nil
      expr = ''
      vals = []
      search_rules.each do |rule|

        # Workaround for :include and nested attributes
        field = rule['field'].split('.', 2)
        if field.length == 2
          rule['field'] = field[0].pluralize + "." + field[1]
        end

        expr << " #{search_rules_op} " unless expr.blank?
        if op = OPS[rule['op']]
          expr << "#{rule['field']} #{op} ?"
          vals << rule['data']
        elsif op = STRING_OPS[rule['op']]
          expr << "#{rule['field']} #{op} ?"
          vals << OPS_PATTERN[rule['op']].gsub('?', rule['data'])
        end
      end
      cond = [expr] + vals
    end

    def build_json(data)
      raw_data = JSON.parse(data.to_json(:include => colInclude))
      raw_data[resource].each do |row|
        colInclude.each do |incl|
          sm = incl.to_s
          if row[sm]
            row[sm].each do |k, v|
              row[sm + "__" + k] = v
            end
          end
        end
      end
      return raw_data.to_json()
    end
  end
end

# # If you need to display error messages
# err = ""
# if user
#   user.errors.entries.each do |error|
#     err << "#{error[0]} : #{error[1]}
"
#   end
# end
#
# render :text => "#{err}"

# { :add => true, :edit => true, :inline_edit => false, :delete => true, :edit_url => "/users/post_data", :error_handler => "after_submit" }


module Gridify
  class GridColumn

    attr_accessor :name, # attribute name (required)
                  :label,
                  :ar_column, # active_record column
                  :value_type, # active_record data type :string, :text, :integer, :float, :boolean, + :currency
                  :key, # true if this is the key (only :id)

                  # column formatting
                  :width, # initial width (150)
                  :fixed_width, # true = not resized when recalc fit to width
                  :formatter, # see jqGrid doc (based on value_type)
                  :format_options, # hash, see jqGrid doc (based on value_type)
                  :align, # 'left', 'right', 'center' (left for strings, right for numerics)
                  #:classes,                # optional classes to add to column

                  # column actions
                  :resizable, # t/f (true)
                  :sortable, # t/f (true) or jqGrid sorttype: :integer, :float, :currency, :date, :text (true)
                  :searchable, # true/false (true) or text or select
                  #:search_options,
                  :editable, # true/false (false),
                  :edit_type, # text, textarea, select, checkbox, password, button, image and file (based on value_type)
                  :edit_options, # html input field options, depends on editable type (eg {"size" => 10, "maxlength" => 15}) see http://www.trirand.com/jqgridwiki/doku.php?id=wiki:common_rules
                  :validations, # jqGrid editrules (TODO derive from AR model validations) see http://www.trirand.com/jqgridwiki/doku.php?id=wiki:common_rules
                  # "required" "integer" "email" "url" etc
                  :form_options, # jqGrid formoptions for formatting input forms
                  # select types
                  #:select_url,             # url to dynamically get select options

                  # visiblity
                  #:always_hidden,          # (false)
                  :sort_type,
                  :search_type,
                  :search_rules,
                  :summary_type,
                  :summary_tpl,
                  :hidden # initial hide state (false)

    def initialize(options)
      update options
    end

    def update(options)
      options.each { |atr, val| send("#{atr}=", val) }
    end

    def to_json
      properties.to_json #_with_js
    end

    def properties
      jqgrid_properties
    end

    def resizable
      if @resizable==false
        false
      else
        # true or nil
        true
      end
    end

    def fixed_width
      if @fixed_width==false
        false
      elsif @fixed_width.nil?
        !resizable
      else
        @fixed_width
      end
    end

    # ----------------
    private

    def jqgrid_type
      return sortable unless sortable==true
      case value_type
        when :string
          'text'
        when :text
          'text'
        when :integer
          'integer'
        when :float
          'float'
        when :boolean
          'boolean'
        when :datetime
          'date'
      end
    end

    # note, we dont vals[:foo] = foo because dont want to bother generating key if its same as jqGrid default
    def jqgrid_properties
      vals = {
          :name => name.gsub(".", "__"),
          :index => name
      }
      #xmlmap not required when same as :name
      # vals[:xmlmap]     = name          if data_type == :xml
      # vals[:jsonmap]    = name          if data_type == :json

      vals[:label] = label || name.titleize
      vals[:resizable] = false if resizable==false
      vals[:fixed] = fixed_width unless fixed_width==false
      vals[:sortable] = false if sortable==false
      vals[:sort_type] = jqgrid_type if sortable
      vals[:search] = false if searchable==false
      vals[:editable] = true if editable
      vals[:align] = align if align
      #vals[:align]      = 'right'       if [:integer, :float, :currency].include?(value_type)
      case value_type
        when :datetime
          vals[:formatter] = 'date'
          vals[:formatoptions] = {:srcformat => 'UniversalSortableDateTime', :newformat => 'FullDateTime'}
      end
      vals[:hidden] = true if hidden
      vals[:width] = width if width
      vals[:formatter] = formatter if formatter
      vals[:formatoptions] = format_options if format_options
      vals[:edittype] = edit_type if editable && edit_type
      vals[:formoptions] = form_options if editable && form_options
      vals[:editoptions] = edit_options if editable && edit_options
      vals[:editrules] = validations if editable && validations

      vals[:sort_type] = sort_type if sort_type
      vals[:search_type] = search_type if search_type
      vals[:search_rules] = search_rules if search_rules
      vals[:summaryType] = summary_type if summary_type
      vals[:summaryTpl] = summary_tpl if summary_tpl
      # and more...

      vals
    end

  end
end

#<ActiveRecord::ConnectionAdapters::SQLiteColumn:0x2515a98 @sql_type="varchar(255)", @name="title", @precision=nil, @primary=false, @default=nil, @limit=255, @null=true, @type=:string, @scale=nil>


module ActionController
  class Base

    # From http://paydrotalks.com/posts/45-standard-json-response-for-rails-and-jquery

    def render_json_response(type, hash)
      unless [:ok, :redirect, :error].include?(type)
        raise "Invalid json response type: #{type}"
      end

      # To keep the structure consistent, we'll build the json
      # structure with the default properties.
      #
      # This will also help other developers understand what
      # is returned by the server by looking at this method.
      default_json_structure = {
          :status => type,
          :html => nil,
          :message => nil,
          :to => nil}.merge(hash)

      render_options = {:json => default_json_structure}
      render_options[:status] = 400 if type == :error

      render(render_options)
    end
  end
end


require 'forwardable'
require 'fileutils'

module Amber
  class Site
    extend Forwardable

    attr_accessor :page_list
    attr_accessor :root
    attr_accessor :continue_on_error

    # @config is the primary SiteConfiguration
    def_delegators :@config, :pages_dir, :dest_dir, :locales, :default_locale, :path_prefix

    def initialize(root_dir)
      @continue_on_error = true
      @config = SiteConfiguration.load(self, root_dir)
    end

    def add_config(config)
      @config.children << config
    end

    def load_pages
      @root          = nil
      @pages_by_path = {}  # hash of pages keyed by page path
      @pages_by_name = {}  # hash of pages keyed by page name
      @page_list     = PageArray.new
      @dir_list      = []  # an array of non-page directories
      @page_paths    = []  # an array of page paths, used for greping through paths.

      # some paths are specific to only one locale (e.g aliases)
      @pages_by_locale_path = POSSIBLE_LANGUAGES.keys.inject(Hash.new({})) {|h,locale| h[locale] = {}; h}

      add_configuration(@config)
    end

    #def reload_pages_if_needed
    #  if @pages_by_path.nil? || @config.pages_changed?
    #    puts "Reloading pages ................."
    #    load_pages
    #  end
    #end

    def render
      @page_list.each do |page|
        page.render_to_file(@config.dest_dir)
        putc '.'; $stdout.flush
      end
      @dir_list.each do |directory|
        src = File.join(@config.pages_dir, directory)
        dst = File.join(@config.dest_dir, directory)
        Render::Asset.render_dir(src, dst)
        putc '.'; $stdout.flush
      end
      if @config.short_paths
        render_short_path_symlinks
      end
      Render::Apache.write_htaccess(@config, @config.pages_dir, @config.dest_dir)
      puts
    end

    def clear
      Dir.glob("#{@config.dest_dir}/*").each do |file|
        FileUtils.rm_r(file)
      end
    end

    def with_destination(new_dest)
      dest_dir = @config.dest_dir
      @config.dest_dir = new_dest
      yield
      @config.dest_dir = dest_dir
    end

    #
    # find pages by a filter.
    # filter is a string composing a path segment.
    # For example:
    #   "chat/security"
    # Which would match "/services/chat/security" but not "/services/security"
    #
    def find_pages(filter)
      filter = filter.downcase
      if filter =~ /\//
        path = filter.split('/').map{|segment| segment.gsub(/[^0-9a-z_-]/, '')}
        path_str = path.join('/')
        if (page = @pages_by_path[path_str])
          page
        elsif matched_path = @page_paths.grep(/#{Regexp.escape(path_str)}/).first
          @pages_by_path[matched_path]
        elsif page = @pages_by_name[path.last]
          page
        else
          nil
        end
      elsif @pages_by_path[filter]
        @pages_by_path[filter]
      else
        @pages_by_name[filter]
      end
    end

    def find_page(filter)
      find_pages(filter)
    end

    def all_pages
      @page_list
    end

    def find_page_by_path(path, locale=I18n.default_locale)
      if locale.is_a? String
        if I18n.locale_available?(locale)
          locale = locale.to_sym
        end
      end
      if path.is_a? Array
        path = path.join('/')
      end
      @pages_by_locale_path[locale][path] || @pages_by_path[path]
    end

    def find_page_by_name(name)
      @pages_by_name[name]
    end

    #
    # returns the shortest possible path for a page, such that the path doesn't collide with
    # another page or another page's aliases.
    #
    def shortest_path(page)
      path_so_far = []
      page.path.reverse.each do |path_segment|
        path_so_far.push(path_segment)
        path_str = path_so_far.join('/')
        if @pages_by_path[path_str].nil? && short_paths[path_str] == page
          return path_so_far
        end
      end
      return []
    end

    private

    def add_configuration(config)
      config.reset_timestamp

      # create base_page
      base_page = begin
        if config.path.nil?
          @root = StaticPage.new(nil, 'root', config.pages_dir)
          add_page(@root)
          @root
        else
          name = File.basename(config.path)
          sub_root = StaticPage.new(find_parent(config.path), name, config.pages_dir, config.path_prefix)
          add_page(sub_root)
          sub_root
        end
      end
      base_page.config = config

      # load menu and locals
      I18n.load_path += Dir[File.join(config.locales_dir, '/**/*.{rb,yml,yaml}')] if config.locales_dir

      # add the full directory tree
      base_page.scan_directory_tree do |page, asset_dir|
        add_page(page) if page
        @dir_list << asset_dir if asset_dir
      end
      @page_paths += @pages_by_path.keys

      # recursively add sub-sites
      config.children.each do |sub_config|
        add_configuration(sub_config)
      end
    end

    #
    # registers a page with the site, indexing the page path in our various hashes
    #
    def add_page(page)
      @pages_by_name[page.name] ||= page
      @pages_by_path[page.path.join('/')] = page
      add_aliases(I18n.default_locale, page, @pages_by_path)
      page.locales.each do |locale|
        next if locale == I18n.default_locale
        add_aliases(locale, page, @pages_by_locale_path[locale])
      end
      @page_list << page
    end

    #
    # registers a page's aliases with the site
    #
    def add_aliases(locale, page, path_hash)
      page.aliases(locale).each do |alias_path|
        alias_path_str = alias_path.join('/')
        if path_hash[alias_path_str]
          Amber.logger.warn "WARNING: page `#{page.path.join('/')}` has alias `#{alias_path_str}`, but this path is already taken by `#{path_hash[alias_path_str].path.join('/')}` (locale = #{locale})."
        else
          path_hash[alias_path_str] = page
        end
      end
    end

    def find_parent(path)
      so_far = []
      path.split('/').compact.each do |path_segment|
        so_far << path_segment
        if page = @pages_by_path[so_far.join('/')]
          return page
        end
      end
      return @root
    end

    #
    # adds symlinks for all possible 'short paths' for every page.
    # this works by:
    # (1) examine all pages in order of longest path depth and assign 'short paths' for each page.
    # (2) examine all short paths in order of shortest path depth and create symlinks
    #
    def render_short_path_symlinks
      short_paths.each do |path, page|
        page.link_page_aliases(@config.dest_dir, [path])
      end
    end

    #
    # returns a hash containing all the automatically determined shortest paths for every page.
    # the data structure looks like so:
    #
    # {
    #   "ddd" => <page 'bbb/ddd'>,
    #   "ccc" => <page 'bbb/ccc'>,
    #   "red" => <page 'autoalias/red'>,
    #   "blue"=> <page 'autoalias/blue'>,
    #   "red/blue" => <page 'autoalias/red/blue'>
    # }
    #
    # short_paths does not include the normal paths or normal aliases, just the automatic short path aliases.
    #
    def short_paths
      @short_paths ||= begin
        hash = {}
        pages_in_path_depth_order.each do |record|
          page = record[:page]
          path = record[:path]
          next if path.length == 1
          path_prefix = path.dup
          path.length.times do |depth|
            path_prefix.shift
            path_str = path_prefix.join('/')
            if @pages_by_path[path_str].nil? && hash[path_str].nil?
              hash[path_str] = page
            end
          end
        end
        # debug:
        #hash.each do |path, record|
        #  puts "#{record[:page].path.join('/')} => #{record[:path].join('/')}"
        #end
        hash
      end
    end

    #
    # Returns an array like this:
    #
    #   [
    #     {:page => <page1>, :path => ['a', 'page1']},
    #     {:page => <page2>, :path => ['a','b', 'page2']},
    #   ]
    #
    # This array is sorted by the depth of the path (shortest first)
    # Pages will appear multiple times (once for each path, including aliases)
    #
    def pages_in_path_depth_order
      paths = {}
      @page_list.each do |page|
        paths[page.path] ||= page
        locales = page.locales
        locales << I18n.default_locale unless locales.include? I18n.default_locale
        locales.each do |locale|
          page.aliases(locale).each do |alias_path|
            paths[alias_path] ||= page
          end
        end
      end
      paths.collect{|path, page| {page:page, path:path}}.sort{|a,b|
        a[:path].length <=> a[:path].length
      }
    end

  end
end


#
# Array of StaticPages
#

require 'bigdecimal'

module Amber
  class PageArray < Array

    def limit(num)
      PageArray.new(self[0..(num-1)])
    end

    #
    # available options:
    #
    # :locale    -- the locale to use when comparing attributes
    # :direction -- either :asc or :desc
    # :numeric   -- if true, attributes are cast as numbers before comparison
    #
    def order_by(attr, options={})
      locale = options[:locale] || I18n.locale
      direction = options[:direction] || :asc
      array = sort do |a,b|
        if direction == :desc
          a, b = b, a
        end
        a_prop = a.prop(locale, attr)
        b_prop = b.prop(locale, attr)
        if options[:numeric]
          a_prop = to_numeric(a_prop)
          b_prop = to_numeric(b_prop)
        end
        if a_prop.nil? && b_prop.nil?
          0
        elsif a_prop.nil?
          1
        elsif b_prop.nil?
          -1
        else
          a_prop <=> b_prop
        end
      end
      # remove pages from the results that have no value set for the attr
      array.delete_if do |page|
        page.prop(locale, attr).nil?
      end
      return PageArray.new.replace array
    end

    def to_numeric(anything)
      num = BigDecimal.new(anything.to_s)
      if num.frac == 0
        num.to_i
      else
        num.to_f
      end
    end

  end
end


#
# class StaticPage
#
# represents a static website page.
# see also static_page/*.rb
#

module Amber
  class StaticPage

    attr_accessor :path,    # array of path segments
      :children,            # array of child pages
      :name,                # the name of the page
      :file_path,           #
      :parent,              # parent page (nil for root page)
      :config,              # associated SiteConfiguration (site might have several)
      :site,                # associated Site (only one)
      :valid                # `false` if there is some problem with this page.

    attr_reader :props      # set of page properties (PropertySet)

    alias_method :valid?, :valid

    ##
    ## INSTANCE METHODS
    ##

    FORBIDDEN_PAGE_CHARS_RE = /[A-Z\.\?\|\[\]\{\}\$\^\*~!@#%&='"<>]/

    def initialize(parent, name, file_path=nil, path_prefix="/")
      @valid     = true
      @children  = PageArray.new  # array of StaticPages
      @nav_title = {} # key is locale
      @title     = {} # key is locale

      @name, @suffix = parse_source_file_name(name)

      # set @parent & @path
      if parent
        @parent = parent
        @config = @parent.config
        @parent.add_child(self)
        @path = [@parent.path, @name].flatten.compact
      else
        @path = (path_prefix||"").split('/')
      end

      if @name =~ FORBIDDEN_PAGE_CHARS_RE
        Amber.logger.error "Illegal page name #{@name} at path /#{self.path.join('/')} -- must not have symbols, uppercase, or periods."
        @valid = false
      end

      # set the @file_path
      if file_path
        @file_path = file_path
      elsif @parent && @parent.file_path
        @file_path = File.join(@parent.file_path, @name)
      else
        raise 'file path must be specified or in parent'
      end

      @simple_page = !File.directory?(@file_path)

      # eval the property headers, if any
      @props = load_properties()
    end

    def add_child(page)
      @children << page
    end

    def all_children
      PageArray.new(child_tree.flatten.compact)
    end

    def inspect
      "<'#{@path.join('/')}' #{children.inspect}>"
    end

    def title(locale=I18n.locale)
      @title[locale] ||= begin
        @props.prop_with_fallback(locale, [:title, :nav_title]) || @name
      end
    end

    def nav_title(locale=I18n.locale)
      @nav_title[locale] ||= begin
        @props.prop_with_fallback(locale, [:nav_title, :title]) || @name
      end
    end

    #
    # returns title iff explicitly set.
    #
    def explicit_title(locale)
      @props.prop_without_inheritance(locale, :title) ||
      @props.prop_without_inheritance(I18n.default_locale, :title)
    end

    def id
      self.name
    end

    #
    # returns a child matching +name+, if any.
    #
    def child(name)
      children.detect {|child| child.name == name}
    end

    def prop(*args)
      @props.prop(*args)
    end

    def vars
      @vars ||= load_variables
    end

    def var(name, locale=I18n.locale)
      (vars[locale] || vars[I18n.default_locale] || {})[name.to_s]
    end

    #
    # Returns array of locale symbols for all locales with properties set
    # Note: there might be a content for a locale that does not show up in this array,
    # if the content file does not set any properties.
    #
    def locales
      @props.locales
    end

    def path_str
      self.path.join('/')
    end

    #
    # returns an array of normalized aliases based on the :alias property
    # defined for a page.
    #
    # aliases are defined with a leading slash for absolute paths, or without a slash
    # for relative paths. this method converts this to a format that amber uses
    # (all absolute, with no leading slash, as an array instead of a string).
    #
    def aliases(locale=I18n.default_locale)
      @aliases ||= begin
        aliases_hash = Hash.new([])
        @props.locales.each do |l|
          aliases = @props.prop_without_inheritance(l, :alias)
          aliases_hash[l] = begin
            if aliases.nil?
              []
            else
              [aliases].flatten.collect {|alias_path|
                if alias_path =~ /^\//
                  alias_path.sub(/^\//, '').split('/')
                elsif @parent
                  @parent.path + [alias_path]
                else
                  alias_path.split('/')
                end
              }
            end
          end
        end
        aliases_hash
      end
      @aliases[locale]
    end

    protected

    def child_tree
      [self, children.collect{|child| child.child_tree}]
    end

  end
end


#
# StaticPage code that interfaces with the filesystem
#

require 'i18n'
require 'pathname'
require 'fileutils'
require 'json'
require 'yaml'

module Amber
  class StaticPage

    public

    #
    # Recursively decends the directory tree, yielding pages and directories it encounters.
    #
    # yield has two arguments:
    #
    # (1) StaticPage instance, or nil.
    # (2) Directory path string if #1 is nil. Path is relative.
    #
    # Directory paths are dirs in the tree that don't contain any pages.
    #
    def self.scan_directory_tree(parent_page, absolute_dir_path, relative_dir_path, &block)
      Dir.foreach(absolute_dir_path).each do |child_path|
        next if child_path =~ /^\./
        abs_path = File.join(absolute_dir_path, child_path)
        rel_path = File.join(relative_dir_path, child_path)
        if parent_page && is_directory_page?(abs_path)
          child_page = StaticPage.new(parent_page, child_path)
          if child_page.valid?
            yield child_page, nil
            scan_directory_tree(child_page, abs_path, rel_path, &block)
          end
        elsif parent_page && is_simple_page?(abs_path)
          child_page = StaticPage.new(parent_page, child_path)
          if child_page.valid?
            yield child_page, nil
          end
        elsif File.directory?(abs_path)
          yield nil, rel_path
          scan_directory_tree(nil, abs_path, rel_path, &block)
        end
      end
    end

    def scan_directory_tree(&block)
      StaticPage.scan_directory_tree(self, self.file_path, File.join(self.path), &block)
    end

    #
    # e.g. /home/user/dev/leap-public-site/app/views/pages/about-us/contact
    #
    #def file_path
    #  "#{@mount_point.directory}/#{@path.join('/')}"
    #end

    #
    # e.g. pages/about-us/contact/en
    # RAILS
    #def template_path(locale=I18n.locale)
    #  absolute_template_path(locale)
    #  #if @simple_page
    #  #  "#{@mount_point.relative_directory}/#{@path.join('/')}"
    #  #else
    #  #  "#{@mount_point.relative_directory}/#{@path.join('/')}/#{locale}"
    #  #end
    #end

    #
    # e.g. pages/about-us/contact/en
    # RAILS
    #def absolute_template_path(locale=I18n.locale)
    #  if @simple_page
    #    "#{@mount_point.pages_dir}/#{@path.join('/')}"
    #  else
    #    "#{@mount_point.pages_dir}/#{@path.join('/')}/#{locale}"
    #  end
    #end

    #
    # full filesystem path name of the source content file
    # e.g. /home/user/mysite/pages/about-us/contact/en.md
    #
    def content_file(locale)
      content_files[locale] || content_files[I18n.default_locale] || content_files.values.first
    end

    def content_file_exists?(locale)
      !!content_files[locale]
    end

    #
    # full filesystem path name of the destination rendered file
    # e.g. /home/user/mysite/public/about-us/contact/index.en.html
    #
    def destination_file(dest_dir, locale)
      File.join(dest_dir, *@path, "index.#{locale}.html")
    end

    # e.g. en, de, pt
    LOCALES_RE  = /(?<locale>#{Amber::POSSIBLE_LANGUAGE_CODES.join('|')})/
    LOCALES_GLOB = "{#{Amber::POSSIBLE_LANGUAGE_CODES.join(',')}}"

    # e.g. haml, md, text
    PAGE_SUFFIXES_RE = /(?<suffix>#{Amber::PAGE_SUFFIXES.join('|')})/
    PAGE_SUFFIXES_GLOB = "{#{Amber::PAGE_SUFFIXES.join(',')}}"

    # e.g. json, yaml
    VAR_SUFFIXES_RE = /(?<suffix>#{Amber::VAR_SUFFIXES.join('|')})/
    VAR_SUFFIXES_GLOB = "{#{Amber::VAR_SUFFIXES.join(',')}}"

    # e.g. en.haml or es.md or index.pt.text
    LOCALE_FILE_MATCH_RE = /^(index\.)?#{LOCALES_RE}\.#{PAGE_SUFFIXES_RE}$/
    LOCALE_FILE_MATCH_GLOB = "{index.,}#{LOCALES_GLOB}.#{PAGE_SUFFIXES_GLOB}"

    VAR_FILE_MATCH_RE = /^(index\.)?#{LOCALES_RE}\.#{VAR_SUFFIXES_RE}$/
    VAR_FILE_MATCH_GLOB = "{index.,}#{LOCALES_GLOB}.#{VAR_SUFFIXES_GLOB}"

    SIMPLE_FILE_MATCH_RE = lambda {|name| /^(#{Regexp.escape(name)})(\.#{LOCALES_RE})?\.#{PAGE_SUFFIXES_RE}$/ }
    SIMPLE_VAR_MATCH_RE = lambda {|name| /^(#{Regexp.escape(name)})(\.#{LOCALES_RE})?\.#{VAR_SUFFIXES_RE}$/ }

    private

    #
    # returns true if the name of a file could be a 'simple' static page
    # that is not a directory.
    #
    # rules:
    # * we include files that end in appriopriate suffixes
    # * we exclude file names that are locales.
    # * we exclude partials
    #
    def self.is_simple_page?(absolute_path)
      name = File.basename(absolute_path)
      name =~ /\.#{PAGE_SUFFIXES_RE}$/ && name !~ LOCALE_FILE_MATCH_RE && name !~ /^_/
    end

    def self.is_directory_page?(absolute_path)
      if File.directory?(absolute_path)
        Dir.glob(absolute_path + '/' + LOCALE_FILE_MATCH_GLOB).each do |file|
          return true
        end
      end
      return false
    end

    #
    # returns [name, suffix]
    # called on new page initialization
    #
    def parse_source_file_name(name)
      matches = name.match(/^(?<name>.*?)(\.#{LOCALES_RE})?(\.#{PAGE_SUFFIXES_RE})$/)
      if matches
        [matches['name'], matches['suffix']]
      else
        [name, nil]
      end
    end

    #
    # returns the files that compose the content for this page,
    # a different file for each locale (or no locale)
    #
    # returns a hash like so:
    #
    #  {
    #     :en => '/path/to/page/en.haml',
    #     :es => '/path/to/page/index.es.md'
    #  }
    #
    # Or this, if page is simple:
    #
    # {
    #   :en => '/path/to/page.haml'
    # }
    #
    #
    def content_files
      @content_files ||= begin
        if @simple_page
          directory = File.dirname(@file_path)
          regexp = SIMPLE_FILE_MATCH_RE.call(@name)
        else
          directory = @file_path
          regexp = LOCALE_FILE_MATCH_RE
        end
        hsh = {}
        Dir.foreach(directory) do |file|
          if file && match = regexp.match(file)
            locale = match['locale'] || I18n.default_locale
            hsh[locale.to_sym] = File.join(directory, file)
          end
        end
        hsh
      end
    end

    #
    # returns an array of files in the folder that corresponds to this page
    # that are not other pages. in other words, the assets in this folder
    #
    # file paths are relative to @file_path
    #
    def asset_files
      if @simple_page
        []
      else
        Dir.foreach(@file_path).collect { |file|
          is_asset = \
            file &&
            file !~ /\.#{PAGE_SUFFIXES_RE}$/ &&
            file !~ /^#{VAR_FILE_MATCH_RE}$/ &&
            !File.directory?(File.join(@file_path, file))
          file if is_asset
        }.compact
      end
    end

    #def self.relative_to_rails_view_root(absolute_path)
    #  if Rails.root
    #    absolute = Pathname.new(absolute_path)
    #    rails_view_root = Pathname.new(Rails.root + 'app/views')
    #    absolute.relative_path_from(rails_view_root).to_s
    #  end
    #end

    #
    # scans the source content files for property headers in the form:
    #
    #    @variable = 'x'
    #    - @variable = 'x'
    #
    # (with or without leading hypen works)
    #
    # This text is extracted and evaluated as ruby to set properties.
    #
    # The first paragraph is loaded into the property "excerpt".
    #
    def load_properties
      props = PageProperties.new(self)
      content_files.each do |locale, content_file|
        if type_from_path(content_file) == :haml
          props.eval(File.read(content_file, :encoding => 'UTF-8'), locale)
        else
          headers, excerpt = parse_headers(content_file)
          props.eval(headers, locale)
          if !excerpt.empty?
            props.set_prop(locale, "excerpt", excerpt)
          end
          props.set_prop(locale, "content_type", type_from_path(content_file))
        end
        cleanup_properties(props, locale)
      end
      unless props.prop_without_inheritance(I18n.default_locale, :name)
        props.set_prop(I18n.default_locale, :name, self.name)
      end
      return props
    end

    def cleanup_properties(props, locale)
      if props.prop(locale, :alias)
        props.set_prop(locale, :alias, [props.prop(locale, :alias)].flatten)
      end
    end

    #
    # parses a content_file's property headers and tries to extract the
    # first paragraph.
    #
    def parse_headers(content_file)
      headers = []
      para1 = []
      para2 = []
      file_type = type_from_path(content_file)

      File.open(content_file, :encoding => 'UTF-8') do |f|
        while (line = f.gets) =~ /^(- |)@\w/
          if line !~ /^-/
            line = '- ' + line
          end
          headers << line
        end
        # eat empty lines
        while line = f.gets
          break unless line =~ /^\s*$/
        end
        # grab first two paragraphs
        para1 << line
        while line = f.gets
          break if line =~ /^\s*$/
          para1 << line
        end
        while line = f.gets
          break if line =~ /^\s*$/
          para2 << line
        end
      end

      headers = headers.join
      para1 = para1.join
      para2 = para2.join
      excerpt = ""

      # pick the first non-heading paragraph.
      # this is stupid, and chokes on nested headings.
      # but is also cheap and fast :)
      if file_type == :textile
        if para1 =~ /^h[1-5]\. /
          excerpt = para2
        else
          excerpt = para1
        end
      elsif file_type == :markdown
        if para1 =~ /^#+ / || para1 =~ /^(===+|---+)\s*$/m
          excerpt = para2
        else
          excerpt = para1
        end
      end
      return [headers, excerpt]
    end

    ##
    ## VARIABLES
    ## Variables are associated with a page, but unlike properties they are not
    ## inheritable. Variables are defined in a separate file.
    ##
    def variable_files
      if @simple_page
        directory = File.dirname(@file_path)
        regexp = SIMPLE_VAR_MATCH_RE.call(@name)
      else
        directory = @file_path
        regexp = VAR_FILE_MATCH_RE
      end
      hsh = {}
      Dir.foreach(directory) do |file|
        if file && match = regexp.match(file)
          locale = match['locale'] || I18n.default_locale
          hsh[locale.to_sym] = File.join(directory, file)
        end
      end
      hsh
    end

    def load_variables
      vars = {}
      variable_files.each do |locale, var_file|
        begin
          if var_file =~ /\.ya?ml$/
            vars[locale] = YAML.load_file(var_file)
          elsif var_file =~ /\.json$/
            vars[locale] = JSON.parse(File.read(var_file))
          end
        rescue StandardError => exc
          Amber.logger.error('ERROR: could not load file #{var_file}: ' + exc.to_s)
        end
      end
      return vars
    end

    def type_from_path(path)
      case File.extname(path)
      when ".text", ".textile"
        :textile
      when ".md", ".markdown"
        :markdown
      when ".haml"
        :haml
      when ".html"
        :html
      when ".erb"
        :erb
      else
        :unknown
      end
    end

    # RAILS
    #def is_haml_template?(locale)
    #  content_file(locale) =~ /\.haml$/
    #  #@suffix == '.haml' || File.exist?(self.absolute_template_path(locale) + '.haml')
    #end

  end
end


#
# A navigation menu class
#

module Amber
  class Menu
    attr_accessor :parent
    attr_accessor :children
    attr_accessor :name

    #
    # load the menu.txt file and build the in-memory menu array
    #
    def load(menu_file_path)
      File.open(menu_file_path) do |file|
        parse_menu(file)
      end
    end

    def initialize(name, parent=nil)
      self.name = name
      self.parent = parent
      self.children = []
    end

    ##
    ## public methods
    ##

    #
    # returns the menu under the item that matches item_name.
    #
    def submenu(item_name=nil)
      if item_name
        self.children.detect {|child| child.name == item_name}
      else
        self.children
      end
    end

    #
    # returns path from root to this leaf as an array
    #
    def path
      @path ||= begin
        if parent == nil
          []
        else
          parent.path + [name]
        end
      end
    end

    def path_str
      @path_str ||= path.join('/')
    end

    def each(&block)
      children.each(&block)
    end

    def size
      children.size
    end

    #
    # returns true if menu's path starts with +path_prefix+
    #
    def path_starts_with?(path_prefix)
      array_starts_with?(path, path_prefix)
    end

    def path_prefix_of?(full_path)
      array_starts_with?(full_path, path)
    end

    #
    # returns true if this menu item is the terminus menu item for path.
    # (meaning that there are no children that match more path segments)
    #
    def leaf_for_path?(path)
      return false unless path_prefix_of?(path)
      next_path_segment = (path - self.path).first
      return false if next_path_segment.nil?
      return !children.detect {|i| i.name == next_path_segment}
    end

    def inspect(indent=0)
      lines = []
      lines << '  '*indent + '- ' + self.name
      self.children.each do |child|
        lines << child.inspect(indent+1)
      end
      lines.join("\n")
    end

    #
    # private & protected methods
    #

    protected

    def add_child(name)
      self.children << Menu.new(name, self)
    end

    private

    def array_starts_with?(big_array, small_array)
      small_array.length.times do |i|
        if small_array[i] != big_array[i]
          return false
        end
      end
      return true
    end

    def parse_menu(file)
      while true
        item = file.readline
        if item.strip.chars.any? && item !~ /^\s*#/
          depth = item.scan("  ").size
          last_menu_at_depth(depth).add_child(item.strip)
        end
      end
    rescue EOFError
      # done loading
    end

    #
    # returns the last list of children at the specified depth
    #
    def last_menu_at_depth(depth)
      menu = self
      depth.times { menu = menu.children.last }
      menu
    end

  end
end


# encoding: utf-8

require 'nokogiri'
require 'cgi'

#
# Generates a table of contents for any HTML markup, and adds anchors to headings.
#

module Amber::Render

  ##
  ## TABLE OF CONTENTS
  ##

  class TableOfContents
    #
    # options:
    #   :content_selector (css selector for headings, nokogiri backend only)
    #   :href_base -- use this href for the toc links
    #   :numeric_prefix  -- prefix toc entries and headings with numeric counter (e.g. 1.1.0, 1.2.0, ...)
    #
    def initialize(html, options = {})
      @html = html
      @toc = TocItem.new
      @levels = {"h1" => 0, "h2" => 0, "h3" => 0, "h4" => 0}
      @heading_anchors = {}
      @options = options
      @options[:tag] ||= 'ol'
      @parsed = nil
    end

    def to_html
      parse_doc unless @parsed
      # override this!
    end

    def to_toc
      parse_doc unless @parsed
      # override this!
    end

    private

    def parse_doc
      each_heading(@html) do |heading, heading_text|
        heading_anchor = anchor_text(heading_text)
        heading_text   = strip_anchors(heading_text)
        if @options[:numeric_prefix]
          increment_level(heading)
          heading_text = level_text + " " + heading_text
        end
        @toc.add_heading(heading, heading_text, heading_anchor)
        '%s' % [heading_anchor, heading_text]
      end
      @parsed = true
    end

    #
    # returns anchor text from heading text.
    # e.g. First Heading! => first-heading
    #
    # if there are duplicates, they get numbered:
    #   heading => heading
    #   heading => heading-2
    #   heading => heading-3
    #
    def anchor_text(heading_text)
      text = nameize(strip_html_tags(heading_text))
      text_with_suffix = text
      i = 2
      while @heading_anchors[text_with_suffix]
        text_with_suffix = "#{text}-#{i}"
        i+=1
      end
      @heading_anchors[text_with_suffix] = true
      text_with_suffix
    end

    #
    # convert any string to one suitable for a url.
    # resist the urge to translit non-ascii slugs to ascii.
    # it is always much better to keep strings as utf8.
    #
    def nameize(str)
      str = str.dup
      str.gsub!(/&(\w{2,6}?|#[0-9A-Fa-f]{2,6});/,'') # remove html entitities
      str.gsub!(/[^- [[:word:]]]/u, '') # remove non-word characters (using unicode definition of a word char)
      str.strip!
      str.downcase!          # upper case characters in urls are confusing
      str.gsub!(/\ +/u, '-') # spaces to dashes, preferred separator char everywhere
      CGI.escape(str)
    end

    # removes all html markup
    def strip_html_tags(html)
      Nokogiri::HTML::DocumentFragment.parse(html, 'UTF-8').children.collect{|child| child.inner_text}.join
    end

    # remove  from html, but leaves all other tags in place.
    def strip_anchors(html)
      Nokogiri::HTML::DocumentFragment.parse(html, 'UTF-8').children.collect{|child|
        if child.name == "text"
          child.inner_text
        elsif child.name != 'a' || !child.attributes.detect{|atr| atr[0] == 'name'}
          child.to_s
        end
      }.join
    end

    #
    # prefix headings with text like 1.2.1, if :numeric_prefix => true
    #
    def level_text
      [@levels["h1"], @levels["h2"], @levels["h3"], @levels["h4"]].join(".").gsub(/\.0/, "")
    end

    #
    # keeps a counter of the latest heading at each level
    #
    def increment_level(heading)
      @levels[heading] += 1
      @levels["h2"] = 0 if heading == "h1"
      @levels["h3"] = 0 if heading == "h1" || heading == "h2"
      @levels["h4"] = 0 if heading == "h1" || heading == "h2" || heading == "h3"
    end

    def each_heading(html, &block)
      raise 'override me'
    end
  end

  ##
  ## NOKOGIRI TOC
  ##

  class NokogiriTableOfContents < TableOfContents
    def to_html
      super
      @nokogiri_doc.to_html.gsub(/(<h\d.*?>)\n/, '\1').gsub(/\n(<\/h\d.*?>)/, '\1')
    end

    def to_toc
      super
      ul = Nokogiri::XML::Node.new(@options[:tag], Nokogiri::HTML.fragment(""))
      @toc.populate_node(ul, @options)
      ul.to_pretty_html
    end

    private

    def each_heading(html, &block)
      @nokogiri_doc = Nokogiri::HTML.fragment(html, "UTF-8")
      if @options[:content_selector]
        selector = @levels.keys.map {|h| "#{@options[:content_selector]} #{h}" }.join(",")
      else
        selector = @levels.keys.join(",")
      end
      @nokogiri_doc.css(selector).each do |node|
        node.inner_html = yield(node.name, node.inner_html)
      end
    end
  end

  ##
  ## REGEX TOC
  ##

  class RegexTableOfContents < TableOfContents
    def to_html
      super
      @new_html
    end

    def to_toc
      super
      @toc.to_html(@options)
    end

    private

    HEADING_EX = %r{
      <\s*((h\d).*?)\s*>   # match starting

        (.+)?              # match innner text
      <\s*\/\2\s*>         # match closing

    }x

    def each_heading(html, &block)
      @new_html = html.gsub(HEADING_EX) do |match|
        "<%s>%s</%s>" % [$1, yield($2, $3), $2]
      end
    end
  end

  ##
  ## TOC ITEM
  ##
  ## A tree of TocItems composes the table of contents outline.
  ##

  class TocItem
    attr_reader :children, :level, :text, :anchor

    def initialize(heading='h0', text=nil, anchor=nil)
      @level    = heading[1].to_i if heading.is_a?(String)
      @text     = text
      @anchor   = anchor
      @children = []
    end

    def add_heading(heading, heading_text, heading_anchor)
      self.parent_for(heading).children << TocItem.new(heading, heading_text, heading_anchor)
    end

    #
    # generates nokogiri html node tree from this toc
    #
    def populate_node(node, options)
      @children.each do |item|
        li = node.document.create_element("li")
        li.add_child(li.document.create_element("a", item.text, :href => "#{options[:href_base]}##{item.anchor}"))
        if item.children.any?
          ul = li.document.create_element(options[:tag])
          item.populate_node(ul, options)
          li.add_child(ul)
        end
        node.add_child(li)
      end
    end

    #
    # generates html string from this toc
    #
    def to_html(options={})
      html   = []
      tag    = options[:tag]
      indent = options[:indent] || 0
      str    = options[:indent_str] || "  "
      html << '%s<%s>' % [(str*indent), tag]
      @children.each do |item|
        html << '%s
' % (str*(indent+1))
        html << '%s%s' % [str*(indent+2), options[:href_base], item.anchor, item.text]
        if item.children.any?
          html << item.to_html({
            :indent => indent+2,
            :indent_str => str,
            :tag => tag,
            :href_base => options[:href_base]
          })
        end
        html << '%s
' % (str*(indent+1))
      end
      html << '%s</%s>' % [(str*indent), tag]
      html.join("\n")
    end

    #
    # Returns the appropriate TocItem for appending a new item
    # at a particular heading level.
    #
    def parent_for(heading)
      heading = heading[1].to_i if heading.is_a?(String)
      if children.any? && children.last.level < heading
        children.last.parent_for(heading)
      else
        self
      end
    end
  end

end

class Nokogiri::XML::Node
  def to_pretty_html(indent=0)
    indent_str = "  " * indent
    children_html = []
    text_html = nil
    if children.size == 1 && children.first.name == "text"
      text_html = children.first.content
    else
      children.each do |child|
        if child.name == "text"
          children_html << "#{"  " * (indent+1)}#{child.content}" if !child.content.empty?
        else
          children_html << child.to_pretty_html(indent+1)
        end
      end
    end
    attrs = []
    attributes.each do |attribute|
      attrs << %(#{attribute[0]}="#{attribute[1]}")
    end
    if attrs.any?
      attr_html = " " + attrs.join(' ')
    else
      attr_html = ""
    end
    html = []
    if text_html
      html << "#{indent_str}<#{name}#{attr_html}>#{text_html}</#{name}>"
    elsif children_html.any?
      html << "#{indent_str}<#{name}#{attr_html}>"
      html += children_html
      html << "#{indent_str}</#{name}>"
    else
      html << "#{indent_str}<#{name}#{attr_html}></#{name}>"
    end
    html.join("\n")
  end
end

=begin

AN ATTEMPT TO GET NOKOGIRI TO OUTPUT REASONABLE HTML5. NO LUCK.

    #
    # convert a Nokogiri::HTML::Document into well formatted html.
    # unfortunately, Nokogiri formatting only works on complete documents, so we strip away the  tags. :(
    #
    def format_doc(doc)
      INDENT_XSLT.apply_to(doc).to_s.sub("<!DOCTYPE html>\n", '').sub('', '')
    end

    # from https://github.com/jarijokinen/html5-beautifier/blob/master/lib/html5-beautifier/xslt/html5-beautifier.xslt
    # MIT License
    INDENT_XSLT_STRING = <<EOF
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="html" omit-xml-declaration="yes" encoding="utf-8" />
  <xsl:param name="indent-increment" select="'__INDENT_STRING__'" />

  <xsl:template name="newline">
    <xsl:text disable-output-escaping="yes">
</xsl:text>
  </xsl:template>

  <xsl:template match="/">
    <xsl:text disable-output-escaping="yes"><!DOCTYPE html></xsl:text>
    <xsl:apply-templates />
  </xsl:template>

  <xsl:template match="comment() | processing-instruction()">
    <xsl:param name="indent" select="''" />
    <xsl:call-template name="newline" />
    <xsl:value-of select="$indent" />
    <xsl:copy />
  </xsl:template>

  <xsl:template match="text()">
    <xsl:param name="indent" select="''" />
    <xsl:call-template name="newline" />
    <xsl:value-of select="$indent" />
    <xsl:value-of select="normalize-space(.)" />
  </xsl:template>

  <xsl:template match="text()[normalize-space(.)='']" />

  <xsl:template match="*">
    <xsl:param name="indent" select="''" />
    <xsl:call-template name="newline" />
    <xsl:value-of select="$indent" />
    <xsl:choose>
      <xsl:when test="count(child::*) > 0 and __EXCLUDE_ELEMENTS__">
        <xsl:copy>
          <xsl:copy-of select="@*" />
          <xsl:apply-templates select="*|text()">
            <xsl:with-param name="indent" select="concat($indent, $indent-increment)" />
          </xsl:apply-templates>
          <xsl:call-template name="newline" />
          <xsl:value-of select="$indent" />
        </xsl:copy>
      </xsl:when>
      <xsl:otherwise>
        <xsl:copy-of select="." />
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>
</xsl:stylesheet>
EOF

    INDENT_XSLT = Nokogiri::XSLT(INDENT_XSLT_STRING.gsub('__INDENT_STRING__', '  '))

=end