# encoding: utf-8

module XCPretty

  module Matchers

    # @regex Captured groups
    # $1 file_path
    # $2 file_name
    ANALYZE_MATCHER = /^Analyze(?:Shallow)?\s(.*\/(.*\.(?:m|mm|cc|cpp|c|cxx)))\s*/

    # @regex Captured groups
    # $1 target
    # $2 project
    # $3 configuration
    BUILD_TARGET_MATCHER = /^=== BUILD TARGET\s(.*)\sOF PROJECT\s(.*)\sWITH.*CONFIGURATION\s(.*)\s===/

    # @regex Captured groups
    # $1 target
    # $2 project
    # $3 configuration
    AGGREGATE_TARGET_MATCHER = /^=== BUILD AGGREGATE TARGET\s(.*)\sOF PROJECT\s(.*)\sWITH.*CONFIGURATION\s(.*)\s===/

    # @regex Captured groups
    # $1 target
    # $2 project
    # $3 configuration
    ANALYZE_TARGET_MATCHER = /^=== ANALYZE TARGET\s(.*)\sOF PROJECT\s(.*)\sWITH.*CONFIGURATION\s(.*)\s===/

    # @regex Nothing returned here for now
    CHECK_DEPENDENCIES_MATCHER = /^Check dependencies/

    # @regex Captured groups
    # $1 command path
    # $2 arguments
    SHELL_COMMAND_MATCHER = /^\s{4}(cd|setenv|(?:[\w\/:\\\s\-.]+?\/)?[\w\-]+)\s(.*)$/

    # @regex Nothing returned here for now
    CLEAN_REMOVE_MATCHER = /^Clean.Remove/

    # @regex Captured groups
    # $1 target
    # $2 project
    # $3 configuration
    CLEAN_TARGET_MATCHER = /^=== CLEAN TARGET\s(.*)\sOF PROJECT\s(.*)\sWITH CONFIGURATION\s(.*)\s===/

    # @regex Captured groups
    # $1 = file
    CODESIGN_MATCHER = /^CodeSign\s((?:\\ |[^ ])*)$/

    # @regex Captured groups
    # $1 = file
    CODESIGN_FRAMEWORK_MATCHER = /^CodeSign\s((?:\\ |[^ ])*.framework)\/Versions/

    # @regex Captured groups
    # $1 file_path
    # $2 file_name (e.g. KWNull.m)
    COMPILE_MATCHER = /^Compile[\w]+\s.+?\s((?:\\.|[^ ])+\/((?:\\.|[^ ])+\.(?:m|mm|c|cc|cpp|cxx|swift)))\s.*/

    # @regex Captured groups
    # $1 compiler_command
    # $2 file_path
    COMPILE_COMMAND_MATCHER = /^\s*(.*clang\s.*\s\-c\s(.*\.(?:m|mm|c|cc|cpp|cxx))\s.*\.o)$/

    # @regex Captured groups
    # $1 file_path
    # $2 file_name (e.g. MainMenu.xib)
    COMPILE_XIB_MATCHER = /^CompileXIB\s(.*\/(.*\.xib))/

    # @regex Captured groups
    # $1 file_path
    # $2 file_name (e.g. Main.storyboard)
    COMPILE_STORYBOARD_MATCHER = /^CompileStoryboard\s(.*\/([^\/].*\.storyboard))/

    # @regex Captured groups
    # $1 source file
    # $2 target file
    COPY_HEADER_MATCHER = /^CpHeader\s(.*\.h)\s(.*\.h)/

    # @regex Captured groups
    # $1 source file
    # $2 target file
    COPY_PLIST_MATCHER = /^CopyPlistFile\s(.*\.plist)\s(.*\.plist)/

    # $1 file
    COPY_STRINGS_MATCHER = /^CopyStringsFile.*\/(.*.strings)/

    # @regex Captured groups
    # $1 resource
    CPRESOURCE_MATCHER = /^CpResource\s(.*)\s\//

    # @regex Captured groups
    #
    EXECUTED_MATCHER = /^\s*Executed/

    # @regex Captured groups
    # $1 = file
    # $2 = test_suite
    # $3 = test_case
    # $4 = reason
    FAILING_TEST_MATCHER = /^\s*(.+:\d+):\serror:\s[\+\-]\[(.*)\s(.*)\]\s:(?:\s'.*'\s\[FAILED\],)?\s(.*)/

    # @regex Captured groups
    # $1 = file
    # $2 = reason
    UI_FAILING_TEST_MATCHER = /^\s{4}t = \s+\d+\.\d+s\s+Assertion Failure: (.*:\d+): (.*)$/

    # @regex Captured groups
    RESTARTING_TESTS_MATCHER = /^Restarting after unexpected exit or crash in.+$/

    # @regex Captured groups
    # $1 = dsym
    GENERATE_DSYM_MATCHER = /^GenerateDSYMFile \/.*\/(.*\.dSYM)/

    # @regex Captured groups
    # $1 = library
    LIBTOOL_MATCHER = /^Libtool.*\/(.*\.a)/

    # @regex Captured groups
    # $1 = target
    # $2 = build_variants (normal, profile, debug)
    # $3 = architecture
    LINKING_MATCHER = /^Ld \/?.*\/(.*?) (.*) (.*)$/

    # @regex Captured groups
    # $1 = suite
    # $2 = test_case
    # $3 = time
    TEST_CASE_PASSED_MATCHER = /^\s*Test Case\s'-\[(.*)\s(.*)\]'\spassed\s\((\d*\.\d{3})\sseconds\)/


    # @regex Captured groups
    # $1 = suite
    # $2 = test_case
    TEST_CASE_STARTED_MATCHER = /^Test Case '-\[(.*) (.*)\]' started.$/

    # @regex Captured groups
    # $1 = suite
    # $2 = test_case
    TEST_CASE_PENDING_MATCHER = /^Test Case\s'-\[(.*)\s(.*)PENDING\]'\spassed/

    # @regex Captured groups
    # $1 = suite
    # $2 = test_case
    # $3 = time
    TEST_CASE_MEASURED_MATCHER = /^[^:]*:[^:]*:\sTest Case\s'-\[(.*)\s(.*)\]'\smeasured\s\[Time,\sseconds\]\saverage:\s(\d*\.\d{3}),/

    PHASE_SUCCESS_MATCHER = /^\*\*\s(.*)\sSUCCEEDED\s\*\*/

    # @regex Captured groups
    # $1 = script_name
    PHASE_SCRIPT_EXECUTION_MATCHER = /^PhaseScriptExecution\s((\\\ |\S)*)\s/

    # @regex Captured groups
    # $1 = file
    PROCESS_PCH_MATCHER = /^ProcessPCH\s.*\s(.*.pch)/

    # @regex Captured groups
    # $1 file_path
    PROCESS_PCH_COMMAND_MATCHER = /^\s*.*\/usr\/bin\/clang\s.*\s\-c\s(.*)\s\-o\s.*/

    # @regex Captured groups
    # $1 = file
    PREPROCESS_MATCHER = /^Preprocess\s(?:(?:\\ |[^ ])*)\s((?:\\ |[^ ])*)$/

    # @regex Captured groups
    # $1 = file
    PBXCP_MATCHER = /^PBXCp\s((?:\\ |[^ ])*)/

    # @regex Captured groups
    # $1 = file
    PROCESS_INFO_PLIST_MATCHER = /^ProcessInfoPlistFile\s.*\.plist\s(.*\/+(.*\.plist))/

    # @regex Captured groups
    # $1 = suite
    # $2 = time
    TESTS_RUN_COMPLETION_MATCHER = /^\s*Test Suite '(?:.*\/)?(.*[ox]ctest.*)' (finished|passed|failed) at (.*)/

    # @regex Captured groups
    # $1 = suite
    # $2 = time
    TEST_SUITE_STARTED_MATCHER = /^\s*Test Suite '(?:.*\/)?(.*[ox]ctest.*)' started at(.*)/

    # @regex Captured groups
    # $1 test suite name
    TEST_SUITE_START_MATCHER = /^\s*Test Suite '(.*)' started at/

    # @regex Captured groups
    # $1 file_name
    TIFFUTIL_MATCHER = /^TiffUtil\s(.*)/

    # @regex Captured groups
    # $1 file_path
    # $2 file_name
    TOUCH_MATCHER = /^Touch\s(.*\/(.+))/

    # @regex Captured groups
    # $1 file_path
    WRITE_FILE_MATCHER = /^write-file\s(.*)/

    # @regex Captured groups
    WRITE_AUXILIARY_FILES = /^Write auxiliary files/

    module Warnings
      # $1 = file_path
      # $2 = file_name
      # $3 = reason
      COMPILE_WARNING_MATCHER = /^(\/.+\/(.*):.*:.*):\swarning:\s(.*)$/

      # $1 = ld prefix
      # $2 = warning message
      LD_WARNING_MATCHER = /^(ld: )warning: (.*)/

      # @regex Captured groups
      # $1 = whole warning
      GENERIC_WARNING_MATCHER = /^warning:\s(.*)$/

      # @regex Captured groups
      # $1 = whole warning
      WILL_NOT_BE_CODE_SIGNED_MATCHER = /^(.* will not be code signed because .*)$/
    end

    module Errors
      # @regex Captured groups
      # $1 = whole error
      CLANG_ERROR_MATCHER = /^(clang: error:.*)$/

      # @regex Captured groups
      # $1 = whole error
      CHECK_DEPENDENCIES_ERRORS_MATCHER = /^(Code\s?Sign error:.*|Code signing is required for product type .* in SDK .*|No profile matching .* found:.*|Provisioning profile .* doesn't .*|Swift is unavailable on .*|.?Use Legacy Swift Language Version.*)$/

      # @regex Captured groups
      # $1 = whole error
      PROVISIONING_PROFILE_REQUIRED_MATCHER = /^(.*requires a provisioning profile.*)$/

      # @regex Captured groups
      # $1 = whole error
      NO_CERTIFICATE_MATCHER = /^(No certificate matching.*)$/

      # @regex Captured groups
      # $1 = file_path
      # $2 = file_name
      # $3 = reason
      COMPILE_ERROR_MATCHER = /^(\/.+\/(.*):.*:.*):\s(?:fatal\s)?error:\s(.*)$/

      # @regex Captured groups
      # $1 cursor (with whitespaces and tildes)
      CURSOR_MATCHER = /^([\s~]*\^[\s~]*)$/

      # @regex Captured groups
      # $1 = whole error.
      # it varies a lot, not sure if it makes sense to catch everything separately
      FATAL_ERROR_MATCHER = /^(fatal error:.*)$/

      # @regex Captured groups
      # $1 = whole error.
      # $2 = file path
      FILE_MISSING_ERROR_MATCHER = /^<unknown>:0:\s(error:\s.*)\s'(\/.+\/.*\..*)'$/

      # @regex Captured groups
      # $1 = whole error
      LD_ERROR_MATCHER = /^(ld:.*)/

      # @regex Captured groups
      # $1 file path
      LINKER_DUPLICATE_SYMBOLS_LOCATION_MATCHER = /^\s+(\/.*\.o[\)]?)$/

      # @regex Captured groups
      # $1 reason
      LINKER_DUPLICATE_SYMBOLS_MATCHER = /^(duplicate symbol .*):$/

      # @regex Captured groups
      # $1 symbol location
      LINKER_UNDEFINED_SYMBOL_LOCATION_MATCHER = /^(.* in .*\.o)$/

      # @regex Captured groups
      # $1 reason
      LINKER_UNDEFINED_SYMBOLS_MATCHER = /^(Undefined symbols for architecture .*):$/

      # @regex Captured groups
      # $1 reason
      PODS_ERROR_MATCHER = /^(error:\s.*)/

      # @regex Captured groups
      # $1 = reference
      SYMBOL_REFERENCED_FROM_MATCHER = /\s+"(.*)", referenced from:$/

      # @regex Captured groups
      # $1 = error reason
      MODULE_INCLUDES_ERROR_MATCHER = /^\<module-includes\>:.*?:.*?:\s(?:fatal\s)?(error:\s.*)$/
    end
  end

  class Parser

    include Matchers
    include Matchers::Errors
    include Matchers::Warnings

    attr_reader :formatter

    def load_dependencies
      unless @@loaded ||= false
        require 'erb'
        @@loaded = true
      end
    end

    def initialize(formatter)
      load_dependencies
      @formatter = formatter
    end

    def parse(text)
      update_test_state(text)
      update_error_state(text)
      update_linker_failure_state(text)

      return format_compile_error if should_format_error?
      return format_compile_warning if should_format_warning?
      return format_undefined_symbols if should_format_undefined_symbols?
      return format_duplicate_symbols if should_format_duplicate_symbols?

      case text
      when ANALYZE_MATCHER
        formatter.format_analyze($2, $1)
      when BUILD_TARGET_MATCHER
        formatter.format_build_target($1, $2, $3)
      when AGGREGATE_TARGET_MATCHER
        formatter.format_aggregate_target($1, $2, $3)
      when ANALYZE_TARGET_MATCHER
        formatter.format_analyze_target($1, $2, $3)
      when CLEAN_REMOVE_MATCHER
        formatter.format_clean_remove
      when CLEAN_TARGET_MATCHER
        formatter.format_clean_target($1, $2, $3)
      when COPY_STRINGS_MATCHER
        formatter.format_copy_strings_file($1)
      when CHECK_DEPENDENCIES_MATCHER
        formatter.format_check_dependencies
      when CLANG_ERROR_MATCHER
        formatter.format_error($1)
      when CODESIGN_FRAMEWORK_MATCHER
        formatter.format_codesign($1)
      when CODESIGN_MATCHER
        formatter.format_codesign($1)
      when CHECK_DEPENDENCIES_ERRORS_MATCHER
        formatter.format_error($1)
      when PROVISIONING_PROFILE_REQUIRED_MATCHER
        formatter.format_error($1)
      when NO_CERTIFICATE_MATCHER
        formatter.format_error($1)
      when COMPILE_MATCHER
        formatter.format_compile($2, $1)
      when COMPILE_COMMAND_MATCHER
        formatter.format_compile_command($1, $2)
      when COMPILE_XIB_MATCHER
        formatter.format_compile_xib($2, $1)
      when COMPILE_STORYBOARD_MATCHER
        formatter.format_compile_storyboard($2, $1)
      when COPY_HEADER_MATCHER
        formatter.format_copy_header_file($1, $2)
      when COPY_PLIST_MATCHER
        formatter.format_copy_plist_file($1, $2)
      when CPRESOURCE_MATCHER
        formatter.format_cpresource($1)
      when EXECUTED_MATCHER
        format_summary_if_needed(text)
      when RESTARTING_TESTS_MATCHER
        formatter.format_failing_test(@test_suite, @test_case, "Test crashed", "n/a")
      when UI_FAILING_TEST_MATCHER
        formatter.format_failing_test(@test_suite, @test_case, $2, $1)
      when FAILING_TEST_MATCHER
        formatter.format_failing_test($2, $3, ERB::Util.html_escape($4), $1)
      when FATAL_ERROR_MATCHER
        formatter.format_error($1)
      when FILE_MISSING_ERROR_MATCHER
        formatter.format_file_missing_error($1, $2)
      when GENERATE_DSYM_MATCHER
        formatter.format_generate_dsym($1)
      when LD_WARNING_MATCHER
        formatter.format_ld_warning($1 + $2)
      when LD_ERROR_MATCHER
        formatter.format_error($1)
      when LIBTOOL_MATCHER
        formatter.format_libtool($1)
      when LINKING_MATCHER
        formatter.format_linking($1, $2, $3)
      when MODULE_INCLUDES_ERROR_MATCHER
        formatter.format_error($1)
      when TEST_CASE_MEASURED_MATCHER
        formatter.format_measuring_test($1, $2, $3)
      when TEST_CASE_PENDING_MATCHER
        formatter.format_pending_test($1, $2)
      when TEST_CASE_PASSED_MATCHER
        formatter.format_passing_test($1, $2, $3)
      when PODS_ERROR_MATCHER
        formatter.format_error($1)
      when PROCESS_INFO_PLIST_MATCHER
        formatter.format_process_info_plist(*unescaped($2, $1))
      when PHASE_SCRIPT_EXECUTION_MATCHER
        formatter.format_phase_script_execution(*unescaped($1))
      when PHASE_SUCCESS_MATCHER
        formatter.format_phase_success($1)
      when PROCESS_PCH_MATCHER
        formatter.format_process_pch($1)
      when PROCESS_PCH_COMMAND_MATCHER
        formatter.format_process_pch_command($1)
      when PREPROCESS_MATCHER
        formatter.format_preprocess($1)
      when PBXCP_MATCHER
        formatter.format_pbxcp($1)
      when TESTS_RUN_COMPLETION_MATCHER
        formatter.format_test_run_finished($1, $3)
      when TEST_SUITE_STARTED_MATCHER
        formatter.format_test_run_started($1)
      when TEST_SUITE_START_MATCHER
        formatter.format_test_suite_started($1)
      when TIFFUTIL_MATCHER
        formatter.format_tiffutil($1)
      when TOUCH_MATCHER
        formatter.format_touch($1, $2)
      when WRITE_FILE_MATCHER
        formatter.format_write_file($1)
      when WRITE_AUXILIARY_FILES
        formatter.format_write_auxiliary_files
      when SHELL_COMMAND_MATCHER
        formatter.format_shell_command($1, $2)
      when GENERIC_WARNING_MATCHER
        formatter.format_warning($1)
      when WILL_NOT_BE_CODE_SIGNED_MATCHER
        formatter.format_will_not_be_code_signed($1)
      else
        formatter.format_other(text)
      end
    end

    private

    def update_test_state(text)
      case text
      when TEST_SUITE_STARTED_MATCHER
        @tests_done = false
        @formatted_summary = false
        @failures = {}
      when TEST_CASE_STARTED_MATCHER
        @test_suite = $1
        @test_case = $2
      when TESTS_RUN_COMPLETION_MATCHER
        @tests_done = true
      when FAILING_TEST_MATCHER
        store_failure(file: $1, test_suite: $2, test_case: $3, reason: $4)
      when UI_FAILING_TEST_MATCHER
        store_failure(file: $1, test_suite: @test_suite, test_case: @test_case, reason: $2)
      when RESTARTING_TESTS_MATCHER
        store_failure(file: "n/a", test_suite: @test_suite, test_case: @test_case, reason: "Test crashed")
      end
    end

    # @ return Hash { :file_name, :file_path, :reason, :line }
    def update_error_state(text)
      update_error = lambda {
        current_issue[:reason]    = $3
        current_issue[:file_path] = $1
        current_issue[:file_name] = $2
      }
      if text =~ COMPILE_ERROR_MATCHER
        @formatting_error = true
        update_error.call
      elsif text =~ COMPILE_WARNING_MATCHER
        @formatting_warning = true
        update_error.call
      elsif text =~ CURSOR_MATCHER
        current_issue[:cursor]    = $1.chomp
      elsif @formatting_error || @formatting_warning
        current_issue[:line]      = text.chomp
      end
    end

    def update_linker_failure_state(text)
      if text =~ LINKER_UNDEFINED_SYMBOLS_MATCHER ||
         text =~ LINKER_DUPLICATE_SYMBOLS_MATCHER

        current_linker_failure[:message] = $1
        @formatting_linker_failure = true
      end
      return unless @formatting_linker_failure

      case text
      when SYMBOL_REFERENCED_FROM_MATCHER
        current_linker_failure[:symbol] = $1
      when LINKER_UNDEFINED_SYMBOL_LOCATION_MATCHER
        current_linker_failure[:reference] = text.strip
      when LINKER_DUPLICATE_SYMBOLS_LOCATION_MATCHER
        current_linker_failure[:files] << $1
      end
    end

    # TODO: clean up the mess around all this
    def should_format_error?
      @formatting_error && error_or_warning_is_present
    end

    def should_format_warning?
      @formatting_warning && error_or_warning_is_present
    end

    def error_or_warning_is_present
      current_issue[:reason] && current_issue[:cursor] && current_issue[:line]
    end

    def should_format_undefined_symbols?
      current_linker_failure[:message] && current_linker_failure[:symbol] && current_linker_failure[:reference]
    end

    def should_format_duplicate_symbols?
      current_linker_failure[:message] && current_linker_failure[:files].count > 1
    end

    def current_issue
      @current_issue ||= {}
    end

    def current_linker_failure
      @linker_failure ||= {files: []}
    end

    def format_compile_error
      error = current_issue.dup
      @current_issue = {}
      @formatting_error = false
      formatter.format_compile_error(error[:file_name],
                                     error[:file_path],
                                     error[:reason],
                                     error[:line],
                                     error[:cursor])
    end

    def format_compile_warning
      warning = current_issue.dup
      @current_issue = {}
      @formatting_warning = false
      formatter.format_compile_warning(warning[:file_name],
                                       warning[:file_path],
                                       warning[:reason],
                                       warning[:line],
                                       warning[:cursor])
    end

    def format_undefined_symbols
      result = formatter.format_undefined_symbols(
        current_linker_failure[:message],
        current_linker_failure[:symbol],
        current_linker_failure[:reference]
      )
      reset_linker_format_state
      result
    end

    def format_duplicate_symbols
      result = formatter.format_duplicate_symbols(
        current_linker_failure[:message],
        current_linker_failure[:files]
      )
      reset_linker_format_state
      result
    end

    def reset_linker_format_state
      @linker_failure = nil
      @formatting_linker_failure = false
    end

    def store_failure(file: nil, test_suite: nil, test_case: nil, reason: nil)
      failures_per_suite[test_suite] ||= []
      failures_per_suite[test_suite] << {
        file_path: file,
        reason: reason,
        test_case: test_case
      }
    end

    def failures_per_suite
      @failures ||= {}
    end

    def format_summary_if_needed(executed_message)
      return "" unless should_format_summary?

      @formatted_summary = true
      formatter.format_test_summary(executed_message, failures_per_suite)
    end

    def should_format_summary?
      @tests_done && !@formatted_summary
    end

    def unescaped(*escaped_values)
      escaped_values.map { |v| v.delete('\\') }
    end

  end
end

