require 'hobosupport'

ActiveSupport::Dependencies.load_paths |= [ File.dirname(__FILE__) ]

module Hobo
  # Empty class to represent the boolean type.
  class Boolean; end
end

module HoboFields

  VERSION = "0.9.101"

  extend self

  PLAIN_TYPES = {
    :boolean       => Hobo::Boolean,
    :date          => Date,
    :datetime      => (defined?(ActiveSupport::TimeWithZone) ? ActiveSupport::TimeWithZone : Time),
    :time          => Time,
    :integer       => Integer,
    :decimal       => BigDecimal,
    :float         => Float,
    :string        => String
  }

  ALIAS_TYPES = {
    Fixnum => "integer",
    Bignum => "integer"
  }

  # Provide a lookup for these rather than loading them all preemptively
  
  STANDARD_TYPES = {
    :raw_html      => "RawHtmlString",
    :html          => "HtmlString",
    :raw_markdown  => "RawMarkdownString",
    :markdown      => "MarkdownString",
    :textile       => "TextileString",
    :password      => "PasswordString",
    :text          => "Text",
    :email_address => "EmailAddress",
    :serialized    => "SerializedObject"
  }

  @field_types   = PLAIN_TYPES.with_indifferent_access
  
  @never_wrap_types = Set.new([NilClass, Hobo::Boolean, TrueClass, FalseClass])

  attr_reader :field_types

  def to_class(type)
    if type.is_one_of?(Symbol, String)
      type = type.to_sym
      field_types[type] || standard_class(type)
    else
      type # assume it's already a class
    end
  end


  def to_name(type)
    field_types.key(type) || ALIAS_TYPES[type]
  end


  def can_wrap?(type, val)
    col_type = type::COLUMN_TYPE
    return false if val.blank? && (col_type == :integer || col_type == :float || col_type == :decimal)
    klass = Object.instance_method(:class).bind(val).call # Make sure we get the *real* class
    arity = type.instance_method(:initialize).arity
    (arity == 1 || arity == -1) && !@never_wrap_types.any? { |c| klass <= c }
  end


  def never_wrap(type)
    @never_wrap_types << type
  end


  def register_type(name, klass)
    field_types[name] = klass
  end


  def plain_type?(type_name)
    type_name.in?(PLAIN_TYPES)
  end


  def standard_class(name)
    class_name = STANDARD_TYPES[name]
    "HoboFields::#{class_name}".constantize if class_name
  end

  def enable
    require "hobo_fields/enum_string"
    require "hobo_fields/fields_declaration"

    # Add the fields do declaration to ActiveRecord::Base
    ActiveRecord::Base.send(:include, HoboFields::FieldsDeclaration)

    # automatically load other rich types from app/rich_types/*.rb
    # don't assume we're in a Rails app
    if defined?(::Rails)
      Dir[File.join(::Rails.root, 'app', 'rich_types', '*.rb')].each do |f|
        # TODO: should we complain if field_types doesn't get a new value? Might be useful to warn people if they're missing a register_type
        require f
      end
    end

    # Monkey patch ActiveRecord so that the attribute read & write methods
    # automatically wrap richly-typed fields.
    ActiveRecord::AttributeMethods::ClassMethods.class_eval do

      # Define an attribute reader method.  Cope with nil column.
      def define_read_method(symbol, attr_name, column)
        cast_code = column.type_cast_code('v') if column
        access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']"

        unless attr_name.to_s == self.primary_key.to_s
          access_code = access_code.insert(0, "missing_attribute('#{attr_name}', caller) " +
                                           "unless @attributes.has_key?('#{attr_name}'); ")
        end

        # This is the Hobo hook - add a type wrapper around the field
        # value if we have a special type defined
        src = if connected? && (type_wrapper = try.attr_type(symbol)) &&
                  type_wrapper.is_a?(Class) && type_wrapper.not_in?(HoboFields::PLAIN_TYPES.values)
                "val = begin; #{access_code}; end; wrapper_type = self.class.attr_type(:#{attr_name}); " +
                  "if HoboFields.can_wrap?(wrapper_type, val); wrapper_type.new(val); else; val; end"
              else
                access_code
              end

        evaluate_attribute_method(attr_name,
                                  "def #{symbol}; @attributes_cache['#{attr_name}'] ||= begin; #{src}; end; end")
      end

      def define_write_method(attr_name)
        src = if connected? && (type_wrapper = try.attr_type(attr_name)) &&
                  type_wrapper.is_a?(Class) && type_wrapper.not_in?(HoboFields::PLAIN_TYPES.values)
                "begin; wrapper_type = self.class.attr_type(:#{attr_name}); " +
                  "if !val.is_a?(wrapper_type) && HoboFields.can_wrap?(wrapper_type, val); wrapper_type.new(val); else; val; end; end"
              else
                "val"
              end
        evaluate_attribute_method(attr_name,
                                  "def #{attr_name}=(val); write_attribute('#{attr_name}', #{src});end", "#{attr_name}=")

      end

    end

  end

end


HoboFields.enable if defined? ActiveRecord