"""
LODCM: Large offset dual-crystal monochrometer.

The scope of this module is to identify where the beam is intended to go after
passing through H1N, to manipulate the diagnostics, and to remove H1N for the
downstream hutches.

It will not be possible to align the device with the class, and there will be
no readback into the device's alignment. This is intended for a future update.
"""
import functools
import logging
from typing import Union

import numpy as np
from lightpath import LightpathState
from ophyd.device import Component as Cpt
from ophyd.device import FormattedComponent as FCpt
from ophyd.signal import EpicsSignalRO
from ophyd.sim import NullStatus
from ophyd.status import wait as status_wait
from pcdscalc import common, diffraction

from pcdsdevices.epics_motor import OffsetIMSWithPreset, OffsetMotor
from pcdsdevices.sim import FastMotor

from .device import GroupDevice
from .doc_stubs import insert_remove
from .epics_motor import IMS
from .inout import InOutRecordPositioner
from .interface import BaseInterface, FltMvInterface, LightpathMixin
from .pseudopos import (PseudoPositioner, PseudoSingleInterface,
                        pseudo_position_argument, real_position_argument)
from .utils import get_status_float, get_status_value, schedule_task

logger = logging.getLogger(__name__)


class H1N(InOutRecordPositioner):
    states_list = ['OUT', 'C', 'Si']
    in_states = ['C', 'Si']
    _states_alias = {'C': 'IN'}
    _transmission = {'C': 0.8, 'Si': 0.7}


class YagLom(InOutRecordPositioner):
    states_list = ['OUT', 'YAG', 'SLIT1', 'SLIT2', 'SLIT3']
    in_states = ['YAG', 'SLIT1', 'SLIT2', 'SLIT3']
    _states_alias = {'YAG': 'IN'}


class Dectris(InOutRecordPositioner):
    states_list = ['OUT', 'DECTRIS', 'SLIT1', 'SLIT2', 'SLIT3', 'OUTLOW']
    in_states = ['DECTRIS', 'SLIT1', 'SLIT2', 'SLIT3']
    out_states = ['OUT', 'OUTLOW']
    _states_alias = {'DECTRIS': 'IN'}


class Diode(InOutRecordPositioner):
    states_list = ['OUT', 'IN']


class Foil(InOutRecordPositioner):
    states_list = ['OUT']
    in_states = []

    def __init__(self, prefix, *args, **kwargs):
        if 'XPP' in prefix:
            self.in_states = ['Mo', 'Zr', 'Zn', 'Cu', 'Ni', 'Fe', 'Ti']
        elif 'XCS' in prefix:
            self.in_states = ['Mo', 'Zr', 'Ge', 'Cu', 'Ni', 'Fe', 'Ti']
        self.states_list = ['OUT'] + self.in_states
        super().__init__(prefix, *args, **kwargs)


class CHI1(InOutRecordPositioner):
    """Rotation axis, it does not have an `OUT` state."""
    states_list = ['C', 'Si']
    in_states = ['C', 'Si']
    out_states = []


class CHI2(InOutRecordPositioner):
    """Rotation axis, it does not have an `OUT` state."""
    states_list = ['C', 'Si']
    in_states = ['C', 'Si']
    out_states = []


class H2N(InOutRecordPositioner):
    states_list = ['C', 'Si']
    in_states = ['C', 'Si']
    out_states = []


class Y1(InOutRecordPositioner):
    """Vertical y motion. Does not have an `OUT` state."""
    states_list = ['C', 'Si']
    in_states = ['C', 'Si']
    out_states = []


class Y2(InOutRecordPositioner):
    states_list = ['C', 'Si']
    in_states = ['C', 'Si']
    out_states = []


class CrystalTower1(BaseInterface, GroupDevice):
    """
    LODCM Crystal Tower 1.

    Has the Si and C crystals with 2 angles and 5 linear motions.
    The `h1n_state` brings the crystal into the beam, moving horizontally
    (normal to the beam and parallel to the ground). It is also the only axis
    that can fully clear the crystal assembly from the beam. If h1n state is
    `OUT` and if we were to insert into the beam, we would be aligned to the C
    crystal.
    The `y1_state` brings the crystal into the beam, moving vertically
    (normal to the beam and normal to the ground).
    The `chi1_state` rotates the crystal assembly.

    Parameters
    ----------
    prefix : str
        Epics base Pv prefix.
    name : str, keyword-only
        The name of this device.
    """
    # x, y, and z are on the base but not touched in normal operations
    z1 = FCpt(IMS, '{self._hutch_prefix}:MON:MMS:04',
              kind='normal', doc='LOM Xtal1 Z')
    x1 = FCpt(IMS, '{self._hutch_prefix}:MON:MMS:05',
              kind='normal', doc='LOM Xtal1 X')
    y1 = FCpt(IMS, '{self._hutch_prefix}:MON:MMS:06',
              kind='normal', doc='LOM Xtal1 Y')
    # theta movement
    th1 = FCpt(IMS, '{self._hutch_prefix}:MON:MMS:07',
               kind='normal', doc='LOM Xtal1 Theta')
    # chi movement
    chi1 = FCpt(IMS, '{self._hutch_prefix}:MON:MMS:08',
                kind='normal', doc='LOM Xtal1 Chi')
    # normal to the crystal surface movement
    h1n = FCpt(IMS, '{self._hutch_prefix}:MON:MMS:09',
               kind='normal', doc='LOM Xtal1 Hn')
    # paralell to the crystal surface movement
    h1p = FCpt(IMS, '{self._hutch_prefix}:MON:MMS:20',
               kind='normal', doc='LOM Xtal1 Hp')

    # states
    h1n_state = Cpt(H1N, ':H1N', kind='hinted')
    y1_state = Cpt(Y1, ':Y1', kind='normal')
    chi1_state = Cpt(CHI1, ':CHI1', kind='normal')

    # reflection pvs (3 1 1 1)
    diamond_reflection = Cpt(EpicsSignalRO, ':T1C:REF', kind='normal',
                             doc='Tower 1 Diamond crystal reflection')
    silicon_reflection = Cpt(EpicsSignalRO, ':T1Si:REF', kind='normal',
                             doc='Tower 1 Silicon crystal reflection')

    # motor offsets
    # the folowing are declared in Energy classes
    # th1_si, th1_c, z1_si, z1_c
    x1C = FCpt(OffsetMotor, prefix='{self._prefix}:X1:OFF_C',
               motor_prefix='{self._hutch_prefix}:MON:MMS:05', kind='normal',
               add_prefix=('prefix', 'motor_prefix'),
               name='x1_c', doc='X1 motor offset for C [mm]')
    x1Si = FCpt(OffsetMotor, prefix='{self._prefix}:X1:OFF_Si',
                motor_prefix='{self._hutch_prefix}:MON:MMS:05', kind='normal',
                add_prefix=('prefix', 'motor_prefix'),
                name='x1_si', doc='X1 motor offset for Si [mm]')
    y1C = FCpt(OffsetIMSWithPreset, prefix='{self._prefix}:Y1:OFF_C',
               motor_prefix='{self._hutch_prefix}:MON:MMS:06', kind='normal',
               add_prefix=('prefix', 'motor_prefix'),
               name='y1_c', doc='Y1 motor offset for C [mm]')
    y1Si = FCpt(OffsetIMSWithPreset, prefix='{self._prefix}:Y1:OFF_Si',
                motor_prefix='{self._hutch_prefix}:MON:MMS:06', kind='normal',
                add_prefix=('prefix', 'motor_prefix'),
                name='y1_si', doc='Y1 motor offset for Si [mm]')
    chi1C = FCpt(OffsetIMSWithPreset, prefix='{self._prefix}:CHI1:OFF_C',
                 motor_prefix='{self._hutch_prefix}:MON:MMS:08', kind='normal',
                 add_prefix=('prefix', 'motor_prefix'),
                 name='chi1_c ', doc='Chi 1 motor offset for C [deg]')
    chi1Si = FCpt(OffsetIMSWithPreset, prefix='{self._prefix}:CHI1:OFF_Si',
                  motor_prefix='{self._hutch_prefix}:MON:MMS:08',
                  kind='normal', add_prefix=('prefix', 'motor_prefix'),
                  name='chi1_si', doc='Chi 1 motor offset for Si [deg]')
    h1nC = FCpt(OffsetIMSWithPreset, prefix='{self._prefix}:H1N:OFF_C',
                motor_prefix='{self._hutch_prefix}:MON:MMS:09', kind='normal',
                add_prefix=('prefix', 'motor_prefix'),
                name='', doc='H1n motor offset for C [mm]')
    h1nSi = FCpt(OffsetIMSWithPreset, prefix='{self._prefix}:H1N:OFF_Si',
                 motor_prefix='{self._hutch_prefix}:MON:MMS:09', kind='normal',
                 add_prefix=('prefix', 'motor_prefix'),
                 name='h1n_si', doc='H1n motor offset for Si [mm]')
    h1pC = FCpt(OffsetMotor, prefix='{self._prefix}:H1P:OFF_C',
                motor_prefix='{self._hutch_prefix}:MON:MMS:20', kind='normal',
                add_prefix=('prefix', 'motor_prefix'),
                name='h1p_c', doc='H1p motor offset for C [mm]')
    h1pSi = FCpt(OffsetMotor, prefix='{self._prefix}:H1P:OFF_Si',
                 motor_prefix='{self._hutch_prefix}:MON:MMS:20', kind='normal',
                 add_prefix=('prefix', 'motor_prefix'),
                 name='h1p_si', doc='H1p motor offset for Si [mm]')

    tab_component_names = True
    tab_whitelist = ['is_diamond', 'is_silicon', 'get_reflection',
                     'get_material']

    def __init__(self, prefix, *args, **kwargs):
        self._hutch_prefix = ''
        self._prefix = prefix
        if 'XPP' in prefix:
            self._hutch_prefix = 'XPP'
        elif 'XCS' in prefix:
            self._hutch_prefix = 'HFX'
        super().__init__(prefix, *args, **kwargs)

    def is_diamond(self):
        """Check if tower 1 is with Diamond (C) material."""
        return ((self.h1n_state.position == 'C' or
                 self.h1n_state.position == 'OUT') and
                self.y1_state.position == 'C' and
                self.chi1_state.position == 'C')

    def is_silicon(self):
        """Check if tower 1 is with Silicon (Si) material."""
        return ((self.h1n_state.position == 'Si' or
                 self.h1n_state.position == 'OUT') and
                self.y1_state.position == 'Si' and
                self.chi1_state.position == 'Si')

    def get_reflection(self):
        """
        Get crystal's reflection.

        Tries to get the reflection depending on the material in use.

        Returns
        -------
        reflection : tuple

        Raises
        ------
        ValueError
            When cannot determine the reflection.
        """
        reflection = None
        if self.is_diamond():
            reflection = self.diamond_reflection.get()
        elif self.is_silicon():
            reflection = self.silicon_reflection.get()

        if reflection is not None:
            return tuple(reflection)
        raise ValueError('Unable to determine the crystal reflection')

    def get_material(self):
        """
        Get the current material.

        Returns
        -------
        material : str
            Material of the crystal.

        Raises
        ------
        ValueError
            When the material could not be determined or is something else
             other than `Si` or `C`.
        """
        if self.is_diamond():
            return 'C'
        elif self.is_silicon():
            return 'Si'
        else:
            raise ValueError(
                "Unable to determine crystal material for Tower 1")

    def format_status_info(self, status_info):
        """Override status info handler to render the crystal tower 1."""
        t1_state = get_status_value(
            status_info, 'h1n_state', 'position')
        state = f'h1n_state: {t1_state}'
        hutch = ''
        if 'XPP' in self.prefix:
            hutch = 'XPP '
        elif 'XCS' in self.prefix:
            hutch = 'XCS '

        try:
            material = self.get_material()
        except Exception:
            material = 'Unknown'

        if material == 'C':
            configuration = 'Diamond'
        elif material == 'Si':
            configuration = 'Silicon'
        else:
            configuration = 'Unknown'

        try:
            ref = self.get_reflection()
            ref = ''.join(map(str, ref))
        except Exception:
            ref = 'Unknown'
        # tower 1
        z_units = get_status_value(
            status_info, 'z1', 'user_setpoint', 'units')
        z_user = get_status_float(
            status_info, 'z1', 'position')
        z_dial = get_status_float(
            status_info, 'z1', 'dial_position', 'value')

        x_units = get_status_value(
            status_info, 'x1', 'user_setpoint', 'units')
        x_user = get_status_float(status_info, 'tower1', 'x1', 'position')
        x_dial = get_status_float(
            status_info, 'x1', 'dial_position', 'value')

        th_units = get_status_value(
            status_info, 'th1', 'user_setpoint', 'units')
        th_user = get_status_float(
            status_info, 'th1', 'position')
        th_dial = get_status_float(
            status_info, 'th1', 'dial_position', 'value')

        chi_units = get_status_value(
            status_info, 'chi1', 'user_setpoint', 'units')
        chi_user = get_status_float(
            status_info, 'chi1', 'position')
        chi_dial = get_status_float(
            status_info, 'chi1', 'dial_position', 'value')

        y_units = get_status_value(
            status_info, 'y1', 'user_setpoint', 'units')
        y_user = get_status_float(
            status_info, 'y1', 'position')
        y_dial = get_status_float(
            status_info, 'y1', 'dial_position', 'value')

        hn_units = get_status_value(
            status_info, 'h1n', 'user_setpoint', 'units')
        hn_user = get_status_float(
            status_info, 'h1n', 'position')
        hn_dial = get_status_float(
            status_info, 'h1n', 'dial_position', 'value')

        hp_units = get_status_value(
            status_info, 'h1p', 'user_setpoint', 'units')
        hp_user = get_status_float(
            status_info, 'h1p', 'position')
        hp_dial = get_status_float(
            status_info, 'h1p', 'dial_position', 'value')

        return f"""\
{hutch}LODCM Crystal Tower 1
{state}
Current Configuration: {configuration} ({ref})
z [{z_units}]  {z_user} ({z_dial})
x [{x_units}]  {x_user} ({x_dial})
th [{th_units}]  {th_user} ({th_dial})
chi [{chi_units}]  {chi_user} ({chi_dial})
y [{y_units}]  {y_user} ({y_dial})
hn [{hn_units}]  {hn_user} ({hn_dial})
hp [{hp_units}]  {hp_user} ({hp_dial})
"""


