# frozen_string_literal: true

require 'io/console'
require 'forwardable'
require 'monitor'
require 'tty-cursor'
require 'tty-screen'
require 'strings-ansi'
require 'unicode/display_width'

require_relative 'progressbar/configuration'
require_relative 'progressbar/formatter'
require_relative 'progressbar/meter'
require_relative 'progressbar/version'

module TTY
  # Used for creating terminal progress bar
  #
  # @api public
  class ProgressBar
    extend Forwardable
    include MonitorMixin

    ECMA_CSI = "\e[".freeze

    CURSOR_LOCK = Monitor.new

    attr_accessor :format

    attr_reader :current

    attr_reader :start_at

    attr_reader :row

    def_delegators :@configuration, :total, :width, :no_width,
                   :complete, :incomplete, :head, :hide_cursor, :clear,
                   :output, :frequency, :interval, :inset, :width=

    def_delegators :@meter, :rate, :mean_rate

    def_delegator :@formatter, :use

    # Determine terminal width
    #
    # @return [Integer]
    #
    # @api public
    def self.max_columns
      TTY::Screen.width
    end

    # Determine the monospace display width of a string
    #
    # @param [String] value
    #   the value to determine width of
    #
    # @return [Integer]
    #
    # @api public
    def self.display_columns(value)
      Unicode::DisplayWidth.of(Strings::ANSI.sanitize(value))
    end

    # Create progress bar
    #
    # @param [String] format
    #   the tokenized string that displays the output
    #
    # @param [Hash] options
    # @option options [Numeric] :total
    #   the total number of steps to completion
    # @option options [Numeric] :width
    #   the maximum width for the bars display including
    #   all formatting options
    # @option options [Boolean] :no_width
    #   true when progression is unknown defaulting to false
    # @option options [Boolean] :clear
    #   whether or not to clear the progress line
    # @option options [Boolean] :hide_cursor
    #   display or hide cursor
    # @option options [Object] :output
    #   the object that responds to print call defaulting to stderr
    # @option options [Number] :frequency
    #   the frequency with which to display bars
    # @option options [Number] :interval
    #   the period for sampling of speed measurement
    #
    # @api public
    def initialize(format, options = {})
      super()
      @format = format
      if format.is_a?(Hash)
        raise ArgumentError, "Expected bar formatting string, " \
                             "got `#{format}` instead."
      end
      @configuration = TTY::ProgressBar::Configuration.new(options)
      yield @configuration if block_given?

      @formatter = TTY::ProgressBar::Formatter.new
      @meter     = TTY::ProgressBar::Meter.new(interval)
      @callbacks = Hash.new { |h, k| h[k] = [] }

      @formatter.load
      reset

      @first_render = true
      @multibar     = nil
      @row          = nil
    end

    # Reset progress to default configuration
    #
    # @api public
    def reset
      @width             = 0 if no_width
      @render_period     = frequency == 0 ? 0 : 1.0 / frequency
      @current           = 0
      @last_render_time  = Time.now
      @last_render_width = 0
      @done              = false
      @stopped           = false
      @start_at          = Time.now
      @started           = false
      @tokens            = {}

      @meter.clear
    end

    # Attach this bar to multi bar
    #
    # @param [TTY::ProgressBar::Multi] multibar
    #   the multibar under which this bar is registered
    #
    # @api private
    def attach_to(multibar)
      @multibar = multibar
    end

    # Start progression by drawing bar and setting time
    #
    # @api public
    def start
      synchronize do
        @started  = true
        @start_at = Time.now
        @meter.start
      end

      advance(0)
    end

    # Advance the progress bar
    #
    # @param [Object|Number] progress
    #
    # @api public
    def advance(progress = 1, tokens = {})
      return if done?

      synchronize do
        emit(:progress, progress)
        if progress.respond_to?(:to_hash)
          tokens, progress = progress, 1
        end
        @start_at  = Time.now if @current.zero? && !@started
        @current  += progress
        @tokens    = tokens
        @meter.sample(Time.now, progress)

        if !no_width && @current >= total
          finish && return
        end

        now = Time.now
        return if (now - @last_render_time) < @render_period
        render
      end
    end

    # Iterate over collection either yielding computation to block
    # or provided Enumerator. If the bar's `total` was not set,
    # it would be taken from `collection.count`, otherwise previously
    # set `total` would be used. This allows using the progressbar
    # with infinite, lazy, or slowly-calculated enumerators.
    #
    # @note
    #   If `total` is set, iteration will NOT stop after this number of
    #   iterations, only when provided Enumerable is finished. It may
    #   be convenient in "unsure number of iterations" situations
    #   (like downloading in chunks, when server may eventually send
    #   more chunks than predicted), but be careful to not pass infinite
    #   enumerators without previosly doing `.take(some_finite_number)`
    #   on them.
    #
    # @example
    #   bar.iterate(30.times) { ... }
    #
    # @param [Enumerable] collection
    #   the collection to iterate over
    #
    # @param [Integer] progress
    #   the amount to move progress bar by
    #
    # @return [Enumerator]
    #
    # @api public
    def iterate(collection, progress = 1, &block)
      update(total: collection.count * progress) unless total
      progress_enum = Enumerator.new do |iter|
        collection.each do |elem|
          advance(progress)
          iter.yield(elem)
        end
      end
      block_given? ? progress_enum.each(&block) : progress_enum
    end

    # Update configuration options for this bar
    #
    # @param [Hash[Symbol]] options
    #   the configuration options to update
    #
    # @api public
    def update(options = {})
      synchronize do
        options.each do |name, val|
          if @configuration.respond_to?("#{name}=")
            @configuration.public_send("#{name}=", val)
          end
        end
      end
    end

    # Advance the progress bar to the updated value
    #
    # @param [Number] value
    #   the desired value to updated to
    #
    # @api public
    def current=(value)
      value = [0, [value, total].min].max
      advance(value - @current)
    end

    # Advance the progress bar to an exact ratio.
    # The target value is set to the closest available value.
    #
    # @param [Float] value
    #   the ratio between 0 and 1 inclusive
    #
    # @api public
    def ratio=(value)
      target = (value * total).floor
      advance(target - @current)
    end

    # Ratio of completed over total steps
    #
    # @return [Float]
    #
    # @api public
    def ratio
      synchronize do
        proportion = total > 0 ? (@current.to_f / total) : 0
        [[proportion, 0].max, 1].min
      end
    end

    # Render progress to the output
    #
    # @api private
    def render
      return if done?
      if hide_cursor && @last_render_width == 0 && !(@current >= total)
        write(TTY::Cursor.hide)
      end

      if @multibar
        characters_in = @multibar.line_inset(self)
        update(inset: self.class.display_columns(characters_in))
      end

      formatted = @formatter.decorate(self, @format)
      @tokens.each do |token, val|
        formatted = formatted.gsub(":#{token}", val)
      end

      padded = padout(formatted)

      write(padded, true)

      @last_render_time  = Time.now
      @last_render_width = self.class.display_columns(formatted)
    end

    # Move cursor to a row of the current bar if the bar is rendered
    # under a multibar. Otherwise, do not move and yield on current row.
    #
    # @api private
    def move_to_row
      if @multibar
        CURSOR_LOCK.synchronize do
          if @first_render
            @row = @multibar.next_row
            yield if block_given?
            output.print "\n"
            @first_render = false
          else
            lines_up = (@multibar.rows + 1) - @row
            output.print TTY::Cursor.save
            output.print TTY::Cursor.up(lines_up)
            yield if block_given?
            output.print TTY::Cursor.restore
          end
        end
      else
        yield if block_given?
      end
    end

    # Write out to the output
    #
    # @param [String] data
    #
    # @api private
    def write(data, clear_first = false)
      return unless tty? # write only to terminal

      move_to_row do
        output.print(TTY::Cursor.column(1)) if clear_first
        characters_in = @multibar.line_inset(self) if @multibar
        output.print("#{characters_in}#{data}")
        output.flush
      end
    end

    # Resize progress bar with new configuration
    #
    # @param [Integer] new_width
    #   the new width for the bar display
    #
    # @api public
    def resize(new_width = nil)
      return if done?
      synchronize do
        clear_line
        if new_width
          self.width = new_width
        end
      end
    end

    # End the progress
    #
    # @api public
    def finish
      return if done?
      @current = total unless no_width
      render
      clear ? clear_line : write("\n", false)
    ensure
      @meter.clear
      @done = true

      # reenable cursor if it is turned off
      if hide_cursor && @last_render_width != 0
        write(TTY::Cursor.show, false)
      end

      emit(:done)
    end

    # Stop and cancel the progress at the current position
    #
    # @api public
    def stop
      # reenable cursor if it is turned off
      if hide_cursor && @last_render_width != 0
        write(TTY::Cursor.show, false)
      end
      return if done?
      render
      clear ? clear_line : write("\n", false)
    ensure
      @meter.clear
      @stopped = true
      emit(:stopped)
    end

    # Clear current line
    #
    # @api public
    def clear_line
      output.print(ECMA_CSI + '0m' + TTY::Cursor.clear_line)
    end

    # Check if progress is finised
    #
    # @return [Boolean]
    #   true when progress finished, false otherwise
    #
    # @api public
    def complete?
      @done
    end

    # Check if progress is stopped
    #
    # @return [Boolean]
    #
    # @api public
    def stopped?
      @stopped
    end

    # Check if progress is finished or stopped
    #
    # @return [Boolean]
    #
    # @api public
    def done?
      @done || @stopped
    end

    # Register callback with this bar
    #
    # @param [Symbol] name
    #   the name for the event to listen for, e.i. :complete
    #
    # @return [self]
    #
    # @api public
    def on(name, &callback)
      synchronize do
        @callbacks[name] << callback
      end
      self
    end

    # Log message above the current progress bar
    #
    # @param [String] message
    #   the message to log out
    #
    # @api public
    def log(message)
      sanitized_message = message.gsub(/\r|\n/, ' ')
      if done?
        write(sanitized_message + "\n", false)
        return
      end
      sanitized_message = padout(sanitized_message)

      write(sanitized_message + "\n", true)
      render
    end

    # Show bar format
    #
    # @return [String]
    #
    # @api public
    def to_s
      @format.to_s
    end

    # Inspect bar properties
    #
    # @return [String]
    #
    # @api public
    def inspect
      "#<#{self.class.name} " \
      "@format=\"#{format}\", " \
      "@current=\"#{@current}\", " \
      "@total=\"#{total}\", " \
      "@width=\"#{width}\", " \
      "@complete=\"#{complete}\", " \
      "@head=\"#{head}\", " \
      "@incomplete=\"#{incomplete}\", " \
      "@interval=\"#{interval}\">"
    end

    private

    # Pad message out with spaces
    #
    # @api private
    def padout(message)
      message_length = self.class.display_columns(message)

      if @last_render_width > message_length
        remaining_width = @last_render_width - message_length
        message += ' ' * remaining_width
      end
      message
    end

    # Emit callback by name
    #
    # @param [Symbol]
    #   the event name
    #
    # @api private
    def emit(name, *args)
      @callbacks[name].each do |callback|
        callback.call(*args)
      end
    end

    # Check if IO is attached to a terminal
    #
    # return [Boolean]
    #
    # @api public
    def tty?
      output.respond_to?(:tty?) && output.tty?
    end
  end # ProgressBar
