# frozen_string_literal: true

require 'active_support/core_ext/string/inflections'
require 'shellwords'

module EacRubyUtils
  module Console
    class DocoptRunner
      SUBCOMMAND_ARG = '<subcommand>'
      SUBCOMMAND_ARGS_ARG = '<subcommand-args>'
      SUBCOMMANDS_MACRO = '__SUBCOMMANDS__'

      def subcommands?
        source_doc.include?(SUBCOMMANDS_MACRO)
      end

      def check_subcommands
        return unless subcommands?

        singleton_class.include(SubcommandsSupport)
        check_subcommands_arg
        return if singleton_class.method_defined?(:run)

        singleton_class.send(:alias_method, :run, :run_with_subcommand)
      end

      module SubcommandsSupport
        EXTRA_AVAILABLE_SUBCOMMANDS_METHOD_NAME = :extra_available_subcommands

        def check_subcommands_arg
          if subcommand_arg_as_list?
            singleton_class.include(SubcommandsSupport::SubcommandArgAsList)
          else
            singleton_class.include(SubcommandsSupport::SubcommandArgAsArg)
          end
        end

        def run_with_subcommand
          if subcommand_name
            check_valid_subcommand
            subcommand.run
          else
            run_without_subcommand
          end
        end

        def subcommand
          @subcommand ||= subcommand_class_name(subcommand_name).constantize.create(
            argv: subcommand_args,
            program_name: subcommand_program,
            parent: self
          )
        end

        def target_doc
          super.gsub(SUBCOMMANDS_MACRO,
                     "#{target_doc_subcommand_arg} [#{SUBCOMMAND_ARGS_ARG}...]") +
            "\n" + subcommands_target_doc
        end

        def docopt_options
          super.merge(options_first: true)
        end

        def subcommand_class_name(subcommand)
          "#{self.class.name}::#{subcommand.underscore.camelize}"
        end

        def subcommand_arg_as_list?
          setting_value(:subcommand_arg_as_list, false) || false
        end

        def subcommand_args
          options.fetch(SUBCOMMAND_ARGS_ARG)
        end

        def subcommand_program
          subcommand_name
        end

        def available_subcommands
          r = ::Set.new(setting_value(:subcommands, false) || auto_available_subcommands)
          if respond_to?(EXTRA_AVAILABLE_SUBCOMMANDS_METHOD_NAME, true)
            r += send(EXTRA_AVAILABLE_SUBCOMMANDS_METHOD_NAME)
          end
          r.sort
        end

        def auto_available_subcommands
          self.class.constants
              .map { |name| self.class.const_get(name) }
              .select { |c| c.instance_of? Class }
              .select { |c| c < ::EacRubyUtils::Console::DocoptRunner }
              .map { |c| c.name.demodulize.underscore.dasherize }
        end

        def run_without_subcommand
          "Method #{__method__} should be overrided in #{self.class.name}"
        end

        protected

        def check_valid_subcommand
          return if available_subcommands.include?(subcommand_name)

          raise ::Docopt::Exit, "\"#{subcommand_name}\" is not a valid subcommand" \
            " (Valid: #{available_subcommands.join(', ')})"
        end

        module SubcommandArgAsArg
          def target_doc_subcommand_arg
            SUBCOMMAND_ARG
          end

          def subcommand_name
            options.fetch(SUBCOMMAND_ARG)
          end

          def subcommands_target_doc
            available_subcommands.inject("Subcommands:\n") do |a, e|
              a + "  #{e}\n"
            end
          end
        end

        module SubcommandArgAsList
          def target_doc_subcommand_arg
            '(' + available_subcommands.join('|') + ')'
          end

          def subcommand_name
            available_subcommands.each do |subcommand|
              return subcommand if options.fetch(subcommand)
            end
            nil
          end

          def subcommands_target_doc
            "\n"
          end
        end
      end
    end
  end
end