# Copyright 2019 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import dataclasses
import itertools
import os
import re
import sys
from typing import List, Optional


def CreateDefaultTemplate(java_class, placeholder):
    assert '.' in java_class, 'Wanted fully-qualified name. Found ' + java_class
    package, _, class_name = java_class.rpartition('.')

    script_name = GetScriptName()
    return f"""\
// Generated by {script_name}

package {package};

public final class {class_name} {{
    private {class_name}() {{}}

{placeholder}
}}
"""


def GetScriptName():
    return os.path.basename(sys.argv[0])


def GetJavaFilePath(java_package, class_name):
    package_path = java_package.replace('.', os.path.sep)
    file_name = class_name + '.java'
    return os.path.join(package_path, file_name)


def KCamelToShouty(s):
    """Convert |s| from kCamelCase or CamelCase to SHOUTY_CASE.

    kFooBar -> FOO_BAR
    FooBar -> FOO_BAR
    FooBAR9 -> FOO_BAR9
    FooBARBaz -> FOO_BAR_BAZ
    """
    if not re.match(r'^k?([A-Z][^A-Z]+|[A-Z0-9]+)+$', s):
        return s
    # Strip the leading k.
    s = re.sub(r'^k', '', s)
    # Treat "WebView" like one word.
    s = re.sub(r'WebView', r'Webview', s)
    # Treat "URLs" like one word.
    s = re.sub(r'URLs', r'Urls', s)
    # Add _ between title words and anything else.
    s = re.sub(r'([^_])([A-Z][^A-Z_0-9]+)', r'\1_\2', s)
    # Add _ between lower -> upper transitions.
    s = re.sub(r'([^A-Z_0-9])([A-Z])', r'\1_\2', s)
    return s.upper()


def PreprocessIfBlocks(lines):
    """Strips C++ preprocessor blocks that are not for Android."""
    if_buildflag_re = re.compile(
        r'^#if !?BUILDFLAG\((\w+)\)(?: \|\| !?BUILDFLAG\((\w+)\))*$'
    )
    android_os = r'ANDROID|POSIX'
    non_android_os = r'AIX|ASMJS|CHROMEOS|FREEBSD|FUCHSIA|IOS|IOS_MACCATALYST|IOS_TVOS|LINUX|MAC|NETBSD|OPENBSD|QNX|SOLARIS|WATCHOS|WIN|APPLE|BSD'
    any_os = android_os + r'|' + non_android_os
    includes_android_buildflag_re = re.compile(
        rf'(?<!!)BUILDFLAG\(IS_(?:{android_os})\)|!BUILDFLAG\(IS_(?:{non_android_os})'
    )
    pos_or_neg_os_buildflag_re = re.compile(rf'BUILDFLAG\(IS_(?:{any_os})\)')
    else_re = re.compile(r'^#else.*$')
    endif_re = re.compile(r'^#endif.*$')

    processed_lines = []
    # Stack to keep track of whether we are in an android-only block.
    if_stack = []

    for line in lines:
        if if_buildflag_re.match(line):
            is_os = pos_or_neg_os_buildflag_re.search(line)
            if (not is_os) or includes_android_buildflag_re.search(line):
                if_stack.append({'is_android': True, 'else_seen': False})
                continue
            if_stack.append({'is_android': False, 'else_seen': False})
            continue
        if else_re.match(line):
            if if_stack:
                if_stack[-1]['else_seen'] = True
            continue
        if endif_re.match(line):
            if if_stack:
                if_stack.pop()
            continue

        include_line = True
        for s in if_stack:
            if s['is_android']:
                if s['else_seen']:
                    include_line = False
                    break
            else:  # not is_android
                if not s['else_seen']:
                    include_line = False
                    break

        if include_line:
            processed_lines.append(line)

    return processed_lines


class JavaString:
    def __init__(self, name, value, comments):
        self.name = KCamelToShouty(name)
        self.value = value
        self.comments = '\n'.join('    ' + x for x in comments)

    def Format(self):
        return '%s\n    public static final String %s = %s;' % (
            self.comments,
            self.name,
            self.value,
        )


def ParseTemplateFile(data):
    if m := re.search(
        r'^package (.*);[\s\S]+\bclass (\w+) {', data, flags=re.MULTILINE
    ):
        package, class_name = m.groups()
        return package, class_name
    raise Exception('Could not find java package.')


