# This is the core of goaloc:  the goal object.
# A goal object is intended to be a vertical slice of an MVC app, with information about its model, views,
# controllers, and routes.  It needs to be extended so that it can handle generating plugins/stylesheets/js/etc.
# Generators call a number of methods on goals that allow them to hook into their code generation process and
# customize the output.

# TODO: how should I let a goal know that it should generate, eg, its models with datamapper and views with haml?
# it would be nice to have prototypical inheritance here, where I'd make a generator object that inherits all
# of the state of the model, and has extra logic to tell it to generate datamapper files.

class Goal
  attr_reader :name
  attr_accessor :associations, :validations, :fields, :options, :routes, :foreign_keys

  def initialize(name, route = nil)
    @name = name.underscore.singularize   
    self.associations = HashWithIndifferentAccess.new
    self.validations = []
    self.fields = HashWithIndifferentAccess.new
    self.foreign_keys = HashWithIndifferentAccess.new
    self.options = { }
    self.routes = [] # of the form [:classname, [:otherclass, :classname], ...]
    if Object.const_defined? self.cs
      Object.send(:remove_const, self.cs)
    end
    Object.const_set self.cs, self
  end

# TODO: support renaming models
#   # this is a bad thing to do until it actually supports pervasive renaming (assocs, etc.)
#   def name=(name)
#     if Object.const_defined? self.cs and name != self.name
#       Object.send(:remove_const, self.cs)
#     end
#     @name = name
#     Object.const_set self.cs, self
#   end

  # === here are a list of name-ish methods
  # This returns the name of the foreign key used to refer to this goal.
  def foreign_key
    self.name + "_id"
  end

  def s; self.name;  end
  def p; self.name.pluralize; end
  def cs; self.name.camelize.singularize; end
  def cp; self.name.camelize.pluralize; end

  # ensure that this goal has the given route.
  def ensure_route(route)
    unless self.routes.member?(route) or route.blank?
      self.routes << route
    end
  end

  
  # === stuff used to introspect on the goal
  # thanks to Josh Ladieu for this: it's the array of things needed to get to an instance of this class, if there is a unique set.
  # this returns the minimal route to this goal, or nothing, if there is no unambiguous minimal route
  def resource_tuple 
    routelist = self.routes.sort { |x, y| x.length <=> y.length }
    if routelist.length == 1 #TODO: maybe should deal with a case where there's a simplest route that all the others contain.
      routelist.first
    else
      []
    end
  end

  # returns a list of all the params that are required for a goal but not inferrable from the path in which it is encountered.  
  def required_nonpath_params
    self.associations.reject { |k,v| v[:type] != :belongs_to }.keys.reject {|x| self.resource_tuple.map { |y| y.to_s.singularize }.member?(x) }
  end

  # if a resource has a resource_tuple, and it is not the first element of that
  # tuple, this will return true.
  def nested?
    self.resource_tuple.length > 1
  end

  # enclosing_resource returns the resource that most directly encloses this resource
  def enclosing_resource
    self.resource_tuple[-2]
  end

  # enclosing_resources returns the whole chain of resources up to but not including this one.
  def enclosing_resources
    self.resource_tuple[0..-2]
  end

  def enclosing_goal
    self.enclosing_resource.to_s.singularize.camelize.constantize
  end

  def enclosing_goals
    self.enclosing_resources.map { |r| r.to_s.singularize.camelize.constantize }
  end
  
  def underscore_tuple
    self.resource_tuple.map { |x| x.to_s.underscore.singularize }
  end

  def ivar_tuple
    self.resource_tuple.map { |x| "@" + x.to_s.underscore.singularize }
  end

  # this is intended to grab the list of elements needed to populate a form_for,
  # propagated back from the named end_element
  # so for [:users, [:posts, [:comments, :ratings]]] in the rating form it would be:
  # form.comment.post.user, form.comment.post, form.comment, form
  def backvar_tuple(end_element = "form")
    self.resource_tuple[0..-2].map {|sym| sym.to_s.singularize }.reverse.inject([end_element]) {|acc, x| acc.unshift(acc.first + "." + x )}
  end
  
  # this returns the set of resources that are nested under this resource.
  def nested_resources
    APP.goals.reject { |k, v| (v.routes != [(self.resource_tuple + [k.pluralize.to_sym])]) and
      (v.routes != [(self.resource_tuple + [k.singularize.to_sym])]) } # TODO: see if this can be cleaned up a bit.
  end

  # validations
  def validates(validation_type, field, opts = { })
    self.validations << opts.merge({ :val_type => validation_type, :field => field})
  end

  # === association stuff
  # set a belongs_to association
  def belongs_to(goal, options = { })
    self.foreign_keys[goal.foreign_key] = "references"
    self.validates(:presence_of, goal.foreign_key)
    self.associate(:belongs_to, goal, options)
  end

  # sets a has-many association from this goal to the target goal.  Also sets up a returning belongs_to association
  def has_many(goal, options = { })
    goal.belongs_to(self) unless options[:skip_belongs_to]
    self.associate(:has_many, goal, options)
  end

  # sets up the little-used has-one association, and the returning belongs_to.
  def has_one(goal, options = { })
    goal.belongs_to(self) unless options[:skip_belongs_to]
    self.associate(:has_one, goal, options)
  end

  # this sets a has-many through association, and by default, the reciprocal hmt.
  def hmt(goal, options)
    thru = options[:through]
    self.has_many(thru)
    self.has_many(goal, :through => thru, :skip_belongs_to => true)
    unless options[:bidi] == false
      goal.has_many(thru)
      goal.has_many( self, { :through => thru, :skip_belongs_to => true })
    end
  end

  def associate(assoc_type, goal, options = { })
    assoc_name = options[:assoc_name] || goal.default_assoc_name(assoc_type)
    self.associations[assoc_name] = { :goal => goal, :name => assoc_name, :type => assoc_type }.merge(options)
  end

  def default_assoc_name(assoc_type)
    :has_many == assoc_type ? name.pluralize : name
  end

  # this method adds attributes to the goal, in the format name1:type1 name2:type2.  Type names have unsociable chars like . , - etc. stripped
  def add_attrs(*args)
    if args.is_a? Array and args.length == 1
      args.first.gsub(/[-.,]/, '').split.each do |s|
        name, field_type = s.split(":")
        add_attr(name, field_type)
      end
    end
  end

  def add_attr(name, field_type)
    self.fields[name] = field_type
  end
end