module Repor
  module Dimensions
    class TimeDimension < BinDimension
      STEPS = %i(seconds minutes hours days weeks months years)
      BIN_STEPS = (STEPS - [:seconds]).map { |s| s.to_s.singularize }
      DURATION_PATTERN = /\A\d+ (?:#{STEPS.map{|s| "#{s}?" }.join('|')})\z/

      def validate_params!
        super

        if params.key?(:bin_width) && !valid_duration?(params[:bin_width])
          invalid_param!(:bin_width, "must be a hash of one of #{STEPS} to an integer")
        end
      end

      def bin_width
        @bin_width ||= if params.key?(:bin_width)
          custom_bin_width
        elsif 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
        if bin_start.nil?
          nil
        elsif step = BIN_STEPS.detect { |step| bin_width == 1.send(step) }
          bin_start.send(:"beginning_of_#{step}")
        else
          bin_start
        end
      end

      private

      def custom_bin_width
        if params[:bin_width].is_a?(Hash)
          params[:bin_width].map { |step, n| n.send(step) }.sum
        elsif params[:bin_width].is_a?(String)
          n, step = params[:bin_width].split.map(&:strip)
          n.to_i.send(step)
        end
      end

      def valid_duration?(d)
        case d
        when Hash
          d.all? { |step, n| step.to_sym.in?(STEPS) && n.is_a?(Fixnum) }
        when String
          d =~ 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 Bin < BinDimension::Bin
        def parse(value)
          Time.zone.parse(value.to_s.gsub('"', ''))
        end

        def cast(value)
          case Repor.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
