require "active_reporter/dimension/base"

module ActiveReporter
  module Dimension
    class Bin < Base
      MAX_BINS = 2_000

      def max_bins
        self.class::MAX_BINS
      end

      # report values are greater than or equal to min, grouped by bin_width
      def min
        @min ||= filter_min || report.records.minimum(expression)
      end
      alias bin_start min

      # report values are less than max, grouped by bin_width
      def max
        @max ||= filter_max || report.records.maximum(expression)
      end

      def bin_end
        @bin_end ||= if max.blank? || min.blank? || min > max
          nil
        else
          bin_edge = bin_start + bin_width

          loop do
            break if bin_edge >= max
            bin_edge += bin_width
          end

          bin_edge += bin_width unless filter_values_for(:max).present? # # # figure out why we need this??

          bin_edge
        end
      end

      def filter_min
        filter_values_for(:min).min
      end

      def filter_max
        filter_values_for(:max).max
      end

      def domain
        min.nil? || max.nil? ? 0 : (max - min)
      end

      def group_values
        @group_values ||= to_bins(array_param(:bins).presence || autopopulate_bins)
      end

      def filter_values
        @filter_values ||= to_bins(super)
      end

      def filter(relation)
        filter_values.filter(relation, expression)
      end

      def group(relation)
        group_values.group(relation, expression, sql_value_name)
      end

      def validate_params!
        super

        if params.key?(:bin_count)
          invalid_param!(:bin_count, "must be numeric") unless ActiveReporter.numeric?(params[:bin_count])
          invalid_param!(:bin_count, "must be greater than 0") unless params[:bin_count].to_i > 0
          invalid_param!(:bin_count, "must be less than #{max_bins}") unless params[:bin_count].to_i <= max_bins
        end

        if array_param(:bins).present?
          invalid_param!(:bins, "must be hashes with min/max keys and valid values, or nil") unless group_values.all?(&:valid?)
        end

        if array_param(:only).present?
          invalid_param!(:only, "must be hashes with min/max keys and valid values, or nil") unless filter_values.all?(&:valid?)
        end
      end

      private

      def filter_values_for(key)
        filter_values.map { |filter_value| filter_value.send(key) }.compact
      end

      def table
        self.class.const_get(:Table)
      end

      def set
        self.class.const_get(:Set)
      end

      def to_bins(bins)
        table.new(bins.map(&method(:to_bin)))
      end

      def to_bin(bin)
        set.from_hash(bin)
      end

      def sanitize_sql_value(value)
        set.from_sql(value)
      end

      def data_contains_nil?
        report.records.where("#{expression} IS NULL").exists?
      end

      def autopopulate_bins
        return [] if bin_start.blank? || bin_end.blank?
        
        bin_count = ((bin_end - bin_start)/(bin_width)).to_i
        invalid_param!(:bin_width, "is too small for the domain; would generate #{bin_count.to_i} bins") if bin_count > max_bins

        bins = bin_count.times.map { |i| { min: (bin_start + (bin_width*i)), max: (bin_start + (bin_width*i.next)) } }

        bins.reverse! if sort_desc?
        ( nulls_last? ? bins.push(nil) : bins.unshift(nil) ) if data_contains_nil?

        bins
      end
    end
  end
end
