require "set"

module Rscons

  # Functionality for an instance of the rscons application invocation.
  class Application

    # @return [Array<Hash>]
    #   Active variants.
    attr_reader :active_variants

    # @return [String]
    #   Top-level build directory.
    attr_accessor :build_dir

    # @return [Boolean]
    #   Whether to output ANSI color escape sequences.
    attr_accessor :do_ansi_color

    # @return [Integer]
    #   The number of threads to use when scheduling subprocesses.
    attr_accessor :n_threads

    # @return [Script]
    #   Build script.
    attr_reader :script

    # @return [Boolean]
    #   Whether to configure silently.
    attr_accessor :silent_configure

    # @return [Boolean]
    #   Whether to run verbosely.
    attr_accessor :verbose

    # Create Application instance.
    def _initialize
      @silent_configure = true
      @build_dir = ENV["RSCONS_BUILD_DIR"] || "build"
      ENV.delete("RSCONS_BUILD_DIR")
      @n_threads = Util.determine_n_threads
      @variant_groups = []
    end

    # Return if Rscons is in the task execution phase.
    #
    # @api private
    #
    # @return [Boolean]
    #   If Rscons is in the task execution phase.
    def task_execution_phase?
      @task_execution_phase
    end

    # Run the application.
    #
    # Execute user-specified tasks.
    #
    # @api private
    #
    # @param rsconscript [String]
    #   Build script file name.
    # @param tasks_and_params [Hash<String => Hash<String => String>>]
    #   List of task(s) to execute.
    # @param show_tasks [Boolean]
    #   Flag to show tasks and exit.
    # @param all_tasks [Boolean]
    #   Flag to show all tasks (not just those with a description).
    # @param enabled_variants [String]
    #   User-specified variants list.
    #
    # @return [Integer]
    #   Process exit code (0 on success).
    def run(rsconscript, tasks_and_params, show_tasks, all_tasks, enabled_variants)
      Cache.instance["failed_commands"] = []
      @tasks_and_params = tasks_and_params
      @enabled_variants = enabled_variants
      if enabled_variants == "" && !tasks_and_params.include?("configure")
        if cache_enabled_variants = Cache.instance["configuration_data"]["enabled_variants"]
          @enabled_variants = cache_enabled_variants
        end
      end
      @script = Script.new
      if should_load_script
        @script.load(rsconscript)
        enable_variants
      end
      if show_tasks
        show_script_tasks(all_tasks)
        return 0
      end
      apply_task_params(false)
      @task_execution_phase = true
      if tasks_and_params.empty?
        check_process_environments
        if Task.tasks["default"]
          Task["default"].check_execute
        end
      else
        tasks_and_params.each do |task_name, params|
          Task[task_name].check_execute
        end
      end
      0
    end

    # Apply user-specified variant enables and complain if they don't make
    # sense given the build script variant configuration.
    def enable_variants
      unless @_variants_enabled
        if @enabled_variants != ""
          exact = !(@enabled_variants =~ /^(\+|-)/)
          enabled_variants = @enabled_variants.split(",")
          specified_variants = {}
          enabled_variants.each do |enable_variant|
            enable_variant =~ /^(\+|-)?(.*)$/
            enable_disable, variant_name = $1, $2
            specified_variants[variant_name] = enable_disable != "-"
          end
          each_variant do |variant|
            if specified_variants.include?(variant[:name])
              variant[:enabled] = specified_variants[variant[:name]]
            elsif exact
              variant[:enabled] = false
            end
          end
        end
        @_variants_enabled = true
      end
      check_enabled_variants
    end

    # Show the last failures.
    #
    # @return [void]
    def show_failure
      failed_commands = Cache.instance["failed_commands"]
      failed_commands.each_with_index do |command, i|
        Ansi.write($stdout, :red, "Failed command (#{i + 1}/#{failed_commands.size}):", :reset, "\n")
        $stdout.puts Util.command_to_s(command)
      end
    end

    # Remove all generated files.
    #
    # @api private
    #
    # @return [void]
    def clean
      cache = Cache.instance
      # remove all built files
      cache.targets(false).each do |target|
        cache.remove_target(target)
        FileUtils.rm_f(target)
      end
      # remove all created directories if they are empty
      cache.directories(false).sort {|a, b| b.size <=> a.size}.each do |directory|
        cache.remove_directory(directory)
        next unless File.directory?(directory)
        if (Dir.entries(directory) - ['.', '..']).empty?
          Dir.rmdir(directory) rescue nil
        end
      end
      cache.write
    end

    # Remove the build directory and clear the cache.
    #
    # @api private
    #
    # @return [void]
    def distclean
      cache = Cache.instance
      clean
      cache.clear
      FileUtils.rm_rf(@build_dir)
    end

    # Check if the project needs to be configured.
    #
    # @api private
    #
    # @return [void]
    def check_configure
      apply_task_params(true)
      enable_variants
      unless Cache.instance["configuration_data"]["configured"]
        if @script.autoconf
          configure
        end
      end
    end

    # Check if environments need to be processed.
    #
    # @api private
    #
    # @return [void]
    def check_process_environments
      unless @_processed_environments
        Environment[].each do |env|
          env.process
        end
        @_processed_environments = true
      end
    end

    # Configure the project.
    #
    # @api private
    #
    # @return [void]
    def configure
      unless @_configured
        @_configured = true
        co = ConfigureOp.new(@script)
        begin
          @script.configure(co)
        rescue RsconsError => e
          co.close(false)
          raise e
        end
        Cache.instance["configuration_data"]["enabled_variants"] = @enabled_variants
        co.close(true)
      end
    end

    # Remove installed files.
    #
    # @api private
    #
    # @return [Integer]
    #   Exit code.
    #
    # @return [void]
    def uninstall
      cache = Cache.instance
      cache.targets(true).each do |target|
        cache.remove_target(target)
        next unless File.exist?(target)
        puts "Removing #{target}" if verbose
        FileUtils.rm_f(target)
      end
      # remove all created directories if they are empty
      cache.directories(true).sort {|a, b| b.size <=> a.size}.each do |directory|
        cache.remove_directory(directory)
        next unless File.directory?(directory)
        if (Dir.entries(directory) - ['.', '..']).empty?
          puts "Removing #{directory}" if verbose
          Dir.rmdir(directory) rescue nil
        end
      end
      cache.write
    end

    # Define a variant, or within a with_variants block, query if it is
    # active.
    #
    # @param name [String]
    #   Variant name.
    def variant(name, options = {})
      if @active_variants
        !!@active_variants.find {|variant| variant[:name] == name}
      else
        if @variant_groups.empty?
          variant_group
        end
        options = options.dup
        options[:name] = name
        options[:enabled] = options.fetch(:default, true)
        options[:key] = options.fetch(:key, name)
        @variant_groups.last[:variants] << options
      end
    end

    # Check if a variant is enabled.
    #
    # This can be used, for example, in a configuration block to omit or
    # include configuration checks based on which variants have been
    # configured.
    #
    # @param variant_name [String]
    #   Variant name.
    #
    # @return [Boolean]
    #   Whether the requested variant is enabled.
    def variant_enabled?(variant_name)
      each_variant do |variant|
        if variant[:name] == variant_name
          return variant[:enabled]
        end
      end
      false
    end

    # Create a variant group.
    def variant_group(*args, &block)
      if args.first.is_a?(String)
        name = args.slice!(0)
      end
      options = args.first || {}
      @variant_groups << options.merge(name: name, variants: [])
      if block
        block[]
      end
    end

    # Iterate through enabled variants.
    #
    # The given block is called for each combination of enabled variants
    # across the defined variant groups.
    def with_variants(&block)
      if @active_variants
        raise "with_variants cannot be called within another with_variants block"
      end
      if @variant_groups.empty?
        raise "with_variants cannot be called with no variants defined"
      end
      iter_vgs = lambda do |iter_variants|
        if iter_variants.size == @variant_groups.size
          @active_variants = iter_variants.compact
          block[]
          @active_variants = nil
        else
          @variant_groups[iter_variants.size][:variants].each do |variant|
            if variant[:enabled]
              iter_vgs[iter_variants + [variant]]
            end
          end
        end
      end
      iter_vgs[[]]
    end

    private

    def check_enabled_variants
      @variant_groups.each do |variant_group|
        enabled_count = variant_group[:variants].count do |variant|
          variant[:enabled]
        end
        if enabled_count == 0
          message = "No variants enabled for variant group"
          if variant_group[:name]
            message += " #{variant_group[:name].inspect}"
          end
          raise RsconsError.new(message)
        end
      end
    end

    def each_variant
      @variant_groups.each do |variant_group|
        variant_group[:variants].each do |variant|
          yield variant
        end
      end
    end

    def show_script_tasks(all_tasks)
      puts "Tasks:"
      Task[].sort.each do |task_name, task|
        if task.description || all_tasks
          puts %[  #{sprintf("%-27s", task_name)} #{task.description}]
          task.params.each do |param_name, param|
            arg_text = "--#{param_name}"
            if param.takes_arg
              arg_text += "=#{param_name.upcase}"
            end
            puts %[    #{sprintf("%-25s", "#{arg_text}")} #{param.description}]
          end
        end
      end

      unless @variant_groups.empty?
        @variant_groups.each do |variant_group|
          puts "\nVariant group#{variant_group[:name] ? " '#{variant_group[:name]}'" : ""}:"
          variant_group[:variants].each do |variant|
            puts "  #{variant[:name]}#{variant[:enabled] ? " (enabled)" : ""}"
          end
        end
      end
    end

    def apply_task_params(only_configure)
      @tasks_and_params.each do |task_name, task_params|
        unless only_configure && task_name != "configure"
          task_params.each do |param_name, param_value|
            if param = Task[task_name].params[param_name]
              Task[task_name].set_param_value(param_name, param_value)
            else
              raise RsconsError.new("Unknown parameter #{param_name.inspect} for task #{task_name}")
            end
          end
        end
      end
    end

    def should_load_script
      return true if @tasks_and_params.empty?
      return true if Cache.instance["configuration_data"]["configured"]
      return false if (@tasks_and_params.keys - %w[distclean clean uninstall]).empty?
      true
    end

  end

end
