#!/usr/bin/env python3
#
# Copyright 2021 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Contains helper class for processing javac output."""

import functools
import os
import pathlib
import posixpath
import re
import shlex
import sys
import traceback

from util import dep_utils


_PACKAGE_RE = re.compile(r'^package\s+(.*?)(;|\s*$)', flags=re.MULTILINE)


# Warnings appear in: com.google.common.util.concurrent.AbstractFuture
_JDK_WARNING_PATTERNS = [
    r'WARNING: A terminally deprecated method in sun\.misc\.Unsafe',
    r'WARNING: sun\.misc\.Unsafe::.* has been called by',
    r'WARNING: Please consider reporting this to the maintainers of',
    r'WARNING: sun\.misc\.Unsafe::.* will be removed in a future release',
]
_JDK_WARNING_RE = re.compile('|'.join(_JDK_WARNING_PATTERNS))


def _FilterJdkWarnings(output):
    return '\n'.join(
        line for line in output.split('\n') if not _JDK_WARNING_RE.search(line)
    )


@functools.cache
def _running_locally():
    return os.path.exists('build.ninja')


@functools.cache  # pylint: disable=method-cache-max-size-none
def _extract_package_name(path):
    data = pathlib.Path(path).read_text('utf-8')
    if m := _PACKAGE_RE.search(data):
        return m.group(1)
    return None


class JavacOutputProcessor:
    def __init__(self, target_name):
        self._target_name = self._RemoveSuffixesIfPresent(
            ["__compile_java", "__errorprone", "__header"], target_name
        )
        self._missing_classes_short = set()
        self._missing_classes_full = set()
        self._suggested_targets = set()
        self._unresolvable_classes = set()

        # Examples:
        # ../../path/to/Foo.java:54: error: cannot find symbol
        #        ClangProfiler.writeClangProfilingProfile();
        #        ^
        # symbol:   variable ClangProfiler
        #
        # ../../path/to/Foo.java:69: error: could not resolve Linker.LibInfo
        #     static Linker.LibInfo anyLibInfo() {
        #            ^
        self._resolution_re = re.compile(
            # ../../ui/android/java/src/org/chromium/ui/base/Clipboard.java:45:
            r'^(?P<path>[-.\w/\\]+.java):(?:[0-9]+):'
            # error: package org.chromium.components.url_formatter does not exist
            r'(?: error: package [\w.]+ does not exist|'
            # error: cannot find symbol
            r' error: cannot find symbol|'
            # error: could not resolve Linker.LibInfo
            r' error: could not resolve (?P<unresolved>[A-Z]\w*).*|'
            # error: symbol not found org.chromium.url.GURL
            r' error: symbol not found [\w.]+)$'
            r'\s*(?:'
            # import org.chromium.url.GURL;
            r'import (?P<import>[\w\.]+);|'
            # import static org.chromium.url.GURL.method;
            r'import static (?P<static_import>[\w\.]+)\.\S+;|'
            r'.*)$'
            r'\s*\^*$'
            # symbol:   variable ClangProfiler
            r'\s*(symbol:\s+variable (?P<variable>[A-Z]\w+))?',
            re.MULTILINE,
        )

        self._class_lookup_index = None

    def Process(self, output):
        if output:
            output = _FilterJdkWarnings(output)
        if not output:
            return output
        self._LookForUnknownSymbols(output)

        if not self._missing_classes_full:
            return output

        sb = []
        sb.append('💡 One or more errors due to missing GN deps 💡')
        if self._unresolvable_classes:
            sb.append('Failed to find targets for the following classes:')
            for class_name in sorted(self._unresolvable_classes):
                sb.append(f'* {class_name}')
        if self._suggested_targets:
            sb.append('Hint: Try adding the following to ' + self._target_name)

            for targets in sorted(self._suggested_targets):
                if len(targets) > 1:
                    suggested_targets_str = 'one of: ' + ', '.join(targets)
                else:
                    suggested_targets_str = targets[0]
                # Show them in quotes so they can be copy/pasted into BUILD.gn files.
                sb.append(f'    "{suggested_targets_str}",')

            sb.append('')
            sb.append(
                'Hint: Run the following command to add the missing deps:'
            )
            missing_targets = {
                targets[0] for targets in self._suggested_targets
            }
            cmd = dep_utils.CreateAddDepsCommand(
                self._target_name, sorted(missing_targets)
            )
            sb.append(f'    {shlex.join(cmd)}')
            sb.append('')
            sb.append(
                'Use tools/android/auto_fix_missing_java_deps.py'
                ' to auto-apply suggestions.'
            )
        elif not self._unresolvable_classes:
            sb.append(
                'Hint: To show missing deps: "autoninja -config no-remote-javac ..." '
                'or: ' + os.path.abspath('siso_failed_commands.sh')
            )

        sb.append('')
        return output + '\n'.join(sb)

    def _LookForUnknownSymbols(self, output):
        for m in self._resolution_re.finditer(output):
            full_class_name = m.group('import') or m.group('static_import')
            if not full_class_name:
                short_class_name = m.group('unresolved') or m.group('variable')
                if not short_class_name:
                    continue
                if short_class_name in self._missing_classes_short:
                    continue
                path = m.group('path')
                if os.path.exists(path):
                    package_name = _extract_package_name(path)
                    if not package_name:
                        continue
                else:
                    # This can happen for .srcjars in turbine.py (which directly supports
                    # .srcjar files).
                    # E.g.: "org/chromium/network/mojom/ParsedHeaders.java"
                    package_name = posixpath.dirname(path).replace('/', '.')
                full_class_name = f'{package_name}.{short_class_name}'

            if full_class_name not in self._missing_classes_full:
                short_class_name = full_class_name.split('.')[-1]
                self._missing_classes_short.add(short_class_name)
                self._missing_classes_full.add(full_class_name)
                if _running_locally():
                    try:
                        self._AnalyzeMissingClass(full_class_name)
                    except Exception:
                        sys.stderr.write(
                            'Error in _AnalyzeMissingClass---\n'
                            + traceback.format_exc()
                        )
                        return

    def _AnalyzeMissingClass(self, class_name):
        if self._class_lookup_index is None:
            self._class_lookup_index = dep_utils.ClassLookupIndex(
                pathlib.Path(os.getcwd()),
                should_build=False,
            )

        suggested_deps = self._class_lookup_index.match(class_name)

        if not suggested_deps:
            self._unresolvable_classes.add(class_name)
            return

        suggested_deps = dep_utils.DisambiguateDeps(suggested_deps)
        self._suggested_targets.add(tuple(d.target for d in suggested_deps))

    @staticmethod
    def _RemoveSuffixesIfPresent(suffixes, text):
        for suffix in suffixes:
            if text.endswith(suffix):
                return text[: -len(suffix)]
        return text


def Process(target_name, output):
    processor = JavacOutputProcessor(target_name)
    return processor.Process(output)
