"""Serializes a Python dictionary into a LookML string."""

from copy import deepcopy
from typing import Any, Dict, Iterator, Sequence, Union

from lkml.keys import (
    EXPR_BLOCK_KEYS,
    KEYS_WITH_NAME_FIELDS,
    PLURAL_KEYS,
    QUOTED_LITERAL_KEYS,
)


class Serializer:
    """Serializes a Python dictionary into a LookML string.

    Review the grammar specified for the Parser class to understand how LookML
    is represented. The grammar details the differences between blocks, pairs, keys,
    and values.

    Attributes:
        parent_key: The name of the key at the previous level in a LookML block
        level: The number of indentations appropriate for the current position
        field_counter: The position of the current field when serializing
            iterable objects
        base_indent: Whitespace representing one tab
        indent: An indent of whitespace dynamically sized for the current position
        newline_indent: A newline plus an indent string

    """

    def __init__(self):
        """Initializes the Serializer."""
        self.parent_key: str = None
        self.level: int = 0
        self.field_counter: int = 0
        self.base_indent: str = " " * 2
        self.indent: str = ""
        self.newline_indent: str = "\n"

    def increase_level(self) -> None:
        """Increases the indent level of the current line by one tab."""
        self.field_counter = 0
        self.level += 1
        self.update_indent()

    def decrease_level(self) -> None:
        """Decreases the indent level of the current line by one tab."""
        self.field_counter = 0
        self.level -= 1
        self.update_indent()

    def update_indent(self) -> None:
        """Sets the indent string based on the current level."""
        self.indent = self.base_indent * self.level
        self.newline_indent = "\n" + self.indent

    def is_plural_key(self, key: str) -> bool:
        """Returns True if the key is a repeatable key.

        For example, `dimension` can be repeated, but `sql` cannot be.

        The key `allowed_value` is a special case and changes behavior depending on its
        parent key. If its parent key is `access_grant`, it is a list and cannot be
        repeated. Otherwise, it can be repeated.

        """
        if key.endswith("s"):
            singular_key = key.rstrip("s")
            return singular_key in PLURAL_KEYS and not (
                singular_key == "allowed_value"
                and self.parent_key.rstrip("s") == "access_grant"
            )
        else:
            return False

    def serialize(self, obj: dict) -> str:  # noqa: D202
        """Returns a LookML string serialized from a dictionary."""

        def chain_with_newline():
            for key, value in deepcopy(obj).items():
                yield from self.write_any(key, value)
                yield "\n"

        return "".join(chain_with_newline())

    def expand_list(self, key: str, values: Sequence) -> Iterator[str]:
        """Expands and serializes a list of values for a repeatable key.

        This method is exclusively used for sequences of values with a repeated key like
        `dimensions` or `views`, which need to be serialized sequentially with a newline
        in between.

        Args:
            key: A repeatable LookML field type (e.g. "views" or "dimension_groups")
            values: A sequence of objects to be serialized

        Returns:
            A generator of serialized string chunks

        """
        modified_key = (
            key.rstrip("s") if key not in ("filters", "bind_filters") else key
        )
        for i, value in enumerate(values):
            if i > 0:
                yield "\n"
            yield from self.write_any(modified_key, value)

    def write_any(
        self, key: str, value: Union[str, list, tuple, dict]
    ) -> Iterator[str]:
        """Dynamically serializes a Python object based on its type.

        Args:
            key: A LookML field type (e.g. "suggestions" or "hidden")
            value: A string, tuple, or list to serialize

        Raises:
            TypeError: If input value is not of a valid type

        Returns:
            A generator of serialized string chunks

        """
        if isinstance(value, str):
            yield from self.write_pair(key, value)
        elif isinstance(value, (list, tuple)):
            if self.is_plural_key(key):
                yield from self.expand_list(key, value)
            else:
                yield from self.write_set(key, value)
        elif isinstance(value, dict):
            if key in KEYS_WITH_NAME_FIELDS or "name" not in value.keys():
                name = None
            else:
                name = value.pop("name")
            yield from self.write_block(key, value, name)
        else:
            raise TypeError(
                "Value must be a string, list, tuple, or dict. Got %s." % type(value)
            )

        self.field_counter += 1

    def write_block(
        self, key: str, fields: Dict[str, Any], name: str = None
    ) -> Iterator[str]:
        """Serializes a dictionary to a LookML block.

        Args:
            key: A LookML field type (e.g. "dimension")
            fields: A dictionary to serialize (e.g. {"sql": "${TABLE}.order_id"})
            name: An optional name of the block (e.g. "order_id")

        Returns:
            A generator of serialized string chunks

        """
        if self.field_counter > 0:
            yield "\n"

        yield from self.write_key(key)
        if name:
            yield f"{name} " + "{"
        else:
            yield "{"

        if fields:
            self.parent_key = key
            self.increase_level()
            yield "\n"
            for i, (key, value) in enumerate(fields.items()):
                if i > 0:
                    yield "\n"
                yield from self.write_any(key, value)
            self.decrease_level()
            yield self.newline_indent

        yield "}"

    def write_set(self, key: str, values: Sequence[str]) -> Iterator[str]:
        """Serializes a sequence to a LookML block.

        Args:
            key: A LookML field type (e.g. "fields")
            value: A sequence to serialize (e.g. ["orders.order_id", "orders.item"])

        Returns:
            A generator of serialized string chunks

        """
        # `suggestions` is only quoted when it's a set, so override the default
        force_quote = True if key == "suggestions" else False

        yield from self.write_key(key)
        yield "["

        if values:
            if len(values) > 5:
                self.increase_level()
                yield self.newline_indent
                for i, value in enumerate(values):
                    if i > 0:
                        yield f",{self.newline_indent}"
                    yield from self.write_value(key, value, force_quote)
                self.decrease_level()
                yield self.newline_indent
            else:
                for i, value in enumerate(values):
                    if i > 0:
                        yield f", "
                    yield from self.write_value(key, value, force_quote)
        yield "]"

    def write_pair(self, key: str, value: str) -> Iterator[str]:
        """Serializes a key and value to a LookML pair.

        Args:
            key: A LookML field type (e.g. "hidden")
            value: The value string (e.g. "yes")

        Returns:
            A generator of serialized string chunks

        """
        yield from self.write_key(key)
        yield from self.write_value(key, value)

    def write_key(self, key: str) -> Iterator[str]:
        """Serializes a key to LookML.

        Args:
            key: A LookML field type (e.g. "sql")

        Returns:
            A generator of serialized string chunks

        """
        yield f"{self.indent}{key}: "

    def write_value(
        self, key: str, value: str, force_quote: bool = False
    ) -> Iterator[str]:
        """Serializes a value to LookML, quoting it required by the key or forced.

        Args:
            key: A LookML field type (e.g. "hidden")
            value: The value string (e.g. "yes")
            force_quote: True if value should always be quoted

        Returns:
            A generator of serialized string chunks

        """
        if force_quote or key in QUOTED_LITERAL_KEYS:
            yield '"'
            yield value
            yield '"'
        elif key in EXPR_BLOCK_KEYS:
            yield value
            yield " ;;"
        else:
            yield value

