module Octopi
  class Base
    VALID = {
      :repo => {
        # FIXME: API currently chokes on repository names containing periods,
        # but presumably this will be fixed.
        :pat => /^[A-Za-z0-9_\.-]+$/,
        :msg => "%s is an invalid repository name"},
      :user => {
        :pat => /^[A-Za-z0-9_\.-]+$/,
        :msg => "%s is an invalid username"},
      :sha => {
        :pat => /^[a-f0-9]{40}$/,
        :msg => "%s is an invalid SHA hash"},
      :state => {
        # FIXME: Any way to access Issue::STATES from here?
        :pat => /^(open|closed)$/, 
        :msg => "%s is an invalid state; should be 'open' or 'closed'."  
      }
    }  
    
    attr_accessor :api
    
    def initialize(attributes={})
      # Useful for finding out what attr_accessor needs for classes
      # puts caller.first.inspect
      # puts "#{self.class.inspect} #{attributes.keys.map { |s| s.to_sym }.inspect}"
      attributes.each do |key, value|
        raise "no attr_accessor set for #{key} on #{self.class}" if !respond_to?("#{key}=")
        self.send("#{key}=", value)
      end
    end
    
    def error=(error)
      if /\w+ not found/.match(error)
        raise NotFound, self.class
      end
    end
    
    def property(p, v)
      path = "#{self.class.path_for(:resource)}/#{p}"
      Api.api.find(path, self.class.resource_name(:singular), v)
    end
    
    def save
      hash = {}
      @keys.each { |k| hash[k] = send(k) }
      Api.api.save(self.path_for(:resource), hash)
    end
    
    private
    
    def self.gather_name(options)
      options[:repository] || options[:repo] || options[:name]
    end
    
    def self.gather_details(options)
      repo = self.gather_name(options)
      repo = Repository.find(:user => options[:user], :name => repo) if !repo.is_a?(Repository)
      user = repo.owner.to_s
      user ||= options[:user].to_s
      branch = options[:branch] || "master"
      self.validate_args(user => :user, repo.name => :repo)
      [user, repo, branch, options[:sha]].compact
    end
    
    def self.extract_user_repository(*args)
      options = args.last.is_a?(Hash) ? args.pop : {}
      if options.empty?
        if args.length > 1
          repo, user = *args
        else
          repo = args.pop
        end
      else
        options[:repo] = options[:repository] if options[:repository]
        repo = args.pop || options[:repo]
        user = options[:user]
      end
      
      user = repo.owner if repo.is_a? Repository
      
      if repo.is_a?(String) && !user
        raise "Need user argument when repository is identified by name"
      end
      ret = extract_names(user, repo)
      ret << options
      ret
    end

    def self.extract_names(*args)
      args.map do |v|
        v = v.name  if v.is_a? Repository
        v = v.login if v.is_a? User
        v
      end
    end
    
    def self.ensure_hash(spec)
      raise ArgumentMustBeHash, "find takes a hash of options as a solitary argument" if !spec.is_a?(Hash)
    end
    
    def self.validate_args(spec)
      m = caller[0].match(/\/([a-z0-9_]+)\.rb:\d+:in `([a-z_0-9]+)'/)
      meth = m ? "#{m[1].camel_case}.#{m[2]}" : 'method'
      raise ArgumentError, "Invalid spec" unless 
        spec.values.all? { |s| VALID.key? s }
      errors = spec.reject{|arg, spec| arg.nil?}.
                    reject{|arg, spec| arg.to_s.match(VALID[spec][:pat])}.
                    map   {|arg, spec| "Invalid argument '%s' for %s (%s)" % 
                      [arg, meth, VALID[spec][:msg] % arg]} 
      raise ArgumentError, "\n" + errors.join("\n") unless errors.empty?
    end  
  end
end