module RDF
  ##
  # An RDF literal.
  #
  # @example Creating a plain literal
  #   value = RDF::Literal.new("Hello, world!")
  #   value.plain?                                   #=> true
  #
  # @example Creating a language-tagged literal (1)
  #   value = RDF::Literal.new("Hello!", :language => :en)
  #   value.has_language?                            #=> true
  #   value.language                                 #=> :en
  #
  # @example Creating a language-tagged literal (2)
  #   RDF::Literal.new("Wazup?", :language => :"en-US")
  #   RDF::Literal.new("Hej!",   :language => :sv)
  #   RDF::Literal.new("¡Hola!", :language => :es)
  #
  # @example Creating an explicitly datatyped literal
  #   value = RDF::Literal.new("2009-12-31", :datatype => RDF::XSD.date)
  #   value.has_datatype?                            #=> true
  #   value.datatype                                 #=> RDF::XSD.date
  #
  # @example Creating an implicitly datatyped literal
  #   value = RDF::Literal.new(Date.today)
  #   value.has_datatype?                            #=> true
  #   value.datatype                                 #=> RDF::XSD.date
  #
  # @example Creating implicitly datatyped literals
  #   RDF::Literal.new(false).datatype               #=> XSD.boolean
  #   RDF::Literal.new(true).datatype                #=> XSD.boolean
  #   RDF::Literal.new(123).datatype                 #=> XSD.integer
  #   RDF::Literal.new(9223372036854775807).datatype #=> XSD.integer
  #   RDF::Literal.new(3.1415).datatype              #=> XSD.double
  #   RDF::Literal.new(Time.now).datatype            #=> XSD.dateTime
  #   RDF::Literal.new(Date.new(2010)).datatype      #=> XSD.date
  #   RDF::Literal.new(DateTime.new(2010)).datatype  #=> XSD.dateTime
  #
  # @see http://www.w3.org/TR/rdf-concepts/#section-Literals
  # @see http://www.w3.org/TR/rdf-concepts/#section-Datatypes-intro
  class Literal < Value
    # @return [String] The normalized string representation of the value.
    attr_accessor :value

    # @return [Symbol] The language tag (optional).
    attr_accessor :language

    # @return [URI] The XML Schema datatype URI (optional).
    attr_accessor :datatype

    ##
    # @param  [Object]
    # @option options [Symbol] :language (nil)
    # @option options [URI]    :datatype (nil)
    def initialize(value, options = {})
      @value = value
      @language = options[:language] ? options[:language].to_s.to_sym : nil

      if datatype = options[:datatype]
        @datatype = datatype.respond_to?(:to_uri) ? datatype.to_uri : URI.new(datatype.to_s)
      else
        @datatype = case value
          when String     then nil # implicit XSD.string
          when TrueClass  then XSD.boolean
          when FalseClass then XSD.boolean
          when Fixnum     then XSD.integer
          when Integer    then XSD.integer
          when Float
            @value = case
              when value.nan? then 'NaN'
              when value.infinite? then value.to_s[0...-'inity'.length].upcase
              else value.to_f
            end
            XSD.double
          when Time, Date, DateTime
            require 'time'
            @value = value.respond_to?(:xmlschema) ? value.xmlschema : value.to_s
            case value
              when DateTime then XSD.dateTime
              when Date     then XSD.date
              when Time     then XSD.dateTime
            end
          else
            require 'bigdecimal' unless defined?(BigDecimal)
            case value
              when BigDecimal
                case
                  when value.nan?      then 'NaN'
                  when value.infinite? then value.to_s[0...-'inity'.length].upcase
                  when value.finite?   then value.to_s('F')
              end
            end
        end
      end

      @value = @value.to_s
    end

    ##
    # @return [Object]
    def object
      case datatype
        when XSD.string, nil
          value
        when XSD.boolean
          %w(true 1).include?(value)
        when XSD.double, XSD.float
          value.to_f
        when XSD.integer, XSD.long, XSD.int, XSD.short, XSD.byte
          value.to_i
        when XSD.decimal
          require 'bigdecimal' unless defined?(BigDecimal)
          BigDecimal.new(value)
        when XSD.date
          require 'date' unless defined?(Date)
          Date.parse(value)
        when XSD.dateTime
          require 'date' unless defined?(DateTime)
          DateTime.parse(value)
        when XSD.time
          require 'time'
          Time.parse(value)
        when XSD.nonPositiveInteger, XSD.negativeInteger
          value.to_i
        when XSD.nonNegativeInteger, XSD.positiveInteger
          value.to_i
        when XSD.unsignedLong, XSD.unsignedInt, XSD.unsignedShort, XSD.unsignedByte
          value.to_i
      end
    end

    ##
    # Returns `true`.
    #
    # @return [Boolean]
    def literal?
      true
    end

    ##
    # @return [Boolean]
    def eql?(other)
      other.is_a?(Literal) && self == other
    end

    ##
    # @return [Boolean]
    def ==(other)
      case other
        when Literal
          value.eql?(other.value) &&
          language.eql?(other.language) &&
          datatype.eql?(other.datatype)
        when String
          value.eql?(other) &&
            language.nil? &&
            datatype.nil?
        else false
      end
    end

    ##
    # Returns `true` if this is a plain literal.
    #
    # @return [Boolean]
    # @see http://www.w3.org/TR/rdf-concepts/#dfn-plain-literal
    def plain?
      language.nil? && datatype.nil?
    end

    ##
    # Returns `true` if this is a language-tagged literal.
    #
    # @return [Boolean]
    # @see http://www.w3.org/TR/rdf-concepts/#dfn-plain-literal
    def has_language?
      !language.nil?
    end

    alias_method :language?, :has_language?

    ##
    # Returns `true` if this is a datatyped literal.
    #
    # @return [Boolean]
    # @see http://www.w3.org/TR/rdf-concepts/#dfn-typed-literal
    def has_datatype?
      !datatype.nil?
    end

    alias_method :datatype?,  :has_datatype?
    alias_method :typed?,     :has_datatype?
    alias_method :datatyped?, :has_datatype?

    ##
    # Returns a string representation of this literal.
    #
    # @return [String]
    def to_s
      quoted = value # FIXME
      output = "\"#{quoted}\""
      output << "@#{language}" if language
      output << "^^<#{datatype}>" if datatype
      output
    end
  end
end