class CrystalTower2(BaseInterface, GroupDevice):
    """
    LODCM Crystal Tower 2.

    Has the second Si and C crystals and a diode behind the crystals.

    Parameters
    ----------
    prefix : str
        Epics base Pv prefix.
    name : str
        The name of this device.
    """
    # x, y, and z are on the base but not touched in normal operations
    z2 = FCpt(IMS, '{self._hutch_prefix}:MON:MMS:10',
              kind='normal', doc='LOM Xtal2 Z')
    x2 = FCpt(IMS, '{self._hutch_prefix}:MON:MMS:11',
              kind='normal', doc='LOM Xtal2 X')
    y2 = FCpt(IMS, '{self._hutch_prefix}:MON:MMS:12',
              kind='normal', doc='LOM Xtal2 Y')
    # thata movement
    th2 = FCpt(IMS, '{self._hutch_prefix}:MON:MMS:13',
               kind='normal', doc='LOM Xtal2 Theta')
    # chi movement
    chi2 = FCpt(IMS, '{self._hutch_prefix}:MON:MMS:14',
                kind='normal', doc='LOM Xtal2 Chi')
    # normal to the crystal surface movement
    h2n = FCpt(IMS, '{self._hutch_prefix}:MON:MMS:15',
               kind='normal', doc='LOM Xtal2 Hn')
    # in the DAQ for scanning in python, only used for commissioning
    diode2 = FCpt(IMS, '{self._hutch_prefix}:MON:MMS:21',
                  kind='normal', doc='LOM Xtal2 PIPS')

    x2_retry_deadband = FCpt(EpicsSignalRO,
                             '{self._hutch_prefix}:MON:MMS:11.RDBD',
                             doc='Retry Deadband for x2', kind='omitted')
    z2_retry_deadband = FCpt(EpicsSignalRO,
                             '{self._hutch_prefix}:MON:MMS:10.RDBD',
                             doc='Retry Deadband for z2', kind='omitted')

    # states
    h2n_state = Cpt(H2N, ':H2N', kind='hinted')
    y2_state = Cpt(Y2, ':Y2', kind='normal')
    chi2_state = Cpt(CHI2, ':CHI2', kind='normal')

    # reflection pvs
    diamond_reflection = Cpt(EpicsSignalRO, ':T2C:REF', kind='normal',
                             doc='Tower 2 Diamond crystal reflection')
    silicon_reflection = Cpt(EpicsSignalRO, ':T2Si:REF', kind='normal',
                             doc='Tower 2 Silicon crystal reflection')

    # motor offsets
    # the following ones are declared in the Energy classes
    # th2_si, th2_c, z2_si, z2_c
    x2C = FCpt(OffsetMotor, prefix='{self._prefix}:X2:OFF_C', name='x2_c ',
               motor_prefix='{self._hutch_prefix}:MON:MMS:11',
               add_prefix=('prefix', 'motor_prefix'), kind='normal',
               doc='X2 motor offset for C [mm]')
    x2Si = FCpt(OffsetMotor, prefix='{self._prefix}:X2:OFF_Si', name='x2_si',
                motor_prefix='{self._hutch_prefix}:MON:MMS:11',
                add_prefix=('prefix', 'motor_prefix'), kind='normal',
                doc='X2 motor offset for Si [mm]')
    y2C = FCpt(OffsetIMSWithPreset, prefix='{self._prefix}:Y2:OFF_C',
               name='y2_c', motor_prefix='{self._hutch_prefix}:MON:MMS:12',
               add_prefix=('prefix', 'motor_prefix'), kind='normal',
               doc='Y2 motor offset for C [mm]')
    y2Si = FCpt(OffsetIMSWithPreset, prefix='{self._prefix}:Y2:OFF_Si',
                name='y2_si', motor_prefix='{self._hutch_prefix}:MON:MMS:12',
                add_prefix=('prefix', 'motor_prefix'), kind='normal',
                doc='Y2 motor offset for Si [mm]')
    chi2C = FCpt(OffsetIMSWithPreset, prefix='{self._prefix}:CHI2:OFF_C',
                 name='chi2_c', motor_prefix='{self._hutch_prefix}:MON:MMS:14',
                 add_prefix=('prefix', 'motor_prefix'), kind='normal',
                 doc='Chi 2 motor offset for C [deg]')
    chi2Si = FCpt(OffsetIMSWithPreset, prefix='{self._prefix}:CHI2:OFF_Si',
                  name='chi2_si',
                  motor_prefix='{self._hutch_prefix}:MON:MMS:14',
                  add_prefix=('prefix', 'motor_prefix'), kind='normal',
                  doc='Chi 2 motor offset for Si [deg]')
    h2nC = FCpt(OffsetMotor, prefix='{self._prefix}:H2N:OFF_C',
                name='h2n_c', motor_prefix='{self._hutch_prefix}:MON:MMS:15',
                add_prefix=('prefix', 'motor_prefix'), kind='normal',
                doc=' H2n motor offset for C [mm]')
    h2nSi = FCpt(OffsetMotor, prefix='{self._prefix}:H2N:OFF_Si',
                 name='h2n_si', motor_prefix='{self._hutch_prefix}:MON:MMS:15',
                 add_prefix=('prefix', 'motor_prefix'), kind='normal',
                 doc='H2n motor offset for Si [mm]')

    tab_component_names = True
    tab_whitelist = ['is_diamond', 'is_silicon', 'get_reflection',
                     'get_material']

    def __init__(self, prefix, *args, **kwargs):
        self._hutch_prefix = ''
        self._prefix = prefix
        if 'XPP' in prefix:
            self._hutch_prefix = 'XPP'
        elif 'XCS' in prefix:
            self._hutch_prefix = 'HFX'
        super().__init__(prefix, *args, **kwargs)

    def is_diamond(self):
        """Check if tower 2 is with Diamond (C) material."""
        return (self.h2n_state.position == 'C' and
                self.y2_state.position == 'C' and
                self.chi2_state.position == 'C')

    def is_silicon(self):
        """Check if tower 2 is with Silicon (Si) material."""
        return (self.h2n_state.position == 'Si' and
                self.y2_state.position == 'Si' and
                self.chi2_state.position == 'Si')

    def get_reflection(self):
        """
        Get crystal's reflection.

        Tries to get the reflection depending on the material in use.

        Returns
        -------
        reflection : tuple

        Raises
        ------
        ValueError
            When cannot determine the reflection.
        """
        reflection = None
        if self.is_diamond():
            reflection = self.diamond_reflection.get()
        elif self.is_silicon():
            reflection = self.silicon_reflection.get()

        if reflection is not None:
            return tuple(reflection)
        raise ValueError('Unable to determine the crystal reflection')

    def get_material(self):
        """
        Get the current material.

        Returns
        -------
        material : str
            Material of the crystal.

        Raises
        ------
        ValueError
            When the material could not be determined or is something else
             other than `Si` or `C`.
        """
        if self.is_diamond():
            return 'C'
        elif self.is_silicon():
            return 'Si'
        else:
            raise ValueError(
                "Unable to determine crystal material for Tower 2")

    def format_status_info(self, status_info):
        """Override status info handler to render the crystal tower 2."""
        t2_state = get_status_value(
            status_info, 'h2n_state', 'position')
        state = f'h2n_state: {t2_state}'
        hutch = ''
        if 'XPP' in self.prefix:
            hutch = 'XPP '
        elif 'XCS' in self.prefix:
            hutch = 'XCS '

        try:
            material = self.get_material()
        except Exception:
            material = 'Unknown'

        if material == 'C':
            configuration = 'Diamond'
        elif material == 'Si':
            configuration = 'Silicon'
        else:
            configuration = 'Unknown'

        try:
            ref = self.get_reflection()
            ref = ''.join(map(str, ref))
        except Exception:
            ref = 'Unknown'
        # tower 2
        z_units = get_status_value(
            status_info, 'z2', 'user_setpoint', 'units')
        z_user = get_status_float(
            status_info, 'z2', 'position')
        z_dial = get_status_float(
            status_info, 'z2', 'dial_position', 'value')

        x_units = get_status_value(
            status_info, 'x2', 'user_setpoint', 'units')
        x_user = get_status_float(
            status_info, 'x2', 'position')
        x_dial = get_status_float(
            status_info, 'x2', 'dial_position', 'value')

        th_units = get_status_value(
            status_info, 'th2', 'user_setpoint', 'units')
        th_user = get_status_float(
            status_info, 'th2', 'position')
        th_dial = get_status_float(
            status_info, 'th2', 'dial_position', 'value')

        chi_units = get_status_value(
            status_info, 'chi2', 'user_setpoint', 'units')
        chi_user = get_status_float(
            status_info, 'chi2', 'position')
        chi_dial = get_status_float(
            status_info, 'chi2', 'dial_position', 'value')

        y_units = get_status_value(
            status_info, 'y2', 'user_setpoint', 'units')
        y_user = get_status_float(
            status_info, 'y2', 'position')
        y_dial = get_status_float(
            status_info, 'y2', 'dial_position', 'value')

        hn_units = get_status_value(
            status_info, 'h2n', 'user_setpoint', 'units')
        hn_user = get_status_float(
            status_info, 'h2n', 'position')
        hn_dial = get_status_float(
            status_info, 'h2n', 'dial_position', 'value')

        diode_units = get_status_value(
            status_info, 'diode2', 'user_setpoint', 'units')
        diode_user = get_status_float(
            status_info, 'diode2', 'position')
        diode_dial = get_status_float(
            status_info, 'diode2', 'dial_position', 'value')

        return f"""\
{hutch}LODCM Crystal Tower 2
{state}
Current Configuration: {configuration} ({ref})
z [{z_units}]  {z_user} ({z_dial})
x [{x_units}]  {x_user} ({x_dial})
th [{th_units}]  {th_user} ({th_dial})
chi [{chi_units}]  {chi_user} ({chi_dial})
y [{y_units}]  {y_user} ({y_dial})
hn [{hn_units}]  {hn_user} ({hn_dial})
diode [{diode_units}]  {diode_user} ({diode_dial})
"""


