# SPDX-License-Identifier: Apache-2.0
# Copyright © 2022-2023 Intel Corporation

"""Rust CFG parser.

Rust uses its `cfg()` format in cargo.

This may have the following functions:
 - all()
 - any()
 - not()

And additionally is made up of `identifier [ = str]`. Where the str is optional,
so you could have examples like:
```
[target.`cfg(unix)`.dependencies]
[target.'cfg(target_arch = "x86_64")'.dependencies]
[target.'cfg(all(target_arch = "x86_64", target_arch = "x86"))'.dependencies]
```
"""

from __future__ import annotations
import dataclasses
import enum
import functools
import typing as T


from . import builder
from .. import mparser
from ..mesonlib import MesonBugException

if T.TYPE_CHECKING:
    _T = T.TypeVar('_T')
    _LEX_TOKEN = T.Tuple['TokenType', T.Optional[str]]
    _LEX_STREAM = T.Iterable[_LEX_TOKEN]
    _LEX_STREAM_AH = T.Iterator[T.Tuple[_LEX_TOKEN, T.Optional[_LEX_TOKEN]]]


class TokenType(enum.Enum):

    LPAREN = enum.auto()
    RPAREN = enum.auto()
    STRING = enum.auto()
    IDENTIFIER = enum.auto()
    ALL = enum.auto()
    ANY = enum.auto()
    NOT = enum.auto()
    COMMA = enum.auto()
    EQUAL = enum.auto()


def lexer(raw: str) -> _LEX_STREAM:
    """Lex a cfg() expression.

    :param raw: The raw cfg() expression
    :return: An iterable of tokens
    """
    buffer: T.List[str] = []
    is_string: bool = False
    for s in raw:
        if s.isspace() or s in {')', '(', ',', '='} or (s == '"' and buffer):
            val = ''.join(buffer)
            buffer.clear()
            if is_string:
                yield (TokenType.STRING, val)
            elif val == 'any':
                yield (TokenType.ANY, None)
            elif val == 'all':
                yield (TokenType.ALL, None)
            elif val == 'not':
                yield (TokenType.NOT, None)
            elif val:
                yield (TokenType.IDENTIFIER, val)

            if s == '(':
                yield (TokenType.LPAREN, None)
                continue
            elif s == ')':
                yield (TokenType.RPAREN, None)
                continue
            elif s == ',':
                yield (TokenType.COMMA, None)
                continue
            elif s == '=':
                yield (TokenType.EQUAL, None)
                continue
            elif s.isspace():
                continue

        if s == '"':
            is_string = not is_string
        else:
            buffer.append(s)
    if buffer:
        # This should always be an identifier
        yield (TokenType.IDENTIFIER, ''.join(buffer))


def lookahead(iter: T.Iterator[_T]) -> T.Iterator[T.Tuple[_T, T.Optional[_T]]]:
    """Get the current value of the iterable, and the next if possible.

    :param iter: The iterable to look into
    :yield: A tuple of the current value, and, if possible, the next
    :return: nothing
    """
    current: _T
    next_: T.Optional[_T]
    try:
        next_ = next(iter)
    except StopIteration:
        # This is an empty iterator, there's nothing to look ahead to
        return

    while True:
        current = next_
        try:
            next_ = next(iter)
        except StopIteration:
            next_ = None

        yield current, next_

        if next_ is None:
            break


@dataclasses.dataclass
class IR:

    """Base IR node for Cargo CFG."""

    filename: str

@dataclasses.dataclass
class String(IR):

    value: str


@dataclasses.dataclass
class Identifier(IR):

    value: str


@dataclasses.dataclass
class Equal(IR):

    lhs: IR
    rhs: IR


@dataclasses.dataclass
class Any(IR):

    args: T.List[IR]


@dataclasses.dataclass
class All(IR):

    args: T.List[IR]


@dataclasses.dataclass
class Not(IR):

    value: IR


def _parse(ast: _LEX_STREAM_AH, filename: str) -> IR:
    (token, value), n_stream = next(ast)
    if n_stream is not None:
        ntoken, _ = n_stream
    else:
        ntoken, _ = (None, None)

    stream: T.List[_LEX_TOKEN]
    if token is TokenType.IDENTIFIER:
        if ntoken is TokenType.EQUAL:
            return Equal(filename, Identifier(filename, value), _parse(ast, filename))
    if token is TokenType.STRING:
        return String(filename, value)
    if token is TokenType.EQUAL:
        # In this case the previous caller already has handled the equal
        return _parse(ast, filename)
    if token in {TokenType.ANY, TokenType.ALL}:
        type_ = All if token is TokenType.ALL else Any
        assert ntoken is TokenType.LPAREN
        next(ast)  # advance the iterator to get rid of the LPAREN
        stream = []
        args: T.List[IR] = []
        while token is not TokenType.RPAREN:
            (token, value), _ = next(ast)
            if token is TokenType.COMMA:
                args.append(_parse(lookahead(iter(stream)), filename))
                stream.clear()
            else:
                stream.append((token, value))
        if stream:
            args.append(_parse(lookahead(iter(stream)), filename))
        return type_(filename, args)
    if token is TokenType.NOT:
        next(ast)  # advance the iterator to get rid of the LPAREN
        stream = []
        # Mypy can't figure out that token is overridden inside the while loop
        while token is not TokenType.RPAREN:  # type: ignore
            (token, value), _ = next(ast)
            stream.append((token, value))
        return Not(filename, _parse(lookahead(iter(stream)), filename))

    raise MesonBugException(f'Unhandled Cargo token: {token}')


def parse(ast: _LEX_STREAM, filename: str) -> IR:
    """Parse the tokenized list into Meson AST.

    :param ast: An iterable of Tokens
    :param filename: The name of the file being parsed
    :return: An mparser Node to be used as a conditional
    """
    ast_i: _LEX_STREAM_AH = lookahead(iter(ast))
    return _parse(ast_i, filename)


@functools.singledispatch
def ir_to_meson(ir: T.Any) -> mparser.BaseNode:
    raise NotImplementedError


@ir_to_meson.register
def _(ir: String) -> mparser.BaseNode:
    return builder.string(ir.value, ir.filename)


@ir_to_meson.register
def _(ir: Identifier) -> mparser.BaseNode:
    host_machine = builder.identifier('host_machine', ir.filename)
    if ir.value == "target_arch":
        return builder.method('cpu_family', host_machine)
    elif ir.value in {"target_os", "target_family"}:
        return builder.method('system', host_machine)
    elif ir.value == "target_endian":
        return builder.method('endian', host_machine)
    raise MesonBugException(f"Unhandled Cargo identifier: {ir.value}")


@ir_to_meson.register
def _(ir: Equal) -> mparser.BaseNode:
    return builder.equal(ir_to_meson(ir.lhs), ir_to_meson(ir.rhs))


@ir_to_meson.register
def _(ir: Not) -> mparser.BaseNode:
    return builder.not_(ir_to_meson(ir.value), ir.filename)


@ir_to_meson.register
def _(ir: Any) -> mparser.BaseNode:
    args = iter(reversed(ir.args))
    last = next(args)
    cur = builder.or_(ir_to_meson(next(args)), ir_to_meson(last))
    for a in args:
        cur = builder.or_(ir_to_meson(a), cur)
    return cur


@ir_to_meson.register
def _(ir: All) -> mparser.BaseNode:
    args = iter(reversed(ir.args))
    last = next(args)
    cur = builder.and_(ir_to_meson(next(args)), ir_to_meson(last))
    for a in args:
        cur = builder.and_(ir_to_meson(a), cur)
    return cur
