# Copyright 2018-2024, The Khronos Group Inc.
# Copyright (c) 2018-2019 Collabora, Ltd.
#
# SPDX-License-Identifier: Apache-2.0
#
# Author(s):    Rylie Pavlik <rylie.pavlik@collabora.com>
"""Provides the BasePrinter base class for MacroChecker/Message output techniques."""

from abc import ABC, abstractmethod
from pathlib import Path

from .macro_checker import MacroChecker
from .macro_checker_file import MacroCheckerFile
from .shared import EntityData, Message, MessageContext, MessageType


def getColumn(message_context):
    """Return the (zero-based) column number of the message context.

    If a group is specified: returns the column of the start of the group.
    If no group, but a match is specified: returns the column of the start of
    the match.
    If no match: returns column 0 (whole line).
    """
    if not message_context.match:
        # whole line
        return 0
    if message_context.group is not None:
        return message_context.match.start(message_context.group)
    return message_context.match.start()


class BasePrinter(ABC):
    """Base class for a way of outputting results of a checker execution."""

    def __init__(self):
        """Constructor."""
        self._cwd = None
        self.show_script_location = False

    def close(self):
        """Write the tail end of the output and close it, if applicable.

        Override if you want to print a summary or are writing to a file.
        """
        pass

    ###
    # Output methods: these should all print/output directly.
    def output(self, obj):
        """Output any object.

        Delegates to other output* methods, if type known,
        otherwise uses self.outputFallback().
        """
        if isinstance(obj, Message):
            self.outputMessage(obj)
        elif isinstance(obj, MacroCheckerFile):
            self.outputCheckerFile(obj)
        elif isinstance(obj, MacroChecker):
            self.outputChecker(obj)
        else:
            self.outputFallback(self.formatBrief(obj))

    @abstractmethod
    def outputResults(self, checker, broken_links=True,
                      missing_includes=False):
        """Output the full results of a checker run.

        Must be implemented.

        Typically will call self.output() on the MacroChecker,
        as well as calling self.outputBrokenAndMissing()
        """
        raise NotImplementedError

    @abstractmethod
    def outputBrokenLinks(self, checker, broken):
        """Output the collection of broken links.

        `broken` is a dictionary of entity names: usage contexts.

        Must be implemented.

        Called by self.outputBrokenAndMissing() if requested.
        """
        raise NotImplementedError

    @abstractmethod
    def outputMissingIncludes(self, checker, missing):
        """Output a table of missing includes.

        `missing` is a iterable entity names.

        Must be implemented.

        Called by self.outputBrokenAndMissing() if requested.
        """
        raise NotImplementedError

    def outputChecker(self, checker):
        """Output the contents of a MacroChecker object.

        Default implementation calls self.output() on every MacroCheckerFile.
        """
        for f in checker.files:
            self.output(f)

    def outputCheckerFile(self, fileChecker):
        """Output the contents of a MacroCheckerFile object.

        Default implementation calls self.output() on every Message.
        """
        for m in fileChecker.messages:
            self.output(m)

    def outputBrokenAndMissing(self, checker, broken_links=True,
                               missing_includes=False):
        """Outputs broken links and missing includes, if desired.

        Delegates to self.outputBrokenLinks() (if broken_links==True)
        and self.outputMissingIncludes() (if missing_includes==True).
        """
        if broken_links:
            broken = checker.getBrokenLinks()
            if broken:
                self.outputBrokenLinks(checker, broken)
        if missing_includes:
            missing = checker.getMissingUnreferencedApiIncludes()
            if missing:
                self.outputMissingIncludes(checker, missing)

    @abstractmethod
    def outputMessage(self, msg):
        """Output a Message.

        Must be implemented.
        """
        raise NotImplementedError

    @abstractmethod
    def outputFallback(self, msg):
        """Output some text in a general way.

        Must be implemented.
        """
        raise NotImplementedError

    ###
    # Format methods: these should all return a string.
    def formatContext(self, context, _message_type=None):
        """Format a message context in a verbose way, if applicable.

        May override, default implementation delegates to
        self.formatContextBrief().
        """
        return self.formatContextBrief(context)

    def formatContextBrief(self, context, _with_color=True):
        """Format a message context in a brief way.

        May override, default is relativeFilename:line:column
        """
        relative = self.getRelativeFilename(context.filename)
        return f'{relative}:{context.lineNum}:{getColumn(context)}'

    def formatMessageTypeBrief(self, message_type, _with_color=True):
        """Format a message type in a brief way.

        May override, default is message_type:
        """
        return f'{message_type}:'

    def formatEntityBrief(self, entity_data, _with_color=True):
        """Format an entity in a brief way.

        May override, default is macro:entity.
        """
        return f'{entity_data.macro}:{entity_data.entity}'

    def formatBrief(self, obj, with_color=True):
        """Format any object in a brief way.

        Delegates to other format*Brief methods, if known,
        otherwise uses str().
        """
        if isinstance(obj, MessageContext):
            return self.formatContextBrief(obj, with_color)
        if isinstance(obj, MessageType):
            return self.formatMessageTypeBrief(obj, with_color)
        if isinstance(obj, EntityData):
            return self.formatEntityBrief(obj, with_color)
        return str(obj)

    @property
    def cwd(self):
        """Get the current working directory, fully resolved.

        Lazy initialized.
        """
        if not self._cwd:
            self._cwd = Path('.').resolve()
        return self._cwd

    ###
    # Helper function
    def getRelativeFilename(self, fn):
        """Return the given filename relative to the current directory,
        if possible.
        """
        try:
            return str(Path(fn).relative_to(self.cwd))
        except ValueError:
            return str(Path(fn))