"""Serializes a Python dictionary into a LookML string."""

from copy import deepcopy
from typing import Any, Dict, Iterator, Sequence, Union

from lkml.keys import (
    EXPR_BLOCK_KEYS,
    KEYS_WITH_NAME_FIELDS,
    PLURAL_KEYS,
    QUOTED_LITERAL_KEYS,
)


class Serializer:
    """Serializes a Python dictionary into a LookML string.

    Review the grammar specified for the Parser class to understand how LookML
    is represented. The grammar details the differences between blocks, pairs, keys,
    and values.

    Attributes:
        parent_key: The name of the key at the previous level in a LookML block
        level: The number of indentations appropriate for the current position
        field_counter: The position of the current field when serializing
            iterable objects
        base_indent: Whitespace representing one tab
        indent: An indent of whitespace dynamically sized for the current position
        newline_indent: A newline plus an indent string

    """

    def __init__(self):
        """Initializes the Serializer."""
        self.parent_key: str = None
        self.level: int = 0
        self.field_counter: int = 0
        self.base_indent: str = " " * 2
        self.indent: str = ""
        self.newline_indent: str = "\n"

    def increase_level(self) -> None:
        """Increases the indent level of the current line by one tab."""
        self.field_counter = 0
        self.level += 1
        self.update_indent()

    def decrease_level(self) -> None:
        """Decreases the indent level of the current line by one tab."""
        self.field_counter = 0
        self.level -= 1
        self.update_indent()

    def update_indent(self) -> None:
        """Sets the indent string based on the current level."""
        self.indent = self.base_indent * self.level
        self.newline_indent = "\n" + self.indent

    def is_plural_key(self, key: str) -> bool:
        """Returns True if the key is a repeatable key.

        For example, `dimension` can be repeated, but `sql` cannot be.

        The key `allowed_value` is a special case and changes behavior depending on its
        parent key. If its parent key is `access_grant`, it is a list and cannot be
        repeated. Otherwise, it can be repeated.

        """
        if key.endswith("s"):
            singular_key = key.rstrip("s")
            return singular_key in PLURAL_KEYS and not (
                singular_key == "allowed_value"
                and self.parent_key.rstrip("s") == "access_grant"
            )
        else:
            return False

    def serialize(self, obj: dict) -> str:  # noqa: D202
        """Returns a LookML string serialized from a dictionary."""

        def chain_with_newline():
            for key, value in deepcopy(obj).items():
                yield from self.write_any(key, value)
                yield "\n"

        return "".join(chain_with_newline())

    def expand_list(self, key: str, values: Sequence) -> Iterator[str]:
        """Expands and serializes a list of values for a repeatable key.

        This method is exclusively used for sequences of values with a repeated key like
        `dimensions` or `views`, which need to be serialized sequentially with a newline
        in between.

        Args:
            key: A repeatable LookML field type (e.g. "views" or "dimension_groups")
            values: A sequence of objects to be serialized

        Returns:
            A generator of serialized string chunks

        """
        modified_key = (
            key.rstrip("s") if key not in ("filters", "bind_filters") else key
        )
        for i, value in enumerate(values):
            if i > 0:
                yield "\n"
            yield from self.write_any(modified_key, value)

    def write_any(
        self, key: str, value: Union[str, list, tuple, dict]
    ) -> Iterator[str]:
        """Dynamically serializes a Python object based on its type.

        Args:
            key: A LookML field type (e.g. "suggestions" or "hidden")
            value: A string, tuple, or list to serialize

        Raises:
            TypeError: If input value is not of a valid type

        Returns:
            A generator of serialized string chunks

        """
        if isinstance(value, str):
            yield from self.write_pair(key, value)
        elif isinstance(value, (list, tuple)):
            if self.is_plural_key(key):
                yield from self.expand_list(key, value)
            else:
                yield from self.write_set(key, value)
        elif isinstance(value, dict):
            if key in KEYS_WITH_NAME_FIELDS or "name" not in value.keys():
                name = None
            else:
                name = value.pop("name")
            yield from self.write_block(key, value, name)
        else:
            raise TypeError(
                "Value must be a string, list, tuple, or dict. Got %s." % type(value)
            )

        self.field_counter += 1

    def write_block(
        self, key: str, fields: Dict[str, Any], name: str = None
    ) -> Iterator[str]:
        """Serializes a dictionary to a LookML block.

        Args:
            key: A LookML field type (e.g. "dimension")
            fields: A dictionary to serialize (e.g. {"sql": "${TABLE}.order_id"})
            name: An optional name of the block (e.g. "order_id")

        Returns:
            A generator of serialized string chunks

        """
        if self.field_counter > 0:
            yield "\n"

        yield from self.write_key(key)
        if name:
            yield f"{name} " + "{"
        else:
            yield "{"

        if fields:
            self.parent_key = key
            self.increase_level()
            yield "\n"
            for i, (key, value) in enumerate(fields.items()):
                if i > 0:
                    yield "\n"
                yield from self.write_any(key, value)
            self.decrease_level()
            yield self.newline_indent

        yield "}"

    def write_set(self, key: str, values: Sequence[str]) -> Iterator[str]:
        """Serializes a sequence to a LookML block.

        Args:
            key: A LookML field type (e.g. "fields")
            value: A sequence to serialize (e.g. ["orders.order_id", "orders.item"])

        Returns:
            A generator of serialized string chunks

        """
        # `suggestions` is only quoted when it's a set, so override the default
        force_quote = True if key == "suggestions" else False

        yield from self.write_key(key)
        yield "["

        if values:
            if len(values) > 5:
                self.increase_level()
                yield self.newline_indent
                for i, value in enumerate(values):
                    if i > 0:
                        yield f",{self.newline_indent}"
                    yield from self.write_value(key, value, force_quote)
                self.decrease_level()
                yield self.newline_indent
            else:
                for i, value in enumerate(values):
                    if i > 0:
                        yield f", "
                    yield from self.write_value(key, value, force_quote)
        yield "]"

    def write_pair(self, key: str, value: str) -> Iterator[str]:
        """Serializes a key and value to a LookML pair.

        Args:
            key: A LookML field type (e.g. "hidden")
            value: The value string (e.g. "yes")

        Returns:
            A generator of serialized string chunks

        """
        yield from self.write_key(key)
        yield from self.write_value(key, value)

    def write_key(self, key: str) -> Iterator[str]:
        """Serializes a key to LookML.

        Args:
            key: A LookML field type (e.g. "sql")

        Returns:
            A generator of serialized string chunks

        """
        yield f"{self.indent}{key}: "

    def write_value(
        self, key: str, value: str, force_quote: bool = False
    ) -> Iterator[str]:
        """Serializes a value to LookML, quoting it required by the key or forced.

        Args:
            key: A LookML field type (e.g. "hidden")
            value: The value string (e.g. "yes")
            force_quote: True if value should always be quoted

        Returns:
            A generator of serialized string chunks

        """
        if force_quote or key in QUOTED_LITERAL_KEYS:
            yield '"'
            yield value
            yield '"'
        elif key in EXPR_BLOCK_KEYS:
            yield value
            yield " ;;"
        else:
            yield value