class CppConstantParser:
    """Parses C++ constants, retaining their comments.

    The Delegate subclass is responsible for matching and extracting the
    constant's variable name and value, as well as generating an object to
    represent the Java representation of this value.
    """

    SINGLE_LINE_COMMENT_RE = re.compile(r'\s*(// [^\n]*)')

    class Delegate:
        def ExtractConstantName(self, line):
            """Extracts a constant's name from line or None if not a match."""
            raise NotImplementedError()

        def ExtractValue(self, line):
            """Extracts a constant's value from line or None if not a match."""
            raise NotImplementedError()

        def CreateJavaConstant(self, name, value, comments):
            """Creates an object representing the Java analog of a C++ constant.

            CppConstantParser will not interact with the object created by this
            method. Instead, it will store this value in a list and return a list of
            all objects from the Parse() method. In this way, the caller may define
            whatever class suits their need.

            Args:
              name: the constant's variable name, as extracted by
                ExtractConstantName()
              value: the constant's value, as extracted by ExtractValue()
              comments: the code comments describing this constant
            """
            raise NotImplementedError()

        def RequiresMultilineValueParsing(self):
            """Whether the parser should look for values across multiple lines."""
            return False

    def __init__(self, delegate, lines):
        self._delegate = delegate
        self._lines = PreprocessIfBlocks(lines)
        self._in_variable = False
        self._in_comment = False
        self._package = ''
        self._current_comments = []
        self._current_name = ''
        self._current_value = ''
        self._constants = []

    def _Reset(self):
        self._current_comments = []
        self._current_name = ''
        self._current_value = ''
        self._in_variable = False
        self._in_comment = False

    def _AppendConstant(self):
        self._constants.append(
            self._delegate.CreateJavaConstant(
                self._current_name, self._current_value, self._current_comments
            )
        )
        self._Reset()

    def _ParseValue(self, line):
        current_value = self._delegate.ExtractValue(line)
        if current_value is not None:
            self._current_value = current_value
            self._AppendConstant()
        elif not self._delegate.RequiresMultilineValueParsing():
            self._Reset()

    def _ParseComment(self, line):
        comment_line = CppConstantParser.SINGLE_LINE_COMMENT_RE.match(line)
        if comment_line:
            self._current_comments.append(comment_line.groups()[0])
            self._in_comment = True
            self._in_variable = True
            return True
        self._in_comment = False
        return False

    def _ParseVariable(self, line):
        current_name = self._delegate.ExtractConstantName(line)
        if current_name is not None:
            self._current_name = current_name
            current_value = self._delegate.ExtractValue(line)
            if current_value is not None:
                self._current_value = current_value
                self._AppendConstant()
            else:
                self._in_variable = True
            return True
        self._in_variable = False
        return False

    def _ParseLine(self, line):
        if not self._in_variable:
            if not self._ParseVariable(line):
                self._ParseComment(line)
            return

        if self._in_comment:
            if self._ParseComment(line):
                return
            if not self._ParseVariable(line):
                self._Reset()
            return

        if self._in_variable:
            self._ParseValue(line)

    def Parse(self):
        """Returns a list of objects representing C++ constants.

        Each object in the list was created by Delegate.CreateJavaValue().
        """
        for line in self._lines:
            self._ParseLine(line)
        return self._constants


@dataclasses.dataclass
class _Macro:
    params: List[str]
    body_lines: List[str]
    is_multiline: bool
    param_re: Optional[re.Pattern] = None

    def IsListMacro(self) -> bool:
        if not self.is_multiline or len(self.params) != 1:
            return False
        param_name = self.params[0]
        call_pattern = re.compile(r'\b' + re.escape(param_name) + r'\s*\(')
        return any(call_pattern.search(line) for line in self.body_lines)


def _SplitArgs(args_str):
    args = []
    current = []
    in_quotes = False
    escaped = False
    paren_depth = 0
    for char in args_str:
        if escaped:
            current.append(char)
            escaped = False
        elif char == '"':
            in_quotes = not in_quotes
            current.append(char)
        elif char == '\\' and in_quotes:
            current.append(char)
            escaped = True
        elif char == '(' and not in_quotes:
            paren_depth += 1
            current.append(char)
        elif char == ')' and not in_quotes:
            paren_depth -= 1
            current.append(char)
        elif char == ',' and not in_quotes and paren_depth == 0:
            args.append(''.join(current).strip())
            current = []
        else:
            current.append(char)
    args.append(''.join(current).strip())
    return args


