"""Service calls related dependencies for LCN component."""
import pypck
import voluptuous as vol

from homeassistant.const import (
    CONF_ADDRESS,
    CONF_BRIGHTNESS,
    CONF_STATE,
    CONF_UNIT_OF_MEASUREMENT,
    TIME_SECONDS,
)
import homeassistant.helpers.config_validation as cv

from .const import (
    CONF_CONNECTIONS,
    CONF_KEYS,
    CONF_LED,
    CONF_OUTPUT,
    CONF_PCK,
    CONF_RELVARREF,
    CONF_ROW,
    CONF_SETPOINT,
    CONF_TABLE,
    CONF_TEXT,
    CONF_TIME,
    CONF_TIME_UNIT,
    CONF_TRANSITION,
    CONF_VALUE,
    CONF_VARIABLE,
    DATA_LCN,
    LED_PORTS,
    LED_STATUS,
    OUTPUT_PORTS,
    RELVARREF,
    SENDKEYCOMMANDS,
    SETPOINTS,
    THRESHOLDS,
    TIME_UNITS,
    VAR_UNITS,
    VARIABLES,
)
from .helpers import (
    get_connection,
    is_address,
    is_key_lock_states_string,
    is_relays_states_string,
)


class LcnServiceCall:
    """Parent class for all LCN service calls."""

    schema = vol.Schema({vol.Required(CONF_ADDRESS): is_address})

    def __init__(self, hass):
        """Initialize service call."""
        self.connections = hass.data[DATA_LCN][CONF_CONNECTIONS]

    def get_address_connection(self, call):
        """Get address connection object."""
        addr, connection_id = call.data[CONF_ADDRESS]
        addr = pypck.lcn_addr.LcnAddr(*addr)
        if connection_id is None:
            connection = self.connections[0]
        else:
            connection = get_connection(self.connections, connection_id)

        return connection.get_address_conn(addr)


class OutputAbs(LcnServiceCall):
    """Set absolute brightness of output port in percent."""

    schema = LcnServiceCall.schema.extend(
        {
            vol.Required(CONF_OUTPUT): vol.All(vol.Upper, vol.In(OUTPUT_PORTS)),
            vol.Required(CONF_BRIGHTNESS): vol.All(
                vol.Coerce(int), vol.Range(min=0, max=100)
            ),
            vol.Optional(CONF_TRANSITION, default=0): vol.All(
                vol.Coerce(float), vol.Range(min=0.0, max=486.0)
            ),
        }
    )

    def __call__(self, call):
        """Execute service call."""
        output = pypck.lcn_defs.OutputPort[call.data[CONF_OUTPUT]]
        brightness = call.data[CONF_BRIGHTNESS]
        transition = pypck.lcn_defs.time_to_ramp_value(
            call.data[CONF_TRANSITION] * 1000
        )

        address_connection = self.get_address_connection(call)
        address_connection.dim_output(output.value, brightness, transition)


class OutputRel(LcnServiceCall):
    """Set relative brightness of output port in percent."""

    schema = LcnServiceCall.schema.extend(
        {
            vol.Required(CONF_OUTPUT): vol.All(vol.Upper, vol.In(OUTPUT_PORTS)),
            vol.Required(CONF_BRIGHTNESS): vol.All(
                vol.Coerce(int), vol.Range(min=-100, max=100)
            ),
        }
    )

    def __call__(self, call):
        """Execute service call."""
        output = pypck.lcn_defs.OutputPort[call.data[CONF_OUTPUT]]
        brightness = call.data[CONF_BRIGHTNESS]

        address_connection = self.get_address_connection(call)
        address_connection.rel_output(output.value, brightness)


class OutputToggle(LcnServiceCall):
    """Toggle output port."""

    schema = LcnServiceCall.schema.extend(
        {
            vol.Required(CONF_OUTPUT): vol.All(vol.Upper, vol.In(OUTPUT_PORTS)),
            vol.Optional(CONF_TRANSITION, default=0): vol.All(
                vol.Coerce(float), vol.Range(min=0.0, max=486.0)
            ),
        }
    )

    def __call__(self, call):
        """Execute service call."""
        output = pypck.lcn_defs.OutputPort[call.data[CONF_OUTPUT]]
        transition = pypck.lcn_defs.time_to_ramp_value(
            call.data[CONF_TRANSITION] * 1000
        )

        address_connection = self.get_address_connection(call)
        address_connection.toggle_output(output.value, transition)


