import time
from time import sleep

import numpy as np
from epics import PV

from logzero import logger as log

from slic.core.adjustable import Adjustable, PVAdjustable, PVEnumAdjustable
from slic.core.scanner.scanbackend import wait_for_all  # , stop_all

import subprocess

UND_NAME_FMT = "SARUN{:02}-UIND030"
N_UND_CHIC = None

N_UNDS = list(range(3, 15 + 1))
# N_UNDS.remove(N_UND_CHIC) # does not exist

# offsets fro undulators, PSSS and DCCM
# convention: energy_device = energy_master - energy_offset_device
# readback_energy is energy_undulator + offset_undulator
# move the PSSS motor and the DCCM motor according to the energy_master
# TODO: improve this hack


### SETTINGS ####

PSSS_MOVE = False
DCCM_MOVE = False
TRAJECTORY_FEEDBACK_DISABLE_ENABLE = False
POINTING_FEEDFORWARD = False

energy_offset_undulators = 43 # eV
energy_offset_PSSS = 0 # eV
energy_offset_DCCM = 0 # eV

DCCM_RX2_angle_offset = 0.0617 # deg

################


def print_configuration():
    print(f"PSSS_MOVE = {PSSS_MOVE}")
    print(f"DCCM_MOVE = {DCCM_MOVE}")
    print(f"TRAJECTORY_FEEDBACK_DISABLE_ENABLE = {TRAJECTORY_FEEDBACK_DISABLE_ENABLE}")
    print(f"POINTING_FEEDFORWARD = {POINTING_FEEDFORWARD}")
    print(f"Undulator energy offset = {energy_offset_undulators}")
    print(f"PSSS energy offset = {energy_offset_PSSS}")
    print(f"DCCM energy offset = {energy_offset_DCCM}")
    # print(f"   DCCM RX2 energy offset = {DCCM_RX2_energy_offset}")
    print(f"   DCCM RX2 angle offset = {DCCM_RX2_angle_offset}")


def pointing_slope_X(energy):
    #delta_X_predicted = -0.0010097 * (energy - 10000)**2 + 0.002622 * (energy - 10000) - 0.3225 # function derived from calibration energy scan
    delta_X_predicted = (energy - 10000) * 10 # function derived from calibration energy scan
    #pointing_X_predicted = delta_X_predicted * 0.01829+ 0.009650
    pointing_X_predicted = delta_X_predicted / 113 * 4.5
    return pointing_X_predicted


def pointing_slope_Y(energy):
    #delta_Y_predicted = ...
    delta_Y_predicted = (energy - 10000) * 10 # function derived from calibration energy scan
    #pointing_Y_predicted = ...
    pointing_Y_predicted = delta_Y_predicted / 113 * 4.5
    return pointing_Y_predictedS


def set_PSSS_energy(energy: float):
    """When scanning the energy with the undulator we
    adjust the spectrometer to follow.
    """
    print(f"Adjusting PSSS to {energy}")
    PSSS_energy_PV_name = "SARFE10-PSSS059:ENERGY"
    PSSS_energy_PV = PV(PSSS_energy_PV_name)
    PSSS_energy_PV.put(energy, wait=True)

    ret = subprocess.run(["python", "/sf/photo/src/PSSS_motor/qt/PSSS_motion.py", "-s", "-m5", "SARFE10-PSSS059"])

    sleep(2)

    if ret.returncode != 0:
        log.warning("WARNING: PSSS adjustment failed.")
    else:
        print("Finished adjusting PSSS.")


def set_DCCM_energy(energy: float):
    """When scanning the energy with the undulator we
    adjust the monochromator to follow.
    """
    print(f"Adjusting DCCM to {energy}")

    DCCM_energy_PV_name = "SAROP31-ODCC110:MOT_ENY"
    DCCM_energy_PV = PV(DCCM_energy_PV_name)

#    DCCM_energy_offset_PV_name = "SAROP31-ODCC110:MOT_OFS"
#    DCCM_energy_offset_PV = PV(DCCM_energy_offset_PV_name)

