#!/usr/bin/env python3

##
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you 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
#
# https://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.

"""Contains the Name classes."""
from typing import TYPE_CHECKING, Dict, Optional

from avro.constants import PRIMITIVE_TYPES

if TYPE_CHECKING:
    from avro.schema import NamedSchema

import re
import warnings

import avro.errors

# The name portion of a fullname, record field names, and enum symbols must:
# start with [A-Za-z_]
# subsequently contain only [A-Za-z0-9_]
_BASE_NAME_PATTERN = re.compile(r"(?:^|\.)[A-Za-z_][A-Za-z0-9_]*$")


def validate_basename(basename: str) -> None:
    """Raise InvalidName if the given basename is not a valid name."""
    if not _BASE_NAME_PATTERN.search(basename):
        raise avro.errors.InvalidName(f"{basename!s} is not a valid Avro name because it does not match the pattern {_BASE_NAME_PATTERN.pattern!s}")


def _validate_fullname(fullname: str) -> None:
    for name in fullname.split("."):
        validate_basename(name)


class Name:
    """Class to describe Avro name."""

    _full: Optional[str] = None
    _name: Optional[str] = None

    def __init__(
        self, name_attr: Optional[str] = None, space_attr: Optional[str] = None, default_space: Optional[str] = None, validate_name: bool = True
    ) -> None:
        """The fullname is determined in one of the following ways:

        - A name and namespace are both specified. For example, one might use "name": "X",
            "namespace": "org.foo" to indicate the fullname org.foo.X.
        - A fullname is specified. If the name specified contains a dot,
            then it is assumed to be a fullname, and any namespace also specified is ignored.
            For example, use "name": "org.foo.X" to indicate the fullname org.foo.X.
        - A name only is specified, i.e., a name that contains no dots.
            In this case the namespace is taken from the most tightly enclosing schema or protocol.
            For example, if "name": "X" is specified, and this occurs within a field of
            the record definition of org.foo.Y, then the fullname is org.foo.X.
            If there is no enclosing namespace then the null namespace is used.

        References to previously defined names are as in the latter two cases above:
        if they contain a dot they are a fullname,
        if they do not contain a dot, the namespace is the namespace of the enclosing definition.

        @arg name_attr: name value read in schema or None.
        @arg space_attr: namespace value read in schema or None. The empty string may be used as a namespace
            to indicate the null namespace.
        @arg default_space: the current default space or None.
        """
        if name_attr is None:
            return
        if name_attr == "":
            raise avro.errors.SchemaParseException("Name must not be the empty string.")
        # The empty string may be used as a namespace to indicate the null namespace.
        self._name = name_attr.split(".")[-1]
        self._full = (
            name_attr
            if "." in name_attr or space_attr == "" or not (space_attr or default_space)
            else f"{space_attr or default_space!s}.{name_attr!s}"
        )
        if validate_name:
            _validate_fullname(self._full)

    def __eq__(self, other: object) -> bool:
        """Equality of names is defined on the fullname and is case-sensitive."""
        return hasattr(other, "fullname") and self.fullname == getattr(other, "fullname")

    @property
    def name(self) -> Optional[str]:
        return self._name

    @property
    def fullname(self) -> Optional[str]:
        return self._full

    @property
    def space(self) -> Optional[str]:
        """Back out a namespace from full name."""
        full = self._full or ""
        return full.rsplit(".", 1)[0] if "." in full else None

    def get_space(self) -> Optional[str]:
        warnings.warn("Name.get_space() is deprecated in favor of Name.space")
        return self.space


class Names:
    """Track name set and default namespace during parsing."""

    names: Dict[str, "NamedSchema"]

    def __init__(self, default_namespace: Optional[str] = None, validate_names: bool = True) -> None:
        self.names = {}
        self.default_namespace = default_namespace
        self.validate_names = validate_names

    def has_name(self, name_attr: str, space_attr: Optional[str] = None) -> bool:
        test = Name(name_attr, space_attr, self.default_namespace, validate_name=self.validate_names).fullname
        return test in self.names

    def get_name(self, name_attr: str, space_attr: Optional[str] = None) -> Optional["NamedSchema"]:
        test = Name(name_attr, space_attr, self.default_namespace, validate_name=self.validate_names).fullname
        return None if test is None else self.names.get(test)

    def prune_namespace(self, properties: Dict[str, object]) -> Dict[str, object]:
        """given a properties, return properties with namespace removed if
        it matches the own default namespace"""
        if self.default_namespace is None:
            # I have no default -- no change
            return properties

        if "namespace" not in properties:
            # he has no namespace - no change
            return properties

        if properties["namespace"] != self.default_namespace:
            # we're different - leave his stuff alone
            return properties

        # we each have a namespace and it's redundant. delete his.
        prunable = properties.copy()
        del prunable["namespace"]
        return prunable

    def add_name(self, name_attr: str, space_attr: Optional[str], new_schema: "NamedSchema") -> Name:
        """
        Add a new schema object to the name set.

        @arg name_attr: name value read in schema
        @arg space_attr: namespace value read in schema.

        @return: the Name that was just added.
        """
        to_add = Name(name_attr, space_attr, self.default_namespace, validate_name=self.validate_names)

        if to_add.fullname in PRIMITIVE_TYPES:
            raise avro.errors.SchemaParseException(f"{to_add.fullname} is a reserved type name.")
        if to_add.fullname in self.names:
            raise avro.errors.SchemaParseException(f'The name "{to_add.fullname}" is already in use.')
        if to_add.fullname is None:
            raise avro.errors.SchemaParseException(f'The name built from "{space_attr}.{name_attr}" is None')

        self.names[to_add.fullname] = new_schema
        return to_add
