"""
    pygments.lexers.php
    ~~~~~~~~~~~~~~~~~~~

    Lexers for PHP and related languages.

    :copyright: Copyright 2006-2023 by the Pygments team, see AUTHORS.
    :license: BSD, see LICENSE for details.
"""

import re

from pygments.lexer import Lexer, RegexLexer, include, bygroups, default, using, this, words, do_insertions, line_re
from pygments.token import Text, Comment, Operator, Keyword, Name, String, Number, Punctuation, Other, Generic
from pygments.util import get_bool_opt, get_list_opt, shebang_matches

__all__ = ["ZephirLexer", "PsyshConsoleLexer", "PhpLexer"]


class ZephirLexer(RegexLexer):
    """
    For Zephir language source code.

    Zephir is a compiled high level language aimed
    to the creation of C-extensions for PHP.

    .. versionadded:: 2.0
    """

    name = "Zephir"
    url = "http://zephir-lang.com/"
    aliases = ["zephir"]
    filenames = ["*.zep"]

    zephir_keywords = ["fetch", "echo", "isset", "empty"]
    zephir_type = ["bit", "bits", "string"]

    flags = re.DOTALL | re.MULTILINE

    tokens = {
        "commentsandwhitespace": [(r"\s+", Text), (r"//.*?\n", Comment.Single), (r"/\*.*?\*/", Comment.Multiline)],
        "slashstartsregex": [
            include("commentsandwhitespace"),
            (r"/(\\.|[^[/\\\n]|\[(\\.|[^\]\\\n])*])+/" r"([gim]+\b|\B)", String.Regex, "#pop"),
            (r"/", Operator, "#pop"),
            default("#pop"),
        ],
        "badregex": [(r"\n", Text, "#pop")],
        "root": [
            (r"^(?=\s|/)", Text, "slashstartsregex"),
            include("commentsandwhitespace"),
            (r"\+\+|--|~|&&|\?|:|\|\||\\(?=\n)|" r"(<<|>>>?|==?|!=?|->|[-<>+*%&|^/])=?", Operator, "slashstartsregex"),
            (r"[{(\[;,]", Punctuation, "slashstartsregex"),
            (r"[})\].]", Punctuation),
            (
                r"(for|in|while|do|break|return|continue|switch|case|default|if|else|loop|"
                r"require|inline|throw|try|catch|finally|new|delete|typeof|instanceof|void|"
                r"namespace|use|extends|this|fetch|isset|unset|echo|fetch|likely|unlikely|"
                r"empty)\b",
                Keyword,
                "slashstartsregex",
            ),
            (r"(var|let|with|function)\b", Keyword.Declaration, "slashstartsregex"),
            (
                r"(abstract|boolean|bool|char|class|const|double|enum|export|extends|final|"
                r"native|goto|implements|import|int|string|interface|long|ulong|char|uchar|"
                r"float|unsigned|private|protected|public|short|static|self|throws|reverse|"
                r"transient|volatile|readonly)\b",
                Keyword.Reserved,
            ),
            (r"(true|false|null|undefined)\b", Keyword.Constant),
            (
                r"(Array|Boolean|Date|_REQUEST|_COOKIE|_SESSION|"
                r"_GET|_POST|_SERVER|this|stdClass|range|count|iterator|"
                r"window)\b",
                Name.Builtin,
            ),
            (r"[$a-zA-Z_][\w\\]*", Name.Other),
            (r"[0-9][0-9]*\.[0-9]+([eE][0-9]+)?[fd]?", Number.Float),
            (r"0x[0-9a-fA-F]+", Number.Hex),
            (r"[0-9]+", Number.Integer),
            (r'"(\\\\|\\[^\\]|[^"\\])*"', String.Double),
            (r"'(\\\\|\\[^\\]|[^'\\])*'", String.Single),
        ],
    }


class PsyshConsoleLexer(Lexer):
    """
    For PsySH console output, such as:

    .. sourcecode:: psysh

        >>> $greeting = function($name): string {
        ...     return "Hello, {$name}";
        ... };
        => Closure($name): string {#2371 …3}
        >>> $greeting('World')
        => "Hello, World"

    .. versionadded:: 2.7
    """

    name = "PsySH console session for PHP"
    url = "https://psysh.org/"
    aliases = ["psysh"]

    def __init__(self, **options):
        options["startinline"] = True
        Lexer.__init__(self, **options)

    def get_tokens_unprocessed(self, text):
        phplexer = PhpLexer(**self.options)
        curcode = ""
        insertions = []
        for match in line_re.finditer(text):
            line = match.group()
            if line.startswith(">>> ") or line.startswith("... "):
                insertions.append((len(curcode), [(0, Generic.Prompt, line[:4])]))
                curcode += line[4:]
            elif line.rstrip() == "...":
                insertions.append((len(curcode), [(0, Generic.Prompt, "...")]))
                curcode += line[3:]
            else:
                if curcode:
                    yield from do_insertions(insertions, phplexer.get_tokens_unprocessed(curcode))
                    curcode = ""
                    insertions = []
                yield match.start(), Generic.Output, line
        if curcode:
            yield from do_insertions(insertions, phplexer.get_tokens_unprocessed(curcode))


