#!/usr/bin/env python3
# Copyright 2023 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import os
import re
import sys
from typing import Optional

_THIS_DIR = os.path.abspath(os.path.dirname(__file__))
# The repo's root directory.
_ROOT_DIR = os.path.abspath(os.path.join(_THIS_DIR, "..", ".."))

# Add the repo's root directory for clearer imports.
sys.path.insert(0, _ROOT_DIR)

import metadata.fields.util as util  # noqa: E402
import metadata.validation_result as vr  # noqa: E402

# Pattern used to check if the entire string is either "yes" or "no",
# case-insensitive.
_PATTERN_YES_OR_NO = re.compile(r"^(yes|no)$", re.IGNORECASE)

# Pattern used to check if the string starts with "yes" or "no",
# case-insensitive. e.g. "No (test only)", "Yes?"
_PATTERN_STARTS_WITH_YES_OR_NO = re.compile(r"^(yes|no)", re.IGNORECASE)


class MetadataField:
    """Base class for all metadata fields."""

    # The delimiter used to separate multiple values.
    VALUE_DELIMITER = ","

    def __init__(self, name: str, structured: bool = True):
        self._name = name
        self._structured = structured

    def __eq__(self, other):
        if not isinstance(other, MetadataField):
            return False

        return self._name.lower() == other._name.lower()

    def __hash__(self):
        return hash(self._name.lower())

    def get_name(self):
        return self._name

    def should_terminate_field(self, field_value) -> bool:
        """Whether this field should be terminated based on the given `field_value`."""
        return False

    def is_structured(self):
        """Whether the field represents structured data, such as a list of
        URLs.

        If true, parser will treat `Field Name Like Pattern:` in subsequent
        lines as a new field, in addition to all known field names.
        If false, parser will only recognize known field names, and unknown
        fields will be merged into the preceding field value.
        """
        return self._structured

    def validate(self, value: str, **kwargs) -> Optional[vr.ValidationResult]:
        """Checks the given value is acceptable for the field.

        Raises: NotImplementedError if called. This method must be
                overridden with the actual validation of the field.
        """
        raise NotImplementedError(f"{self._name} field validation not defined.")

    def narrow_type(self, value):
        """Returns a narrowly typed (e.g. bool) value for this field for
        downstream consumption.

        The alternative being the downstream parses the string again.
        """
        raise NotImplementedError(
            f"{self._name} field value coersion not defined."
        )


class FreeformTextField(MetadataField):
    """Field where the value is freeform text."""

    def validate(self, value: str, **kwargs) -> Optional[vr.ValidationResult]:
        """Checks the given value has at least one non-whitespace
        character.
        """
        if util.is_empty(value):
            return vr.ValidationError(reason=f"{self._name} is empty.")

        return None

    def narrow_type(self, value):
        assert value is not None
        return value


class SingleLineTextField(FreeformTextField):
    """Field where the field as a whole is a single line of text."""

    def __init__(self, name):
        super().__init__(name=name)

    def should_terminate_field(self, field_value) -> bool:
        # Look for line breaks.
        #
        # We don't use `os.linesep`` here because Chromium uses line feed
        # (`\n`) for git checkouts on all platforms (see README.* entries
        # in chromium.src/.gitattributes).
        return field_value.endswith("\n")


class YesNoField(SingleLineTextField):
    """Field where the value must be yes or no."""

    def __init__(self, name: str):
        super().__init__(name=name)

    def validate(self, value: str, **kwargs) -> Optional[vr.ValidationResult]:
        """Checks the given value is either yes or no."""
        if util.matches(_PATTERN_YES_OR_NO, value):
            return None

        if util.matches(_PATTERN_STARTS_WITH_YES_OR_NO, value):
            return vr.ValidationWarning(
                reason=f"{self._name} is invalid.",
                additional=[
                    f"This field should be only {util.YES} or {util.NO}.",
                    f"Current value is '{value}'.",
                ],
            )

        return vr.ValidationError(
            reason=f"{self._name} is invalid.",
            additional=[
                f"This field must be {util.YES} or {util.NO}.",
                f"Current value is '{value}'.",
            ],
        )

    def narrow_type(self, value) -> Optional[bool]:
        return util.infer_as_boolean(super().narrow_type(value))