class DiagnosticsTower(BaseInterface, GroupDevice):
    """
    LODCM Diagnostic Tower.

    Located midway between Crystal Tower 1 and Crystal Tower 2, in the center
    of rotation of the device.
    It contains a pips diode(dd), yag screen(dv), filters wheel (df)
    and vertical (dh) and horizontal slits (dd).

    Parameters
    ----------
    prefix : str
        Epics base PV prefix.
    name : str
        The name of this device.
    """
    # Located midway between T1 and T2 in the center of rotation of the device.
    # horizontal slits
    dh = FCpt(IMS, '{self._hutch_prefix}:MON:MMS:16',
              kind='normal', doc='LOM Dia H')
    # vertical slits
    dv = FCpt(IMS, '{self._hutch_prefix}:MON:MMS:17',
              kind='normal', doc='LOM Dia V')
    dr = FCpt(IMS, '{self._hutch_prefix}:MON:MMS:19',
              kind='normal', doc='LOM Dia Theta')
    # filters wheel
    df = FCpt(IMS, '{self._hutch_prefix}:MON:MMS:{self._df_suffix}',
              kind='normal', doc='LOM Dia Filter Wheel')
    # pips diode
    dd = FCpt(IMS, '{self._hutch_prefix}:MON:MMS:18',
              kind='normal', doc='LOM Dia PIPS')
    # yag screen
    yag_zoom = FCpt(IMS, '{self._hutch_prefix}:MON:CLZ:01',
                    kind='normal', doc='LOM Zoom')

    tab_component_names = True

    def __init__(self, prefix, *args, **kwargs):
        # The df component has a different PV suffix in `XPP` vs `XCS`
        # XPP:MON:MMS:27 vs HFX:MON:MMS:22
        self._hutch_prefix = ''
        self._df_suffix = '27'
        if 'XPP' in prefix:
            self._hutch_prefix = 'XPP'
        elif 'XCS' in prefix:
            self._hutch_prefix = 'HFX'
            self._df_suffix = '22'

        super().__init__(prefix, *args, **kwargs)

    def format_status_info(self, status_info):
        """Override status info handler to render the diagnostics tower."""
        hutch = ''
        if 'XPP' in self.prefix:
            hutch = 'XPP '
        elif 'XCS' in self.prefix:
            hutch = 'XCS '
        # diagnostics
        dh_units = get_status_value(
            status_info, 'dh', 'user_setpoint', 'units')
        dh_user = get_status_float(
            status_info, 'dh', 'position')
        dh_dial = get_status_float(
            status_info, 'dh', 'dial_position', 'value')

        dv_units = get_status_value(
            status_info, 'dv', 'user_setpoint', 'units')
        dv_user = get_status_float(
            status_info, 'dv', 'position')
        dv_dial = get_status_float(
            status_info, 'dv', 'dial_position', 'value')

        dr_units = get_status_value(
            status_info, 'dr', 'user_setpoint', 'units')
        dr_user = get_status_float(
            status_info, 'dr', 'position')
        dr_dial = get_status_float(
            status_info, 'dr', 'dial_position', 'value')

        df_units = get_status_value(
            status_info, 'df', 'user_setpoint', 'units')
        df_user = get_status_float(
            status_info, 'df', 'position')
        df_dial = get_status_float(
            status_info, 'df', 'dial_position', 'value')

        dd_units = get_status_value(
            status_info, 'dd', 'user_setpoint', 'units')
        dd_user = get_status_float(
            status_info, 'dd', 'position')
        dd_dial = get_status_float(
            status_info, 'dd', 'dial_position', 'value')

        yag_zoom_units = get_status_value(status_info, 'yag_zoom',
                                          'user_setpoint', 'units')
        yag_zoom_user = get_status_float(status_info, 'yag_zoom', 'position',
                                         precision=0)
        yag_zoom_dial = get_status_float(status_info, 'yag_zoom',
                                         'dial_position', 'value', precision=0)
        return f"""\
{hutch}Diagnostics Tower
diag r [{dr_units}]  {dr_user} ({dr_dial})
diag h [{dh_units}]  {dh_user} ({dh_dial})
diag v [{dv_units}]  {dv_user} ({dv_dial})
filter [{df_units}]  {df_user} ({df_dial})
diode [{dd_units}]  {dd_user} ({dd_dial})
navitar zoom [{yag_zoom_units}]  {yag_zoom_user} ({yag_zoom_dial})
"""


