# encoding: utf-8
require 'set'
require 'yaml'
require 'command_line_reporter'

require_relative 'directory'

class SassSpec::Annotate::CLI
  include CommandLineReporter

  def self.assert_legal_version(version)
    if version && !SassSpec::LANGUAGE_VERSIONS.include?(version)
      warn "Version #{version} is not valid. " +
           "Did you mean one of: #{SassSpec::LANGUAGE_VERSIONS.join(', ')}"
      return false
    end
    true
  end

  def self.assert_not_file!(string, expected)
    if File.exist?(string) || string.include?(File::SEPARATOR)
      warn "Expected #{expected} but got a file path. Did you forget the argument?"
      return false
    end
    true
  end

  def self.parse(args)
    runner_options = {
    }
    options = {
    }
    parser = OptionParser.new do |opts|
      opts.banner = <<BANNER
Usage: ./sass-spec.rb annotate [options] PATH [PATH...]

This sub command helps you annotate spec tests.

BANNER

      opts.on("--start-version VERSION",
              "Set the Sass language first version for which the test(s) are valid.",
              "Pass a version of 'unset' to remove the start version.") do |version|
        version = nil if version =~ /unset/i
        return unless assert_legal_version(version)
        options[:start_version] = version
      end

      opts.on("--end-version VERSION",
              "Set the Sass language first version for which the test(s) are valid.",
              "Pass a version of 'unset' to remove the end version.") do |version|
        version = nil if version =~ /unset/i
        return unless assert_legal_version(version)
        options[:end_version] = version
      end

      opts.on("--precision INTEGER",
              "Set the numeric output precision for the specified tests.",
              "Pass a precision of 'unset' to remove the precision.") do |precision|
        if precision =~ /unset/i
          precision = nil
        elsif precision =~ /^\d+$/
          precision = precision.to_i
        else
          warn "Precision must be set to a positive integer (or to 'unset')\n\n"
          warn opts.help()
          return nil
        end
        options[:precision] = precision
      end

      opts.on("--pending IMPLEMENTATION",
              "Mark implementation as not having implemented the tests.") do |impl|
        return unless assert_not_file!(impl, "implementation for --pending")
        (options[:add_todo] ||= Set.new) << impl
      end

      opts.on("--activate IMPLEMENTATION",
              "Mark implementation as having implemented the tests.") do |impl|
        return unless assert_not_file!(impl, "implementation for --activate")
        (options[:remove_todo] ||= Set.new) << impl
      end

      opts.on("--pending-warning IMPLEMENTATION",
              "Mark implementation as not having implemented the warnings issued by the tests.") do |impl|
        return unless assert_not_file!(impl, "implementation for --pending")
        (options[:add_warning_todo] ||= Set.new) << impl
      end

      opts.on("--activate-warning IMPLEMENTATION",
              "Mark implementation as having implemented the warnings issued by the tests.") do |impl|
        return unless assert_not_file!(impl, "implementation for --activate")
        (options[:remove_warning_todo] ||= Set.new) << impl
      end

      opts.on("--ignore-for IMPLEMENTATION",
              "Flag test so that it won't run against the specified implementation.") do |impl|
        return unless assert_not_file!(impl, "implementation for --ignore-for")
        (options[:add_ignore_for] ||= Set.new) << impl
      end

      opts.on("--unignore-for IMPLEMENTATION",
              "Remove the ignore flag so that the test will run against the specified implementation.") do |impl|
        return unless assert_not_file!(impl, "implementation for --unignore-for")
        (options[:remove_ignore_for] ||= Set.new) << impl
      end

      opts.on("--ignore-warning-for IMPLEMENTATION",
              "Flag test so that it won't check warnings with the specified implementation.") do |impl|
        return unless assert_not_file!(impl, "implementation for --ignore-warning-for")
        (options[:add_ignore_warning_for] ||= Set.new) << impl
      end

      opts.on("--unignore-warning-for IMPLEMENTATION",
              "Remove the ignore flag so that warnings are checked with the specified implementation.") do |impl|
        return unless assert_not_file!(impl, "implementation for --unignore-warning-for")
        (options[:remove_ignore_warning_for] ||= Set.new) << impl
      end

      opts.on("--report", "Generate a report after running.") do |impl|
        runner_options[:report] = true
      end

      opts.on("-h", "--help", "Print this help message.") do |impl|
        puts opts.help()
        return nil
      end
    end
    parser.parse!(args)
    if args.empty?
      warn parser.help
      return nil
    end
    args.each do |path|
      unless File.exists?(path)
        warn "Error: #{path} does not exist."
        return nil
      end
    end
    new(options, runner_options, args)
  end

  def initialize(options, runner_options, paths)
    @runner_options = runner_options
    @options = options
    @paths = paths
  end

  def annotate
    @paths.each {|path| annotate_path(path)}
    if @runner_options[:report]
      require 'terminfo'
      @paths.each {|path| report_path(path)}
    end
    return true
  end

  # If you change this, also change TestCaseMetadata.merge_options
  def annotate_path(path)
    report(message: "#{path}:", complete: "") do
      options_file = path.end_with?("options.yml") ? path : File.join(path, "options.yml")
      if File.exists?(options_file)
        current_options = YAML.load_file(options_file)
      else
        current_options = {}
      end
      @options.each do |(key, value)|
        if key =~ /add_(.*)/
          key = $1.to_sym
          current_options[key] ||= []
          value.each do |v|
            current_options[key] << v
            log("* adding #{v} to #{key} list")
          end
          current_options[key].uniq!
        elsif key =~ /remove_(.*)/
          key = $1.to_sym
          current_options[key] ||= []
          value.each do |v|
            current_options[key].delete(v)
            log("* removing #{v} from #{key} list")
          end
          current_options.delete(key) if current_options[key].empty?
        elsif value.nil?
          current_options.delete(key)
          log("* unsetting #{key}") {}
        else
          current_options[key] = value
          log("* setting #{key} to #{value}") {}
        end
      end

      if current_options.empty?
        File.delete(options_file) if File.exists?(options_file)
      else
        File.write(options_file, current_options.to_yaml)
      end
    end
  end

  def report_path(path)
    test_case_dirs = Dir.glob(File.join(path, "**/input.scss")).map {|p| File.dirname(p) }.uniq.sort
    metadatas = test_case_dirs.map {|d| SassSpec::TestCaseMetadata.new(SassSpec::Directory.new(d))}
    TermInfo.screen_size
    max_length = [
      metadatas.map {|m| m.name.length}.max,
      TermInfo.screen_size.last - SassSpec::LANGUAGE_VERSIONS.length * 6 - 4
    ].min
    table(border: true, encoding: :ascii, width: max_length + SassSpec::LANGUAGE_VERSIONS.length * 3) do
      row(header: true) do
        column("Test Case", width: max_length)
        SassSpec::LANGUAGE_VERSIONS.each do |version|
          column(version, width: 3, align: "center")
        end
      end
      metadatas.each do |md|
        row do
          report_test_case(md)
        end
      end
    end
  end

  def report_test_case(metadata)
    column(metadata.name)
    SassSpec::LANGUAGE_VERSIONS.each do |version|
      v = Gem::Version.new(version)
      if metadata.valid_for_version?(v)
        column("✓")
      else
        column("")
      end
    end
  end

  def log(message, &block)
    block = Proc.new {} unless block
    report(message: message, type: 'inline', complete: "done", &block)
  end
end
