#
# Copyright (c) Contributors to the Open 3D Engine Project.
# For complete copyright and license terms please see the LICENSE at the root of this distribution.
#
# SPDX-License-Identifier: Apache-2.0 OR MIT
#
#

import abc
import importlib
import os
import pkgutil
import re
import time
from typing import Dict, List, Tuple

VERBOSE = False

class Commit(abc.ABC):
    """An interface for accessing details about a commit"""

    @abc.abstractmethod
    def get_files(self) -> List[str]:
        """Returns a list of local files added/modified by the commit"""
        pass

    @abc.abstractmethod
    def get_removed_files(self) -> List[str]:
        """Returns a list of local files removed by the commit"""
        pass

    @abc.abstractmethod
    def get_file_diff(self, str) -> str:
        """
        Given a file name, returns a string in unified diff format
        that represents the changes made to that file for this commit.
        Most validators will only pay attention to added lines (with + in front)
        """
        pass

    @abc.abstractmethod
    def get_description(self) -> str:
        """Returns the description of the commit"""
        pass

    @abc.abstractmethod
    def get_author(self) -> str:
        """Returns the author of the commit"""
        pass


def validate_commit(commit: Commit, out_errors: List[str] = None, ignore_validators: List[str] = None) -> bool:
    """Validates a commit against all validators

    :param commit: The commit to validate
    :param out_errors: if not None, will populate with the list of errors given by the validators
    :param ignore_validators: Optional list of CommitValidator classes to ignore, by class name
    :return: True if there are no validation errors, and False otherwise
    """
    failed_count = 0
    passed_count = 0
    start_time = time.time()

    # Find all the validators in the validators package (recursively)
    validator_classes = []
    validators_dir = os.path.join(os.path.dirname(__file__), 'validators')
    for _, module_name, is_package in pkgutil.iter_modules([validators_dir]):
        if not is_package:
            module = importlib.import_module('commit_validation.validators.' + module_name)
            validator = module.get_validator()
            if ignore_validators and validator.__name__ in ignore_validators:
                print(f"Disabled validation for '{validator.__name__}'")
            else:
                validator_classes.append(validator)

    error_summary = {}

    # Process validators
    for validator_class in validator_classes:
        validator = validator_class()
        validator_name = validator.__class__.__name__

        error_list = []
        passed = validator.run(commit, errors = error_list)
        if passed:
            passed_count += 1
            print(f'{validator.__class__.__name__} PASSED')
        else:
            failed_count += 1
            print(f'{validator.__class__.__name__} FAILED')
        error_summary[validator_name] = error_list
        
    end_time = time.time()

    if failed_count:
        print("VALIDATION FAILURE SUMMARY")
        for val_name in error_summary.keys():
            errors = error_summary[val_name]
            if errors:
                for error_message in errors:
                    first_line = True
                    for line in error_message.splitlines():
                        if first_line:
                            first_line = False
                            print(f'VALIDATOR_FAILED: {val_name} {line}')
                        else:
                            print(f'    {line}') # extra detail lines do not need machine parsing

    stats_strs = []
    if failed_count > 0:
        stats_strs.append(f'{failed_count} failed')
    if passed_count > 0:
        stats_strs.append(f'{passed_count} passed')
    stats_str = ', '.join(stats_strs) + f' in {end_time - start_time:.2f}s'

    print()
    print(stats_str)

    return failed_count == 0

def IsFileSkipped(file_name) -> bool:
    if os.path.splitext(file_name)[1].lower() not in SOURCE_AND_SCRIPT_FILE_EXTENSIONS:
        skipped = True
        for pattern in SOURCE_AND_SCRIPT_FILE_PATTERNS:
            if pattern.match(file_name):
                skipped = False
                break
        return skipped
    return False

class CommitValidator(abc.ABC):
    """A commit validator"""

    @abc.abstractmethod
    def run(self, commit: Commit, errors: List[str]) -> bool:
        """Validates a commit

        :param commit: The commit to validate
        :param errors: List of errors generated, append them to this list
        :return: True if the commit is valid, and False otherwise
        """
        pass


SOURCE_FILE_EXTENSIONS: Tuple[str, ...] = (
    '.c', '.cc', '.cpp', '.cxx', '.h', '.hpp', '.hxx', '.inl', '.m', '.mm', '.cs', '.java'
)
"""File extensions for compiled source code"""

SCRIPT_FILE_EXTENSIONS: Tuple[str, ...] = (
    '.py', '.lua', '.bat', '.cmd', '.sh', '.js'
)
"""File extensions for interpreted code"""

BUILD_FILE_EXTENSIONS: Tuple[str, ...] = (
    '.cmake',
)
"""File extensions for build files"""

SOURCE_AND_SCRIPT_FILE_EXTENSIONS: Tuple[str, ...] = SOURCE_FILE_EXTENSIONS + SCRIPT_FILE_EXTENSIONS + BUILD_FILE_EXTENSIONS
"""File extensions for both compiled and interpreted code"""

BUILD_FILE_PATTERNS: Tuple[re.Pattern, ...] = (
    re.compile(r'.*CMakeLists\.txt'),
    re.compile(r'.*Jenkinsfile')
)
"""File patterns for build files"""

SOURCE_AND_SCRIPT_FILE_PATTERNS: Tuple[re.Pattern, ...] = BUILD_FILE_PATTERNS

EXCLUDED_VALIDATION_PATTERNS = [
    '*/.git/*',
    '*/3rdParty/*',
    '*/__pycache__/*',
    '*/External/*',
    'build',
    'Cache',
    '*/Code/Framework/AzCore/azgnmx/azgnmx/*',
    'Code/Tools/CryFXC',
    'Code/Tools/HLSLCrossCompiler',
    'Code/Tools/HLSLCrossCompilerMETAL',
    'Docs',
    'python/runtime',
    'restricted/*/Tools/*RemoteControl',
    'Tools/3dsmax',
    '*/user/Cache/*',
    '*/user/log/*',
]

[((62, 17, 62, 28), 'time.time', 'time.time', ({}, {}), '()', False, 'import time\n'), ((67, 38, 67, 76), 'pkgutil.iter_modules', 'pkgutil.iter_modules', ({(67, 59, 67, 75): '[validators_dir]'}, {}), '([validators_dir])', False, 'import pkgutil\n'), ((93, 15, 93, 26), 'time.time', 'time.time', ({}, {}), '()', False, 'import time\n'), ((164, 4, 164, 36), 're.compile', 're.compile', ({(164, 15, 164, 35): '""".*CMakeLists\\\\.txt"""'}, {}), "('.*CMakeLists\\\\.txt')", False, 'import re\n'), ((165, 4, 165, 32), 're.compile', 're.compile', ({(165, 15, 165, 31): '""".*Jenkinsfile"""'}, {}), "('.*Jenkinsfile')", False, 'import re\n'), ((66, 34, 66, 59), 'os.path.dirname', 'os.path.dirname', ({(66, 50, 66, 58): '__file__'}, {}), '(__file__)', False, 'import os\n'), ((69, 21, 69, 91), 'importlib.import_module', 'importlib.import_module', ({(69, 45, 69, 90): "'commit_validation.validators.' + module_name"}, {}), "('commit_validation.validators.' + module_name)", False, 'import importlib\n'), ((122, 7, 122, 34), 'os.path.splitext', 'os.path.splitext', ({(122, 24, 122, 33): 'file_name'}, {}), '(file_name)', False, 'import os\n')]