'''
Composer

Base for manually defining table compositions outside those natural to the schema
hierarchy (i.e., constructable by a `CO4.compose()` call).

Example: suppose we have a simple object hierarchy A(CO4) -> B -> C. C's in-built
`compose()` method may not always be desirable when constructing composite tables and
running related queries. In this case, a custom Composer can be used to make needed
composite tables easier to reference; in the case below, we define the "BC" composite
table.

```
class ExampleComposer(Composer):

    @register_table
    def BC(self):
        full_B = B.compose(full=True)
        full_C = C.compose(full=True)
        
        return full_B.join(
            full_C,
            full_B.c.name == full_C.c.name, # TODO: is this fine? or do we need base table refs
            outer=True
        )
'''
from pathlib import Path

from co3.component import Component


def register_table(table_name=None):
    '''
    Registry decorator for defined composer classes. Decorating a class method simply
    attaches a `table_name` attribute to it, setting it to either a provided value or the
    name of the method itself. Methods with a `table_name` attribute are later swept up at
    the class level and placed in the `table_map`.
    '''
    def decorator(func):
        if table_name is None:
            table_name = func.__name__
        func.table_name = table_name
        return func
    return decorator

class Composer[C: Component]:
    '''
    Base composer wrapper for table groupings.

    The schema is centered around a connected group of tables (via foreign keys). Thus,
    most operations need to be coordinated across tables. The `accessors` submodules
    are mostly intended to provide a "secondary layer" over the base set of tables in the
    schema, exposing common higher level table compositions (i.e., chained JOINs). See
    concrete instances (e.g., CoreAccess, FTSAccessor) for actual implementations these
    tables; the base class does not expose

    Tables in subclasses are registered with the `register_table` decorator, automatically
    indexing them under the provided name and making them available via the `table_map`.
    '''
    def __init__(self):
        self._set_tables()

    def _set_tables(self):
        '''
        Skip properties (so appropriate delays can be used), and 

        Set the table registry at the class level. This only takes place during the first
        instantiation of the class, and makes it possible to definitively tie methods to
        composed tables during lookup with `get_table()`.
        '''
        cls = self.__class__

        # in case the class has already be instantiated
        if hasattr(cls, 'table_map'): return

        table_map = {}
        for key, value in cls.__dict__.items():
            if isinstance(value, property):
                continue  # Skip properties
            if callable(value) and hasattr(value, 'table_name'):
                table_map[value.table_name] = value(self)

        cls.table_map = table_map

    def get_table(self, table_name):
        '''
        Retrieve the named table composition, if defined.
        '''
        return self.table_map.get(table_name)