#    DCCM_energy_offset_PV.put(DCCM_RX2_energy_offset, wait=True)
#    DCCM_energy_PV.put(energy, wait=False)
#    DCCM_energy_offset_PV.put(DCCM_RX2_energy_offset, wait=False)#

    DCCM_angle_offset_PV_name = "SAROP31-ODCC110:MOT_OFS"
    DCCM_angle_offset_PV = PV(DCCM_angle_offset_PV_name)

    DCCM_angle_offset_PV.put(DCCM_RX2_angle_offset, wait=True)
    DCCM_energy_PV.put(energy, wait=False)
    DCCM_angle_offset_PV.put(DCCM_RX2_angle_offset, wait=False)

    print(f"Finished adjusting DCCM.")

    sleep(0.2)


def enable_trajectory_feedback():
    print(f"Enabling trajectory FB")
    TrFB_PV_name = "SFB_POINTING_AR:ONOFF1"
    TrFB_PV = PV(TrFB_PV_name)
    TrFB_PV.put("ON")
    sleep(0.2)

def disable_trajectory_feedback():
    print(f"Disabling trajectory FB")
    TrFB_PV_name = "SFB_POINTING_AR:ONOFF1"
    TrFB_PV = PV(TrFB_PV_name)
    TrFB_PV.put("OFF")
    sleep(0.2)

    
def set_pointing(energy):
    print(f"Pointing correction")
    PointingSlopeX_PV_name = "SGE-HL-FPAR:X-SLOPE1"
    PointingSlopeY_PV_name = "SGE-HL-FPAR:Y-SLOPE1"
    PointingSlopeX_PV = PV(PointingSlopeX_PV_name)
    PointingSlopeY_PV = PV(PointingSlopeY_PV_name)

    PointingSlopeX = pointing_slope_X(energy)
    PointingSlopeY = pointing_slope_Y(energy)
    print(f"    X-slope = {PointingSlopeX} um")
    print(f"    Y-slope = {PointingSlopeY} um")

    PointingSlopeX_PV.put(PointingSlopeX)
    PointingSlopeY_PV.put(PointingSlopeY)
    sleep(0.2)