class LODCMEnergySi(FltMvInterface, PseudoPositioner, GroupDevice):
    """
    Energy calculations for the Si material.

    Assume material is 'Si' without checking if the crystal's materials are
    aligned for both towers.

    Parameters
    ----------
    prefix : str
        Epics base PV prefix.
    name : str
        The name of this device.
    """
    tower1 = FCpt(CrystalTower1, '{self._prefix}', kind='normal')
    tower2 = FCpt(CrystalTower2, '{self._prefix}', kind='normal')
    dr = FCpt(IMS, '{self._hutch_prefix}:MON:MMS:19',
              kind='normal', doc='LOM Dia Theta')

    th1Si = FCpt(OffsetMotor, prefix='{self._prefix}:TH1:OFF_Si',
                 motor_prefix='{self._hutch_prefix}:MON:MMS:07',
                 add_prefix=('prefix', 'motor_prefix'), name='th1_si',
                 doc='Th1 motor offset for Si [deg]')
    th2Si = FCpt(OffsetMotor, prefix='{self._prefix}:TH2:OFF_Si',
                 motor_prefix='{self._hutch_prefix}:MON:MMS:13',
                 add_prefix=('prefix', 'motor_prefix'), name='th2_si',
                 doc='Th2 motor offset for Si [deg]')
    z1Si = FCpt(OffsetMotor, prefix='{self._prefix}:Z1:OFF_Si',
                name='z1_si',
                motor_prefix='{self._hutch_prefix}:MON:MMS:04',
                add_prefix=('prefix', 'motor_prefix'),
                doc='Z1 motor offset for Si [mm]')
    z2Si = FCpt(OffsetMotor, prefix='{self._prefix}:Z2:OFF_Si',
                name='z1_si',
                motor_prefix='{self._hutch_prefix}:MON:MMS:10',
                add_prefix=('prefix', 'motor_prefix'),
                doc='Z2 motor offset for Si [mm]')

    energy = Cpt(PseudoSingleInterface, egu='keV', kind='hinted')

    stage_group = [dr, th1Si, th2Si, z1Si, z2Si]

    def __init__(self, prefix, *args, **kwargs):
        self._prefix = prefix
        self._hutch_prefix = ''
        if 'XPP' in self._prefix:
            self._hutch_prefix = 'XPP'
        elif 'XCS' in self._prefix:
            self._hutch_prefix = 'HFX'

        super().__init__(prefix=prefix, *args, **kwargs)

    def get_reflection(self):
        """
        Get the crystal reflection.

        Check both towers, and compare the if they match.
        If they do not match an error will be raised.

        Returns
        -------
        ref_1 : tuple
            Reflection of the two Crystal Towers.

        Raises
        ------
        ValueError
            When the reflection of first tower does not match the one of
            second tower.
        """
        ref_1 = self.tower1.get_reflection()
        ref_2 = self.tower2.get_reflection()
        if ref_1 != ref_2:
            logger.warning('Crystals do not match: c1: %s, c2: %s',
                           ref_1, ref_2)
            raise ValueError('Invalid Crystal Arrangement')
        return ref_1

    def get_energy(self, material='Si', reflection=None):
        """
        Get photon energy from first tower in keV.

        Energy is determined by the first crystal (Theta motor).

        Parameters
        ----------
        material : str, optional
            Chemical formula. E.g.: `Si`
        reflection : tuple, optional
            Reflection of material. E.g.: `(1, 1, 1)`

        Returns
        -------
        energy : number
            Photon energy in keV.
        """
        reflection = reflection or self.get_reflection()
        th = self.th1Si.wm()
        length = (2 * np.sin(np.deg2rad(th)) *
                  diffraction.d_space(material, reflection))
        return common.wavelength_to_energy(length) / 1000

    def calc_geometry(self, energy, material='Si', reflection=None):
        """
        Calculate the lom geometry.

        Parameters
        ----------
        material : str, optional
            Chemical formula. E.g.: `Si`
        reflection : tuple, optional
            Reflection of material. E.g.: `(1, 1, 1)`

        Returns
        -------
        th, z : tuple
            Returns `theta` in degrees and `zm` TODO: what is this?
        """
        reflection = reflection or self.get_reflection()
        th, z = diffraction.get_lom_geometry(energy*1e3, material, reflection)
        return (th, z)

    @pseudo_position_argument
    def forward(self, pseudo_pos):
        """
        Calculate a RealPosition from a given PseudoPosition.

        If the pseudo positioner is here at `pseudo_pos`, then this is where
        my real motor should be.

        Parameters
        ----------
        pseudo_pos : PseudoPosition
            The pseudo position input, a namedtuple.

        Returns
        -------
        real_pos : RealPosition
            The real position output, a namedtuple.
        """
        pseudo_pos = self.PseudoPosition(*pseudo_pos)

        th, z = self.calc_geometry(energy=pseudo_pos.energy)

        return self.RealPosition(th1Si=th,
                                 th2Si=th,
                                 z1Si=-z,
                                 z2Si=z,
                                 dr=2*th)

    @real_position_argument
    def inverse(self, real_pos):
        """
        Calculate a PseudoPosition from a given RealPosition.

        If the real motor is at this `real_pos`, then this is where my pseudo
        position should be.

        Parameters
        ----------
        real_pos : RealPosition
            The real position input.

        Returns
        -------
        pseudo_pos : PseudoPosition
            The pseudo position output.
        """
        try:
            reflection = self.get_reflection()
        except Exception:
            return self.PseudoPosition(energy=np.NaN)
        real_pos = self.RealPosition(*real_pos)
        length = (2 * np.sin(np.deg2rad(real_pos.th1Si))
                    * diffraction.d_space('Si', reflection))
        if length == 0:
            # don't bother transforming this
            # TODO maybe catch error in common.wave.. when send 0
            return 0
        energy = common.wavelength_to_energy(length) / 1000
        return self.PseudoPosition(energy=energy)

    def setE(self, energy):
        self.set_current_position(energy)
        return

    def status_info(self):
        """Overwrites interface status_info to make things faster."""
        return None

    def format_status_info(self, status_info):
        """Override status info handler to render the energy si."""
        hutch = ''
        if 'XPP' in self.prefix:
            hutch = 'XPP '
        elif 'XCS' in self.prefix:
            hutch = 'XCS '

        try:
            material = self.get_material()
        except Exception:
            material = 'Unknown'

        if material == 'C':
            configuration = 'Diamond'
        elif material == 'Si':
            configuration = 'Silicon'
        else:
            configuration = 'Unknown'

        try:
            energy = self.get_energy()
            energy = f"{energy:.4f}"
        except Exception:
            energy = 'Unknown'

        try:
            ref = self.get_reflection()
            ref = ''.join(map(str, ref))
        except Exception:
            ref = 'Unknown'

        return f"""\
{hutch}LODCM Energy Si
Current Configuration: {configuration} ({ref})
Photon Energy: {energy} [keV]
"""


class LODCMEnergyC(FltMvInterface, PseudoPositioner, GroupDevice):
    """
    Energy calculations for the C material.

    Assume material is 'C' without checking if the crystal's materials are
    aligned for both towers.

    Parameters
    ----------
    prefix : str
        Epics base PV prefix.
    name : str
        The name of this device.
    """
    tower1 = FCpt(CrystalTower1, '{self._prefix}', kind='normal')
    tower2 = FCpt(CrystalTower2, '{self._prefix}', kind='normal')
    dr = FCpt(IMS, '{self._hutch_prefix}:MON:MMS:19',
              kind='normal', doc='LOM Dia Theta')

    th1C = FCpt(OffsetMotor, prefix='{self._prefix}:TH1:OFF_C',
                name='th1_c',
                motor_prefix='{self._hutch_prefix}:MON:MMS:07',
                add_prefix=('prefix', 'motor_prefix'),
                doc='Th1 motor offset for C [deg]')
    th2C = FCpt(OffsetMotor, prefix='{self._prefix}:TH2:OFF_C',
                name='th2_c',
                motor_prefix='{self._hutch_prefix}:MON:MMS:13',
                add_prefix=('prefix', 'motor_prefix'),
                doc='Th2 motor offset for C [deg]')
    z1C = FCpt(OffsetMotor, prefix='{self._prefix}:Z1:OFF_C', name='z1_c',
               motor_prefix='{self._hutch_prefix}:MON:MMS:04',
               add_prefix=('prefix', 'motor_prefix'),
               doc='Z1 motor offset for C [mm]')
    z2C = FCpt(OffsetMotor, prefix='{self._prefix}:Z2:OFF_C', name='z1_c',
               motor_prefix='{self._hutch_prefix}:MON:MMS:10',
               add_prefix=('prefix', 'motor_prefix'),
               doc='Z2 motor offset for C [mm]')

    energy = Cpt(PseudoSingleInterface, egu='keV', kind='hinted')

    stage_group = [dr, th1C, th2C, z1C, z2C]

    def __init__(self, prefix, *args, **kwargs):
        self._prefix = prefix
        self._hutch_prefix = ''
        if 'XPP' in self._prefix:
            self._hutch_prefix = 'XPP'
        elif 'XCS' in self._prefix:
            self._hutch_prefix = 'HFX'

        super().__init__(prefix=prefix, *args, **kwargs)

    def get_reflection(self):
        """
        Get the crystal reflection.

        Check both towers, and compare the if they match.
        If they do not match an error will be raised.

        Returns
        -------
        ref_1 : tuple
            Reflection of the two Crystal Towers.

        Raises
        ------
        ValueError
            When the reflection of first tower does not match the one of
            second tower.
        """
        ref_1 = self.tower1.get_reflection()
        ref_2 = self.tower2.get_reflection()
        if ref_1 != ref_2:
            logger.warning('Crystals do not match: c1: %s, c2: %s',
                           ref_1, ref_2)
            raise ValueError('Invalid Crystal Arrangement')
        return ref_1

    def get_energy(self, material='C', reflection=None):
        """
        Get photon energy from first tower in keV.

        Energy is determined by the first crystal (Theta motor).

        Parameters
        ----------
        material : str, optional
            Chemical formula. Defaults to `C`
        reflection : tuple, optional
            Reflection of material. E.g.: `(1, 1, 1)`

        Returns
        -------
        energy : number
            Photon energy in keV.
        """
        reflection = reflection or self.get_reflection()
        th = self.th1C.wm()
        length = (2 * np.sin(np.deg2rad(th)) *
                  diffraction.d_space(material, reflection))
        return common.wavelength_to_energy(length) / 1000

    def calc_geometry(self, energy, material='C', reflection=None):
        """
        Calculate the lom geometry.

        Parameters
        ----------
        material : str, optional
            Chemical formula. Defaults to `C`
        reflection : tuple, optional
            Reflection of material. E.g.: `(1, 1, 1)`

        Returns
        -------
        th, z : tuple
            Returns `theta` in degrees and `zm` TODO: what is this?
        """
        reflection = reflection or self.get_reflection()
        th, z = diffraction.get_lom_geometry(energy*1e3, material, reflection)
        return (th, z)

    @pseudo_position_argument
    def forward(self, pseudo_pos):
        """
        Calculate a RealPosition from a given PseudoPosition.

        If the pseudo positioner is here at `pseudo_pos`, then this is where
        my real motor should be.

        Parameters
        ----------
        pseudo_pos : PseudoPosition
            The pseudo position input, a namedtuple.

        Returns
        -------
        real_pos : RealPosition
            The real position output, a namedtuple.
        """
        pseudo_pos = self.PseudoPosition(*pseudo_pos)
        th, z = self.calc_geometry(energy=pseudo_pos.energy)

        return self.RealPosition(th1C=th,
                                 th2C=th,
                                 z1C=-z,
                                 z2C=z,
                                 dr=2*th)

    @real_position_argument
    def inverse(self, real_pos):
        """
        Calculate a PseudoPosition from a given RealPosition.

        If the real motor is at this `real_pos`, then this is where my pseudo
        position should be.

        Parameters
        ----------
        real_pos : RealPosition
            The real position input.

        Returns
        -------
        pseudo_pos : PseudoPosition
            The pseudo position output.
        """
        try:
            reflection = self.get_reflection()
        except Exception:
            return self.PseudoPosition(energy=np.NaN)
        real_pos = self.RealPosition(*real_pos)
        length = (2 * np.sin(np.deg2rad(real_pos.th1C))
                    * diffraction.d_space('C', reflection))
        if length == 0:
            # don't bother transforming this
            # TODO maybe catch error in common.wave.. when send 0
            return 0
        energy = common.wavelength_to_energy(length) / 1000
        return self.PseudoPosition(energy=energy)

    def setE(self, energy):
        self.set_current_position(energy)
        return

    def status_info(self):
        """Overwrites interface status_info to make things faster."""
        return None

    def format_status_info(self, status_info):
        """Override status info handler to render the energy c."""
        hutch = ''
        if 'XPP' in self.prefix:
            hutch = 'XPP '
        elif 'XCS' in self.prefix:
            hutch = 'XCS '

        try:
            material = self.get_material()
        except Exception:
            material = 'Unknown'

        if material == 'C':
            configuration = 'Diamond'
        elif material == 'Si':
            configuration = 'Silicon'
        else:
            configuration = 'Unknown'

        try:
            energy = self.get_energy()
            energy = f"{energy:.4f}"
        except Exception:
            energy = 'Unknown'

        try:
            ref = self.get_reflection()
            ref = ''.join(map(str, ref))
        except Exception:
            ref = 'Unknown'

        return f"""\
{hutch}LODCM Energy C
Current Configuration: {configuration} ({ref})
Photon Energy: {energy} [keV]
"""


