require 'eventmachine'

require 'ganymed'
require 'ganymed/client'

module Ganymed

  ##
  # The Collector is a simple thread that polls various system-level metrics
  # that are not pushed to the {Sampler}/{Processor} from third-party
  # applications.
  #
  # The Collector features a very simple DSL to implement custom collector
  # plugins. Simply create a file with a +.rb+ file extension and a +collect+
  # block:
  #
  #   collect do
  #     File.open('/proc/foobar').each do |line|
  #       parse(line)
  #     end
  #   end
  #
  # Copy this plugin to a location of your choice and set
  # +collector.load_paths+ in your configuration to point to the containing
  # directory.
  #
  # The collector thread will then call this plugin once per resolution tick.
  # See {file:doc/configuration.md} for details.
  #
  # Custom intervals can also be specified for high frequency sampling:
  #
  #   collect(0.3) do
  #     File.open('/proc/foobar').each do |line|
  #       parse(line)
  #     end
  #   end
  #
  class Collector
    # The configuration object.
    # @return [Section]
    attr_reader :config

    # The client object.
    # @return [Ganymed::Client]
    attr_reader :client

    # Create a new collector instance and initialize all configured collectors.
    #
    # @param [Section] config  The configuration object.
    def initialize(config)
      @config = config
      @client = Client.new(@config.client)
      load_collectors
    end

    # @private
    def load_collectors
      collectors.each do |file|
        name = File.basename(file, '.rb')
        config = @config.collectors[name.to_sym] || Section.new
        config[:resolution] = @config.resolution

        log.debug("loading collector #{name} from #{file}")
        Plugin.new(config, @client).from_file(file).tap do |collector|
          log.info("collecting #{name} metrics every #{collector.interval} seconds")
          collector.run
        end
      end
    end

    # @private
    def collectors
      load_paths = [@config.collector.load_paths.tap{}].flatten.compact
      load_paths << File.join(Ganymed::LIB_DIR, 'ganymed/collectors')

      [].tap do |files|
        load_paths.each do |load_path|
          log.debug("loading collectors from #{load_path}")
          Dir[File.join(load_path, '*.rb')].each do |file|
            files << file
          end
        end
      end.sort.uniq
    end

    ##
    # Base class and DSL for {Collector} plugins.
    #
    class Plugin

      # The configuration object.
      # @return [Section]
      attr_accessor :config

      # Plugin interval
      # @return [Fixnum,Float]
      attr_accessor :interval

      # Processor socket.
      # @return [Ganymed::Client::ProcessorSocket]
      attr_accessor :processor

      # Sampler socket.
      # @return [Ganymed::Client::SamplerSocket]
      attr_accessor :sampler

      # Create a new plugin instance.
      #
      # @param [Section] config  The configuration object.
      # @param [Ganymed::Client] client  A client instance.
      def initialize(config, client)
        @config, @client = config, client
        @processor = @client.processor
        @sampler = @client.sampler
      end

      # Set the block used to collect metrics with this plugin.
      #
      # @param [Fixnum,Float] interval  Custom plugin interval.
      # @return [void]
      def collect(interval=nil, &block)
        @interval ||= interval
        @interval ||= @config.interval.tap{}
        @interval ||= @config.resolution
        @collector = Proc.new(&block)
      end

      # Start the EventMachine timer to collect metrics at the specified
      # interval.
      # @return [void]
      def run
        EM.add_periodic_timer(@interval) do
          EM.defer { collect! }
        end
      end

      # @private
      def collect!
        begin
          @collector.call if @collector.is_a?(Proc)
        rescue Exception => exc
          log.exception(exc)
        end
      end

      # Loads a given ruby file, and runs instance_eval against it in the
      # context of the current object.
      #
      # @param [String] filename  The absolute path to a plugin file.
      # @return [Plugin] The instance for easy method chaining.
      def from_file(filename)
        if File.exists?(filename) && File.readable?(filename)
          self.instance_eval(IO.read(filename), filename, 1)
        else
          raise IOError, "Cannot open or read #{filename}!"
        end
        self
      end
    end
  end
end