# frozen_string_literal: true

require "optparse"
require "shellwords"

require "extend/optparse"

require "cask/config"

require "cask/cmd/options"

require "cask/cmd/abstract_command"
require "cask/cmd/--cache"
require "cask/cmd/audit"
require "cask/cmd/cat"
require "cask/cmd/create"
require "cask/cmd/doctor"
require "cask/cmd/edit"
require "cask/cmd/fetch"
require "cask/cmd/help"
require "cask/cmd/home"
require "cask/cmd/info"
require "cask/cmd/install"
require "cask/cmd/list"
require "cask/cmd/outdated"
require "cask/cmd/reinstall"
require "cask/cmd/style"
require "cask/cmd/uninstall"
require "cask/cmd/upgrade"
require "cask/cmd/zap"

require "cask/cmd/abstract_internal_command"
require "cask/cmd/internal_help"
require "cask/cmd/internal_stanza"

module Cask
  class Cmd
    ALIASES = {
      "ls"       => "list",
      "homepage" => "home",
      "instal"   => "install", # gem does the same
      "uninstal" => "uninstall",
      "rm"       => "uninstall",
      "remove"   => "uninstall",
      "abv"      => "info",
      "dr"       => "doctor",
    }.freeze

    include Options

    option "--appdir=PATH",               ->(value) { Config.global.appdir               = value }
    option "--colorpickerdir=PATH",       ->(value) { Config.global.colorpickerdir       = value }
    option "--prefpanedir=PATH",          ->(value) { Config.global.prefpanedir          = value }
    option "--qlplugindir=PATH",          ->(value) { Config.global.qlplugindir          = value }
    option "--mdimporterdir=PATH",        ->(value) { Config.global.mdimporterdir        = value }
    option "--dictionarydir=PATH",        ->(value) { Config.global.dictionarydir        = value }
    option "--fontdir=PATH",              ->(value) { Config.global.fontdir              = value }
    option "--servicedir=PATH",           ->(value) { Config.global.servicedir           = value }
    option "--input_methoddir=PATH",      ->(value) { Config.global.input_methoddir      = value }
    option "--internet_plugindir=PATH",   ->(value) { Config.global.internet_plugindir   = value }
    option "--audio_unit_plugindir=PATH", ->(value) { Config.global.audio_unit_plugindir = value }
    option "--vst_plugindir=PATH",        ->(value) { Config.global.vst_plugindir        = value }
    option "--vst3_plugindir=PATH",       ->(value) { Config.global.vst3_plugindir       = value }
    option "--screen_saverdir=PATH",      ->(value) { Config.global.screen_saverdir      = value }

    option "--help", :help, false

    # handled in OS::Mac
    option "--language a,b,c", ->(*) {}

    # override default handling of --version
    option "--version", ->(*) { raise OptionParser::InvalidOption }

    def self.command_classes
      @command_classes ||= constants.map(&method(:const_get))
                                    .select { |klass| klass.respond_to?(:run) }
                                    .reject(&:abstract?)
                                    .sort_by(&:command_name)
    end

    def self.commands
      @commands ||= command_classes.map(&:command_name)
    end

    def self.lookup_command(command_name)
      @lookup ||= Hash[commands.zip(command_classes)]
      command_name = ALIASES.fetch(command_name, command_name)
      @lookup.fetch(command_name, nil)
    end

    def self.run(*args)
      new(*args).run
    end

    def initialize(*args)
      @args = process_options(*args)
    end

    def find_external_command(command)
      @tap_cmd_directories ||= Tap.cmd_directories
      @path ||= PATH.new(@tap_cmd_directories, ENV["HOMEBREW_PATH"])

      external_ruby_cmd = @tap_cmd_directories.map { |d| d/"brewcask-#{command}.rb" }
                                              .find(&:file?)
      external_ruby_cmd ||= which("brewcask-#{command}.rb", @path)

      if external_ruby_cmd
        ExternalRubyCommand.new(command, external_ruby_cmd)
      elsif external_command = which("brewcask-#{command}", @path)
        ExternalCommand.new(external_command)
      end
    end

    def detect_internal_command(*args)
      args.each_with_index do |arg, i|
        if command = self.class.lookup_command(arg)
          args.delete_at(i)
          return [command, args]
        elsif !arg.start_with?("-")
          break
        end
      end

      nil
    end

    def detect_external_command(*args)
      args.each_with_index do |arg, i|
        if command = find_external_command(arg)
          args.delete_at(i)
          return [command, args]
        elsif !arg.start_with?("-")
          break
        end
      end

      nil
    end

    def run
      MacOS.full_version = ENV["MACOS_VERSION"] unless ENV["MACOS_VERSION"].nil?
      Tap.default_cask_tap.install unless Tap.default_cask_tap.installed?

      args = @args.dup
      command, args = detect_internal_command(*args) || detect_external_command(*args) || [NullCommand.new, args]

      if help?
        Help.new(command.command_name).run
      else
        command.run(*args)
      end
    rescue CaskError, MethodDeprecatedError, ArgumentError, OptionParser::InvalidOption => e
      onoe e.message
      $stderr.puts e.backtrace if Homebrew.args.debug?
      exit 1
    rescue StandardError, ScriptError, NoMemoryError => e
      onoe e.message
      $stderr.puts Utils.error_message_with_suggestions
      $stderr.puts e.backtrace
      exit 1
    end

    def self.nice_listing(cask_list)
      cask_taps = {}
      cask_list.each do |c|
        user, repo, token = c.split "/"
        repo.sub!(/^homebrew-/i, "")
        cask_taps[token] ||= []
        cask_taps[token].push "#{user}/#{repo}"
      end
      list = []
      cask_taps.each do |token, taps|
        if taps.length == 1
          list.push token
        else
          taps.each { |r| list.push [r, token].join "/" }
        end
      end
      list.sort
    end

    def process_options(*args)
      exclude_regex = /^--#{Regexp.union(*Config::DEFAULT_DIRS.keys.map(&Regexp.public_method(:escape)))}=/

      non_options = []

      if idx = args.index("--")
        non_options += args.drop(idx)
        args = args.first(idx)
      end

      cask_opts = Shellwords.shellsplit(ENV.fetch("HOMEBREW_CASK_OPTS", ""))
                            .reject { |arg| arg.match?(exclude_regex) }

      all_args = cask_opts + args

      remaining = all_args.select do |arg|
        !process_arguments([arg]).empty?
      rescue OptionParser::InvalidOption, OptionParser::MissingArgument, OptionParser::AmbiguousOption
        true
      end

      remaining + non_options
    end

    class ExternalRubyCommand
      def initialize(command, path)
        @command_name = command.to_s.capitalize.to_sym
        @path = path
      end

      def run(*args)
        require @path

        klass = begin
          Cmd.const_get(@command_name)
        rescue NameError
          return
        end

        klass.run(*args)
      end
    end

    class ExternalCommand
      def initialize(path)
        @path = path
      end

      def run(*)
        exec @path, *ARGV[1..]
      end
    end

    class NullCommand
      def run(*args)
        if args.empty?
          ofail "No subcommand given.\n"
        else
          ofail "Unknown subcommand: #{args.first}"
        end

        $stderr.puts
        $stderr.puts Help.usage
      end

      def help
        run
      end
    end
  end
end