class LODCMEnergyC1(FltMvInterface, PseudoPositioner, GroupDevice):
    """
    Energy calculations for the C material.

    Assume material is 'C' without checking if the crystal's materials are
    aligned for both towers.

    Parameters
    ----------
    prefix : str
        Epics base PV prefix.
    name : str
        The name of this device.
    """
    tower1 = FCpt(CrystalTower1, '{self._prefix}', kind='normal')
    dr = FCpt(IMS, '{self._hutch_prefix}:MON:MMS:19',
              kind='normal', doc='LOM Dia Theta')

    th1C = FCpt(OffsetMotor, prefix='{self._prefix}:TH1:OFF_C',
                name='th1_c',
                motor_prefix='{self._hutch_prefix}:MON:MMS:07',
                add_prefix=('prefix', 'motor_prefix'),
                doc='Th1 motor offset for C [deg]')
    z1C = FCpt(OffsetMotor, prefix='{self._prefix}:Z1:OFF_C', name='z1_c',
               motor_prefix='{self._hutch_prefix}:MON:MMS:04',
               add_prefix=('prefix', 'motor_prefix'),
               doc='Z1 motor offset for C [mm]')

    energy = Cpt(PseudoSingleInterface, egu='keV', kind='hinted')

    stage_group = [dr, th1C, z1C]

    def __init__(self, prefix, *args, **kwargs):
        self._prefix = prefix
        self._hutch_prefix = ''
        if 'XPP' in self._prefix:
            self._hutch_prefix = 'XPP'
        elif 'XCS' in self._prefix:
            self._hutch_prefix = 'HFX'

        super().__init__(prefix=prefix, *args, **kwargs)

    def get_reflection(self):
        """
        Get the crystal reflection.

        Check both towers, and compare the if they match.
        If they do not match an error will be raised.

        Returns
        -------
        ref_1 : tuple
            Reflection of the two Crystal Towers.

        Raises
        ------
        ValueError
            When the reflection of first tower does not match the one of
            second tower.
        """
        ref_1 = self.tower1.get_reflection()
        return ref_1

    def get_energy(self, material='C', reflection=None):
        """
        Get photon energy from first tower in keV.

        Energy is determined by the first crystal (Theta motor).

        Parameters
        ----------
        material : str, optional
            Chemical formula. Defaults to `C`
        reflection : tuple, optional
            Reflection of material. E.g.: `(1, 1, 1)`

        Returns
        -------
        energy : number
            Photon energy in keV.
        """
        reflection = reflection or self.get_reflection()
        th = self.th1C.wm()
        length = (2 * np.sin(np.deg2rad(th)) *
                  diffraction.d_space(material, reflection))
        return common.wavelength_to_energy(length) / 1000

    def calc_geometry(self, energy, material='C', reflection=None):
        """
        Calculate the lom geometry.

        Parameters
        ----------
        material : str, optional
            Chemical formula. Defaults to `C`
        reflection : tuple, optional
            Reflection of material. E.g.: `(1, 1, 1)`

        Returns
        -------
        th, z : tuple
            Returns `theta` in degrees and `zm` TODO: what is this?
        """
        reflection = reflection or self.get_reflection()
        th, z = diffraction.get_lom_geometry(energy*1e3, material, reflection)
        return (th, z)

    @pseudo_position_argument
    def forward(self, pseudo_pos):
        """
        Calculate a RealPosition from a given PseudoPosition.

        If the pseudo positioner is here at `pseudo_pos`, then this is where
        my real motor should be.

        Parameters
        ----------
        pseudo_pos : PseudoPosition
            The pseudo position input, a namedtuple.

        Returns
        -------
        real_pos : RealPosition
            The real position output, a namedtuple.
        """
        pseudo_pos = self.PseudoPosition(*pseudo_pos)
        th, z = self.calc_geometry(energy=pseudo_pos.energy)

        return self.RealPosition(th1C=th,
                                 z1C=-z,
                                 dr=2*th)

    @real_position_argument
    def inverse(self, real_pos):
        """
        Calculate a PseudoPosition from a given RealPosition.

        If the real motor is at this `real_pos`, then this is where my pseudo
        position should be.

        Parameters
        ----------
        real_pos : RealPosition
            The real position input.

        Returns
        -------
        pseudo_pos : PseudoPosition
            The pseudo position output.
        """
        try:
            reflection = self.get_reflection()
        except Exception:
            return self.PseudoPosition(energy=np.NaN)
        real_pos = self.RealPosition(*real_pos)
        length = (2 * np.sin(np.deg2rad(real_pos.th1C))
                    * diffraction.d_space('C', reflection))
        if length == 0:
            # don't bother transforming this
            # TODO maybe catch error in common.wave.. when send 0
            return 0
        energy = common.wavelength_to_energy(length) / 1000
        return self.PseudoPosition(energy=energy)

    def setE(self, energy):
        self.set_current_position(energy)
        return

    def status_info(self):
        """Overwrites interface status_info to make things faster."""
        return None

    def format_status_info(self, status_info):
        """Override status info handler to render the energy c."""
        hutch = ''
        if 'XPP' in self.prefix:
            hutch = 'XPP '
        elif 'XCS' in self.prefix:
            hutch = 'XCS '

        try:
            material = self.get_material()
        except Exception:
            material = 'Unknown'

        if material == 'C':
            configuration = 'Diamond'
        elif material == 'Si':
            configuration = 'Silicon'
        else:
            configuration = 'Unknown'

        try:
            energy = self.get_energy()
            energy = f"{energy:.4f}"
        except Exception:
            energy = 'Unknown'

        try:
            ref = self.get_reflection()
            ref = ''.join(map(str, ref))
        except Exception:
            ref = 'Unknown'

        return f"""\
{hutch}LODCM Energy C
Current Configuration: {configuration} ({ref})
Photon Energy: {energy} [keV]
"""