end # TTY


module Delayed

  class DeserializationError < StandardError
  end

  # A job object that is persisted to the database.
  # Contains the work object as a YAML field.
  class Job < ActiveRecord::Base
    MAX_ATTEMPTS = 25
    MAX_RUN_TIME = 4.hours
    set_table_name :delayed_jobs

    # By default failed jobs are destroyed after too many attempts.
    # If you want to keep them around (perhaps to inspect the reason
    # for the failure), set this to false.
    cattr_accessor :destroy_failed_jobs
    self.destroy_failed_jobs = true

    # Every worker has a unique name which by default is the pid of the process.
    # There are some advantages to overriding this with something which survives worker retarts:
    # Workers can safely resume working on tasks which are locked by themselves. The worker will assume that it crashed before.
    cattr_accessor :worker_name
    self.worker_name = "host:#{Socket.gethostname} pid:#{Process.pid}" rescue "pid:#{Process.pid}"

    NextTaskSQL         = '(run_at <= ? AND (locked_at IS NULL OR locked_at < ?) OR (locked_by = ?)) AND failed_at IS NULL'
    NextTaskOrder       = 'priority DESC, run_at ASC'

    ParseObjectFromYaml = /\!ruby\/\w+\:([^\s]+)/

    cattr_accessor :min_priority, :max_priority
    self.min_priority = nil
    self.max_priority = nil

    # When a worker is exiting, make sure we don't have any locked jobs.
    def self.clear_locks!
      update_all("locked_by = null, locked_at = null", ["locked_by = ?", worker_name])
    end

    def failed?
      failed_at
    end
    alias_method :failed, :failed?

    def payload_object
      @payload_object ||= deserialize(self['handler'])
    end

    def name
      @name ||= begin
        payload = payload_object
        if payload.respond_to?(:display_name)
          payload.display_name
        else
          payload.class.name
        end
      end
    end

    def payload_object=(object)
      self['handler'] = object.to_yaml
    end

    # Reschedule the job in the future (when a job fails).
    # Uses an exponential scale depending on the number of failed attempts.
    def reschedule(message, backtrace = [], time = nil)
      if self.attempts < MAX_ATTEMPTS
        time ||= Job.db_time_now + (attempts ** 4) + 5

        self.attempts    += 1
        self.run_at       = time
        self.last_error   = message + "\n" + backtrace.join("\n")
        self.unlock
        save!
      else
        logger.info "* [JOB] PERMANENTLY removing #{self.name} because of #{attempts} consequetive failures."
        destroy_failed_jobs ? destroy : update_attribute(:failed_at, Delayed::Job.db_time_now)
      end
    end

    # Try to run one job. Returns true/false (work done/work failed) or nil if job can't be locked.
    def run_with_lock(max_run_time, worker_name)
      logger.info "* [JOB] aquiring lock on #{name}"
      unless lock_exclusively!(max_run_time, worker_name)
        # We did not get the lock, some other worker process must have
        logger.warn "* [JOB] failed to aquire exclusive lock for #{name}"
        return nil # no work done
      end

      begin
        runtime =  Benchmark.realtime do
          invoke_job # TODO: raise error if takes longer than max_run_time
          destroy
        end
        # TODO: warn if runtime > max_run_time ?
        logger.info "* [JOB] #{name} completed after %.4f" % runtime
        return true  # did work
      rescue Exception => e
        reschedule e.message, e.backtrace
        log_exception(e)
        return false  # work failed
      end
    end

    # Add a job to the queue
    def self.enqueue(*args, &block)
      object = block_given? ? EvaledJob.new(&block) : args.shift

      unless object.respond_to?(:perform) || block_given?
        raise ArgumentError, 'Cannot enqueue items which do not respond to perform'
      end

      priority = args.first || 0
      run_at   = args[1]

      Job.create(:payload_object => object, :priority => priority.to_i, :run_at => run_at)
    end

    # Find a few candidate jobs to run (in case some immediately get locked by others).
    # Return in random order prevent everyone trying to do same head job at once.
    def self.find_available(limit = 5, max_run_time = MAX_RUN_TIME)

      time_now = db_time_now

      sql = NextTaskSQL.dup

      conditions = [time_now, time_now - max_run_time, worker_name]

      if self.min_priority
        sql << ' AND (priority >= ?)'
        conditions << min_priority
      end

      if self.max_priority
        sql << ' AND (priority <= ?)'
        conditions << max_priority
      end

      conditions.unshift(sql)

      records = ActiveRecord::Base.silence do
        find(:all, :conditions => conditions, :order => NextTaskOrder, :limit => limit)
      end

      records.sort_by { rand() }
    end

    # Run the next job we can get an exclusive lock on.
    # If no jobs are left we return nil
    def self.reserve_and_run_one_job(max_run_time = MAX_RUN_TIME)

      # We get up to 5 jobs from the db. In case we cannot get exclusive access to a job we try the next.
      # this leads to a more even distribution of jobs across the worker processes
      find_available(5, max_run_time).each do |job|
        t = job.run_with_lock(max_run_time, worker_name)
        return t unless t == nil  # return if we did work (good or bad)
      end

      nil # we didn't do any work, all 5 were not lockable
    end

    # Lock this job for this worker.
    # Returns true if we have the lock, false otherwise.
    def lock_exclusively!(max_run_time, worker = worker_name)
      now = self.class.db_time_now
      affected_rows = if locked_by != worker
        # We don't own this job so we will update the locked_by name and the locked_at
        self.class.update_all(["locked_at = ?, locked_by = ?", now, worker], ["id = ? and (locked_at is null or locked_at < ?)", id, (now - max_run_time.to_i)])
      else
        # We already own this job, this may happen if the job queue crashes.
        # Simply resume and update the locked_at
        self.class.update_all(["locked_at = ?", now], ["id = ? and locked_by = ?", id, worker])
      end
      if affected_rows == 1
        self.locked_at    = now
        self.locked_by    = worker
        return true
      else
        return false
      end
    end

    # Unlock this job (note: not saved to DB)
    def unlock
      self.locked_at    = nil
      self.locked_by    = nil
    end

    # This is a good hook if you need to report job processing errors in additional or different ways
    def log_exception(error)
      logger.error "* [JOB] #{name} failed with #{error.class.name}: #{error.message} - #{attempts} failed attempts"
      logger.error(error)
    end

    # Do num jobs and return stats on success/failure.
    # Exit early if interrupted.
    def self.work_off(num = 100)
      success, failure = 0, 0

      num.times do
        case self.reserve_and_run_one_job
        when true
            success += 1
        when false
            failure += 1
        else
          break  # leave if no work could be done
        end
        break if $exit # leave if we're exiting
      end

      return [success, failure]
    end

    # Moved into its own method so that new_relic can trace it.
    def invoke_job
      payload_object.perform
    end

  private

    def deserialize(source)
      handler = YAML.load(source) rescue nil

      unless handler.respond_to?(:perform)
        if handler.nil? && source =~ ParseObjectFromYaml
          handler_class = $1
        end
        attempt_to_load(handler_class || handler.class)
        handler = YAML.load(source)
      end

      return handler if handler.respond_to?(:perform)

      raise DeserializationError,
        'Job failed to load: Unknown handler. Try to manually require the appropiate file.'
    rescue TypeError, LoadError, NameError => e
      raise DeserializationError,
        "Job failed to load: #{e.message}. Try to manually require the required file."
    end

    # Constantize the object so that ActiveSupport can attempt
    # its auto loading magic. Will raise LoadError if not successful.
    def attempt_to_load(klass)
       klass.constantize
    end

    # Get the current time (GMT or local depending on DB)
    # Note: This does not ping the DB to get the time, so all your clients
    # must have syncronized clocks.
    def self.db_time_now
      (ActiveRecord::Base.default_timezone == :utc) ? Time.now.utc : Time.zone.now
    end

  protected

    def before_save
      self.run_at ||= self.class.db_time_now
    end

  end

  class EvaledJob
    def initialize
      @job = yield
    end

    def perform
      eval(@job)
    end
  end