def _ExpandVisitor(visitor_def, args):
    if not visitor_def.params:
        return visitor_def.body_lines

    if visitor_def.param_re is None:
        visitor_def.param_re = re.compile(
            r'\b(' + '|'.join(re.escape(p) for p in visitor_def.params) + r')\b'
        )

    mapping = dict(
        itertools.zip_longest(visitor_def.params, args, fillvalue='')
    )
    replace = lambda m: mapping[m.group(1)]
    return [
        visitor_def.param_re.sub(replace, l) for l in visitor_def.body_lines
    ]


def _ParseMacros(lines):
    macros = {}

    # Regex for start of multi-line macro
    # #define NAME(PARAMS) \
    multiline_start_re = re.compile(
        r'^#\s*define\s+(\w+)\s*\(\s*([^)]*)\s*\)\s*\\'
    )

    # Regex for single-line macro
    # #define NAME(PARAMS) BODY
    single_line_re = re.compile(
        r'^#\s*define\s+(\w+)\s*\(\s*([^)]*)\s*\)\s*(.*)$'
    )

    in_macro = False
    current_name = None
    current_params = []
    current_lines = []

    for line in lines:
        if in_macro:
            stripped = line.strip()
            if stripped.endswith('\\'):
                current_lines.append(stripped[:-1].strip())
            else:
                current_lines.append(stripped)
                macros[current_name] = _Macro(
                    params=current_params,
                    body_lines=current_lines,
                    is_multiline=True,
                )
                in_macro = False
                current_name = None
                current_params = []
                current_lines = []
        else:
            if m := multiline_start_re.match(line):
                current_name, params_str = m.groups()
                current_params = [
                    p.strip() for p in params_str.split(',') if p.strip()
                ]
                in_macro = True
                continue

            if m := single_line_re.match(line):
                name, params_str, body = m.groups()
                params = [p.strip() for p in params_str.split(',') if p.strip()]
                if params:
                    macros[name] = _Macro(
                        params=params,
                        body_lines=[body.strip()],
                        is_multiline=False,
                    )
    return macros


def ProcessListMacros(lines):
    """Evaluates list macros that are defined & used within |lines|.

  A "list macro" is a multiline C++ macro that takes a single parameter
  (traditionally named 'V' or similar) and invokes it as a function call
  on each line of its body to define a list of entries.
  Example:
    #define MY_LIST(V) \
      V(ENTRY_A, "value_a") \
      V(ENTRY_B, "value_b")

  We care about list macros because they are a common C++ pattern used to
  define groups of enums, features, or string constants in a single place.
  This preprocessor expands calls to these list macros using a "visitor"
  macro (e.g., `MY_LIST(MY_VISITOR)`) so that subsequent parsing scripts
  can see the fully expanded constants (e.g. enum entries or variable
  declarations) as if they were written directly in the file, making it
  easy to parse and generate corresponding Java constants.

  Args:
    lines: List of strings representing the C++ header file lines.

  Returns:
    List of strings with list macro calls expanded.
  """
    macros = _ParseMacros(lines)
    if not macros:
        return lines

    # Filter to only include list macros.
    list_macros = {name: d for name, d in macros.items() if d.IsListMacro()}

    if not list_macros:
        return lines

    macro_call_re = re.compile(
        r'\b('
        + '|'.join(re.escape(n) for n in list_macros)
        + r')\s*\(\s*(\w+)\s*\)'
    )

    def replace_macro(match):
        macro_name, visitor_name = match.groups()

        visitor_def = macros.get(visitor_name)
        if visitor_def is None:
            raise Exception(
                f"Visitor macro '{visitor_name}' used in '{macro_name}' call is "
                "not defined in the file."
            )

        macro_def = list_macros[macro_name]
        param_name = macro_def.params[0]
        macro_lines = macro_def.body_lines

        sb = []
        entry_re = re.compile(r'\b' + re.escape(param_name) + r'\s*\((.*)\)')

        for macro_line in macro_lines:
            if m2 := entry_re.match(macro_line):
                args = _SplitArgs(m2.group(1).strip())
                sb.extend(_ExpandVisitor(visitor_def, args))

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

    new_lines = []
    for line in lines:
        if not line.startswith('#'):
            line = macro_call_re.sub(replace_macro, line)
        new_lines.extend(line.splitlines(keepends=True))

    return new_lines
