module JSONAPI::Consumer
  class Schema
    module Types

      class Integer
        def self.cast(value, _)
          value.to_i
        end
      end

      class String
        def self.cast(value, _)
          value.to_s
        end
      end

      class Float
        def self.cast(value, _)
          value.to_f
        end
      end

      class Time
        def self.cast(value, _)
          value.is_a?(::Time) ? value : ::Time.parse(value)
        end
      end

      class Decimal
        def self.cast(value, _)
          value.to_d
        end
      end

      class Boolean
        def self.cast(value, default)
          case value
            when "false", "0", 0, false
              false
            when "true", "1", 1, true
              true
            else
              # if it's unknown, use the default value
              default
          end
        end
      end

    end

    class TypeFactory
      @@types = {}
      # Register a new type key or keys with appropriate classes
      #
      # eg:
      #
      #   require 'money'
      #
      #   class MyMoneyCaster
      #      def self.cast(value, default)
      #         begin
      #           Money.new(value, "USD")
      #         rescue ArgumentError
      #           default
      #         end
      #      end
      #   end
      #
      #   JSONAPI::Consumer::Schema::Types.register money: MyMoneyCaster
      #
      # You can setup several at once:
      #
      #   JSONAPI::Consumer::Schema::Types.register money: MyMoneyCaster,
      #                                         date: MyJsonDateTypeCaster
      #
      #
      #
      #
      def self.register(type_hash)
        @@types.merge!(type_hash)
      end

      def self.type_for(type)
        @@types[type]
      end

      self.register int: Types::Integer,
                    integer: Types::Integer,
                    string: Types::String,
                    float: Types::Float,
                    time: Types::Time,
                    decimal: Types::Decimal,
                    boolean: Types::Boolean

    end

    Property = Struct.new(:name, :type, :default) do
      def cast(value)
        return nil if value.nil?
        return value if type.nil?
        type_caster = TypeFactory.type_for(type)
        return value if type_caster.nil?
        type_caster.cast(value, default)
      end
    end

    def initialize
      @properties = {}
    end

    # Add a property to the schema
    #
    # @param name [Symbol] the name of the property
    # @param options [Hash] property options
    # @option options [Symbol] :type The property type
    # @option options [Symbol] :default The default value for the property
    # @return [void]
    def add(name, options)
      @properties[name.to_sym] = Property.new(name.to_sym, options[:type], options[:default])
    end

    # How many properties are defined
    #
    # @return [Fixnum] the number of defined properties
    def size
      @properties.size
    end

    alias_method :length, :size

    def each_property(&block)
      @properties.values.each(&block)
    end

    alias_method :each, :each_property

    # Look up a property by name
    #
    # @param property_name [String] the name of the property
    # @return [Property, nil] the property definition for property_name or nil
    def find(property_name)
      @properties[property_name.to_sym]
    end

    alias_method :[], :find

    class << self
      def register(type_hash)
        TypeFactory.register(type_hash)
      end
    end
  end
end