end


require 'tracery'
require 'json'

module Elephrame
  module Trace
    include Tracery

    # grammar is a hash { FILENAME => TRACERY RULES }
    attr_accessor :grammar

    ##
    # loads all of our tracery files into our +files+ hash
    # if a file is named 'default' then we load that into +grammar+
    #
    # @param dir [String] path to the directory containing the tracery rules

    def setup_tracery dir_path
      raise "Provided path not a directory" unless Dir.exist?(dir_path)

      @grammar = {}
      Dir.open(dir_path) do |dir|
        dir.each do |file|
          # skip our current and parent dir
          next if file =~ /^\.\.?$/

          # read the rule file into the files hash
          @grammar[file.split('.').first] =
            createGrammar(JSON.parse(File.read("#{dir_path}/#{file}")))
        end
      end

      # go ahead and makes a default mention-handler
      #  if we have a reply rule file
      unless @grammar['reply'].nil?
        on_reply { |bot|
          bot.reply_with_mentions('#default#', rules: 'reply')
        }
      end
    end

    ##
    # a shortcut fuction for expanding text with tracery before posting
    #
    # @param text [String] the tracery text to expand before posting
    # @param options [Hash] a hash of arguments to pass to post
    # @option options rules [String] the grammar rules to load
    # @option options visibility [String] visibility level
    # @option options spoiler [String] text to use as content warning
    # @option options reply_id [String] id of post to reply to
    # @option options hide_media [Bool] should we hide media?
    # @option options media [Array<String>] array of file paths

    def expand_and_post(text, *options)
      opts = Hash[*options]
      rules = opts.fetch(:rules, 'default')
      actually_post(@grammar[rules].flatten(text),
                    **opts.reject {|k|
                      k == :rules
                    })
    end

    alias_method :post, :expand_and_post
  end
end


module Elephrame
  module AllInteractions
    include Elephrame::Reply
    attr :on_fave, :on_boost, :on_follow

    ##
    # Sets on_fave equal to +block+

    def on_fave &block
      @on_fave = block
    end

    ##
    # Sets on_boost to +block+

    def on_boost &block
      @on_boost = block
    end

    ##
    # Sets on_follow to +block+

    def on_follow &block
      @on_follow = block
    end

    ##
    # Starts a loop that checks for any notifications for the authenticated
    # user, running the appropriate stored proc when needed

    def run_interact
      @streamer.user do |update|
        if update.kind_of? Mastodon::Notification

          case update.type

          when 'mention'

            # this makes it so .content calls strip instead
            update.status.class.module_eval { alias_method :content, :strip } if @strip_html
            store_mention_data update.status
            @on_reply.call(self, update.status) unless @on_reply.nil?

          when 'reblog'
            @on_boost.call(self, update) unless @on_boost.nil?

          when 'favourite'
            @on_fave.call(self, update) unless @on_fave.nil?

          when 'follow'
            @on_follow.call(self, update) unless @on_follow.nil?

          end
        end
      end
    end

    alias_method :run, :run_interact
  end
end


module Elephrame
  module Reply
    attr :on_reply, :mention_data

    ##
    # Sets on_reply equal to +block+

    def on_reply &block
      @on_reply = block
    end

    ##
    # Replies to the last mention the bot recieved using the mention's
    #   visibility and spoiler with +text+
    #
    # Automatically includes an @ for the account that mentioned the bot.
    #   Does not include any other @. See +reply_with_mentions+ if you want
    #   to automatically include all mentions
    #
    # @param text [String] text to post as a reply
    # @param options [Hash] a hash of arguments to pass to post, overrides
    #   duplicating settings from last mention

    def reply(text, *options)
      options = Hash[*options]

      post("@#{@mention_data[:account].acct} #{text}",
           **@mention_data.merge(options).reject { |k|
             k == :mentions or k == :account
           })
    end

    ##
    # Replies to the last post and tags everyone who was mentioned
    #  (this function respects #NoBot)
    #
    # @param text [String] text to post as a reply
    # @param options [Hash] arguments to pass to post, overrides settings from
    #  last mention

    def reply_with_mentions(text, *options)
      # build up a string of all accounts mentioned in the post
      #  unless that account is our own, or the tagged account
      #  has #NoBot
      mentions = @mention_data[:mentions].collect do |m|
        "@#{m.acct}" unless m.acct == @username or no_bot? m.id
      end.join ' '

      reply("#{mentions.strip} #{text}", *options)
    end

    ##
    # Starts a loop that checks for mentions from the authenticated user account
    # running a supplied block or, if a block is not provided, on_reply

    def run_reply
      @streamer.user do |update|
        next unless update.kind_of? Mastodon::Notification and update.type == 'mention'

        # this makes it so .content calls strip instead
        update.status.class.module_eval { alias_method :content, :strip } if @strip_html

        store_mention_data update.status

        if block_given?
          yield(self, update.status)
        else
          @on_reply.call(self, update.status)
        end
      end
    end

    alias_method :run, :run_reply

    private

    ##
    # Stores select data about a post into a hash for later use
    #
    # @param mention [Mastodon::Status] the most recent mention the bot received

    def store_mention_data(mention)
      @mention_data = {
        reply_id: mention.id,
        visibility: mention.visibility,
        spoiler: mention.spoiler_text,
        hide_media: mention.sensitive?,
        mentions: mention.mentions,
        account: mention.account
      }
    end
  end
end


require_relative 'reply'
require_relative 'command'
require_relative 'interaction'
require_relative 'watcher'

module Elephrame
  module Streaming
    attr :streamer

    ##
    # Creates the stream client

    def setup_streaming
      stream_uri = @client.instance()
                     .attributes['urls']['streaming_api'].gsub(/^wss?/, 'https')
      @streamer = Mastodon::Streaming::Client.new(base_url: stream_uri,
                                                  bearer_token: ENV['TOKEN'])
    end

  end
end


