# Copyright Mozilla 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.

# The core logic of the parser used here is taken from silme:
# https://github.com/mozilla/silme/blob/2f7af3dd87fff27a3c3650d442a065b5a290268e/lib/silme/format/dtd/parser.py

from __future__ import annotations

from collections.abc import Iterator
from re import DOTALL, MULTILINE, UNICODE, compile
from sys import maxsize

from ...model import (
    Comment,
    Entry,
    Message,
    Metadata,
    PatternMessage,
    Resource,
    Section,
)
from .. import Format

name_start_char = (
    ":A-Z_a-z\xc0-\xd6\xd8-\xf6\xf8-\u02ff"
    "\u0370-\u037d\u037f-\u1fff\u200c-\u200d\u2070-\u218f\u2c00-\u2fef"
    "\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd"
)
name_char = name_start_char + r"\-\.0-9" + "\xb7\u0300-\u036f\u203f-\u2040"
name = f"[{name_start_char}][{name_char}]*"
re_entity = compile(
    r"<!ENTITY\s+(" + name + r")\s+((?:\"[^\"]*\")|(?:'[^']*'))\s*>",
    DOTALL | UNICODE,
)
re_system_include = compile(
    r"\s*<!ENTITY\s+%\s+("
    + name
    + r")\s+SYSTEM\s+((?:\"[^\"]*\")|(?:'[^']*'))\s*>\s*%\1;\s*",
    DOTALL | UNICODE,
)

re_comment = compile(r"\<!\s*--(.*?)--\s*\>", MULTILINE | DOTALL)


def dtd_parse(source: str | bytes) -> Resource[Message]:
    """
    Parse a .dtd file into a message resource.

    The metadata of the parsed resource may include
    references to other DTD files to include with the current one, as in:

        <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
        %brandDTD;
    """
    entries: list[Entry[Message] | Comment] = []
    resource = Resource(Format.dtd, [Section((), entries)])

    def handle_other_content(entry: str) -> None:
        if sm := re_system_include.fullmatch(entry):
            resource.meta.append(Metadata(sm[1], sm[2][1:-1]))
        elif entry and not entry.isspace():
            raise ValueError(f"Unexpected content in DTD: {entry}")

    pos = 0
    at_newline = True
    comment: str = ""
    if not isinstance(source, str):
        source = str(source, "utf8")
    for match in re_comment.finditer(source):
        cstart = match.start(0)
        has_prev_entries = False
        for entry in dtd_iter(source, pos, endpos=cstart):
            if isinstance(entry, str):
                handle_other_content(entry)
                lines = entry.split("\n")
                if comment and len(lines) > 2:
                    if entries or resource.comment:
                        entries.append(Comment(comment))
                    else:
                        resource.comment = comment
                    comment = ""
                at_newline = len(lines) > 1
            else:
                if comment:
                    entry.comment = comment
                    comment = ""
                entries.append(entry)
                has_prev_entries = True
        nc = match.group(1).strip().replace("\r\n", "\n")
        comment = f"{comment}\n{nc}" if comment else nc
        if comment:
            if not at_newline and has_prev_entries:
                prev = entries[-1]
                pc = prev.comment
                prev.comment = f"{pc}\n{comment}" if pc else comment
                comment = ""
            if re_entity.search(comment):
                entries.append(Comment(comment))
                comment = ""
        pos = match.end(0)
    if len(source) > pos:
        for entry in dtd_iter(source, pos):
            if isinstance(entry, str):
                handle_other_content(entry)
            else:
                if comment:
                    entry.comment = comment
                    comment = ""
                entries.append(entry)
    if comment:
        entries.append(Comment(comment))
    return resource


def dtd_iter(
    text: str, pos: int, endpos: int = maxsize
) -> Iterator[str | Entry[Message]]:
    for match in re_entity.finditer(text, pos, endpos):
        yield text[pos : match.start(0)]
        id, value = match.groups()
        yield Entry((id,), PatternMessage([value[1:-1]]))
        pos = match.end(0)
    yield text[pos:endpos]
