# rubocop:disable all
module Fastlane
  module Actions
    module SharedValues
      XCODEBUILD_ARCHIVE ||= :XCODEBUILD_ARCHIVE
      XCODEBUILD_DERIVED_DATA_PATH = :XCODEBUILD_DERIVED_DATA_PATH
    end

    # xcodebuild man page:
    # https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man1/xcodebuild.1.html

    class XcodebuildAction < Action
      ARGS_MAP = {
        # actions
        analyze: "analyze",
        archive: "archive",
        build: "build",
        clean: "clean",
        install: "install",
        installsrc: "installsrc",
        test: "test",

        # parameters
        alltargets: "-alltargets",
        arch: "-arch",
        archive_path: "-archivePath",
        configuration: "-configuration",
        derivedDataPath: "-derivedDataPath",
        destination_timeout: "-destination-timeout",
        dry_run: "-dry-run",
        enableAddressSanitizer: "-enableAddressSanitizer",
        enableThreadSanitizer: "-enableThreadSanitizer",
        enableCodeCoverage: "-enableCodeCoverage",
        export_archive: "-exportArchive",
        export_format: "-exportFormat",
        export_installer_identity: "-exportInstallerIdentity",
        export_options_plist: "-exportOptionsPlist",
        export_path: "-exportPath",
        export_profile: "-exportProvisioningProfile",
        export_signing_identity: "-exportSigningIdentity",
        export_with_original_signing_identity: "-exportWithOriginalSigningIdentity",
        hide_shell_script_environment: "-hideShellScriptEnvironment",
        jobs: "-jobs",
        parallelize_targets: "-parallelizeTargets",
        project: "-project",
        result_bundle_path: "-resultBundlePath",
        scheme: "-scheme",
        sdk: "-sdk",
        skip_unavailable_actions: "-skipUnavailableActions",
        target: "-target",
        toolchain: "-toolchain",
        workspace: "-workspace",
        xcconfig: "-xcconfig"
      }

      def self.is_supported?(platform)
        [:ios, :mac].include? platform
      end

      def self.example_code
        [
          'xcodebuild(
            archive: true,
            archive_path: "./build-dir/MyApp.xcarchive",
            scheme: "MyApp",
            workspace: "MyApp.xcworkspace"
          )'
        ]
      end

      def self.category
        :building
      end

      def self.run(params)
        unless Helper.test?
          UI.user_error!("xcodebuild not installed") if `which xcodebuild`.length == 0
        end

        # The args we will build with
        xcodebuild_args = Array[]

        # Supported ENV vars
        build_path    = ENV["XCODE_BUILD_PATH"] || nil
        scheme        = ENV["XCODE_SCHEME"]
        workspace     = ENV["XCODE_WORKSPACE"]
        project       = ENV["XCODE_PROJECT"]
        buildlog_path = ENV["XCODE_BUILDLOG_PATH"]

        # Set derived data path.
        params[:derivedDataPath] ||= ENV["XCODE_DERIVED_DATA_PATH"]
        Actions.lane_context[SharedValues::XCODEBUILD_DERIVED_DATA_PATH] = params[:derivedDataPath]

        # Append slash to build path, if needed
        if build_path && !build_path.end_with?("/")
          build_path += "/"
        end

        # By default we use xcpretty
        raw_buildlog = false

        # By default we don't pass the utf flag
        xcpretty_utf = false

        if params
          # Operation bools
          archiving    = params.key? :archive
          exporting    = params.key? :export_archive
          testing      = params.key? :test
          xcpretty_utf = params[:xcpretty_utf]

          if params.key? :raw_buildlog
            raw_buildlog = params[:raw_buildlog]
          end

          if exporting
            # If not passed, retrieve path from previous xcodebuild call
            params[:archive_path] ||= Actions.lane_context[SharedValues::XCODEBUILD_ARCHIVE]

            # If not passed, construct export path from env vars
            if params[:export_path].nil?
              ipa_filename = scheme ? scheme : File.basename(params[:archive_path], ".*")
              params[:export_path] = "#{build_path}#{ipa_filename}"
            end

            # Default to ipa as export format
            export_format = params[:export_format] || "ipa"

            # Store IPA path for later deploy steps (i.e. Crashlytics)
            Actions.lane_context[SharedValues::IPA_OUTPUT_PATH] = params[:export_path] + "." + export_format.downcase
          else
            # If not passed, check for archive scheme & workspace/project env vars
            params[:scheme] ||= scheme
            params[:workspace] ||= workspace
            params[:project] ||= project

            # If no project or workspace was passed in or set as an environment
            # variable, attempt to autodetect the workspace.
            if params[:project].to_s.empty? && params[:workspace].to_s.empty?
              params[:workspace] = detect_workspace
            end
          end

          if archiving
            # If not passed, construct archive path from env vars
            params[:archive_path] ||= "#{build_path}#{params[:scheme]}.xcarchive"

            # Cache path for later xcodebuild calls
            Actions.lane_context[SharedValues::XCODEBUILD_ARCHIVE] = params[:archive_path]
          end

          if params.key? :enable_address_sanitizer
            params[:enableAddressSanitizer] = params[:enable_address_sanitizer] ? 'YES' : 'NO'
          end
          if params.key? :enable_thread_sanitizer
            params[:enableThreadSanitizer] = params[:enable_thread_sanitizer] ? 'YES' : 'NO'
          end
          if params.key? :enable_code_coverage
            params[:enableCodeCoverage] = params[:enable_code_coverage] ? 'YES' : 'NO'
          end

          # Maps parameter hash to CLI args
          params = export_options_to_plist(params)
          if hash_args = hash_to_args(params)
            xcodebuild_args += hash_args
          end

          buildlog_path ||= params[:buildlog_path]
        end

        # By default we put xcodebuild.log in the Logs folder
        buildlog_path ||= File.expand_path("#{FastlaneCore::Helper.buildlog_path}/fastlane/xcbuild/#{Time.now.strftime('%F')}/#{Process.pid}")

        # Joins args into space delimited string
        xcodebuild_args = xcodebuild_args.join(" ")

        # Default args
        xcpretty_args = []

        # Formatting style
        if params && params[:output_style]
          output_style = params[:output_style]
          UI.user_error!("Invalid output_style #{output_style}") unless [:standard, :basic].include?(output_style)
        else
          output_style = :standard
        end

        case output_style
        when :standard
          xcpretty_args << '--color' unless Helper.colors_disabled?
        when :basic
          xcpretty_args << '--no-utf'
        end

        if testing
          if params[:reports]
            # New report options format
            reports = params[:reports].reduce("") do |arguments, report|
              report_string = "--report #{report[:report]}"

              if report[:output]
                report_string << " --output \"#{report[:output]}\""
              elsif report[:report] == 'junit'
                report_string << " --output \"#{build_path}report/report.xml\""
              elsif report[:report] == 'html'
                report_string << " --output \"#{build_path}report/report.html\""
              elsif report[:report] == 'json-compilation-database'
                report_string << " --output \"#{build_path}report/report.json\""
              end

              if report[:screenshots]
                report_string << " --screenshots"
              end

              unless arguments == ""
                arguments << " "
              end

              arguments << report_string
            end

            xcpretty_args.push reports

          elsif params[:report_formats]
            # Test report file format
            report_formats = params[:report_formats].map do |format|
              "--report #{format}"
            end.sort.join(" ")

            xcpretty_args.push report_formats

            # Save screenshots flag
            if params[:report_formats].include?("html") && params[:report_screenshots]
              xcpretty_args.push "--screenshots"
            end

            xcpretty_args.sort!

            # Test report file path
            if params[:report_path]
              xcpretty_args.push "--output \"#{params[:report_path]}\""
            elsif build_path
              xcpretty_args.push "--output \"#{build_path}report\""
            end
          end
        end

        # Stdout format
        if testing && !archiving
          xcpretty_args << (params[:xcpretty_output] ? "--#{params[:xcpretty_output]}" : "--test")
        else
          xcpretty_args << (params[:xcpretty_output] ? "--#{params[:xcpretty_output]}" : "--simple")
        end

        xcpretty_args = xcpretty_args.join(" ")

        xcpretty_command = ""
        xcpretty_command = "| xcpretty #{xcpretty_args}" unless raw_buildlog
        unless raw_buildlog
          xcpretty_command = "#{xcpretty_command} --utf" if xcpretty_utf
        end

        pipe_command = "| tee '#{buildlog_path}/xcodebuild.log' #{xcpretty_command}"

        FileUtils.mkdir_p buildlog_path
        UI.message("For a more detailed xcodebuild log open #{buildlog_path}/xcodebuild.log")

        output_result = ""

        override_architecture_prefix = params[:xcodebuild_architecture] ? "arch -#{params[:xcodebuild_architecture]} " : ""
        # In some cases the simulator is not booting up in time
        # One way to solve it is to try to rerun it for one more time
        begin
          output_result = Actions.sh "set -o pipefail && #{override_architecture_prefix}xcodebuild #{xcodebuild_args} #{pipe_command}"
        rescue => ex
          exit_status = $?.exitstatus

          raise_error = true
          if exit_status.eql? 65
            iphone_simulator_time_out_error = /iPhoneSimulator: Timed out waiting/

            if (iphone_simulator_time_out_error =~ ex.message) != nil
              raise_error = false

              UI.important("First attempt failed with iPhone Simulator error: #{iphone_simulator_time_out_error.source}")
              UI.important("Retrying once more...")
              output_result = Actions.sh "set -o pipefail && xcodebuild #{xcodebuild_args} #{pipe_command}"
            end
          end

          raise ex if raise_error
        end

        # If raw_buildlog and some reports had to be created, create xcpretty reports from the build log
        if raw_buildlog && xcpretty_args.include?('--report')
          output_result = Actions.sh "set -o pipefail && cat '#{buildlog_path}/xcodebuild.log' | xcpretty #{xcpretty_args} > /dev/null"
        end

        output_result
      end

      def self.export_options_to_plist(hash)
        # Extract export options parameters from input options
        if hash.has_key?(:export_options_plist) && hash[:export_options_plist].is_a?(Hash)
          export_options = hash[:export_options_plist]

          # Normalize some values
          export_options[:teamID] = CredentialsManager::AppfileConfig.try_fetch_value(:team_id) if !export_options[:teamID] && CredentialsManager::AppfileConfig.try_fetch_value(:team_id)
          export_options[:onDemandResourcesAssetPacksBaseURL] = Addressable::URI.encode(export_options[:onDemandResourcesAssetPacksBaseURL]) if export_options[:onDemandResourcesAssetPacksBaseURL]
          if export_options[:manifest]
            export_options[:manifest][:appURL] = Addressable::URI.encode(export_options[:manifest][:appURL]) if export_options[:manifest][:appURL]
            export_options[:manifest][:displayImageURL] = Addressable::URI.encode(export_options[:manifest][:displayImageURL]) if export_options[:manifest][:displayImageURL]
            export_options[:manifest][:fullSizeImageURL] = Addressable::URI.encode(export_options[:manifest][:fullSizeImageURL]) if export_options[:manifest][:fullSizeImageURL]
            export_options[:manifest][:assetPackManifestURL] = Addressable::URI.encode(export_options[:manifest][:assetPackManifestURL]) if export_options[:manifest][:assetPackManifestURL]
          end

          # Saves options to plist
          path = "#{Tempfile.new('exportOptions').path}.plist"
          File.write(path, export_options.to_plist)
          hash[:export_options_plist] = path
        end
        hash
      end

      def self.hash_to_args(hash)
        # Remove nil value params
        hash = hash.delete_if { |_, v| v.nil? }

        # Maps nice developer param names to CLI arguments
        hash.map do |k, v|
          v ||= ""
          if arg = ARGS_MAP[k]
            value = (v != true && v.to_s.length > 0 ? "\"#{v}\"" : "")
            "#{arg} #{value}".strip
          elsif k == :build_settings
            v.map do |setting, val|
              val = clean_build_setting_value(val)
              "#{setting}=\"#{val}\""
            end.join(' ')
          elsif k == :destination
            [*v].collect { |dst| "-destination \"#{dst}\"" }.join(' ')
          elsif k == :keychain && v.to_s.length > 0
            # If keychain is specified, append as OTHER_CODE_SIGN_FLAGS
            "OTHER_CODE_SIGN_FLAGS=\"--keychain #{v}\""
          elsif k == :xcargs && v.to_s.length > 0
            # Add more xcodebuild arguments
            "#{v}"
          end
        end.compact
      end

      # Cleans values for build settings
      # Only escaping `$(inherit)` types of values since "sh"
      # interprets these as sub-commands instead of passing value into xcodebuild
      def self.clean_build_setting_value(value)
        value.to_s.gsub('$(', '\\$(')
      end

      def self.detect_workspace
        workspace = nil
        workspaces = Dir.glob("*.xcworkspace")

        if workspaces.length > 1
          UI.important("Multiple workspaces detected.")
        end

        unless workspaces.empty?
          workspace = workspaces.first
          UI.important("Using workspace \"#{workspace}\"")
        end

        return workspace
      end

      def self.description
        "Use the `xcodebuild` command to build and sign your app"
      end

      def self.available_options
        [
          ['archive', 'Set to true to build archive'],
          ['archive_path', 'The path to archive the to. Must contain `.xcarchive`'],
          ['workspace', 'The workspace to use'],
          ['scheme', 'The scheme to build'],
          ['build_settings', 'Hash of additional build information'],
          ['xcargs', 'Pass additional xcodebuild options'],
          ['buildlog_path', 'The path where the xcodebuild.log will be created, by default it is created in ~/Library/Logs/fastlane/xcbuild'],
          ['output_style', 'Set the output format to one of: :standard (Colored UTF8 output, default), :basic (black & white ASCII output)'],
          ['xcodebuild_architecture', 'Allows to set the architecture that `xcodebuild` is run with, for example to force it to run under Rosetta on an Apple Silicon mac'],
          ['raw_buildlog', 'Set to true to see xcodebuild raw output. Default value is false'],
          ['xcpretty_output', 'specifies the output type for xcpretty. eg. \'test\', or \'simple\''],
          ['xcpretty_utf', 'Specifies xcpretty should use utf8 when reporting builds. This has no effect when raw_buildlog is specified.']
        ]
      end

      def self.details
        "**Note**: `xcodebuild` is a complex command, so it is recommended to use [_gym_](https://docs.fastlane.tools/actions/gym/) for building your ipa file and [_scan_](https://docs.fastlane.tools/actions/scan/) for testing your app instead."
      end

      def self.author
        "dtrenz"
      end
    end

    class XcarchiveAction < Action
      def self.run(params)
        params_hash = params || {}
        params_hash[:archive] = true
        XcodebuildAction.run(params_hash)
      end

      def self.description
        "Archives the project using `xcodebuild`"
      end

      def self.example_code
        [
          'xcarchive'
        ]
      end

      def self.category
        :building
      end

      def self.author
        "dtrenz"
      end

      def self.is_supported?(platform)
        [:ios, :mac].include? platform
      end

      def self.available_options
        [
          ['archive_path', 'The path to archive the to. Must contain `.xcarchive`'],
          ['workspace', 'The workspace to use'],
          ['scheme', 'The scheme to build'],
          ['build_settings', 'Hash of additional build information'],
          ['xcargs', 'Pass additional xcodebuild options'],
          ['output_style', 'Set the output format to one of: :standard (Colored UTF8 output, default), :basic (black & white ASCII output)'],
          ['xcodebuild_architecture', 'Allows to set the architecture that `xcodebuild` is run with, for example to force it to run under Rosetta on an Apple Silicon mac'],
          ['buildlog_path', 'The path where the xcodebuild.log will be created, by default it is created in ~/Library/Logs/fastlane/xcbuild'],
          ['raw_buildlog', 'Set to true to see xcodebuild raw output. Default value is false'],
          ['xcpretty_output', 'specifies the output type for xcpretty. eg. \'test\', or \'simple\''],
          ['xcpretty_utf', 'Specifies xcpretty should use utf8 when reporting builds. This has no effect when raw_buildlog is specified.']
        ]
      end
    end

    class XcbuildAction < Action
      def self.run(params)
        params_hash = params || {}
        params_hash[:build] = true
        XcodebuildAction.run(params_hash)
      end

      def self.example_code
        [
          'xcbuild'
        ]
      end

      def self.category
        :building
      end

      def self.description
        "Builds the project using `xcodebuild`"
      end

      def self.author
        "dtrenz"
      end

      def self.is_supported?(platform)
        [:ios, :mac].include? platform
      end

      def self.available_options
        [
          ['archive', 'Set to true to build archive'],
          ['archive_path', 'The path to archive the to. Must contain `.xcarchive`'],
          ['workspace', 'The workspace to use'],
          ['scheme', 'The scheme to build'],
          ['build_settings', 'Hash of additional build information'],
          ['xcargs', 'Pass additional xcodebuild options'],
          ['output_style', 'Set the output format to one of: :standard (Colored UTF8 output, default), :basic (black & white ASCII output)'],
          ['buildlog_path', 'The path where the xcodebuild.log will be created, by default it is created in ~/Library/Logs/fastlane/xcbuild'],
          ['raw_buildlog', 'Set to true to see xcodebuild raw output. Default value is false'],
          ['xcpretty_output', 'specifies the output type for xcpretty. eg. \'test\', or \'simple\''],
          ['xcpretty_utf', 'Specifies xcpretty should use utf8 when reporting builds. This has no effect when raw_buildlog is specified.']
        ]
      end
    end

    class XccleanAction < Action
      def self.run(params)
        params_hash = params || {}
        params_hash[:clean] = true
        XcodebuildAction.run(params_hash)
      end

      def self.description
        "Cleans the project using `xcodebuild`"
      end

      def self.example_code
        [
          'xcclean'
        ]
      end

      def self.category
        :building
      end

      def self.author
        "dtrenz"
      end

      def self.is_supported?(platform)
        [:ios, :mac].include? platform
      end

      def self.available_options
        [
          ['archive', 'Set to true to build archive'],
          ['archive_path', 'The path to archive the to. Must contain `.xcarchive`'],
          ['workspace', 'The workspace to use'],
          ['scheme', 'The scheme to build'],
          ['build_settings', 'Hash of additional build information'],
          ['xcargs', 'Pass additional xcodebuild options'],
          ['output_style', 'Set the output format to one of: :standard (Colored UTF8 output, default), :basic (black & white ASCII output)'],
          ['xcodebuild_architecture', 'Allows to set the architecture that `xcodebuild` is run with, for example to force it to run under Rosetta on an Apple Silicon mac'],
          ['buildlog_path', 'The path where the xcodebuild.log will be created, by default it is created in ~/Library/Logs/fastlane/xcbuild'],
          ['raw_buildlog', 'Set to true to see xcodebuild raw output. Default value is false'],
          ['xcpretty_output', 'specifies the output type for xcpretty. eg. \'test\', or \'simple\''],
          ['xcpretty_utf', 'Specifies xcpretty should use utf8 when reporting builds. This has no effect when raw_buildlog is specified.']
        ]
      end
    end

    class XcexportAction < Action
      def self.run(params)
        params_hash = params || {}
        params_hash[:export_archive] = true
        XcodebuildAction.run(params_hash)
      end

      def self.description
        "Exports the project using `xcodebuild`"
      end

      def self.example_code
        [
          'xcexport'
        ]
      end

      def self.category
        :building
      end

      def self.author
        "dtrenz"
      end

      def self.available_options
        [
          ['archive', 'Set to true to build archive'],
          ['archive_path', 'The path to archive the to. Must contain `.xcarchive`'],
          ['workspace', 'The workspace to use'],
          ['scheme', 'The scheme to build'],
          ['build_settings', 'Hash of additional build information'],
          ['xcargs', 'Pass additional xcodebuild options'],
          ['output_style', 'Set the output format to one of: :standard (Colored UTF8 output, default), :basic (black & white ASCII output)'],
          ['xcodebuild_architecture', 'Allows to set the architecture that `xcodebuild` is run with, for example to force it to run under Rosetta on an Apple Silicon mac'],
          ['buildlog_path', 'The path where the xcodebuild.log will be created, by default it is created in ~/Library/Logs/fastlane/xcbuild'],
          ['raw_buildlog', 'Set to true to see xcodebuild raw output. Default value is false'],
          ['xcpretty_output', 'specifies the output type for xcpretty. eg. \'test\', or \'simple\''],
          ['xcpretty_utf', 'Specifies xcpretty should use utf8 when reporting builds. This has no effect when raw_buildlog is specified.']
        ]
      end

      def self.is_supported?(platform)
        [:ios, :mac].include? platform
      end
    end

    class XctestAction < Action
      def self.run(params)
        UI.important("Have you seen the new 'scan' tool to run tests? https://docs.fastlane.tools/actions/scan/")
        params_hash = params || {}
        params_hash[:build] = true
        params_hash[:test] = true

        XcodebuildAction.run(params_hash)
      end

      def self.example_code
        [
          'xctest(
            destination: "name=iPhone 7s,OS=10.0"
          )'
        ]
      end

      def self.category
        :building
      end

      def self.description
        "Runs tests on the given simulator"
      end

      def self.available_options
        [
          ['archive', 'Set to true to build archive'],
          ['archive_path', 'The path to archive the to. Must contain `.xcarchive`'],
          ['workspace', 'The workspace to use'],
          ['scheme', 'The scheme to build'],
          ['build_settings', 'Hash of additional build information'],
          ['xcargs', 'Pass additional xcodebuild options'],
          ['destination', 'The simulator to use, e.g. "name=iPhone 5s,OS=8.1"'],
          ['destination_timeout', 'The timeout for connecting to the simulator, in seconds'],
          ['enable_code_coverage', 'Turn code coverage on or off when testing. eg. true|false. Requires Xcode 7+'],
          ['output_style', 'Set the output format to one of: :standard (Colored UTF8 output, default), :basic (black & white ASCII output)'],
          ['xcodebuild_architecture', 'Allows to set the architecture that `xcodebuild` is run with, for example to force it to run under Rosetta on an Apple Silicon mac'],
          ['buildlog_path', 'The path where the xcodebuild.log will be created, by default it is created in ~/Library/Logs/fastlane/xcbuild'],
          ['raw_buildlog', 'Set to true to see xcodebuild raw output. Default value is false'],
          ['xcpretty_output', 'specifies the output type for xcpretty. eg. \'test\', or \'simple\''],
          ['xcpretty_utf', 'Specifies xcpretty should use utf8 when reporting builds. This has no effect when raw_buildlog is specified.']
        ]
      end

      def self.is_supported?(platform)
        [:ios, :mac].include? platform
      end

      def self.author
        "dtrenz"
      end
    end
  end
end
# rubocop:enable all
