require 'active_reporter/inflector'
require 'active_reporter/dimension/bin'

module ActiveReporter
  module Dimension
    class Time < Bin
      STEPS = %i(seconds minutes hours days weeks months years)
      BIN_STEPS = (STEPS - [:seconds]).map { |step| step.to_s.singularize(:_gem_active_reporter) }
      DURATION_PATTERN = /\A\d+ (?:#{STEPS.map{ |step| "#{step}?" }.join('|')})\z/

      def validate_params!
        super

        invalid_param!(:bin_width, "must be a valid width value or a hash where each hash keys is a valid value and each hash value\n  is interger representing the count of the key width\n  valid :bin_width values include #{STEPS.to_sentence}.") if params.key?(:bin_width) && !valid_duration?(params[:bin_width])
      end

      def bin_width
        @bin_width ||= case
        when params.key?(:bin_width)
          custom_bin_width
        when params.key?(:bin_count) && domain > 0
          (domain / params[:bin_count].to_f).seconds
        else
          default_bin_width
        end
      end

      def bin_start
        # ensure that each autogenerated bin represents a correctly aligned
        # day/week/month/year
        bin_start = super
        
        return if bin_start.nil?

        step = BIN_STEPS.detect { |step| bin_width == 1.send(step) }
        step.present? ? bin_start.send(:"beginning_of_#{step}") : bin_start
      end

      private

      def custom_bin_width
        case params[:bin_width]
        when Hash
          params[:bin_width].map { |step, n| n.send(step) }.sum
        when String, Symbol
          n, step = params[:bin_width].to_s.split.map(&:strip)
          if step.nil?
            step = n
            n = 1
          end
          n.to_i.send(step)
        end
      end

      def valid_duration?(duration)
        case duration
        when Hash
          duration.all? { |step, n| step.to_s.pluralize(:_gem_active_reporter).to_sym.in?(STEPS) && n.is_a?(Numeric) }
        when String, Symbol
          duration.to_s.pluralize(:_gem_active_reporter).to_sym.in?(STEPS) || duration =~ DURATION_PATTERN
        else
          false
        end
      end

      def default_bin_width
        case domain
        when 0 then 1.day
        when 0..1.minute then 1.second
        when 0..2.hours then 1.minute
        when 0..2.days then 1.hour
        when 0..2.weeks then 1.day
        when 0..2.months then 1.week
        when 0..2.years then 1.month
        else 1.year
        end
      end

      class Set < Bin::Set
        def parse(value)
          ::Time.zone.parse(value.to_s.gsub('"', ''))
        end

        def cast(value)
          case ActiveReporter.database_type
          when :postgres
            "CAST(#{super} AS timestamp with time zone)"
          when :sqlite
            "DATETIME(#{super})"
          else
            "CAST(#{super} AS DATETIME)"
          end
        end
      end
    end
  end
end