# frozen_string_literal: true

module Flor

  class Hooker

    # NB: logger configuration entries start with "hok_"

    attr_reader :hooks

    KEYS = {
      consumed:   [:consumed, :c].freeze,
      point:      [:point, :p].freeze,
      nid:        [:nid].freeze,
      exid:       [:exid].freeze,
      domain:     [:domain, :d].freeze,
      subdomain:  [:subdomain, :sd].freeze,
      tag:        [:tag, :t].freeze,
      name:       [:name, :n].freeze,
      subnid:     [:subnid].freeze,
      heap:       [:heap, :hp].freeze,
      heat:       [:heat, :ht].freeze
    }.freeze

    def initialize(unit)

      @unit = unit

      class << unit

        def hook(*args, &block)

          @hooker.add(*args, &block)
        end
      end

      @hooks = []
    end

    def shutdown

      @hooks.each do |n, o, hook, b|

        hook.shutdown if hook.respond_to?(:shutdown)
      end
    end

    def [](name)

      h = @hooks.find { |n, _, _, _| n == name }

      h ? h[2] || h[3] : nil
    end

    def wlist

      @wlist ||= self['wlist']
    end

    def add(*args, &block)

      name = nil
      hook = nil
      opts = {}

      args.each do |arg|
        case arg
        when String then name = arg
        when Hash then opts = arg
        else hook = arg
        end
      end

      hook = Flor::Hook.instantiate(@unit, hook) \
        if hook.is_a?(Class)

      name ||= hook.name \
        if hook.respond_to?(:name) && hook.method(:name).arity < 1

      @hooks << [ name, opts, hook, block ]
    end

    def notify(executor, message)

      (
        @hooks + executor.traps_and_hooks
      )
        .inject([]) { |a, (_, opts, hook, block)|
          # name of hook is piped into "_" oblivion

          a.concat(
            if ! match?(executor, hook, opts, message)
              []
            elsif hook.is_a?(Flor::Trap)
              executor.trigger_trap(hook, message)
            elsif hook
              executor.trigger_hook(hook, message)
            else
              executor.trigger_block(block, opts, message)
            end) }
    end

    protected

    def o(opts, key, array: false)

      r = nil
      KEYS[key].each { |k| break r = opts[k] if opts.has_key?(k) }

      return nil if r == nil
      array ? Array(r) : r
    end

    def includes?(arr, value)

      Array(value)
        .find { |v| arr.find { |e| e.is_a?(Regexp) ? e.match(v) : e == v } }
    end

    def match?(executor, hook, opts, message)

      opts = hook.opts if hook.respond_to?(:opts) && opts.empty?

      c = o(opts, :consumed)
      return false if c == true && ! message['consumed']
      return false if c == false && message['consumed']

      if hook.is_a?(Flor::Trap)
        return false if message['point'] == 'trigger'
        return false if hook.within_itself?(executor, message)
      end

      ps = o(opts, :point, array: true)
      return false if ps && ! ps.include?(message['point'])

      if nid = o(opts, :nid)
        return false \
          unless nid.include?(message['nid'])
      end

      if exi = o(opts, :exid)
        return false \
          unless message['exid'] == exi
      end

      dm = Flor.domain(message['exid'])

      if dm && ds = o(opts, :domain, array: true)
        return false \
          unless ds.find { |d| d.is_a?(Regexp) ? (!! d.match(dm)) : (d == dm) }
      end

      if dm && sds = o(opts, :subdomain, array: true)
        return false \
          unless sds.find do |sd|
            dm[0, sd.length] == sd
          end
      end

      if ts = o(opts, :tag, array: true)
        return false unless %w[ entered left ].include?(message['point'])
        return false unless includes?(ts, message['tags'])
      end

      if ns = o(opts, :name)
        name = message['name']
        return false \
          unless ns.find { |n|
            case n
            when String then name == n
            when Array then name.match(Flor.to_regex(n))
            else false
            end }
      end

      node = nil

      if hook.is_a?(Flor::Trap) && o(opts, :subnid)
        if node = executor.node(message['nid'], true)
          return false unless node.descendant_of?(hook.bnid, true)
          node = node.h
        else
          return false if hook.bnid != '0'
        end
      end

      if hps = o(opts, :heap, array: true)
        return false unless node ||= executor.node(message['nid'])
        return false unless includes?(hps, node['heap'])
      end

      if hts = o(opts, :heat, array: true)
        return false unless node ||= executor.node(message['nid'])
        return false unless includes?(hts, node['heat0'])
      end

      true
    end
  end
end