module Elephrame
  module Command
    include Elephrame::Reply

    attr_reader :commands, :prefix
    attr :cmd_hash, :cmd_regex, :not_found

    ##
    # Initializes the +commands+ array, +cmd_hash+
    #
    # @param prefix [String] sets the command prefix, defaults to '!'
    # @param usage [String] the response to the help command
    #

    def setup_command(prefix = '!', usage = nil)
        @commands = []
        @cmd_hash = {}

        set_prefix prefix
        set_help usage unless usage.nil?
    end

    ##
    # sets the prefix for commands
    #
    # @param pf [String] the prefix

    def set_prefix pf
      @prefix = pf
    end

    ##
    # Shortcut method to provide usage docs in response to help command
    #
    # @param usage [String]

    def set_help usage
      add_command 'help' do |bot, content, status|
        bot.reply("#{usage}")
      end
    end

    ##
    # Adds the command and block into the bot to process later
    # also sets up the command regex
    #
    # @param cmd [String] a command to add
    # @param block [Proc] the code to execute when +cmd+ is recieved

    def add_command cmd, &block
      @commands.append cmd unless @commands.include? cmd
      @cmd_hash[cmd.to_sym] = block

      # build up our regex (this regex should be fine, i guess :shrug:)
      @cmd_regex = /\A#{@prefix}(?<cmd>#{@commands.join('|')})\b(?<data>.*)/m
    end

    ##
    # What to do if we don't match anything
    #
    # @param block [Proc] a block to run when we don't match a command

    def if_not_found &block
      @not_found = block
    end

    ##
    # Starts loop to process any mentions, running command procs set up earlier
    #
    # If a block is passed to this function it gets ran when no commands
    # get matched. Otherwise the framework checks if +not_found+ exists
    # and runs it

    def run_commands
      @streamer.user do |update|
        next unless update.kind_of? Mastodon::Notification and update.type == 'mention'

        # set up the status to strip html, if needed
        update.status.class
          .module_eval { alias_method :content, :strip } if @strip_html
        store_mention_data update.status

        # strip our username out of the status
        post = update.status.content.gsub(/@#{@username} /, '')

        # see if the post matches our regex, running the stored proc if it does
        matches = @cmd_regex.match(post)

        unless matches.nil?
          @cmd_hash[matches[:cmd].to_sym]
            .call(self,
                  matches[:data].strip,
                  update.status)
        else

          if block_given?
            yield(self, update.status)
          else
            @not_found.call(self, update.status) unless @not_found.nil?
          end

        end
      end
    end

    alias_method :run, :run_commands
  end
end


#
# Copyright 2015-2016, Noah Kantrowitz
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require 'pathname'

module PoiseService
  # Utility methods for PoiseService.
  #
  # @api public
  # @since 1.0.0
  module Utils
    # Methods are also available as module-level methods as well as a mixin.
    extend self

    # Common segments to ignore
    COMMON_SEGMENTS = %w{var www current etc}.inject({}) {|memo, seg| memo[seg] = true; memo }

    # Parse the service name from a path. Look at the last component of the
    # path, ignoring some common names.
    #
    # @param path [String] Path to parse.
    # @return [String]
    # @example
    #   attribute(:service_name, kind_of: String, default: lazy { PoiseService::Utils.parse_service_name(path) })
    def parse_service_name(path)
      parts = Pathname.new(path).each_filename.to_a.reverse!
      # Find the last segment not in common segments, fall back to the last segment.
      parts.find {|seg| !COMMON_SEGMENTS[seg] } || parts.first
    end
  end
end


module Net
  # Make Socket calls resilient by adding timeouts, retries and specific
  # exception categories
  #
  # TCP Client with:
  # * Connection Timeouts
  #   Ability to timeout if a connect does not complete within a reasonable time
  #   For example, this can occur when the server is turned off without shutting down
  #   causing clients to hang creating new connections
  #
  # * Automatic retries on startup connection failure
  #   For example, the server is being restarted while the client is starting
  #   Gives the server a few seconds to restart to
  #
  # * Automatic retries on active connection failures
  #   If the server is restarted during
  #
  # Connection and Read Timeouts are fully configurable
  #
  # Raises Net::TCPClient::ConnectionTimeout when the connection timeout is exceeded
  # Raises Net::TCPClient::ReadTimeout when the read timeout is exceeded
  # Raises Net::TCPClient::ConnectionFailure when a network error occurs whilst reading or writing
  #
  # Note: Only the following methods currently have auto-reconnect enabled:
  #  * read
  #  * write
  #
  # Future:
  # * Add auto-reconnect feature to sysread, syswrite, etc...
  # * To be a drop-in replacement to TCPSocket should also need to implement the
  #   following TCPSocket instance methods:  :addr, :peeraddr
  #
  # Design Notes:
  # * Does not inherit from Socket or TCP Socket because the socket instance
  #   has to be completely destroyed and recreated after a connection failure
  #
  class TCPClient
    include SemanticLogger::Loggable if defined?(SemanticLogger::Loggable)

    attr_accessor :connect_timeout, :read_timeout, :write_timeout,
      :connect_retry_count, :connect_retry_interval, :retry_count,
      :policy, :close_on_error, :buffered, :ssl, :proxy_server, :keepalive
    attr_reader :servers, :address, :socket, :ssl_handshake_timeout

    # Supports embedding user supplied data along with this connection
    # such as sequence number and other connection specific information
    # Not used or modified by TCPClient
    attr_accessor :user_data

    @reconnect_on_errors = [
      Errno::ECONNABORTED,
      Errno::ECONNREFUSED,
      Errno::ECONNRESET,
      Errno::EHOSTUNREACH,
      Errno::EIO,
      Errno::ENETDOWN,
      Errno::ENETRESET,
      Errno::EPIPE,
      Errno::ETIMEDOUT,
      EOFError,
      Net::TCPClient::ConnectionTimeout,
      IOError
    ]

    # Return the array of errors that will result in an automatic connection retry
    #  To add any additional errors to the standard list:
    #    Net::TCPClient.reconnect_on_errors << Errno::EPROTO
    def self.reconnect_on_errors
      @reconnect_on_errors
    end

    # Create a connection, call the supplied block and close the connection on
    # completion of the block
    #
    # See #initialize for the list of parameters
    #
    # Example
    #   Net::TCPClient.connect(
    #     server:                 'server:3300',
    #     connect_retry_interval: 0.1,
    #     connect_retry_count:    5
    #   ) do |client|
    #     client.retry_on_connection_failure do
    #       client.write('Update the database')
    #     end
    #     response = client.read(20)
    #     puts "Received: #{response}"
    #   end
    #
    def self.connect(params={})
      begin
        connection = self.new(params)
        yield(connection)
      ensure
        connection.close if connection
      end
    end

    # Create a new TCP Client connection
    #
    # Parameters:
    #   :server [String]
    #     URL of the server to connect to with port number
    #     'localhost:2000'
    #     '192.168.1.10:80'
    #
    #   :servers [Array of String]
    #     Array of URL's of servers to connect to with port numbers
    #     ['server1:2000', 'server2:2000']
    #
    #     The second server will only be attempted once the first server
    #     cannot be connected to or has timed out on connect
    #     A read failure or timeout will not result in switching to the second
    #     server, only a connection failure or during an automatic reconnect
    #
    #   :connect_timeout [Float]
    #     Time in seconds to timeout when trying to connect to the server
    #     A value of -1 will cause the connect wait time to be infinite
    #     Default: 10 seconds
    #
    #   :read_timeout [Float]
    #     Time in seconds to timeout on read
    #     Can be overridden by supplying a timeout in the read call
    #     Default: 60
    #
    #   :write_timeout [Float]
    #     Time in seconds to timeout on write
    #     Can be overridden by supplying a timeout in the write call
    #     Default: 60
    #
    #   :buffered [true|false]
    #     Whether to use Nagle's Buffering algorithm (http://en.wikipedia.org/wiki/Nagle's_algorithm)
    #     Recommend disabling for RPC style invocations where we don't want to wait for an
    #     ACK from the server before sending the last partial segment
    #     Buffering is recommended in a browser or file transfer style environment
    #     where multiple sends are expected during a single response.
    #     Also sets sync to true if buffered is false so that all data is sent immediately without
    #     internal buffering.
    #     Default: true
    #
    #   :keepalive [true|false]
    #     Makes the OS check connections even when not in use, so that failed connections fail immediately
    #     upon use instead of possibly taking considerable time to fail.
    #     Default: true
    #
    #   :connect_retry_count [Fixnum]
    #     Number of times to retry connecting when a connection fails
    #     Default: 10
    #
    #   :connect_retry_interval [Float]
    #     Number of seconds between connection retry attempts after the first failed attempt
    #     Default: 0.5
    #
    #   :retry_count [Fixnum]
    #     Number of times to retry when calling #retry_on_connection_failure
    #     This is independent of :connect_retry_count which still applies with
    #     connection failures. This retry controls upto how many times to retry the
    #     supplied block should a connection failure occur during the block
    #     Default: 3
    #
    #   :on_connect [Proc]
    #     Directly after a connection is established and before it is made available
    #     for use this Block is invoked.
    #     Typical Use Cases:
    #     - Initialize per connection session sequence numbers.
    #     - Pass authentication information to the server.
    #     - Perform a handshake with the server.
    #
    #   :policy [Symbol|Proc]
    #     Specify the policy to use when connecting to servers.
    #       :ordered
    #         Select a server in the order supplied in the array, with the first
    #         having the highest priority. The second server will only be connected
    #         to if the first server is unreachable
    #       :random
    #         Randomly select a server from the list every time a connection
    #         is established, including during automatic connection recovery.
    #       :ping_time
    #         FUTURE - Not implemented yet - Pull request anyone?
    #         The server with the lowest ping time will be tried first
    #       Proc:
    #         When a Proc is supplied, it will be called passing in the list
    #         of servers. The Proc must return one server name
    #           Example:
    #             :policy => Proc.new do |servers|
    #               servers.last
    #             end
    #       Default: :ordered
    #
    #   :close_on_error [True|False]
    #     To prevent the connection from going into an inconsistent state
    #     automatically close the connection if an error occurs
    #     This includes a Read Timeout
    #     Default: true
    #
    #   :proxy_server [String]
    #     The host name and port in the form of 'host_name:1234' to forward
    #     socket connections though.
    #     Default: nil ( none )
    #
    #   SSL Options
    #   :ssl [true|false|Hash]
    #      true:  SSL is enabled using the SSL context defaults.
    #      false: SSL is not used.
    #      Hash:
    #        Keys from OpenSSL::SSL::SSLContext:
    #          ca_file, ca_path, cert, cert_store, ciphers, key, ssl_timeout, ssl_version
    #          verify_callback, verify_depth, verify_mode
    #        handshake_timeout: [Float]
    #          The number of seconds to timeout the SSL Handshake.
    #          Default: connect_timeout
    #      Default: false.
    #        See OpenSSL::SSL::SSLContext::DEFAULT_PARAMS for the defaults.
    #
    # Example:
    #   client = Net::TCPClient.new(
    #     server:                 'server:3300',
    #     connect_retry_interval: 0.1,
    #     connect_retry_count:    5
    #   )
    #
    #   client.retry_on_connection_failure do
    #     client.write('Update the database')
    #   end
    #
    #   # Read upto 20 characters from the server
    #   response = client.read(20)
    #
    #   puts "Received: #{response}"
    #   client.close
    #
    # SSL Example:
    #   client = Net::TCPClient.new(
    #     server:                 'server:3300',
    #     connect_retry_interval: 0.1,
    #     connect_retry_count:    5,
    #     ssl:                    true
    #   )
    #
    # SSL with options Example:
    #   client = Net::TCPClient.new(
    #     server:                 'server:3300',
    #     connect_retry_interval: 0.1,
    #     connect_retry_count:    5,
    #     ssl:                    {
    #       verify_mode: OpenSSL::SSL::VERIFY_NONE
    #     }
    #   )
    def initialize(server: nil, servers: nil,
      policy: :ordered, buffered: true, keepalive: true,
      connect_timeout: 10.0, read_timeout: 60.0, write_timeout: 60.0,
      connect_retry_count: 10, retry_count: 3, connect_retry_interval: 0.5, close_on_error: true,
      on_connect: nil, proxy_server: nil, ssl: nil
    )
      @read_timeout           = read_timeout.to_f
      @write_timeout          = write_timeout.to_f
      @connect_timeout        = connect_timeout.to_f
      @buffered               = buffered
      @keepalive              = keepalive
      @connect_retry_count    = connect_retry_count
      @retry_count            = retry_count
      @connect_retry_interval = connect_retry_interval.to_f
      @on_connect             = on_connect
      @proxy_server           = proxy_server
      @policy                 = policy
      @close_on_error         = close_on_error
      if ssl
        @ssl                   = ssl == true ? {} : ssl
        @ssl_handshake_timeout = (@ssl.delete(:handshake_timeout) || @connect_timeout).to_f
      end
      @servers = [server] if server
      @servers = servers if servers

      raise(ArgumentError, 'Missing mandatory :server or :servers') unless @servers

      connect
    end

    # Connect to the TCP server
    #
    # Raises Net::TCPClient::ConnectionTimeout when the time taken to create a connection
    #        exceeds the :connect_timeout
    # Raises Net::TCPClient::ConnectionFailure whenever Socket raises an error such as Error::EACCESS etc, see Socket#connect for more information
    #
    # Error handling is implemented as follows:
    # 1. TCP Socket Connect failure:
    #    Cannot reach server
    #    Server is being restarted, or is not running
    #    Retry 50 times every 100ms before raising a Net::TCPClient::ConnectionFailure
    #    - Means all calls to #connect will take at least 5 seconds before failing if the server is not running
    #    - Allows hot restart of server process if it restarts within 5 seconds
    #
    # 2. TCP Socket Connect timeout:
    #    Timed out after 5 seconds trying to connect to the server
    #    Usually means server is busy or the remote server disappeared off the network recently
    #    No retry, just raise a Net::TCPClient::ConnectionTimeout
    #
    # Note: When multiple servers are supplied it will only try to connect to
    #       the subsequent servers once the retry count has been exceeded
    #
    # Note: Calling #connect on an open connection will close the current connection
    #       and create a new connection
    def connect
      start_time = Time.now
      retries    = 0
      close

      # Number of times to try
      begin
        connect_to_server(servers, policy)
        logger.info(message: "Connected to #{address}", duration: (Time.now - start_time) * 1000) if respond_to?(:logger)
      rescue ConnectionFailure, ConnectionTimeout => exception
        cause = exception.is_a?(ConnectionTimeout) ? exception : exception.cause
        # Retry-able?
        if self.class.reconnect_on_errors.include?(cause.class) && (retries < connect_retry_count.to_i)
          retries += 1
          logger.warn "#connect Failed to connect to any of #{servers.join(',')}. Sleeping:#{connect_retry_interval}s. Retry: #{retries}" if respond_to?(:logger)
          sleep(connect_retry_interval)
          retry
        else
          message = "#connect Failed to connect to any of #{servers.join(',')} after #{retries} retries. #{exception.class}: #{exception.message}"
          logger.benchmark_error(message, exception: exception, duration: (Time.now - start_time)) if respond_to?(:logger)
          raise ConnectionFailure.new(message, address.to_s, cause)
        end
      end
    end

    # Write data to the server
    #
    # Use #with_retry to add resilience to the #write method
    #
    # Raises Net::TCPClient::ConnectionFailure whenever the write fails
    #        For a description of the errors, see Socket#write
    #
    # Parameters
    #   timeout [Float]
    #     Optional: Override the default write timeout for this write
    #     Number of seconds before raising Net::TCPClient::WriteTimeout when no data has
    #     been written.
    #     A value of -1 will wait forever
    #     Default: :write_timeout supplied to #initialize
    #
    #  Note: After a Net::TCPClient::ReadTimeout #read can be called again on
    #        the same socket to read the response later.
    #        If the application no longer wants the connection after a
    #        Net::TCPClient::ReadTimeout, then the #close method _must_ be called
    #        before calling _connect_ or _retry_on_connection_failure_ to create
    #        a new connection
    def write(data, timeout = write_timeout)
      data = data.to_s
      if respond_to?(:logger)
        payload        = {timeout: timeout}
        # With trace level also log the sent data
        payload[:data] = data if logger.trace?
        logger.benchmark_debug('#write', payload: payload) do
          payload[:bytes] = socket_write(data, timeout)
        end
      else
        socket_write(data, timeout)
      end
    rescue Exception => exc
      close if close_on_error
      raise exc
    end

    # Returns a response from the server
    #
    # Raises Net::TCPClient::ConnectionTimeout when the time taken to create a connection
    #        exceeds the :connect_timeout
    #        Connection is closed
    # Raises Net::TCPClient::ConnectionFailure whenever Socket raises an error such as
    #        Error::EACCESS etc, see Socket#connect for more information
    #        Connection is closed
    # Raises Net::TCPClient::ReadTimeout if the timeout has been exceeded waiting for the
    #        requested number of bytes from the server
    #        Partial data will not be returned
    #        Connection is _not_ closed and #read can be called again later
    #        to read the response from the connection
    #
    # Parameters
    #   length [Fixnum]
    #     The number of bytes to return
    #     #read will not return until 'length' bytes have been received from
    #     the server
    #
    #   buffer [String]
    #    Optional buffer into which to write the data that is read.
    #
    #   timeout [Float]
    #     Optional: Override the default read timeout for this read
    #     Number of seconds before raising Net::TCPClient::ReadTimeout when no data has
    #     been returned
    #     A value of -1 will wait forever for a response on the socket
    #     Default: :read_timeout supplied to #initialize
    #
    #  Note: After a Net::TCPClient::ReadTimeout #read can be called again on
    #        the same socket to read the response later.
    #        If the application no longer wants the connection after a
    #        Net::TCPClient::ReadTimeout, then the #close method _must_ be called
    #        before calling _connect_ or _retry_on_connection_failure_ to create
    #        a new connection
    def read(length, buffer = nil, timeout = read_timeout)
      if respond_to?(:logger)
        payload = {bytes: length, timeout: timeout}
        logger.benchmark_debug('#read', payload: payload) do
          data           = socket_read(length, buffer, timeout)
          # With trace level also log the received data
          payload[:data] = data if logger.trace?
          data
        end
      else
        socket_read(length, buffer, timeout)
      end
    rescue Exception => exc
      close if close_on_error
      raise exc
    end

    # Write and/or receive data with automatic retry on connection failure
    #
    # On a connection failure, it will create a new connection and retry the block.
    # Returns immediately on exception Net::TCPClient::ReadTimeout
    # The connection is always closed on Net::TCPClient::ConnectionFailure regardless of close_on_error
    #
    # 1. Example of a resilient _readonly_ request:
    #
    #    When reading data from a server that does not change state on the server
    #    Wrap both the write and the read with #retry_on_connection_failure
    #    since it is safe to write the same data twice to the server
    #
    #    # Since the write can be sent many times it is safe to also put the receive
    #    # inside the retry block
    #    value = client.retry_on_connection_failure do
    #      client.write("GETVALUE:count\n")
    #      client.read(20).strip.to_i
    #    end
    #
    # 2. Example of a resilient request that _modifies_ data on the server:
    #
    #    When changing state on the server, for example when updating a value
    #    Wrap _only_ the write with #retry_on_connection_failure
    #    The read must be outside the #retry_on_connection_failure since we must
    #    not retry the write if the connection fails during the #read
    #
    #    value = 45
    #    # Only the write is within the retry block since we cannot re-write once
    #    # the write was successful since the server may have made the change
    #    client.retry_on_connection_failure do
    #      client.write("SETVALUE:#{count}\n")
    #    end
    #    # Server returns "SAVED" if the call was successful
    #    result = client.read(20).strip
    #
    # Error handling is implemented as follows:
    #    If a network failure occurs during the block invocation the block
    #    will be called again with a new connection to the server.
    #    It will only be retried up to 3 times
    #    The re-connect will independently retry and timeout using all the
    #    rules of #connect
    def retry_on_connection_failure
      retries = 0
      begin
        connect if closed?
        yield(self)
      rescue ConnectionFailure => exception
        exc_str = exception.cause ? "#{exception.cause.class}: #{exception.cause.message}" : exception.message
        # Re-raise exceptions that should not be retried
        if !self.class.reconnect_on_errors.include?(exception.cause.class)
          logger.info "#retry_on_connection_failure not configured to retry: #{exc_str}" if respond_to?(:logger)
          raise exception
        elsif retries < @retry_count
          retries += 1
          logger.warn "#retry_on_connection_failure retry #{retries} due to #{exception.class}: #{exception.message}" if respond_to?(:logger)
          connect
          retry
        end
        logger.error "#retry_on_connection_failure Connection failure: #{exception.class}: #{exception.message}. Giving up after #{retries} retries" if respond_to?(:logger)
        raise ConnectionFailure.new("After #{retries} retries to host '#{server}': #{exc_str}", server, exception.cause)
      end
    end

    # Close the socket only if it is not already closed
    #
    # Logs a warning if an error occurs trying to close the socket
    def close
      socket.close if socket && !socket.closed?
      @socket  = nil
      @address = nil
      true
    rescue IOError => exception
      logger.warn "IOError when attempting to close socket: #{exception.class}: #{exception.message}" if respond_to?(:logger)
      false
    end

    def flush
      return unless socket
      respond_to?(:logger) ? logger.benchmark_debug('#flush') { socket.flush } : socket.flush
    end

    def closed?
      socket.nil? || socket.closed?
    end

    def eof?
      socket.nil? || socket.eof?
    end

    # Returns whether the connection to the server is alive
    #
    # It is useful to call this method before making a call to the server
    # that would change data on the server
    #
    # Note: This method is only useful if the server closed the connection or
    #       if a previous connection failure occurred.
    #       If the server is hard killed this will still return true until one
    #       or more writes are attempted
    #
    # Note: In testing the overhead of this call is rather low, with the ability to
    # make about 120,000 calls per second against an active connection.
    # I.e. About 8.3 micro seconds per call
    def alive?
      return false if socket.nil? || closed?

      if IO.select([socket], nil, nil, 0)
        !socket.eof? rescue false
      else
        true
      end
    rescue IOError
      false
    end

    def setsockopt(*args)
      socket.nil? || socket.setsockopt(*args)
    end

    private

    # Connect to one of the servers in the list, per the current policy
    # Returns [Socket] the socket connected to or an Exception
    def connect_to_server(servers, policy)
      # Iterate over each server address until it successfully connects to a host
      last_exception = nil
      Policy::Base.factory(policy, servers).each do |address|
        begin
          return connect_to_address(address)
        rescue ConnectionTimeout, ConnectionFailure => exception
          last_exception = exception
        end
      end

      # Raise Exception once it has failed to connect to any server
      last_exception ? raise(last_exception) : raise(ArgumentError, "No servers supplied to connect to: #{servers.join(',')}")
    end

    # Returns [Socket] connected to supplied address
    #   address [Net::TCPClient::Address]
    #     Host name, ip address and port of server to connect to
    # Connect to the server at the supplied address
    # Returns the socket connection
    def connect_to_address(address)
      socket =
        if proxy_server
          ::SOCKSSocket.new("#{address.ip_address}:#{address.port}", proxy_server)
        else
          ::Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
        end
      unless buffered
        socket.sync = true
        socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
      end
      socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true) if keepalive

      socket_connect(socket, address, connect_timeout)

      @socket  = ssl ? ssl_connect(socket, address, ssl_handshake_timeout) : socket
      @address = address

      # Invoke user supplied Block every time a new connection has been established
      @on_connect.call(self) if @on_connect
    end

    # Connect to server
    #
    # Raises Net::TCPClient::ConnectionTimeout when the connection timeout has been exceeded
    # Raises Net::TCPClient::ConnectionFailure
    def socket_connect(socket, address, timeout)
      socket_address = Socket.pack_sockaddr_in(address.port, address.ip_address)

      # Timeout of -1 means wait forever for a connection
      return socket.connect(socket_address) if timeout == -1

      deadline = Time.now.utc + timeout
      begin
        non_blocking(socket, deadline) { socket.connect_nonblock(socket_address) }
      rescue Errno::EISCONN
        # Connection was successful.
      rescue NonBlockingTimeout
        raise ConnectionTimeout.new("Timed out after #{timeout} seconds trying to connect to #{address}")
      rescue SystemCallError, IOError => exception
        message = "#connect Connection failure connecting to '#{address.to_s}': #{exception.class}: #{exception.message}"
        logger.error message if respond_to?(:logger)
        raise ConnectionFailure.new(message, address.to_s, exception)
      end
    end

    # Write to the socket
    def socket_write(data, timeout)
      if timeout < 0
        socket.write(data)
      else
        deadline = Time.now.utc + timeout
        length = data.bytesize
        total_count = 0
        non_blocking(socket, deadline) do
          loop do
            begin
              count = socket.write_nonblock(data)
            rescue Errno::EWOULDBLOCK
              retry
            end
            total_count += count
            return total_count if total_count >= length
            data = data.byteslice(count..-1)
          end
        end
      end
    rescue NonBlockingTimeout
      logger.warn "#write Timeout after #{timeout} seconds" if respond_to?(:logger)
      raise WriteTimeout.new("Timed out after #{timeout} seconds trying to write to #{address}")
    rescue SystemCallError, IOError => exception
      message = "#write Connection failure while writing to '#{address.to_s}': #{exception.class}: #{exception.message}"
      logger.error message if respond_to?(:logger)
      raise ConnectionFailure.new(message, address.to_s, exception)
    end

    def socket_read(length, buffer, timeout)
      result =
        if timeout < 0
          buffer.nil? ? socket.read(length) : socket.read(length, buffer)
        else
          deadline = Time.now.utc + timeout
          non_blocking(socket, deadline) do
            buffer.nil? ? socket.read_nonblock(length) : socket.read_nonblock(length, buffer)
          end
        end

      # EOF before all the data was returned
      if result.nil? || (result.length < length)
        logger.warn "#read server closed the connection before #{length} bytes were returned" if respond_to?(:logger)
        raise ConnectionFailure.new('Connection lost while reading data', address.to_s, EOFError.new('end of file reached'))
      end
      result
    rescue NonBlockingTimeout
      logger.warn "#read Timeout after #{timeout} seconds" if respond_to?(:logger)
      raise ReadTimeout.new("Timed out after #{timeout} seconds trying to read from #{address}")
    rescue SystemCallError, IOError => exception
      message = "#read Connection failure while reading data from '#{address.to_s}': #{exception.class}: #{exception.message}"
      logger.error message if respond_to?(:logger)
      raise ConnectionFailure.new(message, address.to_s, exception)
    end

    class NonBlockingTimeout< ::SocketError
    end

    def non_blocking(socket, deadline)
      yield
    rescue IO::WaitReadable
      time_remaining = check_time_remaining(deadline)
      raise NonBlockingTimeout unless IO.select([socket], nil, nil, time_remaining)
      retry
    rescue IO::WaitWritable
      time_remaining = check_time_remaining(deadline)
      raise NonBlockingTimeout unless IO.select(nil, [socket], nil, time_remaining)
      retry
    end

    def check_time_remaining(deadline)
      time_remaining = deadline - Time.now.utc
      raise NonBlockingTimeout if time_remaining < 0
      time_remaining
    end

    # Try connecting to a single server
    # Returns the connected socket
    #
    # Raises Net::TCPClient::ConnectionTimeout when the connection timeout has been exceeded
    # Raises Net::TCPClient::ConnectionFailure
    def ssl_connect(socket, address, timeout)
      ssl_context = OpenSSL::SSL::SSLContext.new
      ssl_context.set_params(ssl.is_a?(Hash) ? ssl : {})

      ssl_socket            = OpenSSL::SSL::SSLSocket.new(socket, ssl_context)
      ssl_socket.hostname   = address.host_name
      ssl_socket.sync_close = true

      begin
        if timeout == -1
          # Timeout of -1 means wait forever for a connection
          ssl_socket.connect
        else
          deadline = Time.now.utc + timeout
          begin
            non_blocking(socket, deadline) { ssl_socket.connect_nonblock }
          rescue Errno::EISCONN
            # Connection was successful.
          rescue NonBlockingTimeout
            raise ConnectionTimeout.new("SSL handshake Timed out after #{timeout} seconds trying to connect to #{address.to_s}")
          end
        end
      rescue SystemCallError, OpenSSL::SSL::SSLError, IOError => exception
        message = "#connect SSL handshake failure with '#{address.to_s}': #{exception.class}: #{exception.message}"
        logger.error message if respond_to?(:logger)
        raise ConnectionFailure.new(message, address.to_s, exception)
      end

      # Verify Peer certificate
      ssl_verify(ssl_socket, address) if ssl_context.verify_mode != OpenSSL::SSL::VERIFY_NONE
      ssl_socket
    end

    # Raises Net::TCPClient::ConnectionFailure if the peer certificate does not match its hostname
    def ssl_verify(ssl_socket, address)
      unless OpenSSL::SSL.verify_certificate_identity(ssl_socket.peer_cert, address.host_name)
        domains = extract_domains_from_cert(ssl_socket.peer_cert)
        ssl_socket.close
        message = "#connect SSL handshake failed due to a hostname mismatch. Request address was: '#{address.to_s}'" +
                  "  Certificate valid for hostnames: #{domains.map { |d| "'#{d}'"}.join(',')}"
        logger.error message if respond_to?(:logger)
        raise ConnectionFailure.new(message, address.to_s)
      end
    end

    def extract_domains_from_cert(cert)
      cert.subject.to_a.each{|oid, value|
        return [value] if oid == "CN"
      }
    end
  end
