module CassandraObject
  module IntegerType
    REGEX = /\A[-+]?\d+\Z/
    def encode(int)
      raise ArgumentError.new("#{self} requires an Integer. You passed #{int.inspect}") unless int.kind_of?(Integer)
      int.to_s
    end
    module_function :encode

    def decode(str)
      return nil if str.empty?
      raise ArgumentError.new("Cannot convert #{str} into an Integer") unless str.kind_of?(String) && str.match(REGEX)
      str.to_i
    end
    module_function :decode
  end

  module FloatType
    REGEX = /\A[-+]?\d+(\.\d+)?\Z/
    def encode(float)
      raise ArgumentError.new("#{self} requires a Float") unless float.kind_of?(Float)
      float.to_s
    end
    module_function :encode

    def decode(str)
      return nil if str.empty?
      raise ArgumentError.new("Cannot convert #{str} into a Float") unless str.kind_of?(String) && str.match(REGEX)
      str.to_f
    end
    module_function :decode
  end
  
  module DateType
    FORMAT = '%Y-%m-%d'
    REGEX = /\A\d{4}-\d{2}-\d{2}\Z/
    def encode(date)
      raise ArgumentError.new("#{self} requires a Date") unless date.kind_of?(Date)
      date.strftime(FORMAT)
    end
    module_function :encode

    def decode(str)
      return nil if str.empty?
      raise ArgumentError.new("Cannot convert #{str} into a Date") unless str.kind_of?(String) && str.match(REGEX)
      Date.strptime(str, FORMAT)
    end
    module_function :decode
  end

  module TimeType
    # lifted from the implementation of Time.xmlschema and simplified
    REGEX = /\A\s*
              (-?\d+)-(\d\d)-(\d\d)
              T
              (\d\d):(\d\d):(\d\d)
              (\.\d*)?
              (Z|[+-]\d\d:\d\d)?
              \s*\z/ix

    def encode(time)
      raise ArgumentError.new("#{self} requires a Time") unless time.kind_of?(Time)
      time.xmlschema(6)
    end
    module_function :encode

    def decode(str)
      return nil if str.empty?
      raise ArgumentError.new("Cannot convert #{str} into a Time") unless str.kind_of?(String) && str.match(REGEX)
      Time.xmlschema(str)
    end
    module_function :decode
  end
  
  module TimeWithZoneType
    def encode(time)
      raise ArgumentError.new("#{self} requires a Time") unless time.kind_of?(Time)
      time.utc.xmlschema(6)
    end
    module_function :encode

    def decode(str)
      return nil if str.empty?
      raise ArgumentError.new("Cannot convert #{str} into a Time") unless str.kind_of?(String) && str.match(TimeType::REGEX)
      Time.xmlschema(str).in_time_zone
    end
    module_function :decode
  end

  module StringType
    def encode(str)
      raise ArgumentError.new("#{self} requires a String") unless str.kind_of?(String)
      str.dup
    end
    module_function :encode

    def decode(str)
      str
    end
    module_function :decode
  end
  
  module UTF8StringType
    def encode(str)
      # This is technically the most correct, but it is a pain to require utf-8 encoding for all strings. Should revisit.
      #raise ArgumentError.new("#{self} requires a UTF-8 encoded String") unless str.kind_of?(String) && str.encoding == Encoding::UTF_8
      raise ArgumentError.new("#{self} requires a String") unless str.kind_of?(String)
      str.dup
    end
    module_function :encode

    def decode(str)
      str.force_encoding('UTF-8')
    end
    module_function :decode
  end

  module HashType
    def encode(hash)
      raise ArgumentError.new("#{self} requires a Hash") unless hash.kind_of?(Hash)
      ActiveSupport::JSON.encode(hash)
    end
    module_function :encode

    def decode(str)
      ActiveSupport::JSON.decode(str)
    end
    module_function :decode
  end

  module BooleanType
    TRUE_VALS = [true, 'true', '1']
    FALSE_VALS = [false, 'false', '0', '', nil]
    def encode(bool)
      unless TRUE_VALS.any? { |a| bool == a } || FALSE_VALS.any? { |a| bool == a }
        raise ArgumentError.new("#{self} requires a boolean")
      end
      TRUE_VALS.include?(bool) ? '1' : '0'
    end
    module_function :encode

    def decode(str)
      raise ArgumentError.new("Cannot convert #{str} into a boolean") unless TRUE_VALS.any? { |a| str == a } || FALSE_VALS.any? { |a| str == a }
      TRUE_VALS.include?(str)
    end
    module_function :decode
  end
end