require 'time'
require 'date'

class Time #:nodoc:
  class << self
    def mock_time
      mocked_time_stack_item = Timecop.top_stack_item
      mocked_time_stack_item.nil? ? nil : mocked_time_stack_item.time(self)
    end

    alias_method :now_without_mock_time, :now

    def now_with_mock_time
      mock_time || now_without_mock_time
    end

    alias_method :now, :now_with_mock_time

    alias_method :new_without_mock_time, :new

    def new_with_mock_time(*args, **kwargs)
      if args.empty? && kwargs.empty?
        now
      elsif kwargs.any?
        new_without_mock_time(*args, **kwargs)
      else
        new_without_mock_time(*args)
      end
    end

    alias_method :new, :new_with_mock_time
  end
end

class Date #:nodoc:
  class << self
    def mock_date
      mocked_time_stack_item.nil? ? nil : mocked_time_stack_item.date(self)
    end

    alias_method :today_without_mock_date, :today

    def today_with_mock_date
      mock_date || today_without_mock_date
    end

    alias_method :today, :today_with_mock_date

    alias_method :strptime_without_mock_date, :strptime

    def strptime_with_mock_date(str = '-4712-01-01', fmt = '%F', start = Date::ITALY)
      #If date is not valid the following line raises
      Date.strptime_without_mock_date(str, fmt, start)

      d = Date._strptime(str, fmt)
      now = Time.now.to_date

      # If "current" time falls near a year boundary, with a year-ambiguous str, we need to explicitly handle it
      cwday = d[:cwday] && (now + (d[:cwday] - now.wday))
      wday = d[:wday] && (now + (d[:wday] - now.wday))

      year = d[:year] || d[:cwyear] || (cwday || wday || now).year
      mon = d[:mon] || now.mon
      if d.keys == [:year]
        Date.new(year, 1, 1, start)
      elsif d[:mday]
        Date.new(year, mon, d[:mday], start)
      elsif d[:yday]
        Date.new(year, 1, 1, start).next_day(d[:yday] - 1)
      elsif d[:cwyear] || d[:cweek] || d[:wnum0] || d[:wnum1] || d[:wday] || d[:cwday]
        # When only a day (wday/cwday) is present, derive the week from the resolved date; otherwise fall back to now's week
        week = d[:cweek] || d[:wnum1] || d[:wnum0] || (cwday || wday || now).strftime('%W').to_i
        if d[:wnum0] #Week of year where week starts on sunday
          if d[:cwday] #monday based day of week
            Date.strptime_without_mock_date("#{year} #{week} #{d[:cwday]}", '%Y %U %u', start)
          else
            Date.strptime_without_mock_date("#{year} #{week} #{d[:wday] || 0}", '%Y %U %w', start)
          end
        else #Week of year where week starts on monday
          if d[:wday] #sunday based day of week
            Date.strptime_without_mock_date("#{year} #{week} #{d[:wday]}", '%Y %W %w', start)
          else
            Date.strptime_without_mock_date("#{year} #{week} #{d[:cwday] || 1}", '%Y %W %u', start)
          end
        end
      elsif d[:seconds]
        Time.at(d[:seconds]).to_date
      else
        Date.new(year, mon, 1, start)
      end
    end

    alias_method :strptime, :strptime_with_mock_date

    def parse_with_mock_date(*args)
      parsed_date = parse_without_mock_date(*args)
      return parsed_date unless mocked_time_stack_item
      date_hash = Date._parse(*args)

      case
      when date_hash[:year] && date_hash[:mon]
        parsed_date
      when date_hash[:mon] && date_hash[:mday]
        Date.new(mocked_time_stack_item.year, date_hash[:mon], date_hash[:mday])
      when date_hash[:mday]
        Date.new(mocked_time_stack_item.year, mocked_time_stack_item.month, date_hash[:mday])
      when date_hash[:wday]
        closest_wday(date_hash[:wday])
      else
        parsed_date + mocked_time_stack_item.travel_offset_days
      end
    end

    alias_method :parse_without_mock_date, :parse
    alias_method :parse, :parse_with_mock_date

    def mocked_time_stack_item
      Timecop.top_stack_item
    end

    def closest_wday(wday)
      today = Date.today
      result = today - today.wday
      result += 1 until wday == result.wday
      result
    end
  end
end

class DateTime #:nodoc:
  class << self
    def mock_time
      mocked_time_stack_item.nil? ? nil : mocked_time_stack_item.datetime(self)
    end

    def now_with_mock_time
      mock_time || now_without_mock_time
    end

    alias_method :now_without_mock_time, :now

    alias_method :now, :now_with_mock_time

    def parse_with_mock_date(*args)
      parsed_date = parse_without_mock_date(*args)
      return parsed_date unless mocked_time_stack_item
      date_hash = DateTime._parse(*args)

      case
      when date_hash[:year] && date_hash[:mon]
        parsed_date
      when date_hash[:mon] && date_hash[:mday]
        DateTime.new(mocked_time_stack_item.year, date_hash[:mon], date_hash[:mday])
      when date_hash[:mday]
        DateTime.new(mocked_time_stack_item.year, mocked_time_stack_item.month, date_hash[:mday])
      when date_hash[:wday] && date_hash[:hour] && date_hash[:min]
        closest_date = Date.closest_wday(date_hash[:wday]).to_datetime

        DateTime.new(
          closest_date.year, closest_date.month, closest_date.day,
          date_hash[:hour], date_hash[:min]
        )
      when date_hash[:wday]
        Date.closest_wday(date_hash[:wday]).to_datetime
      when date_hash[:hour] && date_hash[:min] && date_hash[:sec]
        DateTime.new(mocked_time_stack_item.year, mocked_time_stack_item.month, mocked_time_stack_item.day, date_hash[:hour], date_hash[:min], date_hash[:sec])
      when date_hash[:hour] && date_hash[:min]
        DateTime.new(mocked_time_stack_item.year, mocked_time_stack_item.month, mocked_time_stack_item.day, date_hash[:hour], date_hash[:min], 0)
      else
        parsed_date + mocked_time_stack_item.travel_offset_days
      end
    end

    alias_method :parse_without_mock_date, :parse
    alias_method :parse, :parse_with_mock_date

    def mocked_time_stack_item
      Timecop.top_stack_item
    end
  end
end

module Process #:nodoc:
  class << self
    alias_method :clock_gettime_without_mock, :clock_gettime

    def clock_gettime_mock_time(clock_id, unit = :float_second)
      mock_time = case clock_id
                  when Process::CLOCK_MONOTONIC
                    mock_time_monotonic
                  when Process::CLOCK_REALTIME
                    mock_time_realtime
                  end

      return clock_gettime_without_mock(clock_id, unit) unless Timecop.mock_process_clock? && mock_time

      divisor = case unit
                when :float_second
                  1_000_000_000.0
                when :second
                  1_000_000_000
                when :float_millisecond
                  1_000_000.0
                when :millisecond
                  1_000_000
                when :float_microsecond
                  1000.0
                when :microsecond
                  1000
                when :nanosecond
                  1
                end

      (mock_time / divisor)
    end

    alias_method :clock_gettime, :clock_gettime_mock_time

    private

    def mock_time_monotonic
      mocked_time_stack_item = Timecop.top_stack_item
      mocked_time_stack_item.nil? ? nil : mocked_time_stack_item.monotonic
    end

    def mock_time_realtime
      mocked_time_stack_item = Timecop.top_stack_item

      return nil if mocked_time_stack_item.nil?

      t = mocked_time_stack_item.time
      t.to_i * 1_000_000_000 + t.nsec
    end
  end
end
