require File.expand_path('silverware.rb', File.dirname(__FILE__))

module Ironfan

  # An *aspect* is an external property, commonly encountered across multiple
  # systems, that decoupled agents may wish to act on.
  #
  # For example, many systems have a Dashboard aspect -- phpMySQL, the hadoop
  # jobtracker web console, a one-pager generated by the minidash recipe, or a
  # purpose-built backend for your website. The following independent concerns
  # can act on such dashboard aspects: * a dashboard dashboard creates a page
  # linking to all of them * your firewall grants access from internal machines
  # and denies access on public interfaces * the monitoring system checks that
  # the port is open and listening
  #
  # Aspects are able to do the following:
  #
  # * Convert to and from a plain hash,
  #
  # * ...and thusly to and from plain node metadata attributes
  #
  # * discover its manifestations across all systems (on all or some
  #   machines): for example, all dashboards, or all open ports.
  #
  # * identify instances from a system's by-convention metadata. For
  #   example, given a chef server system at 10.29.63.45 with attributes
  #     `:chef_server => { :server_port => 4000, :dash_port => 4040 }`
  #   the PortAspect class would produce instances for 4000 and 4040, since by
  #   convention an attribute ending in `_port` means "I have a port aspect`;
  #   the DashboardAspect would recognize the `dash_port` attribute and
  #   produce an instance for `http://10.29.63.45:4040`.
  #
  # Note:
  #
  # * separate *identifiable conventions* from *concrete representation* of
  #   aspects. A system announces that it has a log aspect, and by convention
  #   declares a `:log_dir` attribute. At that point it is regularized into a
  #   LogAspect instance and stored in the `node[:aspects]` tree. External
  #   concerns should only inspect these concrete Aspects, and never go
  #   hunting for thins with a `:log_dir` attribute.
  #
  # * conventions can be messy, but aspects are perfectly uniform
  #
  class Aspect
    include AttrStruct
    extend  Ironfan::NodeUtils

    dsl_attr(:component, :kind_of => Ironfan::Component)
    dsl_attr(:name,      :kind_of => [String, Symbol])

    # checks that the aspect is well-formed. returns non-empty array if there is lint.
    #
    # @abstract
    #   override to provide guidance, filling an array with warning strings. Include
    #       errors + super
    #   as the last line.
    #
    def lint
      []
    end

    def lint!
      lint.flatten.compact.each{|l| Chef::Log.warn(l) }
    end

    def lint_flavor
      self.class.allowed_flavors.include?(self.flavor) ? [] : ["Unexpected #{self.class.handle} flavor #{flavor.inspect}"]
    end

      # include AttrStruct::ClassMethods
      # include Ironfan::NodeUtils

      def self.register!
        Ironfan::Component.has_aspect(self)
      end

      #
      # Extract attributes matching the given pattern.
      #
      # @param [Hash]   info   -- hash of key-val pairs
      # @param [Regexp] regex  -- filter for keys matching this pattern
      #
      # @yield on each match
      # @yieldparam [String, Symbol] key   -- the matching key
      # @yieldparam [Object]         val   -- its value in the info hash
      # @yieldparam [MatchData]      match -- result of the regexp match
      # @yieldreturn [Aspect]        block should return an aspect
      #
      # @return [Array<Aspect>] collection of the block's results
      def self.attr_matches(component, regexp)
        results = Mash.new
        component.node_info.each do |key, val|
          next unless (match = regexp.match(key.to_s))
          result = yield(key, val, match) or next
          result.lint!
          results[result.name] ||= result
        end
        results
      end

      def self.rsrc_matches(rsrc_clxn, resource_name, cookbook_name)
        results = Mash.new
        rsrc_clxn.each do |rsrc|
          next unless rsrc.resource_name.to_s == resource_name.to_s
          next unless rsrc.cookbook_name.to_s =~ /#{cookbook_name}/
          result = yield(rsrc) or next
          results[result.name] ||= result
        end
        results
      end

      # strip off module part and '...Aspect' from class name
      # @example Ironfan::FooAspect.handle # :foo
      def self.handle
        @handle ||= self.name.to_s.gsub(/.*::(\w+)Aspect\z/,'\1').gsub(/([a-z\d])([A-Z])/,'\1_\2').downcase.to_sym
      end

      def self.plural_handle
        "#{handle}s".to_sym
      end

    # end
    # def self.included(base) ; base.extend(ClassMethods) ; end
  end
end
