require 'listings/configuration_methods'
require 'listings/view_helper_methods'
require 'csv'

module Listings
  class Base
    include Listings::ConfigurationMethods
    include Listings::ViewHelperMethods
    include ERB::Util # html_escape methods are private in the view_context
                      # so they need to be included in order to be available

    attr_accessor :view_context
    attr_accessor :params

    attr_accessor :search_criteria
    attr_accessor :search_filters

    attr_reader :data_source
    delegate :items, to: :data_source

    def initialize
      @page_size = self.class.page_size
    end

    def name
      self.class.name.underscore.sub(/_listing$/,'')
    end

    def param_scope; :scope; end
    def param_page; :page; end
    def param_search; :s; end
    def param_sort_by; :sort_by; end
    def param_sort_direction; :sort_d; end

    def parse_search
      if !filterable?
        # if it is not filterable, all the search is used as search criteria
        self.search_criteria = self.search
        self.search_filters = {}
      else
        # otherwise parse the search stripping out allowed filterable fields
        self.search_filters, self.search_criteria = parse_filter(self.search, self.filters.map(&:key))
      end
    end

    def parse_filter(text, filter_keys)
      filters = {}
      filter_keys.each do |key|
        text = collect_filter text, key, filters
      end

      return filters, text
    end

    def collect_filter(text, filter, filters_hash)
      ["#{filter}:\s*(\\w+)",
       "#{filter}:\s*\"([^\"]+)\"",
       "#{filter}:\s*\'([^\']+)\'"].each do |pattern|
        m = Regexp.new(pattern, Regexp::IGNORECASE).match(text)
        if m
          filters_hash[filter] = m[1]
          return "#{m.pre_match.strip} #{m.post_match.strip}".strip
        end
      end

      return text
    end

    def filter_items(params)
      columns # prepare columns
      filters # prepare filters

      self.page = params[param_page] || 1
      self.scope = scope_by_name(params[param_scope])
      self.search = params[param_search]
      parse_search

      unless scope.nil?
        data_source.scope do |items|
          scope.apply(self, items)
        end
      end

      if search_criteria.present? && self.searchable?
        search_fields = self.columns.select(&:searchable?).map &:field
        data_source.search(search_fields, search_criteria)
      end

      if filterable?
        filters.each do |filter_view|
          filter_view.values # prepare values
        end

        self.search_filters.each do |key, filter_value|
          data_source.filter(filter_with_key(key).field, filter_value)
        end
      end

      if params.include?(param_sort_by)
        sort_col = column_with_key(params[param_sort_by])
        sort_col.sort = params[param_sort_direction]
        data_source.sort(sort_col.field, params[param_sort_direction])
      end

      if paginated?
        data_source.paginate(page, page_size)
      end

      self.items
    end

    def query_items(params)
      @params = params
      @data_source = Sources::DataSource.for(self.model_class)
      @has_active_model_source = items.respond_to? :human_attribute_name

      filter_items(self.scoped_params)
    end

    def has_active_model_source?
      @has_active_model_source
    end

    def selectable?
      self.class.selectable?
    end

    def paginated?
      self.page_size != :none
    end

    def scoped_params
      @params.except(:listing, :controller, :action)
    end

    def scope_by_name(name)
      default_scope = scopes.find { |s| s.is_default? }
      selected_scope = scopes.find { |s| s.name == name.try(:to_sym) }
      selected_scope || default_scope
    end

    def value_for(column, item)
      column.value_for(self, item)
    end

    def column_with_key(key)
      self.columns.find { |c| c.key == key }
    end

    def filter_with_key(key)
      self.filters.find { |c| c.key == key }
    end

    def human_name(field)
      field.human_name
    end

    def searchable?
      self.columns.any? &:searchable?
    end

    def filterable?
      !self.filters.empty?
    end

    def selectable_id(model)
      model.id
    end

    def url
      view_context.listings.listing_full_path(self.name, self.params)
    end

    def search_data
      { criteria: self.search_criteria, filters: self.search_filters }
    end

    def format
      (params[:format] || :html).to_sym
    end

    def to_array
      data = []

      data << self.columns.map { |c| c.human_name }

      self.items.each do |item|
        row = []
        self.columns.each do |col|
          row << col.value_for(item)
        end
        data << row
      end

      data
    end

    def to_csv
      CSV.generate do |csv|
        self.to_array.each do |row|
          csv << row
        end
      end
    end

    def method_missing(m, *args, &block)
      view_context.send(m, *args, &block)
    end

    def kaminari_theme
      case Listings.configuration.theme
      when 'twitter-bootstrap-2'
        'twitter-bootstrap'
      else
        Listings.configuration.theme
      end
    end
  end
end