class Relays(LcnServiceCall):
    """Set the relays status."""

    schema = LcnServiceCall.schema.extend(
        {vol.Required(CONF_STATE): is_relays_states_string}
    )

    def __call__(self, call):
        """Execute service call."""
        states = [
            pypck.lcn_defs.RelayStateModifier[state] for state in call.data[CONF_STATE]
        ]

        address_connection = self.get_address_connection(call)
        address_connection.control_relays(states)


class Led(LcnServiceCall):
    """Set the led state."""

    schema = LcnServiceCall.schema.extend(
        {
            vol.Required(CONF_LED): vol.All(vol.Upper, vol.In(LED_PORTS)),
            vol.Required(CONF_STATE): vol.All(vol.Upper, vol.In(LED_STATUS)),
        }
    )

    def __call__(self, call):
        """Execute service call."""
        led = pypck.lcn_defs.LedPort[call.data[CONF_LED]]
        led_state = pypck.lcn_defs.LedStatus[call.data[CONF_STATE]]

        address_connection = self.get_address_connection(call)
        address_connection.control_led(led, led_state)


class VarAbs(LcnServiceCall):
    """Set absolute value of a variable or setpoint.

    Variable has to be set as counter!
    Regulator setpoints can also be set using R1VARSETPOINT, R2VARSETPOINT.
    """

    schema = LcnServiceCall.schema.extend(
        {
            vol.Required(CONF_VARIABLE): vol.All(
                vol.Upper, vol.In(VARIABLES + SETPOINTS)
            ),
            vol.Optional(CONF_VALUE, default=0): vol.All(
                vol.Coerce(int), vol.Range(min=0)
            ),
            vol.Optional(CONF_UNIT_OF_MEASUREMENT, default="native"): vol.All(
                vol.Upper, vol.In(VAR_UNITS)
            ),
        }
    )

    def __call__(self, call):
        """Execute service call."""
        var = pypck.lcn_defs.Var[call.data[CONF_VARIABLE]]
        value = call.data[CONF_VALUE]
        unit = pypck.lcn_defs.VarUnit.parse(call.data[CONF_UNIT_OF_MEASUREMENT])

        address_connection = self.get_address_connection(call)
        address_connection.var_abs(var, value, unit)


class VarReset(LcnServiceCall):
    """Reset value of variable or setpoint."""

    schema = LcnServiceCall.schema.extend(
        {vol.Required(CONF_VARIABLE): vol.All(vol.Upper, vol.In(VARIABLES + SETPOINTS))}
    )

    def __call__(self, call):
        """Execute service call."""
        var = pypck.lcn_defs.Var[call.data[CONF_VARIABLE]]

        address_connection = self.get_address_connection(call)
        address_connection.var_reset(var)


class VarRel(LcnServiceCall):
    """Shift value of a variable, setpoint or threshold."""

    schema = LcnServiceCall.schema.extend(
        {
            vol.Required(CONF_VARIABLE): vol.All(
                vol.Upper, vol.In(VARIABLES + SETPOINTS + THRESHOLDS)
            ),
            vol.Optional(CONF_VALUE, default=0): int,
            vol.Optional(CONF_UNIT_OF_MEASUREMENT, default="native"): vol.All(
                vol.Upper, vol.In(VAR_UNITS)
            ),
            vol.Optional(CONF_RELVARREF, default="current"): vol.All(
                vol.Upper, vol.In(RELVARREF)
            ),
        }
    )

    def __call__(self, call):
        """Execute service call."""
        var = pypck.lcn_defs.Var[call.data[CONF_VARIABLE]]
        value = call.data[CONF_VALUE]
        unit = pypck.lcn_defs.VarUnit.parse(call.data[CONF_UNIT_OF_MEASUREMENT])
        value_ref = pypck.lcn_defs.RelVarRef[call.data[CONF_RELVARREF]]

        address_connection = self.get_address_connection(call)
        address_connection.var_rel(var, value, unit, value_ref)


class LockRegulator(LcnServiceCall):
    """Locks a regulator setpoint."""

    schema = LcnServiceCall.schema.extend(
        {
            vol.Required(CONF_SETPOINT): vol.All(vol.Upper, vol.In(SETPOINTS)),
            vol.Optional(CONF_STATE, default=False): bool,
        }
    )

    def __call__(self, call):
        """Execute service call."""
        setpoint = pypck.lcn_defs.Var[call.data[CONF_SETPOINT]]
        state = call.data[CONF_STATE]

        reg_id = pypck.lcn_defs.Var.to_set_point_id(setpoint)
        address_connection = self.get_address_connection(call)
        address_connection.lock_regulator(reg_id, state)


