import pandas as pd
from abc import ABC, abstractmethod
from types import MappingProxyType
from typing_extensions import Self
from canopy.core.grid.spatial_axis import SpatialAxis, EmptyAxis

class Grid(ABC):

    _grid_type: str = "grid_abc"
    _xaxis: SpatialAxis = EmptyAxis
    _yaxis: SpatialAxis = EmptyAxis
    _xaxis_label: str = 'x1'
    _yaxis_label: str = 'x2'

    def __init__(self,
                 xaxis_gridop: str | None = None, yaxis_gridop: str | None = None,
                 ):

        self._gridops = {self.xaxis_key: xaxis_gridop, self.yaxis_key: yaxis_gridop}

    @property
    def grid_type(self):
        return self._grid_type

    @property
    def xaxis(self):
        return self._xaxis

    @property
    def yaxis(self):
        return self._yaxis

    @property
    def axes(self):
        return MappingProxyType({self._xaxis_key: self._xaxis, self._yaxis_key: self._yaxis})

    @property
    def gridops(self):
        return MappingProxyType(self._gridops)

    @property
    def axis_names(self):
        return list(self.axes.keys())

    @property
    def xaxis_key(self):
        return self._xaxis_key

    @property
    def yaxis_key(self):
        return self._yaxis_key

    def is_reduced(self, axis_key: str):

        if axis_key == 'both':
            axis_keys = self.axis_names
        else:
            axis_keys = [axis_key]

        reduced = []
        for k in axis_keys:
            try:
                reduced.append(self.gridops[k] is not None)
            except KeyError:
                raise KeyError(f"Grid of type '{self.grid_type}' does not have axis '{k}'.")

        return all(reduced)

    def __str__(self):
        return self.__repr__()
    
    def __repr__(self):
        repr_str = [f"Grid type: {self.grid_type}"]
        for axis_key in self.axis_names:
            if self.is_reduced(axis_key):
                repr_str.append(f"{axis_key} is reduced: [{self.gridops[axis_key]}]")
        return '\n'.join(repr_str)
    
    @classmethod
    @abstractmethod
    def from_frame(cls, df: pd.DataFrame) -> Self:
        pass

    @abstractmethod
    def validate_frame(self, df: pd.DataFrame) -> bool:
        pass

    @abstractmethod
    def crop(self, df: pd.DataFrame) -> 'Grid':
        pass

    @abstractmethod
    def reduce(self, gridop: str, axis_key: str) -> 'Grid':
        pass

    @abstractmethod
    def is_compatible(self, other: 'Grid') -> bool:
        pass

    @abstractmethod
    def __add__(self, other: 'Grid') -> 'Grid':
        pass

