module Kuroko2
  module Workflow
    class Node
      PATH_REGEXP = %r(\A(?:/|(?:/\d+-[a-z0-9_]+)+)\z)
      TASK_REGISTRY = {
        root:                  Task::Sequence,
        noop:                  Task::Noop,
        sequence:              Task::Sequence,
        auto_skip_error:       Task::AutoSkipError,
        fork:                  Task::Fork,
        parallel_fork:         Task::ParallelFork,
        env:                   Task::Env,
        execute:               Task::Execute,
        queue:                 Task::Queue,
        sub_process:           Task::SubProcess,
        subprocess:            Task::SubProcess,
        timeout:               Task::Timeout,
        expected_time:         Task::ExpectedTime,
        wait:                  Task::Wait,
        sleep:                 Task::Sleep,
        rails_env:             Task::RailsEnv,
        kuroko_runner:         Task::KurokoRunner,
        echo:                  Task::Echo,
        retry:                 Task::Retry,
      }

      attr_reader :type, :option, :children
      attr_accessor :parent

      def self.register(key: nil, klass:)
        key ||= klass.to_s.demodulize.underscore.to_sym

        unless TASK_REGISTRY.has_key?(key)
          TASK_REGISTRY.store(key, klass)
        else
          Kuroko2.logger.warn("Unable to add '#{klass}' to task registry. '#{TASK_REGISTRY[key]}' is already registered.")
        end
      end

      def self.deregister(key)
        TASK_REGISTRY.delete(key)
      end

      def initialize(type, option = nil)
        @type       = type.to_sym
        @task_klass = TASK_REGISTRY.fetch(@type, nil)
        @option     = option.try(:strip)
        @parent     = nil
        @children   = []

        raise AssertionError, "`#{@type}` is not registered in task repository." unless @task_klass
      end

      def append_child(child)
        child.parent = self
        @children << child
      end

      def execute(token)
        Kuroko2.logger.debug { "(token #{token.uuid}) Execute #{@type} with option '#{@option}'." }
        @task_klass.new(self, token).execute.tap do |result|
          Kuroko2.logger.debug("(token #{token.uuid}) Result is '#{result}'.")
        end
      end

      def find(path)
        raise AssertionError, "path query('#{path}') is invalid." unless PATH_REGEXP === path

        query = path.split('/')
        query.shift # drop first empty string.

        traverse(query)
      end

      def next(index = 0)
        if (child = children[index])
          child
        else
          next_sibling
        end
      end

      def next_sibling
        if parent
          parent.next(current_index + 1)
        else
          nil
        end
      end

      def path
        if parent
          parent.path + "/#{current_index}-#{type}"
        else
          ''
        end
      end

      def to_script(indent = 0)
        "#{'  ' * indent}#{type}: #{option}\n" + children.map { |child| child.to_script(indent + 1) }.join
      end

      def validate_all
        @task_klass.new(self, nil).validate
        @children.each do |child|
          child.validate_all
        end
      end

      protected

      def current_index
        @_current_index = parent.children.index(self)
      end

      def traverse(query)
        return self if query.empty?

        first    = query.shift
        index, _ = first.split('-')

        @children[index.to_i].traverse(query)
      end

    end
  end
end