class PhotonEnergy(Adjustable):
    """
    for n_und_ref=None (default), the reference undulator currently used by the machine will be used
    """

    def __init__(
        self, n_unds=N_UNDS, n_und_ref=None, scaled=True, ID="ARAMIS_UNDULATORS", name="Cristallina photon energy", units="eV"
    ):
        #        # don't allow setting these since there's no chic :)
        #        chic_fudge_offset = 0
        #        adjust_chic = False

        super().__init__(ID, name=name, units=units)

        machine_n_und_ref = get_machine_n_und_ref()

        if n_und_ref is None:
            if machine_n_und_ref is None:
                raise ValueError(
                    f"could not read reference undulator currently used by the machine, please specify n_und_ref"
                )
            n_und_ref = machine_n_und_ref

        if n_und_ref != machine_n_und_ref:
            log.warning(
                f"the chosen reference undulator ({n_und_ref}) is not the reference undulator currently used by the machine ({machine_n_und_ref})"
            )

        n_unds = list(n_unds)

        if N_UND_CHIC in n_unds:
            log.warning(
                f"the CHIC ({N_UND_CHIC}) is in the list of active undulators: {n_unds}, and will be ignored/removed"
            )
            n_unds.remove(N_UND_CHIC)

        if n_und_ref not in n_unds:
            raise ValueError(f"the reference undulator ({n_und_ref}) is not in the list of active undulators: {n_unds}")

        self.n_unds = n_unds
        self.n_und_ref = n_und_ref

        self.und_names = und_names = [UND_NAME_FMT.format(n) for n in n_unds]
        self.und_name_cal = und_name_cal = UND_NAME_FMT.format(n_und_ref)

        self.adjs = {name: Undulator(name) for name in und_names}
        #        self.chic = CHIC(chic_fudge_offset, name, units)
        #        self.phases = Phases(n_unds)

        #        self.adjust_chic = adjust_chic
        self.scaled = scaled

        self.convert = ConverterEK()

        a = self.adjs[und_name_cal]
        self.scale = ScalerEK(a)

    def set_target_value(self, energy_master, hold=False):

        value = energy_master + energy_offset_undulators
        k = self.convert.K(value)

        if np.isnan(k):
            print("K is nan for", value)
            return
        print(f"{k} <- {value}")

        ks_current = [a.get_current_value(readback=False) for a in self.adjs.values()]

        if self.scaled:
            header = "scaled:   "
            ks_target = self.scale.K(value, ks_current)
        else:
            header = "all equal:"
            ks_target = k * np.ones_like(ks_current)

        print(header, ks_target)
        print()

        def change():


            if not DCCM_MOVE:
                print("no DCCM movement enabled")
            else:
                energy_DCCM = energy_master + energy_offset_DCCM
                set_DCCM_energy(energy_DCCM)

            if not TRAJECTORY_FEEDBACK_DISABLE_ENABLE:
                print("No trajectory feedback disabling performed.")
            else:
                disable_trajectory_feedback()

            print(f"Setting undulator energy to {value}")

            # TODO: replace by set_all_target_values_and_wait when print not needed anymore
            tasks = []
            for (name, a), k_old, k_new in zip(self.adjs.items(), ks_current, ks_target):
                delta = k_old - k_new
                print(f"{name}: {k_old}\t->\t{k_new}\t({delta})")
                if np.isnan(k_new):
                    print(f"{name} skipped since target K is nan")
                    continue
                t = a.set_target_value(k_new, hold=False)
                tasks.append(t)
            wait_for_all(tasks)

            #            # make sure new K values have been written TODO: needed?
            #            sleep(2) # check if this can be shortened back to 0.5

            #            # switching on radial motors ...
            #            wait_for_all([
            #                a.adj_radial_on.set_target_value(1, hold=False) for a in self.adjs.values()
            #            ])

            #            # press a few times
            #            for _ in range(3):
            #                # ... and pushing go to ensure proper movements
            #                wait_for_all([
            #                    a.adj_radial_go.set_target_value(1, hold=False) for a in self.adjs.values()
            #                ])
            #                sleep(0.2)

            #            # make sure the undulators finished moving TODO: needed?
            #            sleep(5)

            #            if self.adjust_chic:
            #                print("CHIC adjustment follows")
            ##                self.chic.set_target_value(value, hold=False).wait() #TODO: test whether an additional sleep is needed
            #                self.phases.set(value)
            #                print("CHIC adjustment done")
            #            else:
            #                print("CHIC adjustment skipped")

            # make sure the undulators and phases finished moving TODO: needed?
            sleep(5)

            if not PSSS_MOVE:
                print("no PSSS movement enabled")
            else:
                energy_PSSS = energy_master + energy_offset_PSSS
                print(f"Adjusting PSSS to {energy_PSSS}")
                set_PSSS_energy(energy_PSSS)

            if not POINTING_FEEDFORWARD:
                print("No pointing correction performed.")
            else:
                set_pointing(energy_master)


            if not TRAJECTORY_FEEDBACK_DISABLE_ENABLE:
                print("No trajectory feedback enabling performed.")
            else:
                enable_trajectory_feedback()



        return self._as_task(change, hold=hold)


    def get_current_value(self):
        n = self.und_name_cal
        a = self.adjs[n]
        k = a.get_current_value()
        energy = self.convert.E(k) - energy_offset_undulators

        #        all_ks = [a.get_current_value() for a in self.adjs.values()]
        #        checks = np.isclose(all_ks, k, rtol=0, atol=0.001)
        #        if not all(checks):
        #            print(f"Warning: Ks are not all close to {k}:")
        #            for name, k, chk in zip(self.adjs.keys(), all_ks, checks):
        #                if not chk:
        #                    print(name, k)

        return energy  # if we need to fudge the number to match the mono, do it here!


    def get_current_value_undulators_real(self):
        n = self.und_name_cal
        a = self.adjs[n]
        k = a.get_current_value()
        energy = self.convert.E(k)

        #        all_ks = [a.get_current_value() for a in self.adjs.values()]
        #        checks = np.isclose(all_ks, k, rtol=0, atol=0.001)
        #        if not all(checks):
        #            print(f"Warning: Ks are not all close to {k}:")
        #            for name, k, chk in zip(self.adjs.keys(), all_ks, checks):
        #                if not chk:
        #                    print(name, k)

        return energy  # if we need to fudge the number to match the mono, do it here!


    def get_current_value_DCCM(self):
    
        DCCM_energy_PV_name = "SAROP31-ODCC110:MOT_ENY"
        DCCM_energy_PV = PV(DCCM_energy_PV_name)

        energy = DCCM_energy_PV.get() - energy_offset_DCCM

        return energy

    def get_current_value_DCCM_real(self):
    
        DCCM_energy_PV_name = "SAROP31-ODCC110:MOT_ENY"
        DCCM_energy_PV = PV(DCCM_energy_PV_name)

        energy = DCCM_energy_PV.get()

        return energy


    def get_current_value_PSSS(self):
    
        PSSS_energy_PV_name = "SARFE10-PSSS059:ENERGY"
        PSSS_energy_PV = PV(PSSS_energy_PV_name)

        energy = PSSS_energy_PV.get() - energy_offset_PSSS

        return energy

    def get_current_value_PSSS_real(self):
    
        PSSS_energy_PV_name = "SARFE10-PSSS059:ENERGY"
        PSSS_energy_PV = PV(PSSS_energy_PV_name)

        energy = PSSS_energy_PV.get()

        return energy

    def print_energies_with_offset_correction(self):
        print("Real photon energies with offset correction")
        print(f"  Undulator = {self.get_current_value()} eV")
        print(f"  PSSS      = {self.get_current_value_PSSS()} eV")
        print(f"  DCCM      = {self.get_current_value_DCCM()} eV")

    def print_energies(self):
        print("Real photon energies")
        print(f"  Undulator = {self.get_current_value_undulators_real()} eV")
        print(f"  PSSS      = {self.get_current_value_PSSS_real()} eV")
        print(f"  DCCM      = {self.get_current_value_DCCM_real()} eV")


    def is_moving(self):
        return any(a.is_moving() for a in self.adjs)