class LODCM(BaseInterface, GroupDevice, LightpathMixin):
    """
    Large Offset Dual Crystal Monochromator.

    This is the device that allows XPP and XCS to multiplex with downstream
    hutches. It contains two crystals that steer/split the beam and a number of
    diagnostic devices between them. Beam can continue onto the main line, onto
    the mono line, onto both, or onto neither.

    Tower 1 has the Si and C crystals with 2 angles and 5 linear motions.
    Tower 2 has the second Si and C crystals and a diode behind the crystals.

    Parameters
    ----------
    prefix : str
        The PV prefix.

    name : str
        The name of this device.

    main_line : str, optional
        Name of the main, no-bounce beamline.

    mono_line : str, optional
        Name of the mono, double-bounce beamline.
    """
    yag = Cpt(YagLom, ":DV", kind='omitted')
    dectris = Cpt(Dectris, ":DH", kind='omitted')
    diode = Cpt(Diode, ":DIODE", kind='omitted')
    foil = Cpt(Foil, ":FOIL", kind='omitted')

    tower1 = FCpt(CrystalTower1, '{self._prefix}', name='T1',
                  kind='normal')
    tower2 = FCpt(CrystalTower2, '{self._prefix}', name='T2',
                  kind='normal')
    # diagnostics tower
    diag_tower = FCpt(DiagnosticsTower, '{self._prefix}', name='DT',
                      kind='normal')

    energy_si = FCpt(LODCMEnergySi, '{self._prefix}', kind='normal')
    energy_c = FCpt(LODCMEnergyC, '{self._prefix}', kind='normal')
    energy_c1 = FCpt(LODCMEnergyC1, '{self._prefix}', kind='normal')
    # QIcon for UX
    _icon = 'fa.share-alt-square'

    tab_whitelist = ['h1n_state', 'yag', 'dectris', 'diode', 'foil',
                     'remove_dia', 'tower1', 'tower2',
                     'diag_tower', 'calc', 'E', 'EC', 'ESi', 'E1C', 'tweak_x',
                     'tweakXC']

    lightpath_cpts = ['tower1.h1n_state.state']

    def __init__(self, prefix, *, name, main_line='MAIN', mono_line='MONO',
                 **kwargs):
        self._prefix = prefix
        self._hutch_prefix = ''
        if 'XPP' in self._prefix:
            self._hutch_prefix = 'XPP'
        elif 'XCS' in self._prefix:
            self._hutch_prefix = 'HFX'

        super().__init__(prefix, name=name, **kwargs)
        self.main_line = main_line
        self.mono_line = mono_line
        # first tower
        self.z1 = self.tower1.z1
        self.x1 = self.tower1.x1
        self.y1 = self.tower1.y1
        self.th1 = self.tower1.th1
        self.chi1 = self.tower1.chi1
        self.h1n = self.tower1.h1n
        self.h1p = self.tower1.h1p
        # second tower
        self.z2 = self.tower2.z2
        self.x2 = self.tower2.x2
        self.y2 = self.tower2.y2
        self.th2 = self.tower2.th2
        self.chi2 = self.tower2.chi2
        self.h2n = self.tower2.h2n
        self.diode2 = self.tower2.diode2
        # diagnostic tower
        self.dh = self.diag_tower.dh
        self.dv = self.diag_tower.dv
        self.dr = self.diag_tower.dr
        self.df = self.diag_tower.df
        self.dd = self.diag_tower.dd
        self.yag_zoom = self.diag_tower.yag_zoom
        # states
        self.h1n_state = self.tower1.h1n_state
        self.y1_state = self.tower1.y1_state
        self.chi1_state = self.tower1.chi1_state
        self.h2n_state = self.tower2.h2n_state
        self.y2_state = self.tower2.y2_state
        self.chi2_state = self.tower2.chi2_state
        # offset positioners - tower 1
        self.th1Si = self.energy_si.th1Si
        self.z1Si = self.energy_si.z1Si
        self.th1C = self.energy_c.th1C
        self.z1C = self.energy_c.z1C

        self.x1C = self.tower1.x1C
        self.x1Si = self.tower1.x1Si
        self.y1C = self.tower1.y1C
        self.y1Si = self.tower1.y1Si
        self.chi1C = self.tower1.chi1C
        self.chi1Si = self.tower1.chi1Si
        self.h1nC = self.tower1.h1nC
        self.h1nSi = self.tower1.h1nSi
        self.h1pC = self.tower1.h1pC
        self.h1pSi = self.tower1.h1pSi
        # offset positioners - tower 2
        self.th2Si = self.energy_si.th2Si
        self.z2Si = self.energy_si.z2Si
        self.th2C = self.energy_c.th2C
        self.z2C = self.energy_c.z2C

        self.x2C = self.tower2.x2C
        self.x2Si = self.tower2.x2Si
        self.y2C = self.tower2.y2C
        self.y2Si = self.tower2.y2Si
        self.chi2C = self.tower2.chi2C
        self.chi2Si = self.tower2.chi2Si
        self.h2nC = self.tower2.h2nC
        self.h2nSi = self.tower2.h2nSi

        # energy aliases
        self.EC = self.energy_c
        self.ESi = self.energy_si
        self.E1C = self.energy_c1

    @property
    def energy(self):
        material = self.get_material()
        if material == 'C':
            return self.energy_c
        elif material == 'Si':
            return self.energy_si
        # return energy_c as default if could not determine the material
        # TODO consider "energy" proxy motor object that picks where to
        # forward the commands instead of this property
        return self.energy_c

    @property
    def E(self):
        return self.energy

    def calc_lightpath_state(
        self,
        tower1_h1n_state_state: Union[int, str]
    ) -> LightpathState:
        return LightpathState(
            inserted=True,
            removed=True,
            ouptut={self.output_branches[0]: 0}
        )

    @property
    def inserted(self):
        """Returns `True` if either h1n crystal is in."""
        return self.h1n_state.inserted

    @property
    def removed(self):
        """Returns `True` if neither h1n crystal is in."""
        return self.h1n_state.removed

    def remove(self, moved_cb=None, timeout=None, wait=False):
        """Moves the h1n crystal out of the beam."""
        return self.h1n_state.remove(moved_cb=moved_cb, timeout=timeout,
                                     wait=wait)

    @property
    def transmission(self):
        """Returns h1n's transmission value."""
        return self.h1n_state.transmission

    @property
    def branches(self):
        """Returns possible destinations as a list of strings."""
        return [self.main_line, self.mono_line]

    @property
    def destination(self):
        """
        Which beamline the light is reaching.

        Indeterminate states will show as blocked.

        Returns
        -------
        destination : list of str
            `.main_line` if the light continues on the main line.
            `.mono_line` if the light continues on the mono line.
        """

        if self.h1n_state.position == 'OUT':
            dest = [self.main_line]
        elif self.h1n_state.position == 'Si':
            dest = [self.mono_line]
        elif self.h1n_state.position == 'C':
            dest = [self.main_line, self.mono_line]
        else:
            dest = []

        if not self._dia_clear and self.mono_line in dest:
            dest.remove(self.mono_line)

        return dest

    @property
    def _dia_clear(self):
        """
        Check if the diagnostics are clear. All but the diode may prevent beam.

        Returns
        -------
        diag_clear : bool
            :keyword:`False` if the diagnostics will prevent beam.
        """

        yag_clear = self.yag.removed
        dectris_clear = self.dectris.removed
        foil_clear = self.foil.removed
        return all((yag_clear, dectris_clear, foil_clear))

    def remove_dia(self, moved_cb=None, timeout=None, wait=False):
        """Remove all diagnostic components."""
        logger.debug('Removing %s diagnostics', self.name)
        status = NullStatus()
        for dia in (self.yag, self.dectris, self.diode, self.foil):
            status = status & dia.remove(timeout=timeout, wait=False)

        if moved_cb is not None:
            status.add_callback(functools.partial(moved_cb, obj=self))

        if wait:
            status_wait(status)

        return status

    remove_dia.__doc__ += insert_remove

    def get_reflection(self):
        """
        Get the crystal reflection.

        Check both towers, and compare the if they match.
        If they do not match an error will be raised.

        Returns
        -------
        ref_1 : tuple
            Reflection of the two Crystal Towers.

        Raises
        ------
        ValueError
            When the reflection of first tower does not match the one of
            second tower.
        """
        ref_1 = self.tower1.get_reflection()
        ref_2 = self.tower2.get_reflection()
        if ref_1 != ref_2:
            logger.warning('Crystals do not match: c1: %s, c2: %s',
                           ref_1, ref_2)
            raise ValueError('Invalid Crystal Arrangement')
        return ref_1

    def get_material(self):
        """
        Get the current crystals material.

        Parameters
        ----------
        check : bool
            Indicates if an exception should occure in case it could not
            determine the material for a tower.

        Returns
        -------
        m_1 : str
            Crystals material.
        """
        m_1 = self.tower1.get_material()
        m_2 = self.tower2.get_material()
        if m_1 != m_2:
            logger.warning('Crystals do not match: c1: %s, c2: %s', m_1, m_2)
            raise ValueError('Invalid Crystal Arrangement.')
        return m_1

    def get_energy(self, material=None, reflection=None):
        """
        Get photon energy from first tower in keV.

        Energy is determined by the first crystal (Theta motor).

        Parameters
        ----------
        material : str, optional
            Chemical formula.
        reflection : tuple, optional
            Reflection of material. E.g.: `(1, 1, 1)`

        Returns
        -------
        energy : number
            Photon energy in keV.
        """
        material = material or self.get_material()
        reflection = reflection or self.get_reflection()
        if material == 'Si':
            return self.energy_si.get_energy()
        elif material == 'C':
            return self.energy_c.get_energy()
        else:
            raise ValueError('Cannot decide the energy motor because could not'
                             ' determine the material.')

    def calc_geometry(self, energy, material=None, reflection=None):
        """
        Calculate the lom geometry.

        Parameters
        ----------
        material : str, optional
            Chemical formula. E.g.: `Si`
        reflection : tuple, optional
            Reflection of material. E.g.: `(1, 1, 1)`

        Returns
        -------
        th, z : tuple
        """
        # try to determine the material and reflection:
        material = material or self.get_material()
        reflection = reflection or self.get_reflection()
        return self.energy.calc_geometry(energy, material, reflection)

    def set_energy(self, energy, material=None, reflection=None):
        """
        Set the current positions of th1, th2, z1 and z2 offset motors.

        Parameters
        ----------
        energy : number
            Energy in keV.
        material : str, optional
            Crystal material. E.g.: `Si`
        reflection : tuple, optional
            Crystal's reflections. E.g.: `(1,1,1)`
        """
        # try to determine the material and reflection:
        material = material or self.get_material()
        reflection = reflection or self.get_reflection()
        th, z = diffraction.get_lom_geometry(energy * 1e3, material,
                                             reflection)
        if material == 'Si':
            self.th1Si.set_current_position(th)
            self.th2Si.set_current_position(th)
            self.z1Si.set_current_position(-z)
            self.z2Si.set_current_position(z)
        elif material == 'C':
            self.th1C.set_current_position(th)
            self.th2C.set_current_position(th)
            self.z1C.set_current_position(-z)
            self.z2C.set_current_position(z)
        else:
            raise ValueError("Invalid material: %s", material)

    def tweak_x(self, x_value, material=None, wait=False):
        """
        Tweak some motors to center the beam on YAG2 horizontally based on
        the calibrated s4 position.

        Parameters
        ----------
        x_value : number
            The value that we want to move `x2` by from current position.
        material : str, optional
            Crystal material. E.g.: `Si`
        wait : bool, optional
            Wait for motion to complete if wait is `True`. Defaults to `False`.
        """
        # try to determine the material:
        material = material or self.get_material()
        if material == 'Si':
            th = self.th2Si.wm()
        elif material == 'C':
            th = self.th2C.wm()
        else:
            raise ValueError("Invalid material: %s", material)
        z_value = x_value / common.tand(2 * th)
        start_x = self.x2.wm()
        start_z = self.z2.wm()
        self.x2.mvr(x_value, wait=wait)
        self.z2.mvr(z_value, wait=wait)
        end_x = self.x2.wm()
        end_z = self.z2.wm()
        dx = end_x - start_x
        dz = end_z - start_z

        if abs(dx - x_value) > self.tower2.x2_retry_deadband.get():
            logger.warning("x2 did not reach to the desired position of %f! "
                           "Check the motors, now at: %f, deadband %f" % (
                               start_x + x_value, end_x, self.x2.position))
        if abs(dz - z_value) > self.tower2.z2_retry_deadband.get():
            logger.warning("z2 did not reach the desired position of %f! "
                           "Check the motors, now at %f, deadband %f" % (
                               start_z + z_value, end_z, self.z2.position))

    def tweakXC(self, x, wait=False):
        self.tweak_x(x, material='C', wait=wait)

    def tweak_parallel(self, p_value, material=None, wait=False):
        """
        Tweak the `x2` and `z2` motors.

        Parameters
        ----------
        p_value : number
            The number we want to use to tweak the position of x2 and z2
        material : str, optional
            Crystal material. E.g.: `Si`
        wait : bool
            Wait for motion to complete if wait is `True`. Defaults to `False`.
        """
        # try to determine the material:
        material = material or self.get_material()
        if material == 'Si':
            th = self.th2Si.wm()
            self.x2.mvr(p_value * np.sin(th * np.pi / 180), wait=wait)
            self.z2.mvr(p_value * np.cos(th * np.pi / 180), wait=wait)
        elif material == 'C':
            th = self.th2C.wm()
            self.x2.mvr(p_value * np.sin(th * np.pi / 180), wait=wait)
            self.z2.mvr(p_value * np.cos(th * np.pi / 180), wait=wait)
        else:
            raise ValueError("Invalid material: %s", material)

    def wait_energy(self, timeout=None):
        """
        Block all these motors untill some action completes.

        Parameters
        ----------
        timeout: number, optional
            If None, wait indefinitely until the status finishes.
            Defaults to None.
        """
        self.th1.wait(timeout=timeout)
        self.th2.wait(timeout=timeout)
        self.dr.wait(timeout=timeout)
        self.z1.wait(timeout=timeout)
        self.z2.wait(timeout=timeout)

    def format_status_info(self, status_info):
        """Override status info handler to render the lodcm."""
        t1_state = get_status_value(
            status_info, 'tower1', 'h1n_state', 'position')
        t2_state = get_status_value(
            status_info, 'tower2', 'h2n_state', 'position')
        state = ''
        hutch = ''
        if 'XPP' in self.prefix:
            hutch = 'XPP '
            state = f'Crystal 1 state: {t1_state}'
        elif 'XCS' in self.prefix:
            hutch = 'XCS '
            state = f'Crystal 1 state: {t1_state}'
            state = f'{state}\nCrystal 2 state: {t2_state}'

        try:
            material = self.get_material()
        except Exception:
            material = 'Unknown'

        if material == 'C':
            configuration = 'Diamond'
        elif material == 'Si':
            configuration = 'Silicon'
        else:
            configuration = 'Unknown'

        try:
            energy = self.get_energy()
            energy = f"{energy:.4f}"
        except Exception:
            energy = 'Unknown'

        try:
            ref = self.get_reflection()
            ref = ''.join(map(str, ref))
        except Exception:
            ref = 'Unknown'
        # tower 1
        z_units = get_status_value(
            status_info, 'tower1', 'z1', 'user_setpoint', 'units')
        z_user = get_status_float(
            status_info, 'tower1', 'z1', 'position')
        z_dial = get_status_float(
            status_info, 'tower1', 'z1', 'dial_position', 'value')

        x_units = get_status_value(
            status_info, 'tower1', 'x1', 'user_setpoint', 'units')
        x_user = get_status_float(status_info, 'tower1', 'x1', 'position')
        x_dial = get_status_float(
            status_info, 'tower1', 'x1', 'dial_position', 'value')

        th_units = get_status_value(
            status_info, 'tower1', 'th1', 'user_setpoint', 'units')
        th_user = get_status_float(
            status_info, 'tower1', 'th1', 'position')
        th_dial = get_status_float(
            status_info, 'tower1', 'th1', 'dial_position', 'value')

        chi_units = get_status_value(
            status_info, 'tower1', 'chi1', 'user_setpoint', 'units')
        chi_user = get_status_float(
            status_info, 'tower1', 'chi1', 'position')
        chi_dial = get_status_float(
            status_info, 'tower1', 'chi1', 'dial_position', 'value')

        y_units = get_status_value(
            status_info, 'tower1', 'y1', 'user_setpoint', 'units')
        y_user = get_status_float(
            status_info, 'tower1', 'y1', 'position')
        y_dial = get_status_float(
            status_info, 'tower1', 'y1', 'dial_position', 'value')

        hn_units = get_status_value(
            status_info, 'tower1', 'h1n', 'user_setpoint', 'units')
        hn_user = get_status_float(
            status_info, 'tower1', 'h1n', 'position')
        hn_dial = get_status_float(
            status_info, 'tower1', 'h1n', 'dial_position', 'value')

        hp_units = get_status_value(
            status_info, 'tower1', 'h1p', 'user_setpoint', 'units')
        hp_user = get_status_float(
            status_info, 'tower1', 'h1p', 'position')
        hp_dial = get_status_float(
            status_info, 'tower1', 'h1p', 'dial_position', 'value')

        diode_units = get_status_value(
            status_info, 'tower1', 'diode2', 'user_setpoint', 'units')
        diode_user = get_status_float(
            status_info, 'tower1', 'diode', 'position')
        diode_dial = get_status_float(
            status_info, 'tower1', 'diode', 'dial_position', 'value')
        # tower 2
        z2_user = get_status_float(
            status_info, 'tower2', 'z2', 'position')
        z2_dial = get_status_float(
            status_info, 'tower2', 'z2', 'dial_position', 'value')

        x2_user = get_status_float(
            status_info, 'tower2', 'x2', 'position')
        x2_dial = get_status_float(
            status_info, 'tower2', 'x2', 'dial_position', 'value')

        th2_user = get_status_float(
            status_info, 'tower2', 'th2', 'position')
        th2_dial = get_status_float(
            status_info, 'tower2', 'th2', 'dial_position', 'value')

        chi2_user = get_status_float(
            status_info, 'tower2', 'chi2', 'position')
        chi2_dial = get_status_float(
            status_info, 'tower2', 'chi2', 'dial_position', 'value')

        y2_user = get_status_float(
            status_info, 'tower2', 'y2', 'position')
        y2_dial = get_status_float(
            status_info, 'tower2', 'y2', 'dial_position', 'value')

        hn2_user = get_status_float(
            status_info, 'tower2', 'h2n', 'position')
        hn2_dial = get_status_float(
            status_info, 'tower2', 'h2n', 'dial_position', 'value')

        hp2_user = get_status_float(
            status_info, 'tower2', 'h2p', 'position')
        hp2_dial = get_status_float(
            status_info, 'tower2', 'h2p', 'dial_position', 'value')

        diode2_user = get_status_float(
            status_info, 'tower2', 'diode2', 'position')
        diode2_dial = get_status_float(
            status_info, 'tower2', 'diode2', 'dial_position', 'value')
        # diagnostics
        dh_units = get_status_value(
            status_info, 'diag_tower', 'dh', 'user_setpoint', 'units')
        dh_user = get_status_float(
            status_info, 'diag_tower', 'dh', 'position')
        dh_dial = get_status_float(
            status_info, 'diag_tower', 'dh', 'dial_position', 'value')

        dv_units = get_status_value(
            status_info, 'diag_tower', 'dv', 'user_setpoint', 'units')
        dv_user = get_status_float(
            status_info, 'diag_tower', 'dv', 'position')
        dv_dial = get_status_float(
            status_info, 'diag_tower', 'dv', 'dial_position', 'value')

        dr_units = get_status_value(
            status_info, 'diag_tower', 'dr', 'user_setpoint', 'units')
        dr_user = get_status_float(
            status_info, 'diag_tower', 'dr', 'position')
        dr_dial = get_status_float(
            status_info, 'diag_tower', 'dr', 'dial_position', 'value')

        df_units = get_status_value(
            status_info, 'diag_tower', 'df', 'user_setpoint', 'units')
        df_user = get_status_float(
            status_info, 'diag_tower', 'df', 'position')
        df_dial = get_status_float(
            status_info, 'diag_tower', 'df', 'dial_position', 'value')

        dd_units = get_status_value(
            status_info, 'diag_tower', 'dd', 'user_setpoint', 'units')
        dd_user = get_status_float(
            status_info, 'diag_tower', 'dd', 'position')
        dd_dial = get_status_float(
            status_info, 'diag_tower', 'dd', 'dial_position', 'value')

        yag_zoom_units = get_status_value(status_info, 'diag_tower',
                                          'yag_zoom', 'user_setpoint', 'units')
        yag_zoom_user = get_status_float(status_info, 'diag_tower',
                                         'yag_zoom', 'position', precision=0)
        yag_zoom_dial = get_status_float(status_info, 'diag_tower',
                                         'yag_zoom', 'dial_position', 'value',
                                         precision=0)

        def form(left_str, center_str, right_str):
            return f'{left_str:<16}{center_str:>26}{right_str:>26}'

        return f"""\
{hutch}LODCM Motor Status Positions
{state}
Current Configuration: {configuration} ({ref})
Photon Energy: {energy} [keV]
-----------------------------------------------------------------
{form(' ', 'Crystal Tower 1', 'Crystal Tower 2')}
{form(f'z [{z_units}]', f'{z_user} ({z_dial})',
      f'{z2_user} ({z2_dial})')}
{form(f'x [{x_units}]', f'{x_user} ({x_dial})',
      f'{x2_user} ({x2_dial})')}
{form(f'th [{th_units}]', f'{th_user} ({th_dial})',
      f'{th2_user} ({th2_dial})')}
{form(f'chi [{chi_units}]', f'{chi_user} ({chi_dial})',
      f'{chi2_user} ({chi2_dial})')}
{form(f'y [{y_units}]', f'{y_user} ({y_dial})',
      f'{y2_user} ({y2_dial})')}
{form(f'hn [{hn_units}]', f'{hn_user} ({hn_dial})',
      f'{hn2_user} ({hn2_dial})')}
{form(f'hp [{hp_units}]', f'{hp_user} ({hp_dial})',
      f'{hp2_user} ({hp2_dial})')}
{form(f'diode [{diode_units}]', f'{diode_user} ({diode_dial})',
      f'{diode2_user} ({diode2_dial})')}
-----------------------------------------------------------------
{form(' ', 'Diagnostic Tower', ' ')}
{form(f'diag r [{dr_units}]', f'{dr_user} ({dr_dial})', '')}
{form(f'diag h [{dh_units}]', f'{dh_user} ({dh_dial})', '')}
{form(f'diag v [{dv_units}]', f'{dv_user} ({dv_dial})', '')}
{form(f'filter [{df_units}]', f'{df_user} ({df_dial})', '')}
{form(f'diode [{dd_units}]', f'{dd_user} ({dd_dial})', '')}
{form(f'navitar zoom [{yag_zoom_units}]',
      f'{yag_zoom_user} ({yag_zoom_dial})', '')}
"""


