from abc import abstractmethod, ABCMeta
import pandas as pd
import numpy as np

from pybrotlib import Telemetry


class Pointing(metaclass=ABCMeta):
    def __init__(self, telescope_params: list[str], model_params: list[str]):
        self.telescope_params = telescope_params
        self.model_params = model_params

    @abstractmethod
    def get_telescope_data(self, telemetry: Telemetry) -> dict[str, float]: ...

    @staticmethod
    def get_model(name: str):
        if name.lower() == "altaz":
            from .altazpointing import AltAzPointing

            return AltAzPointing()
        elif name.lower() == "hadec":
            from .hadecpointing import HaDecPointing

            return HaDecPointing()

        else:
            raise ValueError("Unknown pointing model")

    @abstractmethod
    def fit_model(self, data: pd.DataFrame, latitude: float) -> pd.DataFrame: ...


class PointingModel(metaclass=ABCMeta):
    def __init__(self, **kwargs):
        super(PointingModel, self).__setattr__("parameters", kwargs)

    @abstractmethod
    def __call__(self, x: float, y: float) -> float: ...

    def __getattr__(self, item: str) -> float:
        return self.parameters[item]

    def __setattr__(self, key: str, value: float):
        if key in self.parameters:
            self.parameters[key] = value
        else:
            raise KeyError

    def values(self):
        return self.parameters.values()

    def _fitfunc(self, p, x, y, off):
        for key in self.parameters.keys():
            self.parameters[key] = p[key]
        return self.__call__(x, y) - off

    def fit(self, x, y, off, verbose: bool = False):
        from lmfit import Parameters, minimize, report_fit

        params = Parameters()
        for key, val in self.parameters.items():
            params.add(key, value=val)
        out = minimize(self._fitfunc, params, args=(x, y, off))
        if verbose:
            report_fit(out)

        for key in self.parameters.keys():
            self.parameters[key] = out.params[key].value

    def rms(self, x, y, off) -> float:
        d = (self.__call__(x, y) - off) * 3600.0
        return np.sqrt(np.sum(np.power(d, 2)) / len(d))


__all__ = ["Pointing", "PointingModel"]