class Undulator(PVAdjustable):
    def __init__(self, name, accuracy=0.0005):
        pvname_setvalue = name + ":K_SET"
        pvname_readback = pvname_setvalue  # name + ":K_READ" #TODO: there are no readback values?
        super().__init__(
            pvname_setvalue,
            pvname_readback=pvname_readback,
            accuracy=accuracy,
            active_move=True,
            name=name,
            internal=True,
        )
        self.adj_energy = PVAdjustable(name + ":FELPHOTENE", internal=True)

    #        self.adj_radial_on = PVAdjustable(name + ":RADIAL-ON", internal=True) #TODO: do not exist
    #        self.adj_radial_go = PVAdjustable(name + ":RADIAL-GO", internal=True) #TODO: do not exist
    #        self.adj_radial_on_proc = PVAdjustable(name + ":RADIAL-ON.PROC", internal=True)
    #        self.adj_radial_go_proc = PVAdjustable(name + ":RADIAL-GO.PROC", internal=True)

    @property
    def energy(self):
        return self.adj_energy.get_current_value() * 1000


class ConverterEK:
    h = 4.135667696e-15  # eV * s
    c = 299792458  # m / s
    lambda_u = 15e-3  # m
    const = 2 * h * c / lambda_u  # eV

    electron_rest_energy = 0.51099895  # MeV

    # was: pvname_electron_energy="SATCL01-MBND100:P-READ"
    # was: pvname_electron_energy="SATCB01:ENE-FILT-OP"
    def __init__(self, pvname_electron_energy="SARCL02-MBND100:P-READ"):  # S30CB13:ENE-FILT-OP
        self.pv_electron_energy = PV(pvname_electron_energy)

    def K(self, energy):
        f = self.get_factor()
        v = f / energy - 1
        return np.sqrt(2 * v)

    def E(self, k_value):
        f = self.get_factor()
        v = 1 + k_value**2 / 2
        return f / v

    def get_factor(self):
        return self.const * self.get_gamma_squared()

    def get_gamma_squared(self):
        electron_energy = self.pv_electron_energy.get()
        gamma = electron_energy / self.electron_rest_energy
        return gamma**2


class ScalerEK:
    def __init__(self, und_reference):
        self.und = und_reference

    def K(self, energy_target, K_current=None):
        if K_current is None:
            K_current = self.und.get_current_value()
        K_current = np.asarray(K_current)
        energy_current = self.und.energy
        energy_ratio = energy_current / energy_target
        K_target_squared = energy_ratio * (K_current**2 + 2) - 2
        return np.sqrt(K_target_squared)