end


module Sonos
  # The Sonos system. The root object to manage the collection of groups and devices. This is
  # intended to be a singleton accessed from `Sonos.system`.
  class System
    attr_reader :topology
    attr_reader :groups
    attr_reader :devices

    # Initialize the system
    # @param [Array] the system topology. If this is nil, it will autodiscover.
    def initialize(topology = Discovery.new.topology)
      rescan topology
    end

    # Returns all speakers
    def speakers
      @devices.select(&:speaker?)
    end

    # Pause all speakers
    def pause_all
      speakers.each do |speaker|
        speaker.pause if speaker.has_music?
      end
    end

    # Play all speakers
    def play_all
      speakers.each do |speaker|
        speaker.play if speaker.has_music?
      end
    end

    # Party Mode!  Join all speakers into a single group.
    def party_mode new_master = nil
      return nil unless speakers.length > 1

      new_master = find_party_master if new_master.nil?

      party_over
      speakers.each do |slave|
        next if slave.uid == new_master.uid
        slave.join new_master
      end
      rescan @topology
    end

    def find_party_master
      # 1: If there are any pre-existing groups playing something, use
      # the lowest-numbered group's master. But ensure to only check a
      # master_speaker that is actually a speaker, and not an Accessory.
      groups.each do |group|
        if group.master_speaker.speaker? and group.master_speaker.has_music?
          return group.master_speaker
        end
      end

      # 2: Lowest-number speaker that's playing something
      speakers.each do |speaker|
        return speaker if speaker.has_music?
      end

      # 3: lowest-numbered speaker
      speakers[0]
    end

    def find_speaker_by_name(name)
      speakers.each do |speaker|
        return speaker if(speaker.name == name)
      end
      return nil
    end

    def find_speaker_by_uid(uid)
      uid = "uuid:" + uid unless uid[0,5] == "uuid:"
      speakers.each do |speaker|
        return speaker if(speaker.uid == uid)
      end
      return nil
    end

    # Party's over :(
    def party_over
      groups.each { |g| g.disband }
      rescan @topology
    end

    def rescan(topology = Discovery.new.topology)
      @topology = topology
      @groups = []
      @devices = @topology.collect(&:device)

      construct_groups

      speakers.each do |speaker|
        speaker.group_master = speaker
        @groups.each do |group|
          if group.master_speaker.uid == speaker.uid
            speaker.group_master = speaker
            group.master_speaker = speaker
          end
          group.slave_speakers.each do |slave|
            speaker.group_master = group.master_speaker if slave.uid == speaker.uid
          end
        end
      end
    end

  private

    def construct_groups
      # Loop through all of the unique groups
      @topology.collect(&:group).uniq.each do |group_uid|

        # set group's master nade using topology's coordinator parameter
        master = nil
        @topology.each do |node|
          # Select only the nodes with this group uid
          next unless node.group == group_uid
          master = node if node.coordinator == "true"
        end

        # Skip this group if there is no master
        next if master.nil?

        # register other nodes in groups as slave nodes
        nodes = []
        @topology.each do |node|
          # Select only the nodes with this group uid
          next unless node.group == group_uid
          nodes << node unless node.uuid == master.uuid
        end

        # Add the group
        @groups << Group.new(master.device, nodes.collect(&:device))
      end
    end
  end