class XCSLODCM(LODCM):
    """
    The XCS implementation of a LODCM.  Currently (as of 09/28/2022),
    the XCS has 3 operating modes:

    1. the 1st crystal of the lodcm is OUT then the beam goes to CXI.
    2. The diamond crystal is IN then the pink beam goes to CXI while
        a monochromatic beam can go to XCS at the same time.
    3. The Silicon crystal is IN in which case monochromatic beam
        only goes to XCS.
    """
    __doc__ += LODCM.__doc__

    def calc_lightpath_state(
        self,
        tower1_h1n_state_state: Union[int, str]
    ) -> LightpathState:
        """
        Calculates lightpath state for XCS LODCM.  This LODCM has 3
        operating modes (as of 9/28/2022)

        1. the 1st crystal of the lodcm is OUT then the beam goes to CXI.
        2. The diamond crystal is IN then the pink beam goes to CXI while
           a monochromatic beam can go to XCS at the same time.
        3. The Silicon crystal is IN in which case monochromatic beam
           only goes to XCS.

        Parameters
        ----------
        tower1_h1n_state_state : Union[int, str]
            The first crystal state.

        Returns
        -------
        LightpathState
        """
        h1n_state = tower1_h1n_state_state
        if not self.tower1.h1n_state._state_initialized:
            self.log.debug('tower1 state not initialized, scheduling '
                           'lightpath calc for later')
            if self._retry_lightpath:
                self._retry_lightpath = False
                schedule_task(self._calc_cache_lightpath_state, delay=2.0)

            return LightpathState(
                inserted=True,
                removed=True,
                output={'L0': 0}
            )

        self._retry_lightpath = True
        # avoid extra get calls in this method
        state_label = self.tower1.h1n_state.get_state(h1n_state).name

        if state_label in ['C', 'Si']:
            inserted = True
            removed = False
            if state_label == 'C':
                # Diamond crystal, beam is split between outputs
                output = {'L0': 0.5, 'L3': 0.5}
            elif state_label == 'Si':
                # Silicon crystal in, monochromatic beam to XCS
                output = {'L3': 1}
        elif state_label == 'OUT':
            # out, straight through to CXI
            inserted = False
            removed = True
            output = {'L0': 1}
        else:
            # unknown state blocks
            inserted = False
            removed = False
            output = {'L0': 0}

        return LightpathState(
            inserted=inserted,
            removed=removed,
            output=output
        )