# class CHIC(PVAdjustable):
#
#    def __init__(self, fudge_offset, name, units):
#        self.fudge_offset = fudge_offset
#        name += " CHIC Energy"
#        super().__init__("SATUN-CHIC:PHOTON-ENERGY", name=name)
#        self.pvs.start  = PV("SATUN-CHIC:APPLY-DELAY-OFFSET-PHASE")
#        self.units = units
#
#
#    def set_target_value(self, value, hold=False):
#        fudge_offset = self.fudge_offset
#        print("CHIC fudge offset is", fudge_offset)
#        value -= fudge_offset
#        value /= 1000
#
#        def change():
#            sleep(1)
#            print("CHIC setvalue")
#            self.pvs.setvalue.put(value, wait=True)
#            print("CHIC start")
#            self.pvs.start.put(1, wait=True)
#            #TODO: test whether an additional sleep is needed
#            sleep(1)
#
#        return self._as_task(change, hold=hold)
#
#
#    def get_current_value(self):
#        return super().get_current_value() * 1000


# class TwoColorChicane(PVAdjustable):
#
#    def __init__(self, name):#, t0=0):
##        self.t0 = t0
##        name += " Two Color Chicane"
#        super().__init__("SATUN14-MBND100:I-SET", "SATUN14-MBND100:I-READ", process_time=1, name=name)
#
##    def set_target_value(self, value, hold=False):
##        super().set_target_value(value)
#
##    def get_current_value(self):
##        return super().get_current_value()


# class Phases:
#
#    def __init__(self, n_unds=N_UNDS):
#        # 22 does not have a chicane
#        n_unds = n_unds.copy()
#        if 22 in n_unds:
#            n_unds.remove(22)
#
#        # 22 does not have a chicane
#        n_unds_all = N_UNDS.copy()
#        if 22 in n_unds_all:
#            n_unds_all.remove(22)
#
#        self.cb_auto_good = {i: PV(f"SATUN{i:02}-CHIC:AUTO-PHASING") for i in n_unds}
#        self.cb_auto_bad  = {i: PV(f"SATUN{i:02}-CHIC:AUTO-PHASING") for i in set(n_unds_all) - set(n_unds)}
#
#        self.pv_fixed_energy = PV("SATUN-CHIC:FIX_PHOTON_ENERGY")
#        self.pv_energy = PV("SATUN-CHIC:PHOTON-ENERGY")
#
#
#    def set(self, energy):
#        for cb in self.cb_auto_good.values(): cb.put(int(False))
#        for cb in self.cb_auto_bad.values():  cb.put(int(False))
#        sleep(0.1)
#
#        pv_fixed_energy = self.pv_fixed_energy
#
#        current_state = pv_fixed_energy.get()
#        pv_fixed_energy.put(int(True)) # enforce fixed energy
#
#        self.pv_energy.put(energy / 1000) # in keV
#        sleep(0.1)
#
#        for cb in self.cb_auto_good.values(): cb.put(int(True))
#        sleep(0.1)
#
#        for cb in self.cb_auto_good.values(): cb.put(int(False))
#        for cb in self.cb_auto_bad.values():  cb.put(int(False))
#        sleep(0.1)
#
##        pv_fixed_energy.put(current_state) # reset to original state


# class Phases:

#    def __init__(self, n_unds=N_UNDS):
#        # 22 does not have a chicane
#        n_unds = n_unds.copy()
#        if 22 in n_unds:
#            n_unds.remove(22)
#        self.pv_energy = PV("SATUN-CHIC:PHOTON-ENERGY")
#        self.pv_fixed_energy = PV("SATUN-CHIC:FIX_PHOTON_ENERGY")
#        self.adjs_apply = {i: PVAdjustable(f"SATUN{i:02}-CHIC:APPLY-DOP", internal=True) for i in n_unds}


#    def set(self, energy):
#        pv_energy = self.pv_energy
#        pv_fixed_energy = self.pv_fixed_energy

#        current_state = pv_fixed_energy.get()
#        pv_fixed_energy.put(int(True)) # enforce fixed energy

#        pv_energy.put(energy / 1000) # in keV

#        sleep(0.1)
#        self.update()
##        sleep(10)

#        pv_fixed_energy.put(current_state) # reset to original state


#    def update(self):
#        wait_for_all([
#            adj_apply.set_target_value(1) for adj_apply in self.adjs_apply.values()
#        ])


def get_machine_n_und_ref():

    for attempt in range(3):
        try:
            res = PVEnumAdjustable("SARUN:REF-UND").get()
            if not res.startswith("SARUN"):
                return None
        except AttributeError as e:
            print("Error getting undulator, retrying")
            print(e)
            time.sleep(1)
        else:
            break

    n = len("SARUN")
    res = res[n:]
    try:
        res = int(res)
    except ValueError:
        return None
    return res