end


require 'sonos/topology_node'
require 'ssdp'

#
# Inspired by https://github.com/rahims/SoCo, https://github.com/turboladen/upnp,
# and http://onestepback.org/index.cgi/Tech/Ruby/MulticastingInRuby.red.
#
# Turboladen's uPnP work is super-smart, but doesn't seem to work with 1.9.3 due to soap4r dep's.
#
# Some day this nonsense should be asynchronous / nonblocking / decorated with rainbows.
#

module Sonos
  class Discovery
    MULTICAST_ADDR = '239.255.255.250'
    MULTICAST_PORT = 1900
    DEFAULT_TIMEOUT = 2

    attr_reader :timeout
    attr_reader :first_device_ip
    attr_reader :default_ip

    def initialize(timeout = DEFAULT_TIMEOUT)
      @timeout = timeout
    end

    # Look for Sonos devices on the network and return the first IP address found
    # @return [String] the IP address of the first Sonos device found
    def discover
      result = SSDP::Consumer.new.search(service: 'urn:schemas-upnp-org:device:ZonePlayer:1', first_only: true, timeout: @timeout, filter: lambda {|r| r[:params]["ST"].match(/ZonePlayer/) })
      @first_device_ip = result[:address]
    end

    # Find all of the Sonos devices on the network
    # @return [Array] an array of TopologyNode objects
    def topology
      self.discover unless @first_device_ip
      return [] unless @first_device_ip

      doc = Nokogiri::XML(open("http://#{@first_device_ip}:#{Sonos::PORT}/status/topology"))
      doc.xpath('//ZonePlayers/ZonePlayer').map do |node|
        TopologyNode.new(node)
      end
    end
  end
