begin
  JSON::JSON_LOADED
rescue NameError
  require 'json'
end

# Fabricate a +Boolean+ class.
unless defined? Boolean
  module Boolean; end
  [TrueClass, FalseClass].each { |klass| klass.send :include, Boolean }
end

# = Structure
#
# Structure is a Struct-like key/value container.
#
#    class Person < Structure
#      key  :name
#      many :friends
#    end
#
class Structure
  include Enumerable

  autoload :Static, 'structure/static'

  # Available data type.
  TYPES = [Array, Boolean, Float, Hash, Integer, String, Structure]

  class << self
    # Defines an attribute that represents an array of objects.
    def many(name, options = {})
      key name, Array, { :default => [] }.merge(options)
    end

    # Defines an attribute that represents another structure.
    def one(name)
      key name, Structure
    end

    # Builds a structure out of its JSON representation.
    def json_create(object)
      object.delete 'json_class'
      new object
    end

    # Defines an attribute.
    #
    # Takes a name, an optional type, and an optional hash of options.
    #
    # The type can be +Array+, +Boolean+, +Float+, +Hash+, +Integer+,
    # +String+, a +Structure+, or a subclass thereof. If none is
    # specified, this defaults to +String+.
    #
    # Available options are:
    #
    # * +:default+, which sets the default value for the attribute.
    def key(name, *args)
      name    = name.to_sym
      options = args.last.is_a?(Hash) ?  args.pop : {}
      type    = args.shift || String
      default = options[:default]

      if method_defined? name
        raise NameError, "#{name} is already defined"
      end

      if (type.ancestors & TYPES).empty?
        raise TypeError, "#{type} is not a valid type"
      end

      if default.nil? || default.is_a?(type)
        default_attributes[name] = default
      else
        msg = "#{default} isn't a#{'n' if type.name.match(/^[AI]/)} #{type}"
        raise TypeError, msg
      end

      module_eval do
        # A proc that typecasts value based on type.
        typecaster =
          case type.name
          when 'Boolean'
            lambda { |value|
              # This should take care of the different representations
              # of truth we might be feeding into the model.
              #
              # Any string other than "0" or "false" will evaluate to
              # true.
              #
              # Any integer other than 0 will evaluate to true.
              #
              # Otherwise, we do the double-bang trick to non-boolean
              # values.
              case value
              when Boolean
                value
              when String
                value !~ /0|false/i
              when Integer
                value != 0
              else
                !!value
              end
            }
          when /Hash|Structure/
            # We could possibly check if the value responds to #to_hash
            # and cast to hash if it does, but I don't see any use case
            # for this right now.
            lambda { |value|
              unless value.is_a? type
                raise TypeError, "#{value} is not a #{type}"
              end
              value
            }
          else
            lambda { |value| Kernel.send(type.to_s, value) }
          end

        # Define attribute accessors.
        define_method(name) { @attributes[name] }

        define_method("#{name}=") do |value|
          @attributes[name] = value.nil? ? nil : typecaster.call(value)
        end
      end
    end

    # Returns a hash of all attributes with default values.
    def default_attributes
      @default_attributes ||= {}
    end
  end

  # Creates a new structure.
  #
  # A hash, if provided, will seed its attributes.
  def initialize(hash = {})
    @attributes = {}
    self.class.default_attributes.each do |key, value|
      @attributes[key] = value.is_a?(Array) ? value.dup : value
    end

    hash.each { |key, value| self.send("#{key}=", value) }
  end

  # A hash that stores the attributes of the structure.
  attr :attributes

  # Returns a Rails-friendly JSON representation of the structure.
  def as_json(options = nil)
    subset = if options
      if attrs = options[:only]
        @attributes.slice(*Array.wrap(attrs))
      elsif attrs = options[:except]
        @attributes.except(*Array.wrap(attrs))
      else
        @attributes.dup
      end
    else
      @attributes.dup
    end

    klass = self.class.name
    { JSON.create_id => klass }.
      merge(subset)
  end

  # Calls block once for each attribute in the structure, passing that
  # attribute as a parameter.
  def each(&block)
    @attributes.each { |value| block.call(value) }
  end

  # Returns a JSON representation of the structure.
  def to_json(*args)
    klass = self.class.name
    { JSON.create_id => klass }.
      merge(@attributes).
      to_json(*args)
  end

  # Compares this object with another object for equality. A Structure
  # is equal to the other object when latter is of the same class and
  # the two objects' attributes are the same.
  def ==(other)
    other.is_a?(self.class) && @attributes == other.attributes
  end
end