class XPPLODCM(LODCM):
    """
    The XPP implementation of a LODCM.  Currently (as of 09/28/2022), the XPP
    LODCM does not consider differing crystal states.  For now we only consider
    whether the first crystal is in or out.
    """
    __doc__ += LODCM.__doc__

    def calc_lightpath_state(
        self,
        tower1_h1n_state_state: Union[int, str]
    ) -> LightpathState:
        """
        Currently (as of 09/28/2022), the XPP LODCM does not consider
        differing crystal states.  For now we only consider whether the
        first crystal is in or out.

        Parameters
        ----------
        tower1_h1n_state_state : Union[int, str]
            The insertion state of the first tower crystal

        Returns
        -------
        LightpathState
        """

        h1n_state = tower1_h1n_state_state
        if not self.tower1.h1n_state._state_initialized:
            self.log.debug('tower1 state not initialized, scheduling '
                           'lightpath calc for later')
            if self._retry_lightpath:
                self._retry_lightpath = False
                schedule_task(self._calc_cache_lightpath_state, delay=2.0)
            return LightpathState(
                inserted=True,
                removed=True,
                output={'L0': 0}
            )

        self._retry_lightpath = True
        inserted = self.tower1.h1n_state.check_inserted(h1n_state)
        removed = self.tower1.h1n_state.check_removed(h1n_state)

        if inserted and not removed:
            # ignore attenuation from the LODCM splitting beam
            output = {'L0': 1,
                      'L2': 1}
        elif not inserted and removed:
            # if removed, full transmission through L0
            output = {'L0': 1}
        else:
            # for unknown states, mark as blocking
            output = {'L0': 0}

        return LightpathState(
            inserted=inserted,
            removed=removed,
            output=output
        )


class SimFirstTower(CrystalTower1):
    """Crystal Tower 1 Simulator for Testing."""
    # first tower
    z1 = Cpt(FastMotor, limits=(-1000, 1000))
    x1 = Cpt(FastMotor, limits=(-1000, 1000))
    y1 = Cpt(FastMotor, limits=(-1000, 1000))
    th1 = Cpt(FastMotor, limits=(-1000, 1000))
    chi1 = Cpt(FastMotor, limits=(-1000, 1000))
    h1n = Cpt(FastMotor, limits=(-1000, 1000))
    h1p = Cpt(FastMotor, limits=(-1000, 1000))


class SimSecondTower(CrystalTower2):
    """Crystal Tower 2 Simulator for Testing."""
    # second tower
    z2 = Cpt(FastMotor, limits=(-1000, 1000))
    x2 = Cpt(FastMotor, limits=(-1000, 1000))
    y2 = Cpt(FastMotor, limits=(-1000, 1000))
    th2 = Cpt(FastMotor, limits=(-1000, 1000))
    chi2 = Cpt(FastMotor, limits=(-1000, 1000))
    h2n = Cpt(FastMotor, limits=(-1000, 1000))
    diode2 = Cpt(FastMotor, limits=(-1000, 1000))


class SimDiagnosticsTower(DiagnosticsTower):
    """Diagnostics Tower Simulator for Testing."""
    # diagnostic tower
    dh = Cpt(FastMotor, limits=(-1000, 1000))
    dv = Cpt(FastMotor, limits=(-1000, 1000))
    dr = Cpt(FastMotor, limits=(-1000, 1000))
    df = Cpt(FastMotor, limits=(-1000, 1000))
    dd = Cpt(FastMotor, limits=(-1000, 1000))
    yag_zoom = Cpt(FastMotor, limits=(-1000, 1000))


class SimEnergyC(LODCMEnergyC):
    """Energy C Simulator for Testing"""
    th1C = Cpt(FastMotor, limits=(-1000, 1000))
    th2C = Cpt(FastMotor, limits=(-1000, 1000))
    z1C = Cpt(FastMotor, limits=(-1000, 1000))
    z2C = Cpt(FastMotor, limits=(-1000, 1000))
    dr = Cpt(FastMotor, limits=(-1000, 1000))

    def get_reflection(self):
        return (1, 1, 1)


class SimEnergySi(LODCMEnergySi):
    """Energy Si Simulator for Testing"""
    th1Si = Cpt(FastMotor, limits=(-1000, 1000))
    th2Si = Cpt(FastMotor, limits=(-1000, 1000))
    z1Si = Cpt(FastMotor, limits=(-1000, 1000))
    z2Si = Cpt(FastMotor, limits=(-1000, 1000))
    dr = Cpt(FastMotor, limits=(-1000, 1000))

    def get_reflection(self):
        return (1, 1, 1)


class SimLODCM(LODCM):
    """LODCM Simulator for Testing"""
    tower1 = Cpt(SimFirstTower, 'TOWER:1', name='tower1')
    tower2 = Cpt(SimSecondTower, 'TOWER:2', name='tower2')
    diag_tower = Cpt(SimDiagnosticsTower, 'DIAG', name='diag')
    energy_si = Cpt(SimEnergySi, 'ENERGY:SI', name='energy_si')
    energy_c = Cpt(SimEnergyC, 'ENERGY:C', name='energy_c')

    def __init__(self, prefix, *args, **kwargs):

        super().__init__(prefix=prefix, *args, **kwargs)
        # first tower
        self.z1 = FastMotor(limits=(-1000, 1000))
        self.x1 = FastMotor(limits=(-1000, 1000))
        self.y1 = FastMotor(limits=(-1000, 1000))
        self.th1 = FastMotor(limits=(-1000, 1000))
        self.chi1 = FastMotor(limits=(-1000, 1000))
        self.h1n = FastMotor(limits=(-1000, 1000))
        self.h1p = FastMotor(limits=(-1000, 1000))
        # second tower
        self.z2 = FastMotor(limits=(-1000, 1000))
        self.x2 = FastMotor(limits=(-1000, 1000))
        self.y2 = FastMotor(limits=(-1000, 1000))
        self.th2 = FastMotor(limits=(-1000, 1000))
        self.chi2 = FastMotor(limits=(-1000, 1000))
        self.h2n = FastMotor(limits=(-1000, 1000))
        self.diode2 = FastMotor(limits=(-1000, 1000))
        # diagnostic tower
        self.dh = FastMotor(limits=(-1000, 1000))
        self.dv = FastMotor(limits=(-1000, 1000))
        self.dr = FastMotor(limits=(-1000, 1000))
        self.df = FastMotor(limits=(-1000, 1000))
        self.dd = FastMotor(limits=(-1000, 1000))
        self.yag_zoom = FastMotor(limits=(-1000, 1000))