class SendKeys(LcnServiceCall):
    """Sends keys (which executes bound commands)."""

    schema = LcnServiceCall.schema.extend(
        {
            vol.Required(CONF_KEYS): vol.All(
                vol.Upper, cv.matches_regex(r"^([A-D][1-8])+$")
            ),
            vol.Optional(CONF_STATE, default="hit"): vol.All(
                vol.Upper, vol.In(SENDKEYCOMMANDS)
            ),
            vol.Optional(CONF_TIME, default=0): vol.All(int, vol.Range(min=0)),
            vol.Optional(CONF_TIME_UNIT, default=TIME_SECONDS): vol.All(
                vol.Upper, vol.In(TIME_UNITS)
            ),
        }
    )

    def __call__(self, call):
        """Execute service call."""
        address_connection = self.get_address_connection(call)

        keys = [[False] * 8 for i in range(4)]

        key_strings = zip(call.data[CONF_KEYS][::2], call.data[CONF_KEYS][1::2])

        for table, key in key_strings:
            table_id = ord(table) - 65
            key_id = int(key) - 1
            keys[table_id][key_id] = True

        delay_time = call.data[CONF_TIME]
        if delay_time != 0:
            hit = pypck.lcn_defs.SendKeyCommand.HIT
            if pypck.lcn_defs.SendKeyCommand[call.data[CONF_STATE]] != hit:
                raise ValueError(
                    "Only hit command is allowed when sending deferred keys."
                )
            delay_unit = pypck.lcn_defs.TimeUnit.parse(call.data[CONF_TIME_UNIT])
            address_connection.send_keys_hit_deferred(keys, delay_time, delay_unit)
        else:
            state = pypck.lcn_defs.SendKeyCommand[call.data[CONF_STATE]]
            address_connection.send_keys(keys, state)


class LockKeys(LcnServiceCall):
    """Lock keys."""

    schema = LcnServiceCall.schema.extend(
        {
            vol.Optional(CONF_TABLE, default="a"): vol.All(
                vol.Upper, cv.matches_regex(r"^[A-D]$")
            ),
            vol.Required(CONF_STATE): is_key_lock_states_string,
            vol.Optional(CONF_TIME, default=0): vol.All(int, vol.Range(min=0)),
            vol.Optional(CONF_TIME_UNIT, default=TIME_SECONDS): vol.All(
                vol.Upper, vol.In(TIME_UNITS)
            ),
        }
    )

    def __call__(self, call):
        """Execute service call."""
        address_connection = self.get_address_connection(call)

        states = [
            pypck.lcn_defs.KeyLockStateModifier[state]
            for state in call.data[CONF_STATE]
        ]
        table_id = ord(call.data[CONF_TABLE]) - 65

        delay_time = call.data[CONF_TIME]
        if delay_time != 0:
            if table_id != 0:
                raise ValueError(
                    "Only table A is allowed when locking keys for a specific time."
                )
            delay_unit = pypck.lcn_defs.TimeUnit.parse(call.data[CONF_TIME_UNIT])
            address_connection.lock_keys_tab_a_temporary(delay_time, delay_unit, states)
        else:
            address_connection.lock_keys(table_id, states)

        address_connection.request_status_locked_keys_timeout()


class DynText(LcnServiceCall):
    """Send dynamic text to LCN-GTxD displays."""

    schema = LcnServiceCall.schema.extend(
        {
            vol.Required(CONF_ROW): vol.All(int, vol.Range(min=1, max=4)),
            vol.Required(CONF_TEXT): vol.All(str, vol.Length(max=60)),
        }
    )

    def __call__(self, call):
        """Execute service call."""
        row_id = call.data[CONF_ROW] - 1
        text = call.data[CONF_TEXT]

        address_connection = self.get_address_connection(call)
        address_connection.dyn_text(row_id, text)


class Pck(LcnServiceCall):
    """Send arbitrary PCK command."""

    schema = LcnServiceCall.schema.extend({vol.Required(CONF_PCK): str})

    def __call__(self, call):
        """Execute service call."""
        pck = call.data[CONF_PCK]
        address_connection = self.get_address_connection(call)
        address_connection.pck(pck)