end


module MTG
  class QueryBuilder
    include RestClient
    attr_accessor :type, :query

    def initialize(type)
      @type = type
      @query = {}
    end

    # Adds a parameter to the hash of query parameters
    #
    # @param args [Hash] the query parameter
    # @return [QueryBuilder] the QueryBuilder
    def where(args)
      @query.merge!(args)
      self
    end

    # Find a single resource by the resource id
    #
    # @param id [String] the resource id
    # @return [Object] the Type object response
    def find(id)
      response = RestClient.get("#{@type.Resource}/#{id}")
      singular_resource = @type.Resource[0...-1]
      if response.body[singular_resource].nil?
        raise ArgumentError, 'Resource not found'
      end

      type.new.from_json(response.body[singular_resource].to_json)
    end

    # Get all resources from a query by paging through data
    #
    # @return [Array<Object>] Array of resources
    def all
      list = []
      page = 1
      fetch_all = true

      if @query.has_key?(:page)
        page = @query[:page]
        fetch_all = false
      end

      while true
        response = RestClient.get(@type.Resource, @query)
        data = response.body[@type.Resource]
        if !data.empty?
          data.each {|item| list << @type.new.from_json(item.to_json)}

          if !fetch_all
            break
          else
            where(page: page += 1)
          end
        else
          break
        end
      end

      return list
    end
  end
end


module Reform::Form::ORM
  def model_for_property(name)
    return model unless is_a?(Reform::Form::Composition) # i am too lazy for proper inheritance. there should be a ActiveRecord::Composition that handles this.

    model_name = options_for(name)[:on]
    model[model_name]
  end

  module UniquenessValidator
    # when calling validates it should create the Vali instance already and set @klass there! # TODO: fix this in AM.
    def validate(form)
      property = attributes.first

      # here is the thing: why does AM::UniquenessValidator require a filled-out record to work properly? also, why do we need to set
      # the class? it would be way easier to pass #validate a hash of attributes and get back an errors hash.
      # the class for the finder could either be infered from the record or set in the validator instance itself in the call to ::validates.
      record = form.model_for_property(property)
      record.send("#{property}=", form.send(property))

      @klass = record.class # this is usually done in the super-sucky #setup method.
      super(record).tap do |res|
        form.errors.add(property, record.errors.first.last) if record.errors.present?
      end
    end
  end
end


require "uber/delegates"

module Reform::Form::ActiveModel
  def self.included(base)
    base.class_eval do
      extend ClassMethods
      register_feature ActiveModel

      extend Uber::Delegates
      delegates :model, *[:persisted?, :to_key, :to_param, :id] # Uber::Delegates

      def to_model # this is called somewhere in FormBuilder and ActionController.
        self
      end
    end
  end

  module ClassMethods
    # this module is only meant to extend (not include). # DISCUSS: is this a sustainable concept?
    def self.extended(base)
      base.class_eval do
        extend Uber::InheritableAttribute
        inheritable_attr :model_options
      end
    end

    # DISCUSS: can we achieve that somehow via features in build_inline?
    def property(*)
      super.tap do |dfn|
        return dfn unless dfn[:nested]
        _name = dfn[:name]
        dfn[:nested].instance_eval do
          @_name = _name.singularize.camelize
          # this adds Form::name for AM::Validations and I18N.
          def name
            @_name
          end
        end
      end
    end

    # moved from reform as not applicable to dry
    def validates(*args, &block)
      validation(name: :default, inherit: true) { validates *args, &block }
    end

    def validate(*args, &block)
      validation(name: :default, inherit: true) { validate *args, &block }
    end

    def validates_with(*args, &block)
      validation(name: :default, inherit: true) { validates_with *args, &block }
    end

    def validates_each(*args, &block)
      validation(name: :default, inherit: true) { validates_each *args, &block }
    end

    # Set a model name for this form if the infered is wrong.
    #
    #   class CoverSongForm < Reform::Form
    #     model :song
    #
    # or we can setup a isolated namespace model ( which defined in isolated rails egine )
    #
    #   class CoverSongForm < Reform::Form
    #     model "api/v1/song", namespace: "api"
    def model(main_model, options={})
      self.model_options = [main_model, options]
    end

    def model_name
      if model_options
        form_name = model_options.first.to_s.camelize
        namespace = model_options.last[:namespace].present? ? model_options.last[:namespace].to_s.camelize.constantize : nil
      else
        if name
          form_name = name.sub(/(::)?Form$/, "") # Song::Form => "Song"
          namespace = nil
        else # anonymous forms. let's drop AM and forget about all this.
          form_name = "reform"
          namespace = nil
        end
      end

      active_model_name_for(form_name, namespace)
    end

  private
    def active_model_name_for(string, namespace=nil)
      ::ActiveModel::Name.new(self, namespace, string)
    end
  end # ClassMethods

  def model_name(*args)
    self.class.model_name(*args)
  end
