module TZInfo
  # Represents a transition from one timezone offset to another at a particular
  # date and time.
  class TimezoneTransition
    # The offset this transition changes to (a TimezoneOffset instance).
    attr_reader :offset
    
    # The offset this transition changes from (a TimezoneOffset instance).
    attr_reader :previous_offset
    
    # Initializes a new TimezoneTransition.
    #
    # TimezoneTransition instances should not normally be constructed manually.
    def initialize(offset, previous_offset)
      @offset = offset
      @previous_offset = previous_offset
      @local_end_at = nil
      @local_start_at = nil
    end
    
    # A TimeOrDateTime instance representing the UTC time when this transition
    # occurs.
    def at
      raise_not_implemented('at')
    end
    
    # The UTC time when this transition occurs, returned as a DateTime instance.
    def datetime
      at.to_datetime
    end
    
    # The UTC time when this transition occurs, returned as a Time instance.
    def time
      at.to_time
    end
    
    # A TimeOrDateTime instance representing the local time when this transition
    # causes the previous observance to end (calculated from at using 
    # previous_offset).
    def local_end_at
      # Thread-safety: It is possible that the value of @local_end_at may be
      # calculated multiple times in concurrently executing threads. It is not 
      # worth the overhead of locking to ensure that @local_end_at is only
      # calculated once.
    
      unless @local_end_at
        result = at.add_with_convert(@previous_offset.utc_total_offset)
        return result if frozen?
        @local_end_at = result
      end

      @local_end_at
    end
    
    # The local time when this transition causes the previous observance to end,
    # returned as a DateTime instance.
    def local_end
      local_end_at.to_datetime
    end
    
    # The local time when this transition causes the previous observance to end,
    # returned as a Time instance.
    def local_end_time
      local_end_at.to_time
    end
    
    # A TimeOrDateTime instance representing the local time when this transition
    # causes the next observance to start (calculated from at using offset).
    def local_start_at
      # Thread-safety: It is possible that the value of @local_start_at may be
      # calculated multiple times in concurrently executing threads. It is not 
      # worth the overhead of locking to ensure that @local_start_at is only
      # calculated once.
    
      unless @local_start_at
        result = at.add_with_convert(@offset.utc_total_offset)
        return result if frozen?
        @local_start_at = result
      end

      @local_start_at
    end
    
    # The local time when this transition causes the next observance to start,
    # returned as a DateTime instance.
    def local_start
      local_start_at.to_datetime
    end
    
    # The local time when this transition causes the next observance to start,
    # returned as a Time instance.
    def local_start_time
      local_start_at.to_time
    end
    
    # Returns true if this TimezoneTransition is equal to the given
    # TimezoneTransition. Two TimezoneTransition instances are 
    # considered to be equal by == if offset, previous_offset and at are all 
    # equal.
    def ==(tti)
      tti.kind_of?(TimezoneTransition) &&
        offset == tti.offset && previous_offset == tti.previous_offset && at == tti.at
    end
    
    # Returns true if this TimezoneTransition is equal to the given
    # TimezoneTransition. Two TimezoneTransition instances are 
    # considered to be equal by eql? if offset, previous_offset and at are all
    # equal and the type used to define at in both instances is the same.
    def eql?(tti)
      tti.kind_of?(TimezoneTransition) &&
        offset == tti.offset && previous_offset == tti.previous_offset && at.eql?(tti.at)
    end
    
    # Returns a hash of this TimezoneTransition instance.
    def hash
      @offset.hash ^ @previous_offset.hash ^ at.hash
    end
    
    # Returns internal object state as a programmer-readable string.
    def inspect
      "#<#{self.class}: #{at.inspect},#{@offset.inspect}>"      
    end

    private

    def raise_not_implemented(method_name)
      raise NotImplementedError, "Subclasses must override #{method_name}"
    end
  end
end