class PhpLexer(RegexLexer):
    """
    For PHP source code.
    For PHP embedded in HTML, use the `HtmlPhpLexer`.

    Additional options accepted:

    `startinline`
        If given and ``True`` the lexer starts highlighting with
        php code (i.e.: no starting ``<?php`` required).  The default
        is ``False``.
    `funcnamehighlighting`
        If given and ``True``, highlight builtin function names
        (default: ``True``).
    `disabledmodules`
        If given, must be a list of module names whose function names
        should not be highlighted. By default all modules are highlighted
        except the special ``'unknown'`` module that includes functions
        that are known to php but are undocumented.

        To get a list of allowed modules have a look into the
        `_php_builtins` module:

        .. sourcecode:: pycon

            >>> from pygments.lexers._php_builtins import MODULES
            >>> MODULES.keys()
            ['PHP Options/Info', 'Zip', 'dba', ...]

        In fact the names of those modules match the module names from
        the php documentation.
    """

    name = "PHP"
    url = "https://www.php.net/"
    aliases = ["php", "php3", "php4", "php5"]
    filenames = ["*.php", "*.php[345]", "*.inc"]
    mimetypes = ["text/x-php"]

    # Note that a backslash is included, PHP uses a backslash as a namespace
    # separator.
    _ident_inner = r"(?:[\\_a-z]|[^\x00-\x7f])(?:[\\\w]|[^\x00-\x7f])*"
    # But not inside strings.
    _ident_nons = r"(?:[_a-z]|[^\x00-\x7f])(?:\w|[^\x00-\x7f])*"

    flags = re.IGNORECASE | re.DOTALL | re.MULTILINE
    tokens = {
        "root": [(r"<\?(php)?", Comment.Preproc, "php"), (r"[^<]+", Other), (r"<", Other)],
        "php": [
            (r"\?>", Comment.Preproc, "#pop"),
            (
                r'(<<<)([\'"]?)(' + _ident_nons + r")(\2\n.*?\n\s*)(\3)(;?)(\n)",
                bygroups(String, String, String.Delimiter, String, String.Delimiter, Punctuation, Text),
            ),
            (r"\s+", Text),
            (r"#\[", Punctuation, "attribute"),
            (r"#.*?\n", Comment.Single),
            (r"//.*?\n", Comment.Single),
            # put the empty comment here, it is otherwise seen as
            # the start of a docstring
            (r"/\*\*/", Comment.Multiline),
            (r"/\*\*.*?\*/", String.Doc),
            (r"/\*.*?\*/", Comment.Multiline),
            (r"(->|::)(\s*)(" + _ident_nons + ")", bygroups(Operator, Text, Name.Attribute)),
            (r"[~!%^&*+=|:.<>/@-]+", Operator),
            (r"\?", Operator),  # don't add to the charclass above!
            (r"[\[\]{}();,]+", Punctuation),
            (r"(new)(\s+)(class)\b", bygroups(Keyword, Text, Keyword)),
            (r"(class)(\s+)", bygroups(Keyword, Text), "classname"),
            (r"(function)(\s*)(?=\()", bygroups(Keyword, Text)),
            (r"(function)(\s+)(&?)(\s*)", bygroups(Keyword, Text, Operator, Text), "functionname"),
            (r"(const)(\s+)(" + _ident_inner + ")", bygroups(Keyword, Text, Name.Constant)),
            (
                r"(and|E_PARSE|old_function|E_ERROR|or|as|E_WARNING|parent|"
                r"eval|PHP_OS|break|exit|case|extends|PHP_VERSION|cfunction|"
                r"FALSE|print|for|require|continue|foreach|require_once|"
                r"declare|return|default|static|do|switch|die|stdClass|"
                r"echo|else|TRUE|elseif|var|empty|if|xor|enddeclare|include|"
                r"virtual|endfor|include_once|while|endforeach|global|"
                r"endif|list|endswitch|new|endwhile|not|"
                r"array|E_ALL|NULL|final|php_user_filter|interface|"
                r"implements|public|private|protected|abstract|clone|try|"
                r"catch|throw|this|use|namespace|trait|yield|"
                r"finally|match)\b",
                Keyword,
            ),
            (r"(true|false|null)\b", Keyword.Constant),
            include("magicconstants"),
            (r"\$\{", Name.Variable, "variablevariable"),
            (r"\$+" + _ident_inner, Name.Variable),
            (_ident_inner, Name.Other),
            (r"(\d+\.\d*|\d*\.\d+)(e[+-]?[0-9]+)?", Number.Float),
            (r"\d+e[+-]?[0-9]+", Number.Float),
            (r"0[0-7]+", Number.Oct),
            (r"0x[a-f0-9]+", Number.Hex),
            (r"\d+", Number.Integer),
            (r"0b[01]+", Number.Bin),
            (r"'([^'\\]*(?:\\.[^'\\]*)*)'", String.Single),
            (r"`([^`\\]*(?:\\.[^`\\]*)*)`", String.Backtick),
            (r'"', String.Double, "string"),
        ],
        "variablevariable": [(r"\}", Name.Variable, "#pop"), include("php")],
        "magicfuncs": [
            # source: http://php.net/manual/en/language.oop5.magic.php
            (
                words(
                    (
                        "__construct",
                        "__destruct",
                        "__call",
                        "__callStatic",
                        "__get",
                        "__set",
                        "__isset",
                        "__unset",
                        "__sleep",
                        "__wakeup",
                        "__toString",
                        "__invoke",
                        "__set_state",
                        "__clone",
                        "__debugInfo",
                    ),
                    suffix=r"\b",
                ),
                Name.Function.Magic,
            ),
        ],
        "magicconstants": [
            # source: http://php.net/manual/en/language.constants.predefined.php
            (
                words(
                    (
                        "__LINE__",
                        "__FILE__",
                        "__DIR__",
                        "__FUNCTION__",
                        "__CLASS__",
                        "__TRAIT__",
                        "__METHOD__",
                        "__NAMESPACE__",
                    ),
                    suffix=r"\b",
                ),
                Name.Constant,
            ),
        ],
        "classname": [(_ident_inner, Name.Class, "#pop")],
        "functionname": [include("magicfuncs"), (_ident_inner, Name.Function, "#pop"), default("#pop")],
        "string": [
            (r'"', String.Double, "#pop"),
            (r'[^{$"\\]+', String.Double),
            (r'\\([nrt"$\\]|[0-7]{1,3}|x[0-9a-f]{1,2})', String.Escape),
            (r"\$" + _ident_nons + r"(\[\S+?\]|->" + _ident_nons + ")?", String.Interpol),
            (r"(\{\$\{)(.*?)(\}\})", bygroups(String.Interpol, using(this, _startinline=True), String.Interpol)),
            (r"(\{)(\$.*?)(\})", bygroups(String.Interpol, using(this, _startinline=True), String.Interpol)),
            (r"(\$\{)(\S+)(\})", bygroups(String.Interpol, Name.Variable, String.Interpol)),
            (r"[${\\]", String.Double),
        ],
        "attribute": [
            (r"\]", Punctuation, "#pop"),
            (r"\(", Punctuation, "attributeparams"),
            (_ident_inner, Name.Decorator),
            include("php"),
        ],
        "attributeparams": [(r"\)", Punctuation, "#pop"), include("php")],
    }

    def __init__(self, **options):
        self.funcnamehighlighting = get_bool_opt(options, "funcnamehighlighting", True)
        self.disabledmodules = get_list_opt(options, "disabledmodules", ["unknown"])
        self.startinline = get_bool_opt(options, "startinline", False)

        # private option argument for the lexer itself
        if "_startinline" in options:
            self.startinline = options.pop("_startinline")

        # collect activated functions in a set
        self._functions = set()
        if self.funcnamehighlighting:
            from pygments.lexers._php_builtins import MODULES

            for key, value in MODULES.items():
                if key not in self.disabledmodules:
                    self._functions.update(value)
        RegexLexer.__init__(self, **options)

    def get_tokens_unprocessed(self, text):
        stack = ["root"]
        if self.startinline:
            stack.append("php")
        for index, token, value in RegexLexer.get_tokens_unprocessed(self, text, stack):
            if token is Name.Other:
                if value in self._functions:
                    yield index, Name.Builtin, value
                    continue
            yield index, token, value

    def analyse_text(text):
        if shebang_matches(text, r"php"):
            return True
        rv = 0.0
        if re.search(r"<\?(?!xml)", text):
            rv += 0.3
        return rv
