#
# url: http://s3.amazonaws.com/drawohara.com.snippets/joins_rb.html
# src: http://s3.amazonaws.com/drawohara.com.ruby/joins.rb
#
# file: RAILS_ROOT/lib/joins.rb 
#
# using joins.rb you can escape join model hell.  never again will you need to
# generate countless join tables for every has_many or has_one relationship.
# joins.rb is both a class generator and association generator in 59 lines of
# code that manages all joins via a single sti based db table, allowing you to
# develop rapidly while never prohibiting you from moving to dedicated join
# models should your table space feel too roomy.  usage is as follows
#
#   class Child < ActiveRecord::Base
#     has_many :parents, :through => join(:child => :parent)
#   end 
#
#   class Parent < ActiveRecord::Base
#     has_many :children, :through => join(:parent => :child)
#   end 
#
# notice the direction if the relationship is declared via what's on the
# lhs/rhs of the ':key => :val' pair.  let's break down what those two calls
# do:
#
#   * has_many :parents, :through => join(:child => :parent)
#
#     . creates a class Join::ChildParent.  the naming is always determined
#     via the lexical sort!  no pluralization rules are ever applied.
#
#     . creates a has_many association named in the order of arguments, in
#     this case
#
#       has_many(
#         :child_parent_joins,
#         :foreign_key => :src,
#         :class_name => '::Join::ChildParent'
#       )
#
#   * has_many :children, :through => join(:parent => :child)
#
#     . creates a class Join::ChildParent.  the naming is always determined
#     via the lexical sort!  no pluralization rules are ever applied.
#
#     . creates a has_many association named in the order of arguments, in
#     this case
#
#       has_many(
#         :parent_child_joins,
#         :foreign_key => :dst,
#         :class_name => '::Join::ChildParent'
#       )
#
# notice that each through association shares the join model, yet builds a
# directional (named like it was specified) has_many join assocications on the
# target class.  the join model itself is stored in the single join db table
# which you can migrate using something like
#
#   ./script/runner ' Join::Migration.up '
#
# or with a line like this in a migration
#
#   CreateJoins = Join::Migration
#
# of course you need to put the file RAILS_ROOT/lib and add
#
#   require 'joins'
#
# to config/environment.rb or one of your initializers
#
# that's it.  enjoy!
#





class Join < ActiveRecord::Base

  module Method
    def join *args
      desc = args.shift
      src, dst, *ignored = desc.to_a.first
      src_options, dst_options, *ignored = args

      src = src.to_s.underscore
      dst = dst.to_s.underscore

      model = Join.model_for(src, dst, src_options, dst_options)

      joins = [src, dst, 'joins'].join('_')
      foreign_key = [src,dst] == [src,dst].sort ? 'src' : 'dst'
      class_name = model.name

      has_many(
        joins,
        :foreign_key => foreign_key,
        :class_name  => class_name,
        :uniq        => true,
        :dependent   => :destroy
      )

      joins
    end
  end
  ::ActiveRecord::Base.send :extend, Method


  Models = {}

  def Join.model_for src, dst, src_options=nil, dst_options=nil
    src = src.to_s.underscore
    dst = dst.to_s.underscore

    src_options = (src_options||{}).to_options
    dst_options = (dst_options||{}).to_options

    if [src, dst] != [src, dst].sort
      src, dst = dst, src
      src_options, dst_options = dst_options, src_options
    end

    model_name = [src, dst].join('_').camelize

    return Models[model_name] if Models.has_key?(model_name)

    model = Class.new(Join)

    const_set model_name, model
    Models[model_name] = model

    model.module_eval do
      belongs_to src.to_s.to_sym, src_options.reverse_merge(:foreign_key => :src)
      belongs_to dst.to_s.to_sym, dst_options.reverse_merge(:foreign_key => :dst)
    end

    return Models[model_name]
  end


  class Migration < ActiveRecord::Migration
    def self.up
      create_table :joins, :force => true do |t|
        t.string :type  # sti
        t.integer :src
        t.integer :dst
        # t.timestamps
      end

      add_index :joins, [:type]
      add_index :joins, [:src]
      add_index :joins, [:dst]
      add_index :joins, [:src, :dst], :unique => true
    end

    def self.down
      remove_index :joins, [:src, :dst]
      remove_index :joins, [:dst]
      remove_index :joins, [:src]
      remove_index :joins, [:type]

      drop_table :joins
    end
  end
end