Serializes a Python dictionary into a LookML string. Serializes a Python dictionary into a LookML string.      Review the grammar specified for the Parser class to understand how LookML     is represented. The grammar details the differences between blocks, pairs, keys,     and values.      Attributes:         parent_key: The name of the key at the previous level in a LookML block         level: The number of indentations appropriate for the current position         field_counter: The position of the current field when serializing             iterable objects         base_indent: Whitespace representing one tab         indent: An indent of whitespace dynamically sized for the current position         newline_indent: A newline plus an indent string Initializes the Serializer. Increases the indent level of the current line by one tab. Decreases the indent level of the current line by one tab. Sets the indent string based on the current level. Returns True if the key is a repeatable key.          For example, `dimension` can be repeated, but `sql` cannot be.          The key `allowed_value` is a special case and changes behavior depending on its         parent key. If its parent key is `access_grant`, it is a list and cannot be         repeated. Otherwise, it can be repeated. # noqa: D202 Returns a LookML string serialized from a dictionary. Expands and serializes a list of values for a repeatable key.          This method is exclusively used for sequences of values with a repeated key like         `dimensions` or `views`, which need to be serialized sequentially with a newline         in between.          Args:             key: A repeatable LookML field type (e.g. "views" or "dimension_groups")             values: A sequence of objects to be serialized          Returns:             A generator of serialized string chunks Dynamically serializes a Python object based on its type.          Args:             key: A LookML field type (e.g. "suggestions" or "hidden")             value: A string, tuple, or list to serialize          Raises:             TypeError: If input value is not of a valid type          Returns:             A generator of serialized string chunks Serializes a dictionary to a LookML block.          Args:             key: A LookML field type (e.g. "dimension")             fields: A dictionary to serialize (e.g. {"sql": "${TABLE}.order_id"})             name: An optional name of the block (e.g. "order_id")          Returns:             A generator of serialized string chunks Serializes a sequence to a LookML block.          Args:             key: A LookML field type (e.g. "fields")             value: A sequence to serialize (e.g. ["orders.order_id", "orders.item"])          Returns:             A generator of serialized string chunks # `suggestions` is only quoted when it's a set, so override the default Serializes a key and value to a LookML pair.          Args:             key: A LookML field type (e.g. "hidden")             value: The value string (e.g. "yes")          Returns:             A generator of serialized string chunks Serializes a key to LookML.          Args:             key: A LookML field type (e.g. "sql")          Returns:             A generator of serialized string chunks Serializes a value to LookML, quoting it required by the key or forced.          Args:             key: A LookML field type (e.g. "hidden")             value: The value string (e.g. "yes")             force_quote: True if value should always be quoted          Returns:             A generator of serialized string chunks