end


require "rexml/xpath_parser"

module ROXML
  class RequiredElementMissing < ArgumentError # :nodoc:
  end

  #
  # Internal base class that represents an XML - Class binding.
  #
  class XMLRef # :nodoc:
    attr_reader :opts
    delegate :required?, :array?, :accessor, :default, :wrapper, :to => :opts

    def initialize(opts, instance)
      @opts = opts
      @instance = instance
    end

    def blocks
      opts.blocks || []
    end

    def to_xml(instance)
      val = instance.__send__(accessor)
      opts.to_xml.respond_to?(:call) ? opts.to_xml.call(val) : val
    end

    def name
      opts.name_explicit? ? opts.name : conventionize(opts.name)
    end

    def xpath_name
      namespacify(name)
    end

    def value_in(xml)
      xml = XML::Node.from(xml)
      value = fetch_value(xml)
      value = default if default && (value.nil? || value.to_s.empty?)

      value = apply_blocks(value)
      value = freeze(value) if value && opts.frozen?
      value
    end

  private
    def conventionize(what)
      convention ||= @instance.class.respond_to?(:roxml_naming_convention) && @instance.class.roxml_naming_convention
      if !what.blank? && convention.respond_to?(:call)
        URI.unescape(convention.call(URI.escape(what, /\/|::/)))
      else
        what
      end
    end

    def namespacify(what)
      if what.to_s.present? && opts.namespace != false && ns = [opts.namespace, @instance.class.roxml_namespace, @default_namespace].compact.map(&:to_s).first
        parser = REXML::Parsers::XPathParser.new
        parsed = parser.parse what

        parsed.each_cons(4).with_index.each do |a,i|
          if a[0..2] == [:child, :qname, ""]
            if ns == "*"
              parsed[i+1,3] = [:any, :predicate, [:eq, [:function, "local-name", []], [:literal, a[3]]]] if a[3] != "*"
            else
              a[2].replace ns
            end
          end
        end

        parser.abbreviate parsed
      else
        what
      end
    end

    def apply_blocks(val)
      blocks.inject(val) {|val, block| block.call(val) }
    rescue Exception => ex
      raise ex, "#{accessor}: #{ex.message}"
    end

    def freeze(val)
      val.each(&:freeze) if val.is_a?(Enumerable)
      val.freeze
    end

    def xpath
      opts.wrapper ? "#{namespacify(opts.wrapper)}/#{xpath_name}" : xpath_name.to_s
    end

    def auto_wrapper
      namespacify(conventionize(opts.name.pluralize))
    end

    def auto_xpath
      "#{auto_wrapper}/#{xpath_name}" if array?
    end

    def several?
      array?
    end

    def wrap(xml, opts = {:always_create => false})
      wrap_with = @auto_vals ? auto_wrapper : wrapper

      return xml if !wrap_with || xml.name == wrap_with

      wraps = wrap_with.to_s.split('/')
      wraps.inject(xml) do |node,wrap|
        if !opts[:always_create] && (child = node.children.find {|c| c.name == wrap })
          child
        else
          XML.add_node(node, wrap)
        end
      end
    end

    def nodes_in(xml)
      @default_namespace = XML.default_namespace(xml)
      vals = XML.search(xml, xpath, @instance.class.roxml_namespaces)

      if several? && vals.empty? && !wrapper && auto_xpath
        vals = XML.search(xml, auto_xpath, @instance.class.roxml_namespaces)
        @auto_vals = !vals.empty?
      end

      if vals.empty?
        raise RequiredElementMissing, "#{name} from #{xml} for #{accessor}" if required?
        default
      elsif several?
        vals.map do |val|
          yield val
        end
      else
        yield(vals.first)
      end
    end
  end

  # Interal class representing an XML attribute binding
  #
  # In context:
  #  <element attribute="XMLAttributeRef">
  #   XMLTextRef
  #  </element>
  class XMLAttributeRef < XMLRef # :nodoc:
    # Updates the attribute in the given XML block to
    # the value provided.
    def update_xml(xml, values)
      if array?
        values.each do |value|
          wrap(xml, :always_create => true).tap do |node|
            XML.set_attribute(node, name, value.to_s)
          end
        end
      else
        wrap(xml).tap do |xml|
          XML.set_attribute(xml, name, values.to_s)
        end
      end
    end

  private
    def fetch_value(xml)
      nodes_in(xml) do |node|
        node.value
      end
    end

    def xpath_name
      "@#{name}"
    end
  end

  # Interal class representing XML content text binding
  #
  # In context:
  #  <element attribute="XMLAttributeRef">
  #   XMLTextRef
  #  </element>
  class XMLTextRef < XMLRef # :nodoc:
    delegate :cdata?, :content?, :name?, :to => :opts

    # Updates the text in the given _xml_ block to
    # the _value_ provided.
    def update_xml(xml, value)
      wrap(xml).tap do |xml|
        if content?
          add(xml, value)
        elsif name?
          xml.name = value
        elsif array?
          value.each do |v|
            add(XML.add_node(xml, name), v)
          end
        else
          add(XML.add_node(xml, name), value)
        end
      end
    end

  private
    def fetch_value(xml)
      if content? || name?
        value =
          if content?
            xml.content.to_s
          elsif name?
            xml.name
          end

        if value.blank?
          raise RequiredElementMissing, "#{name} from #{xml} for #{accessor}" if required?
          default
        else
          value
        end
      else
        nodes_in(xml) do |node|
          node.content
        end
      end
    end

    def add(dest, value)
      if cdata?
        XML.add_cdata(dest, value.to_s)
      else
        XML.set_content(dest, value.to_s)
      end
    end
  end

  class XMLNameSpaceRef < XMLRef # :nodoc:
    private
      def fetch_value(xml)
        xml.namespace.prefix
      end
  end

  class XMLHashRef < XMLTextRef # :nodoc:
    delegate :hash, :to => :opts

    def initialize(opts, inst)
      super(opts, inst)
      @key = opts.hash.key.to_ref(inst)
      @value = opts.hash.value.to_ref(inst)
    end

    def several?
      true
    end

    # Updates the composed XML object in the given XML block to
    # the value provided.
    def update_xml(xml, value)
      wrap(xml).tap do |xml|
        value.each_pair do |k, v|
          node = XML.add_node(xml, hash.wrapper)
          @key.update_xml(node, k)
          @value.update_xml(node, v)
        end
      end
    end

  private
    def fetch_value(xml)
      nodes_in(xml) do |node|
        [@key.value_in(node), @value.value_in(node)]
      end
    end

    def apply_blocks(vals)
      unless blocks.empty?
        vals.collect! do |kvp|
          super(kvp)
        end
      end
      to_hash(vals) if vals
    end

    def freeze(vals)
      vals.each_pair{|k, v| k.freeze; v.freeze }
      vals.freeze
    end

    def to_hash(array)
      hash = array.inject({}) do |result, (k, v)|
        result[k] ||= []
        result[k] << v
        result
      end
      hash.each_pair do |k, v|
        hash[k] = v.first if v.size == 1
      end
    end
  end

  class XMLObjectRef < XMLTextRef # :nodoc:
    delegate :sought_type, :to => :opts

    # Updates the composed XML object in the given XML block to
    # the value provided.
    def update_xml(xml, value)
      wrap(xml).tap do |xml|
        params = {:name => name, :namespace => opts.namespace}
        if array?
          value.each do |v|
            XML.add_child(xml, v.to_xml(params))
          end
        elsif value.is_a?(ROXML)
          XML.add_child(xml, value.to_xml(params))
        else
          XML.add_node(xml, name).tap do |node|
            XML.set_content(node, value.to_xml)
          end
        end
      end
    end

  private
    def fetch_value(xml)
      nodes_in(xml) do |node|
        if sought_type.respond_to? :from_xml
          sought_type.from_xml(node)
        else
          sought_type.new(node)
        end
      end
    end
  end
end