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

module Ironfan
  #
  #
  #
  #
  #
  class Component
    include Ironfan::AttrStruct
    include Ironfan::NodeUtils
    attr_reader(:node)
    dsl_attr(:sys,       :kind_of => Symbol, :coerce => :to_sym)
    dsl_attr(:subsys,    :kind_of => Symbol, :coerce => :to_sym)
    dsl_attr(:name,      :kind_of => String, :coerce => :to_s)
    dsl_attr(:realm,     :kind_of => Symbol, :coerce => :to_sym)
    dsl_attr(:timestamp, :kind_of => String, :regex => /\d{10}/)
    dsl_attr(:info,      :kind_of => Mash)

    def initialize(node, sys, subsys, hsh={})
      @node  = node
      hsh    = Mash.new(hsh.to_hash)
      sys    = sys.to_sym
      subsys = (subsys.to_s == '') ? nil : subsys.to_sym
      super(sys, subsys)
      merge!(hsh)
      self.name      [sys, subsys].compact.join('_').to_sym
      self.timestamp Ironfan::NodeUtils.timestamp
      self.info      hsh
    end

    # A segmented name for the component
    # @example
    #   Ironfan::Component.new(rc, :redis, :server, :realm => 'krypton').fullname
    #   # => 'krypton-redis-server'
    #   Ironfan::Component.new(rc, :nfs, nil, :realm => 'krypton').fullname
    #   # => 'krypton-nfs'
    #
    # @return [String] the component's dotted name
    def fullname
      self.class.fullname(realm, sys, subsys)
    end

    # A segmented name for the component
    def self.fullname(realm, sys, subsys=nil)
      [realm, sys, subsys].compact.join('-')
    end

    def subsys?
      not subsys.nil?
    end

    #
    # Sugar for essential node attributes
    #

    # Node's cluster name
    def cluster()     node[:cluster_name] ; end
    # Node's facet name
    def facet()       node[:facet_name] ;  end
    # Node's facet index
    def facet_index() node[:facet_index].to_i ; end

    def public_ip
      public_ip_of(node)
    end

    def private_ip
      private_ip_of(node)
    end

    def private_hostname
      private_hostname_of(node)
    end

    #
    # Aspects
    #

    # Harvest all aspects findable in the given node metadata hash
    #
    # @example
    #   component.harvest_all(run_context)
    #   component.dashboard(:webui)      # #<DashboardAspect name='webui' url="http://10.x.x.x:4040/">
    #   component.port(:webui_dash_port) # #<PortAspect port=4040 addr="10.x.x.x">
    #
    def harvest_all(run_context)
      self.class.aspect_types.each do |aspect_name, aspect_klass|
        res = aspect_klass.harvest(run_context, self)
        self.send(aspect_name, res)
      end
    end

    # list of known aspects
    def self.aspect_types
      @aspect_types ||= Mash.new
    end

    # add this class to the list of registered aspects
    #
    #     def log(name, val=nil, &block)
    #       self.logs[name] = val if val
    #       if block # autovivify and pass to block
    #           self.logs[name] ||= LogAspect.new(self, name)
    #           self.logs[name].instance_eval(&block)
    #       end
    #       self.logs
    #     end
    #
    def self.has_aspect(klass)
      self.aspect_types[klass.plural_handle] = klass
      # 'logs' method
      dsl_attr(klass.plural_handle, :kind_of => Mash, :dup_default => Mash.new)
      # 'log' method: this is just a slightly modified dsl_attr
      define_method(klass.handle) do |*args, &block|
        name,val = args
        hsh = self.send(klass.plural_handle)
        #
        hsh[name] = val if val
        # instance eval if block given (auto-vivify if necessary
        if block
          hsh[name] ||= klass.new(self, name)
          hsh[name].instance_eval(&block)
        end
        #
        hsh[name]
      end
    end

    #
    # Serialize in/out of Node
    #

    # Combines the hash for a system with the hash for its given subsys.
    # This lets us ask about the +:user+ for the 'redis.server' component,
    # whether it's set in +node[:redis][:server][:user]+ or
    # +node[:redis][:user]+. If an attribute exists on both the parent and
    # subsys hash, the subsys hash's value wins (see +:user+ in the
    # example below).
    #
    # @example
    #   node.to_hash
    #   # { :hadoop => {
    #   #     :user => 'hdfs', :log_dir => '/var/log/hadoop',
    #   #     :jobtracker => { :user => 'mapred', :port => 50030 } }
    #   # }
    #   node_info(:hadoop, jobtracker)
    #   # { :user => 'mapred', :log_dir => '/var/log/hadoop', :port => 50030,
    #   #   :jobtracker => { :user => 'mapred', :port => 50030 } }
    #   node_info(:hadoop, nil)
    #   # { :user => 'hdfs', :log_dir => '/var/log/hadoop',
    #   #   :jobtracker => { :user => 'mapred', :port => 50030 } }
    #
    #
    def node_info
      unless node[sys] then Chef::Log.warn("no system data in component '#{name}', node '#{node}'") ; return Mash.new ;  end
      hsh = Mash.new(node[sys].to_hash)
      if    subsys? && node[sys][subsys] then hsh.merge!(node[sys][subsys])
      elsif subsys?                      then Chef::Log.warn("no subsystem data in component '#{name}', node '#{node}'")
      end
      hsh
    end

    def node_attr(attr, required=nil)
      if required && (not node_info.has_key?(attr))
        Chef::Log.warn "No definition for #{attr} in #{name} - set node[:#{sys}][:#{subsys}][#{attr.inspect}] or node[:#{sys}][#{attr.inspect}]\n#{caller[0..4].join("\n    ")}"
      end
      node_info[attr]
    end

  end
end
