$:.unshift File.dirname(__FILE__)

%w( rubygems activerecord aux_codes/migration ).each {|lib| require lib }

# top-level class for AuxCodes
#
# used for namespacing and global configuration (once added)
#
class AuxCodes
end

#
# the basic AuxCode ActiveRecord class
#
class AuxCode < ActiveRecord::Base

  validates_presence_of :name
  validates_uniqueness_of :name, :scope => :aux_code_id

  belongs_to :aux_code
  alias code aux_code
  alias category aux_code

  has_many :aux_codes
  alias codes aux_codes

  before_create :set_default_values

  def code_names
    codes.map &:name
  end

  def is_a_category?
    aux_code_id == 0
  end

  def class_name
    name.gsub(/[^[:alpha:]]/,'_').titleize.gsub(' ','').singularize
  end

  def to_s
    name
  end

  def [] attribute_or_code_name
    if attributes.include?attribute_or_code_name
      attributes[attribute_or_code_name]
    else
      found = codes.select {|c| c.name.to_s =~ /#{attribute_or_code_name}/ }
        if found.empty? # try case insensitive (sans underscores)
          found = codes.select {|c| c.name.downcase.gsub('_',' ').to_s =~ 
            /#{attribute_or_code_name.to_s.downcase.gsub('_',' ')}/ }
        end
      found.first if found
    end
  end

  def deserialized_meta_hash
    require 'yaml'
    self.meta ||= ""
    YAML::load(self.meta) || { }
  end

  def get_meta_attribute meta_attribute
    deserialized_meta_hash[meta_attribute.to_s]
  end

  def set_meta_attribute meta_attribute, value
    require 'yaml'
    meta_hash = deserialized_meta_hash
    meta_hash[meta_attribute.to_s] = value
    self.meta = meta_hash.to_yaml
  end

  # this allows us to say things like:
  #
  #   foo = AuxCode.create :name => 'foo'
  #   foo.codes.create :name => 'bar'
  #
  #   foo.bar # should return the bar aux code under the foo category
  #
  #   if bar doesn't exist, we throw a normal NoMethodError
  #
  #   this should check meta_attributes on the object too
  #
  def method_missing_with_indifferent_hash_style_values name, *args, &block
    method_missing_without_indifferent_hash_style_values name, *args, &block
  rescue NoMethodError => ex
    begin
      if name.to_s[/=$/]
        self.set_meta_attribute(name.to_s.sub(/=$/,''), args.first) # we said `code.foo= X` so we should set the foo meta attribute to X
        save
      else
        code = self[name]
        code = self.get_meta_attribute(name) unless code
        raise ex unless code
        return code
      end
    rescue
      raise ex
    end
  end
  alias_method_chain :method_missing, :indifferent_hash_style_values

  # class methods
  class << self

    def categories
      AuxCode.find_all_by_aux_code_id(0)
    end

    def category_names
      AuxCode.categories.map &:name
    end

    def category category_object_or_id_or_name
      obj = category_object_or_id_or_name
      return obj if obj.is_a?AuxCode
      return AuxCode.find(obj) if obj.is_a?Fixnum
      if obj.is_a?(String) || obj.is_a?(Symbol)
        obj = obj.to_s
        found = AuxCode.find_by_name_and_aux_code_id(obj, 0)
        if found.nil?
          # try replacing underscores with spaces and doing a 'LIKE' search
          found = AuxCode.find :first, :conditions => ["name LIKE ? AND aux_code_id = ?", obj.gsub('_', ' '), 0]
        end
        return found
      end
      raise "I don't know how to find an AuxCode of type #{ obj.class }"
    end
    alias [] category

    def category_codes category_object_or_id_or_name
      category( category_object_or_id_or_name ).codes
    end
    alias category_values category_codes

    def category_code_names category_object_or_id_or_name
      category( category_object_or_id_or_name ).code_names
    end

    def create_classes!
      AuxCode.categories.each do |category|
        Kernel::const_set category.class_name, category.aux_code_class
      end
    end

    # this allows us to say things like:
    #
    #   AuxCode.create :name => 'foo'
    #
    #   AuxCode.foo # should return the foo category aux code
    #
    def method_missing_with_indifferent_hash_style_values name, *args, &block
      unless self.respond_to?:aux_code_id # in which case, this is a *derived* class, not AuxCode
        begin
          method_missing_without_indifferent_hash_style_values name, *args, &block
        rescue NoMethodError => ex
          begin
            self[name]
          rescue
            raise ex
          end
        end
      else
        method_missing_without_indifferent_hash_style_values name, *args, &block
      end
    end
    alias_method_chain :method_missing, :indifferent_hash_style_values

    def load_yaml yaml_string
      require 'yaml'
      self.load YAML::load(yaml_string)
    end

    def load_file serialized_yaml_file_path
      load_yaml File.read(serialized_yaml_file_path)
    end

    # initialize AuxCodes ... looks for config/aux_codes.yml
    # and creates classes
    def init # should eventually take configuration options (hash || block)
      if ActiveRecord::Base.connection.tables.include? 'aux_codes'
        aux_codes_yml = File.join 'config', 'aux_codes.yml'
        if File.file? aux_codes_yml
          load_file aux_codes_yml
          create_classes!
        end
      end
    end

    # 
    # loads AuxCodes (creates them) from a Hash, keyed on the name of the aux code categories to create
    #
    # hash: a Hash or an Array [ [key,value], [key,value] ] or anything with an enumerator 
    #       that'll work with `hash.each {|key,value| ... }`
    #
    def load hash
      return unless hash.is_a?Hash
      hash.each do |category_name, codes|
        category = AuxCode.find_or_create_by_name( category_name.to_s ).aux_code_class
        codes.each do |name, values|

          # only a name given
          if values.nil? or values.empty?
            if name.is_a? String or name.is_a? Symbol # we have a String || Symbol, it's likely the code's name, eg. :foo or 'bar'
              category.create :name => name.to_s unless category.code_names.include?(name.to_s)

            elsif name.is_a? Hash # we have a Hash, likely with the create options, eg. { :name => 'hi', :foo =>'bar' }
              category.create name

            else
              raise "not sure how to create code in category #{ category.name } with: #{ name.inspect }"
            end

            # we have a name and values
          else
            if values.is_a? Hash and (name.is_a? String or name.is_a? Symbol) # we have a Hash, likely with the create options ... we'll merge the name in as :name and create
              code = category[ name.to_s ]
              if code
                values.each do |attribute, new_value|
                  code.send "#{attribute}=", new_value # update values
                end
              else
                code = category.create values.merge({ :name => name.to_s })
              end

            else
              raise "not sure how to create code in category #{ category.name } with: #{ name.inspect }, #{ values.inspect }"

            end
          end
        end
      end
    end

  end

  protected

  def set_default_values
    self.aux_code_id = 0 unless aux_code_id
  end

end

%w( aux_codes/aux_code_class ).each {|lib| require lib } # define the class returned by #aux_code_class
