module Fastlane
  module Actions
    module SharedValues
      FL_OCLINT_REPORT_PATH = :FL_OCLINT_REPORT_PATH
    end

    class OclintAction < Action
      # rubocop:disable Metrics/PerceivedComplexity
      def self.run(params)
        oclint_path = params[:oclint_path]
        if `which #{oclint_path}`.to_s.empty? && !Helper.test?
          UI.user_error!("You have to install oclint or provide path to oclint binary. Fore more details: ") + "http://docs.oclint.org/en/stable/intro/installation.html".yellow
        end

        compile_commands = params[:compile_commands]
        compile_commands_dir = params[:compile_commands]
        UI.user_error!("Could not find json compilation database at path '#{compile_commands}'") unless File.exist?(compile_commands)

        # We'll attempt to sort things out so that we support receiving either a path to a
        # 'compile_commands.json' file (as our option asks for), or a path to a directory
        # *containing* a 'compile_commands.json' file (as oclint actually wants)
        if File.file?(compile_commands_dir)
          compile_commands_dir = File.dirname(compile_commands_dir)
        else
          compile_commands = File.join(compile_commands_dir, 'compile_commands.json')
        end

        if params[:select_reqex]
          UI.important("'select_reqex' parameter is deprecated. Please use 'select_regex' instead.")
          select_regex = params[:select_reqex]
        end

        select_regex = params[:select_regex] if params[:select_regex] # Overwrite deprecated select_reqex
        select_regex = ensure_regex_is_not_string!(select_regex)

        exclude_regex = params[:exclude_regex]
        exclude_regex = ensure_regex_is_not_string!(exclude_regex)

        files = JSON.parse(File.read(compile_commands)).map do |compile_command|
          file = compile_command['file']
          File.exist?(file) ? file : File.join(compile_command['directory'], file)
        end

        files.uniq!
        files.select! do |file|
          file_ruby = file.gsub('\ ', ' ')
          File.exist?(file_ruby) and
            (!select_regex or file_ruby =~ select_regex) and
            (!exclude_regex or file_ruby !~ exclude_regex)
        end

        command_prefix = [
          'cd',
          File.expand_path('.').shellescape,
          '&&'
        ].join(' ')

        report_type = params[:report_type]
        report_path = params[:report_path] ? params[:report_path] : 'oclint_report.' + report_type

        oclint_args = ["-report-type=#{report_type}", "-o=#{report_path}"]

        oclint_args << "-list-enabled-rules" if params[:list_enabled_rules]

        if params[:rc]
          UI.important("It's recommended to use 'thresholds' instead of deprecated 'rc' parameter")
          oclint_args << "-rc=#{params[:rc]}" if params[:rc] # Deprecated
        end

        oclint_args << ensure_array_is_not_string!(params[:thresholds]).map { |t| "-rc=#{t}" } if params[:thresholds]
        # Escape ' in rule names with \' when passing on to shell command
        oclint_args << params[:enable_rules].map { |r| "-rule #{r.shellescape}" } if params[:enable_rules]
        oclint_args << params[:disable_rules].map { |r| "-disable-rule #{r.shellescape}" } if params[:disable_rules]

        oclint_args << "-max-priority-1=#{params[:max_priority_1]}" if params[:max_priority_1]
        oclint_args << "-max-priority-2=#{params[:max_priority_2]}" if params[:max_priority_2]
        oclint_args << "-max-priority-3=#{params[:max_priority_3]}" if params[:max_priority_3]

        oclint_args << "-enable-clang-static-analyzer" if params[:enable_clang_static_analyzer]
        oclint_args << "-enable-global-analysis" if params[:enable_global_analysis]
        oclint_args << "-allow-duplicated-violations" if params[:allow_duplicated_violations]
        oclint_args << "-p #{compile_commands_dir.shellescape}"

        oclint_args << "-extra-arg=#{params[:extra_arg]}" if params[:extra_arg]

        command = [
          command_prefix,
          oclint_path,
          oclint_args,
          '"' + files.join('" "') + '"'
        ].join(' ')

        Actions.lane_context[SharedValues::FL_OCLINT_REPORT_PATH] = File.expand_path(report_path)

        return Action.sh(command)
      end

      # return a proper regex object if regex string is single-quoted
      def self.ensure_regex_is_not_string!(regex)
        return regex unless regex.kind_of?(String)

        Regexp.new(regex)
      end

      # return a proper array of strings if array string is single-quoted
      def self.ensure_array_is_not_string!(array)
        return array unless array.kind_of?(String)

        array.split(',')
      end

      #####################################################
      # @!group Documentation
      #####################################################

      def self.description
        "Lints implementation files with OCLint"
      end

      def self.available_options
        [
          FastlaneCore::ConfigItem.new(key: :oclint_path,
                                       env_name: 'FL_OCLINT_PATH',
                                       description: 'The path to oclint binary',
                                       default_value: 'oclint',
                                       optional: true),
          FastlaneCore::ConfigItem.new(key: :compile_commands,
                                       env_name: 'FL_OCLINT_COMPILE_COMMANDS',
                                       description: 'The json compilation database, use xctool reporter \'json-compilation-database\'',
                                       default_value: 'compile_commands.json',
                                       optional: true),
          FastlaneCore::ConfigItem.new(key: :select_reqex,
                                       env_name: 'FL_OCLINT_SELECT_REQEX',
                                       description: 'Select all files matching this reqex',
                                       skip_type_validation: true, # allows Regex
                                       deprecated: "Use `:select_regex` instead",
                                       optional: true),
          FastlaneCore::ConfigItem.new(key: :select_regex,
                                       env_name: 'FL_OCLINT_SELECT_REGEX',
                                       description: 'Select all files matching this regex',
                                       skip_type_validation: true, # allows Regex
                                       optional: true),
          FastlaneCore::ConfigItem.new(key: :exclude_regex,
                                       env_name: 'FL_OCLINT_EXCLUDE_REGEX',
                                       description: 'Exclude all files matching this regex',
                                       skip_type_validation: true, # allows Regex
                                       optional: true),
          FastlaneCore::ConfigItem.new(key: :report_type,
                                       env_name: 'FL_OCLINT_REPORT_TYPE',
                                       description: 'The type of the report (default: html)',
                                       default_value: 'html',
                                       optional: true),
          FastlaneCore::ConfigItem.new(key: :report_path,
                                       env_name: 'FL_OCLINT_REPORT_PATH',
                                       description: 'The reports file path',
                                       optional: true),
          FastlaneCore::ConfigItem.new(key: :list_enabled_rules,
                                       env_name: "FL_OCLINT_LIST_ENABLED_RULES",
                                       description: "List enabled rules",
                                       type: Boolean,
                                       default_value: false),
          FastlaneCore::ConfigItem.new(key: :rc,
                                       env_name: 'FL_OCLINT_RC',
                                       description: 'Override the default behavior of rules',
                                       optional: true),
          FastlaneCore::ConfigItem.new(key: :thresholds,
                                       env_name: 'FL_OCLINT_THRESHOLDS',
                                       description: 'List of rule thresholds to override the default behavior of rules',
                                       type: Array,
                                       optional: true),
          FastlaneCore::ConfigItem.new(key: :enable_rules,
                                       env_name: 'FL_OCLINT_ENABLE_RULES',
                                       description: 'List of rules to pick explicitly',
                                       type: Array,
                                       optional: true),
          FastlaneCore::ConfigItem.new(key: :disable_rules,
                                       env_name: 'FL_OCLINT_DISABLE_RULES',
                                       description: 'List of rules to disable',
                                       type: Array,
                                       optional: true),
          FastlaneCore::ConfigItem.new(key: :max_priority_1,
                                       env_names: ["FL_OCLINT_MAX_PRIOTITY_1", "FL_OCLINT_MAX_PRIORITY_1"], # The version with typo must be deprecated
                                       description: 'The max allowed number of priority 1 violations',
                                       type: Integer,
                                       optional: true),
          FastlaneCore::ConfigItem.new(key: :max_priority_2,
                                       env_names: ["FL_OCLINT_MAX_PRIOTITY_2", "FL_OCLINT_MAX_PRIORITY_2"], # The version with typo must be deprecated
                                       description: 'The max allowed number of priority 2 violations',
                                       type: Integer,
                                       optional: true),
          FastlaneCore::ConfigItem.new(key: :max_priority_3,
                                       env_names: ["FL_OCLINT_MAX_PRIOTITY_3", "FL_OCLINT_MAX_PRIORITY_3"], # The version with typo must be deprecated
                                       description: 'The max allowed number of priority 3 violations',
                                       type: Integer,
                                       optional: true),
          FastlaneCore::ConfigItem.new(key: :enable_clang_static_analyzer,
                                       env_name: "FL_OCLINT_ENABLE_CLANG_STATIC_ANALYZER",
                                       description: "Enable Clang Static Analyzer, and integrate results into OCLint report",
                                       type: Boolean,
                                       default_value: false),
          FastlaneCore::ConfigItem.new(key: :enable_global_analysis,
                                       env_name: "FL_OCLINT_ENABLE_GLOBAL_ANALYSIS",
                                       description: "Compile every source, and analyze across global contexts (depends on number of source files, could results in high memory load)",
                                       type: Boolean,
                                       default_value: false),
          FastlaneCore::ConfigItem.new(key: :allow_duplicated_violations,
                                       env_name: "FL_OCLINT_ALLOW_DUPLICATED_VIOLATIONS",
                                       description: "Allow duplicated violations in the OCLint report",
                                       type: Boolean,
                                       default_value: false),
          FastlaneCore::ConfigItem.new(key: :extra_arg,
                                       env_name: 'FL_OCLINT_EXTRA_ARG',
                                       description: 'Additional argument to append to the compiler command line',
                                       optional: true)
        ]
      end
      # rubocop:enable Metrics/PerceivedComplexity

      def self.output
        [
          ['FL_OCLINT_REPORT_PATH', 'The reports file path']
        ]
      end

      def self.author
        'HeEAaD'
      end

      def self.is_supported?(platform)
        true
      end

      def self.details
        "Run the static analyzer tool [OCLint](http://oclint.org) for your project. You need to have a `compile_commands.json` file in your _fastlane_ directory or pass a path to your file."
      end

      def self.example_code
        [
          'oclint(
            compile_commands: "commands.json",    # The JSON compilation database, use xctool reporter "json-compilation-database"
            select_regex: /ViewController.m/,     # Select all files matching this regex
            exclude_regex: /Test.m/,              # Exclude all files matching this regex
            report_type: "pmd",                   # The type of the report (default: html)
            max_priority_1: 10,                   # The max allowed number of priority 1 violations
            max_priority_2: 100,                  # The max allowed number of priority 2 violations
            max_priority_3: 1000,                 # The max allowed number of priority 3 violations
            thresholds: [                         # Override the default behavior of rules
              "LONG_LINE=200",
              "LONG_METHOD=200"
            ],
            enable_rules: [                       # List of rules to pick explicitly
              "DoubleNegative",
              "SwitchStatementsDon\'TNeedDefaultWhenFullyCovered"
            ],
            disable_rules: ["GotoStatement"],     # List of rules to disable
            list_enabled_rules: true,             # List enabled rules
            enable_clang_static_analyzer: true,   # Enable Clang Static Analyzer, and integrate results into OCLint report
            enable_global_analysis: true,         # Compile every source, and analyze across global contexts (depends on number of source files, could results in high memory load)
            allow_duplicated_violations: true,    # Allow duplicated violations in the OCLint report
            extra_arg: "-Wno-everything"          # Additional argument to append to the compiler command line
          )'
        ]
      end

      def self.category
        :testing
      end
    end
  end
end
