module MerchCalendar

  # Represents the Merch Week for a specified date.
  class MerchWeek

    MONTHS = %w(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec).freeze

    # The Julian date that is being represented
    #
    # @!attribute [r] date
    #   @return [Date] the date for this merch week
    attr_reader :date

    class << self

      # Locates the +MerchWeek+ for a given Julian date.
      #
      # @overload from_date(String)
      #   @param [String] julian_date a julian date in the format of +YYYY-MM-DD+
      # @overload from_date(Date)
      #   @param [Date] julian_date a +Date+ object
      #
      # @return [MerchWeek]
      def from_date(julian_date)
        MerchWeek.new Date.parse("#{julian_date}")
      end

      # Returns an array of merch weeks for a month, or a specific week.
      #
      # @overload find(year, julian_month)
      #   Returns an array of +MerchWeek+s for a given month
      #   @param year [Fixnum] the merch year to locate
      #   @param julian_month [Fixnum] the month to find merch months for
      #   @return [Array<MerchWeek>]
      # @overload find(year, julian_month, week_number)
      #   @param year [Fixnum] the merch year to locate
      #   @param julian_month [Fixnum] the month to find merch months for
      #   @param week_number [Fixnum] the specific week number.
      #   @return [MerchWeek] the specific merch week
      def find(year, julian_month, week_number=nil)
        if week_number.nil?
          MerchCalendar.weeks_for_month(year, julian_month)
        else
          MerchCalendar.weeks_for_month(year, julian_month)[week_number-1]
        end
      end

      # Returns the +MerchWeek+ for today's date
      #
      # @return [MerchWeek]
      def today
        MerchWeek.from_date Date.today
      end
    end



    # Pass in a date, make sure it is a valid date object
    # @private
    def initialize(date, options = {})
      @date = date

      # Placeholders. These should be populated by functions if nil
      # week_start: nil, week_end: nil, week_number: nil
      @start_of_year = options[:start_of_year]
      @end_of_year = options[:end_of_year]

      @start_of_week = options[:start_of_week]
      @end_of_week = options[:end_of_week]
      @week = options[:week]

      @start_of_month = options[:start_of_month]
      @end_of_month = options[:end_of_month]
    end

    # What week it is within the year from 1-53
    #
    # @return [Fixnum] The week number of the year, from 1-53
    def year_week
      @year_week ||= (((date-start_of_year)+1)/7.0).ceil
    end

    # This returns the "merch month" number for a date
    # Merch months are shifted by one. Month 1 is Feb
    #
    # @return [Fixnum]
    def merch_month
      # TODO: This is very inefficient, but less complex than strategic guessing
      # maybe switch to a binary search or something
      @merch_month ||= (1..12).detect do |num|
        date_calc.end_of_month(start_of_year.year, num) >= date && date >= date_calc.start_of_month(start_of_year.year, num)
      end
    end

    # The merch year
    #
    # @return [Fixnum]
    def year
      start_of_year.year
    end

    # The julian month that this merch week falls in
    #
    # @return [Fixnum]
    def month
      @month ||= date_calc.merch_to_julian(merch_month)
    end

    # The specific quarter this week falls in
    #
    # @return [Fixnum]
    def quarter
      case merch_month
      when 7,8,9
        return 1
      when 10,11,12
        return 2
      when 1,2,3
        return 3
      else
        return 4
      end
    end

    # Returns the date of the start of this week
    #
    # @return [Date]
    def start_of_week
      @start_of_week ||= (start_of_month + (7 * (week - 1)))
    end

    # Returns the date of the end of this week
    #
    # @return [Date]
    def end_of_week
      @end_of_week ||= (start_of_week + 6)
    end

    # the number of the week within the given month
    # will be between 1 and 5
    #
    # @return [Fixnum]
    def week
      @week ||= (((date-start_of_month)+1)/7.0).ceil
    end

    # The date of the start of the corresponding merch year
    #
    # @return [Date]
    def start_of_year
      @start_of_year ||= year_start_date
    end

    # The end date of the corresponding merch year
    #
    # @return [Date]
    def end_of_year
      @end_of_year ||= date_calc.end_of_year(year)
    end

    # The start date of the merch month
    #
    # @return [Date]
    def start_of_month
      @start_of_month ||= date_calc.start_of_month(year, merch_month)
    end

    # The end date of the merch month
    #
    # @return [Date]
    def end_of_month
      @end_of_month ||= date_calc.end_of_month(year, merch_month)
    end

    # The merch season this date falls under.
    # Returns a string of +Fall/Winter+ or +Spring/Summer+
    #
    # @return [String]
    def season
      case merch_month
      when 1,2,3,4,5,6
        "Spring/Summer"
      when 7,8,9,10,11,12
        "Fall/Winter"
      end
    end

    # Outputs a text representation of this merch week
    #
    # Format keys:
    # * +:short+ (default) "Dec W5"
    # * +:long+ "2012:48 Dec W5"
    # * +:elasticsearch+ (default) "2012-12w05"
    #
    # @param format [Symbol] the format identifier to return. Default is +:short+
    #
    # @return [Date]
    def to_s(format = :short)
      case format
      when :elasticsearch
        sprintf("%04d-%02dw%02d", year, month, week)
      when :long
        "#{year}:#{year_week} #{self.to_s(:short)}"
      else
        "#{MONTHS[month - 1]} W#{week}"
      end
    end

    private

    def year_start_date
      start_date = date_calc.start_of_year(date.year)
      if start_date > date
        start_date = date_calc.start_of_year(date.year - 1)
      end
      start_date
    end

    def date_calc
      @date_calc ||= DateCalculator.new
    end

  end
end