#  Copyright 2008-2015 Nokia Networks
#  Copyright 2016-     Robot Framework Foundation
#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.

import re
from collections.abc import Iterator

from .tokens import Token


class Tokenizer:
    _space_splitter = re.compile(r"(\s{2,}|\t)", re.UNICODE)
    _pipe_splitter = re.compile(r"((?:\A|\s+)\|(?:\s+|\Z))", re.UNICODE)

    def tokenize(self, data: str, data_only: bool = False) -> "Iterator[list[Token]]":
        current: "list[Token]" = []
        for lineno, line in enumerate(data.splitlines(not data_only), start=1):
            tokens = self._tokenize_line(line, lineno, not data_only)
            tokens, starts_new = self._cleanup_tokens(tokens, data_only)
            if starts_new:
                if current:
                    yield current
                current = tokens
            else:
                current.extend(tokens)
        yield current

    def _tokenize_line(
        self,
        line: str,
        lineno: int,
        include_separators: bool,
    ) -> "list[Token]":
        # Performance optimized code.
        tokens = []
        append = tokens.append
        offset = 0
        if line[:1] == "|" and line[:2].strip() == "|":
            splitter = self._split_from_pipes
        else:
            splitter = self._split_from_spaces
        for value, is_data in splitter(line.rstrip()):
            if is_data:
                append(Token(None, value, lineno, offset))
            elif include_separators:
                append(Token(Token.SEPARATOR, value, lineno, offset))
            offset += len(value)
        if include_separators:
            trailing_whitespace = line[len(line.rstrip()) :]
            append(Token(Token.EOL, trailing_whitespace, lineno, offset))
        return tokens

    def _split_from_spaces(self, line: str) -> "Iterator[tuple[str, bool]]":
        is_data = True
        for value in self._space_splitter.split(line):
            yield value, is_data
            is_data = not is_data

    def _split_from_pipes(self, line) -> "Iterator[tuple[str, bool]]":
        splitter = self._pipe_splitter
        _, separator, rest = splitter.split(line, 1)
        yield separator, False
        while splitter.search(rest):
            token, separator, rest = splitter.split(rest, 1)
            yield token, True
            yield separator, False
        yield rest, True

    def _cleanup_tokens(
        self,
        tokens: "list[Token]",
        data_only: bool,
    ) -> "tuple[list[Token], bool]":
        has_data, comments, continues = self._handle_comments_and_continuation(tokens)
        self._remove_trailing_empty(tokens)
        if continues:
            self._remove_leading_empty(tokens)
            if not has_data:
                self._ensure_data_after_continuation(tokens)
            starts_new = False
        else:
            starts_new = has_data
        if data_only and (comments or continues):
            tokens = [t for t in tokens if t.type is None]
        return tokens, starts_new

    def _handle_comments_and_continuation(
        self,
        tokens: "list[Token]",
    ) -> "tuple[bool, bool, bool]":
        first = True
        has_data = False
        comments = False
        continues = False
        for token in tokens:
            if token.type is None:
                # The first token may have a leading space. With others spaces
                # have been consumed as separators.
                value = token.value.lstrip() if first else token.value
                if comments:
                    token.type = Token.COMMENT
                elif value:
                    if value[0] == "#":
                        token.type = Token.COMMENT
                        comments = True
                    elif not has_data:
                        if value == "..." and not continues:
                            token.type = Token.CONTINUATION
                            continues = True
                        else:
                            has_data = True
            first = False
        return has_data, comments, continues

    def _remove_trailing_empty(self, tokens: "list[Token]"):
        for token in reversed(tokens):
            if not token.value and token.type != Token.EOL:
                tokens.remove(token)
            elif token.type is None:
                break

    def _remove_leading_empty(self, tokens: "list[Token]"):
        data_or_continuation = (None, Token.CONTINUATION)
        for token in list(tokens):
            if not token.value:
                tokens.remove(token)
            elif token.type in data_or_continuation:
                break

    def _ensure_data_after_continuation(self, tokens: "list[Token]"):
        cont = self._find_continuation(tokens)
        token = Token(lineno=cont.lineno, col_offset=cont.end_col_offset)
        tokens.insert(tokens.index(cont) + 1, token)

    def _find_continuation(self, tokens: "list[Token]") -> Token:
        for token in tokens:
            if token.type == Token.CONTINUATION:
                return token
        raise ValueError("Continuation not found.")
