# array_base.py
import numpy as np
import operator


class ArrayBase:

    def __init__(self, data):
        self.data = np.asarray(data)

    # ---- info ----

    @property
    def shape(self):
        return self.data.shape

    @property
    def ndim(self):
        return self.data.ndim

    # ---- representation ----

    def __repr__(self):
        return f"{self.__class__.__name__}({self.data})"
    
    def __str__(self):
        return str(self.data)

    # ---- helpers ----

    def _unwrap(self, other):
        return other.data if isinstance(other, ArrayBase) else other

    def _op(self, other, func):
        return self.__class__(func(self.data, self._unwrap(other)))

    def _rop(self, other, func):
        return self.__class__(func(self._unwrap(other), self.data))


# operator map
_ops = {
    "__add__": operator.add,
    "__sub__": operator.sub,
    "__mul__": operator.mul,
    "__truediv__": operator.truediv,
    "__matmul__": operator.matmul,
}

# reverse operators
_rops = {
    "__radd__": operator.add,
    "__rsub__": operator.sub,
    "__rmul__": operator.mul,
    "__rtruediv__": operator.truediv,
    "__rmatmul__": operator.matmul,
}

# in-place operators
_inplace_ops = {
    "__iadd__": operator.add,
    "__isub__": operator.sub,
    "__imul__": operator.mul,
    "__itruediv__": operator.truediv,
    "__imatmul__": operator.matmul,
}

# dynamically attach methods
for name, func in _ops.items():
    setattr(ArrayBase, name, lambda self, other, f=func: self._op(other, f))

for name, func in _rops.items():
    setattr(ArrayBase, name, lambda self, other, f=func: self._rop(other, f))

for name, func in _inplace_ops.items():
    setattr(ArrayBase, name, lambda self, other, f=func: self._op(other, f))