# Author::    Matt Knox  (mailto:matthewknox@gmail.com)
# Copyright:: Copyright (c) 2009 Matt Knox
# License::   Distributes under the same terms as Ruby
#
# This is Generate on a Lot of Crack, which aims to speed and extend the initial definition of a rails app.
# It was motivated by the fact that to make a nested resource (ie, to get  /posts/1/comments to resolve),
# one must specify the relation between post and comment in 4 places:  the routes, the migration, and in
# both models.  That's silly, and not so DRY.  Enter GoaLoC, and the "blog in 15 minutes" talk essentially
# reduces to:
#   goaloc
#   >> @app.name = "myblog"
#   >> route [:posts, :comments]
#   >> add_attrs :posts => "body:text title:string", :comments => "body:text"
#   >> generate
#
# generate presently only knows how to make rails apps, and part of merb apps, but in principle, any
# REST-centric MVC app could be targeted comfortably, and even PHP apps could be done.

class App
  attr_accessor :name, :routes, :goals, :options, :log

  def initialize(name = nil)
    self.name = (name or generate_name)
    self.routes = []
    self.log = []
    self.goals = HashWithIndifferentAccess.new
  end

  # take in any number of ruby expressions, (presently limited to symbols and nested arrays of symbols),
  # and define those expressions as valid routes.  If the routed resources don't exist, define them,
  # and the relations with other resources implied by the route expression.  For instance, route :posts
  # implies that you'll want a model Post.  More interestingly,
  #   route [:posts, :comments, :wiki]
  # implies that there must be Post, Comment, and Wiki models, and that a Post has many comments and
  # has one wiki.  
  def route(*args)
    if valid_routeset?(args)
      self.routes += args # FIXME: make this so that it just maps over args again
      args.map { |elt| route_elt(elt, []) }
    end
  end

  # add_attrs takes in a hash of symbol => string pairs, and attaches to the goal named by the key the
  # attributes named in the string.  For example:
  #   add_attrs :posts => "body:text title:string", :ratings => "score:integer"
  # adds an integer field named score to Rating, a text field called body to Post, and a string field 
  # called title to Post.
  def add_attrs(h)
    h.map {  |k, v| self.goals[k.to_s.singularize].add_attrs v rescue nil }
  end

  # Returns a generator for the given target output format.  Currently Rails and Merb are supported.
  def generator(target = Rails)  
    generator = Generator.build(self, target)
  end

  #gets a generator and calls the generate method on it. 
  def generate(*args)
    generator(*args).generate
  end

  # generate a log of the commands that goaloc would have to recieve to reach its current state.
  # this currently double-counts some commands, and might miss others, for instance if one got
  # a goal object and poked at it's internal state directly. 
  def goaloc_log
    gen = []
    out = log.clone
    out.unshift "@app.name = '#{self.name}'" unless self.name.to_s.match(/goaloc_app20/)
    gen = [out.pop] if out.last.to_s.match(/^generate/)
    out << ("route " + self.routes.inspect[1..-2]) unless routes.empty?
    self.goals.each do |key, goal|
      goal.associations.each do |name, assoc|
        if assoc.has_key?(:through)
          out << "#{goal.name}.hmt({ :class => #{assoc[:class]}, :through => #{assoc[:through].name})"
        else
          out << "#{goal.name}.#{assoc[:type]}(#{assoc[:goal].name})"
        end
      end
    end
    out + gen
  end

  # this returns the goal (resource generated by goaloc) named by a given string or symbol.  Goal names
  # are always singular, so it allows for singular or plural variants of anything that responds to #to_s
  def fetch_goal(x) # this will take in anything stringlike and return a goal
    self.goals[x.to_s.singularize.underscore]
  end
  
  private
  def route_elt(arg, route_prefix)
    if arg.is_a? Symbol
      goal_for_sym(arg, route_prefix.clone << arg)
    elsif arg.is_a? Array
      base = route_elt(arg.first, route_prefix)
      res = [base]
      arg[1..-1].each do |elt|
        route_frag = route_elt(elt, route_prefix.clone << arg.first)
        goal = ( route_frag.is_a?(Array) ? route_frag.first : route_frag )
        if plural?(elt)
          base.has_many(goal)
        else
          base.has_one(goal)
        end
        res << route_frag
      end
      res
    end
  end

  def generate_name
    "goaloc_app" + Time.now.strftime("%Y%m%d%H%M%S")
  end

  def goal_for_sym(sym, route_prefix)
    name = sym.to_s.singularize
    goal = self.goals[name] ||= Goal.new(name, route_prefix) # dynamic var would be nice here.
    goal.routes << route_prefix.clone
    goal
  end

  def valid_routeset?(arg)
    arg.is_a?(Symbol) or
      valid_routeset_array?(arg)
  end
  
  def valid_routeset_array?(arg)
    arg.is_a? Array and
      !arg.empty? and
      arg.all? { |x| valid_routeset?(x) }
  end

  def plural?(sym)
    sym.to_s.pluralize == sym.to_s
  end
end