# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#

import collections
import collections.abc
import datetime
import enum
import logging
from typing import (
    Any,
    Callable,
    Dict,
    Iterable,
    Iterator,
    Optional,
    Tuple,
    TypeVar,
    List,
)

from volatility3.framework import constants, exceptions, interfaces, objects

vollog = logging.getLogger(__name__)

SymbolSpaceReturnType = TypeVar(
    "SymbolSpaceReturnType",
    interfaces.objects.Template,
    interfaces.symbols.SymbolInterface,
    Dict[str, Any],
)


class SymbolType(enum.Enum):
    TYPE = 1
    SYMBOL = 2
    ENUM = 3


class SymbolSpace(interfaces.symbols.SymbolSpaceInterface):
    """Handles an ordered collection of SymbolTables.

    This collection is ordered so that resolution of symbols can proceed
    down through the ranks if a namespace isn't specified.
    """

    def __init__(self) -> None:
        super().__init__()
        self._dict: Dict[str, interfaces.symbols.BaseSymbolTableInterface] = (
            collections.OrderedDict()
        )
        # Permanently cache all resolved symbols
        self._resolved: Dict[str, interfaces.objects.Template] = {}
        self._resolved_symbols: Dict[str, interfaces.objects.Template] = {}

    def clear_symbol_cache(self, table_name: Optional[str] = None) -> None:
        """Clears the symbol cache for the specified table name. If no table
        name is specified, the caches of all symbol tables are cleared."""
        table_list: List[interfaces.symbols.BaseSymbolTableInterface] = []
        if table_name is None:
            table_list = list(self._dict.values())
        else:
            table_list.append(self._dict[table_name])
        for table in table_list:
            table.clear_symbol_cache()

    def free_table_name(self, prefix: str = "layer") -> str:
        """Returns an unused table name to ensure no collision occurs when
        inserting a symbol table."""
        count = 1
        while prefix + str(count) in self:
            count += 1
        return prefix + str(count)

    ### Symbol functions

    def get_symbols_by_type(self, type_name: str) -> Iterable[str]:
        """Returns all symbols based on the type of the symbol."""
        for table in self._dict:
            for symbol_name in self._dict[table].get_symbols_by_type(type_name):
                yield table + constants.BANG + symbol_name

    def get_symbols_by_location(
        self, offset: int, size: int = 0, table_name: Optional[str] = None
    ) -> Iterable[str]:
        """Returns all symbols that exist at a specific relative address."""
        table_list: Iterable[interfaces.symbols.BaseSymbolTableInterface] = (
            self._dict.values()
        )
        if table_name is not None:
            if table_name in self._dict:
                table_list = [self._dict[table_name]]
            else:
                table_list = []
        for table in table_list:
            for symbol_name in table.get_symbols_by_location(offset=offset, size=size):
                yield table.name + constants.BANG + symbol_name

    ### Space functions

    def __len__(self) -> int:
        """Returns the number of tables within the space."""
        return len(self._dict)

    def __getitem__(self, i: str) -> Any:
        """Returns a specific table from the space."""
        return self._dict[i]

    def __iter__(self) -> Iterator[str]:
        """Iterates through all available tables in the symbol space."""
        return iter(self._dict)

    def append(self, value: interfaces.symbols.BaseSymbolTableInterface) -> None:
        """Adds a symbol_list to the end of the space."""
        if not isinstance(value, interfaces.symbols.BaseSymbolTableInterface):
            raise TypeError(value)
        if value.name in self._dict:
            self.remove(value.name)
        self._dict[value.name] = value

    def remove(self, key: str) -> None:
        """Removes a named symbol_list from the space."""
        # Reset the resolved list, since we're removing some symbols
        self._resolved = {}
        del self._dict[key]

    def verify_table_versions(
        self,
        producer: str,
        validator: Callable[[Optional[Tuple], Optional[datetime.datetime]], bool],
        tables: Optional[List[str]] = None,
    ) -> bool:
        """Verifies the producer metadata and version of tables

        Args:
            producer: String name of a table producer to have validation performed
            validator: callable that takes an optional version and an optional datetime that returns False if table is invalid

        Returns:
            False if an invalid table was found or True if no invalid table was found
        """
        if tables is None:
            tables = self._dict.keys()
        for table_name in tables:
            table = self._dict[table_name]
            if not table.producer:
                vollog.debug(
                    f"Symbol table {table_name} could not be validated because no producer metadata was found"
                )
                continue
            if table.producer.name == producer:
                # Run the verification
                if not validator(
                    table.producer.version,
                    table.producer.datetime,
                ):
                    vollog.debug(f"Symbol table {table_name} does not pass validator")
                    return False
            else:
                continue
        return True

    ### Resolution functions

    class UnresolvedTemplate(objects.templates.ReferenceTemplate):
        """Class to highlight when missing symbols are present.

        This class is identical to a reference template, but differentiable by its classname.
        It will output a debug log to indicate when it has been instantiated and with what name.

        This class is designed to be output ONLY as part of the SymbolSpace resolution system.
        Individual SymbolTables that cannot resolve a symbol should still return a SymbolError to
        indicate this failure in resolution.
        """

        def __init__(self, type_name: str, **kwargs) -> None:
            vollog.debug(f"Unresolved reference: {type_name}")
            super().__init__(type_name=type_name, **kwargs)

    def _weak_resolve(
        self, resolve_type: SymbolType, name: str
    ) -> SymbolSpaceReturnType:
        """Takes a symbol name and resolves it with ReferentialTemplates."""
        if resolve_type == SymbolType.TYPE:
            get_function = "get_type"
        elif resolve_type == SymbolType.SYMBOL:
            get_function = "get_symbol"
        elif resolve_type == SymbolType.ENUM:
            get_function = "get_enumeration"
        else:
            raise TypeError("Weak_resolve called without a proper SymbolType")

        name_array = name.split(constants.BANG)
        if len(name_array) == 2:
            table_name = name_array[0]
            component_name = name_array[1]
            try:
                return getattr(self._dict[table_name], get_function)(component_name)
            except KeyError as e:
                raise exceptions.SymbolError(
                    component_name,
                    table_name,
                    f"Type {name} references missing Type/Symbol/Enum: {e}",
                )
        raise exceptions.SymbolError(name, None, f"Malformed name: {name}")

    def _iterative_resolve(self, traverse_list):
        """Iteratively resolves a type, populating linked child
        ReferenceTemplates with their properly resolved counterparts."""
        replacements = set()
        # Whole Symbols that still need traversing
        while traverse_list:
            template_traverse_list, traverse_list = (
                [self._resolved[traverse_list[0]]],
                traverse_list[1:],
            )
            # Traverse a single symbol looking for any ReferenceTemplate objects
            while template_traverse_list:
                traverser, template_traverse_list = (
                    template_traverse_list[0],
                    template_traverse_list[1:],
                )
                for child in traverser.children:
                    if isinstance(child, objects.templates.ReferenceTemplate):
                        # If we haven't seen it before, subresolve it and also add it
                        # to the "symbols that still need traversing" list
                        if child.vol.type_name not in self._resolved:
                            traverse_list.append(child.vol.type_name)
                            try:
                                self._resolved[child.vol.type_name] = (
                                    self._weak_resolve(
                                        SymbolType.TYPE, child.vol.type_name
                                    )
                                )
                            except exceptions.SymbolError:
                                self._resolved[child.vol.type_name] = (
                                    self.UnresolvedTemplate(child.vol.type_name)
                                )
                        # Stash the replacement
                        replacements.add((traverser, child))
                    elif child.children:
                        template_traverse_list.append(child)
        for parent, child in replacements:
            parent.replace_child(child, self._resolved[child.vol.type_name])

    def get_type(self, type_name: str) -> interfaces.objects.Template:
        """Takes a symbol name and resolves it.

        This method ensures that all referenced templates (including
        self-referential templates) are satisfied as ObjectTemplates
        """
        # Traverse down any resolutions
        if type_name not in self._resolved:
            self._resolved[type_name] = self._weak_resolve(SymbolType.TYPE, type_name)  # type: ignore
            self._iterative_resolve([type_name])
        if isinstance(self._resolved[type_name], objects.templates.ReferenceTemplate):
            table_name = None
            index = type_name.find(constants.BANG)
            if index > 0:
                table_name, type_name = type_name[:index], type_name[index + 1 :]
            raise exceptions.SymbolError(
                type_name, table_name, f"Unresolvable symbol requested: {type_name}"
            )
        return self._resolved[type_name]

    def get_symbol(self, symbol_name: str) -> interfaces.symbols.SymbolInterface:
        """Look-up a symbol name across all the contained symbol spaces."""
        retval = self._weak_resolve(SymbolType.SYMBOL, symbol_name)
        if symbol_name not in self._resolved_symbols and retval.type is not None:
            self._resolved_symbols[symbol_name] = self._subresolve(retval.type)
        if not isinstance(retval, interfaces.symbols.SymbolInterface):
            table_name = None
            index = symbol_name.find(constants.BANG)
            if index > 0:
                table_name, symbol_name = symbol_name[:index], symbol_name[index + 1 :]
            raise exceptions.SymbolError(
                symbol_name, table_name, f"Unresolvable Symbol: {symbol_name}"
            )
        return retval

    def _subresolve(
        self, object_template: interfaces.objects.Template
    ) -> interfaces.objects.Template:
        """Ensure an ObjectTemplate doesn't contain any ReferenceTemplates"""
        for child in object_template.children:
            if isinstance(child, objects.templates.ReferenceTemplate):
                new_child = self.get_type(child.vol.type_name)
            else:
                new_child = self._subresolve(child)
            object_template.replace_child(old_child=child, new_child=new_child)
        return object_template

    def get_enumeration(self, enum_name: str) -> interfaces.objects.Template:
        """Look-up a set of enumeration choices from a specific symbol
        table."""
        retval = self._weak_resolve(SymbolType.ENUM, enum_name)
        if not isinstance(retval, interfaces.objects.Template):
            table_name = None
            index = enum_name.find(constants.BANG)
            if index > 0:
                table_name, enum_name = enum_name[:index], enum_name[index + 1 :]
            raise exceptions.SymbolError(
                enum_name, table_name, f"Unresolvable Enumeration: {enum_name}"
            )
        return retval

    def _membership(self, member_type: SymbolType, name: str) -> bool:
        """Test for membership of a component within a table."""

        name_array = name.split(constants.BANG)
        if len(name_array) == 2:
            table_name = name_array[0]
            component_name = name_array[1]
        else:
            return False

        if table_name not in self:
            return False
        table = self[table_name]

        if member_type == SymbolType.TYPE:
            return component_name in table.types
        elif member_type == SymbolType.SYMBOL:
            return component_name in table.symbols
        elif member_type == SymbolType.ENUM:
            return component_name in table.enumerations
        return False

    def has_type(self, name: str) -> bool:
        return self._membership(SymbolType.TYPE, name)

    def has_symbol(self, name: str) -> bool:
        return self._membership(SymbolType.SYMBOL, name)

    def has_enumeration(self, name: str) -> bool:
        return self._membership(SymbolType.ENUM, name)


def symbol_table_is_64bit(
    context: interfaces.context.ContextInterface, symbol_table_name: str
) -> bool:
    """Returns a boolean as to whether a particular symbol table within a
    context is 64-bit or not."""
    return (
        context.symbol_space.get_type(
            symbol_table_name + constants.BANG + "pointer"
        ).size
        == 8
    )
