"""
Client library for the LG Smart TV running NetCast 3 or 4.

LG Smart TV models released in 2012 (NetCast 3.0) and LG Smart TV models
released in 2013 (NetCast 4.0) are supported.
For pre 2012 LG TV remote commands are supported by the "hdcp" protocol.

The client is inspired by the work of
https://github.com/ubaransel/lgcommander
"""
import logging
import requests
import time
from xml.etree import ElementTree

_LOGGER = logging.getLogger(__name__)

__all__ = [
    "LgNetCastClient",
    "LG_COMMAND",
    "LG_QUERY",
    "LgNetCastError",
    "AccessTokenError",
    "SessionIdError",
]


# LG TV handler
LG_HANDLE_KEY_INPUT = "HandleKeyInput"
LG_HANDLE_MOUSE_MOVE = "HandleTouchMove"
LG_HANDLE_MOUSE_CLICK = "HandleTouchClick"
LG_HANDLE_TOUCH_WHEEL = "HandleTouchWheel"
LG_HANDLE_CHANNEL_CHANGE = "HandleChannelChange"
LG_CHANGE_INPUT_SOURCE = "ChangeInputSource"

DEFAULT_PORT = 8080
DEFAULT_TIMEOUT = 3


class LG_COMMAND(object):
    """LG TV remote control commands."""

    POWER = 1
    NUMBER_0 = 2
    NUMBER_1 = 3
    NUMBER_2 = 4
    NUMBER_3 = 5
    NUMBER_4 = 6
    NUMBER_5 = 7
    NUMBER_6 = 8
    NUMBER_7 = 9
    NUMBER_8 = 10
    NUMBER_9 = 11
    UP = 12
    DOWN = 13
    LEFT = 14
    RIGHT = 15
    OK = 20
    HOME_MENU = 21
    BACK = 23
    VOLUME_UP = 24
    VOLUME_DOWN = 25
    MUTE_TOGGLE = 26
    CHANNEL_UP = 27
    CHANNEL_DOWN = 28
    BLUE = 29
    GREEN = 30
    RED = 31
    YELLOW = 32
    PLAY = 33
    PAUSE = 34
    STOP = 35
    FAST_FORWARD = 36
    REWIND = 37
    SKIP_FORWARD = 38
    SKIP_BACKWARD = 39
    RECORD = 40
    RECORDING_LIST = 41
    REPEAT = 42
    LIVE_TV = 43
    EPG = 44
    PROGRAM_INFORMATION = 45
    ASPECT_RATIO = 46
    EXTERNAL_INPUT = 47
    PIP_SECONDARY_VIDEO = 48
    SHOW_SUBTITLE = 49
    PROGRAM_LIST = 50
    TELE_TEXT = 51
    MARK = 52
    VIDEO_3D = 400
    LR_3D = 401
    DASH = 402
    PREVIOUS_CHANNEL = 403
    FAVORITE_CHANNEL = 404
    QUICK_MENU = 405
    TEXT_OPTION = 406
    AUDIO_DESCRIPTION = 407
    ENERGY_SAVING = 409
    AV_MODE = 410
    SIMPLINK = 411
    EXIT = 412
    RESERVATION_PROGRAM_LIST = 413
    PIP_CHANNEL_UP = 414
    PIP_CHANNEL_DOWN = 415
    SWITCH_VIDEO = 416
    APPS = 417


class LG_QUERY(object):
    """LG TV data queries."""

    CUR_CHANNEL = "cur_channel"
    CHANNEL_LIST = "channel_list"
    CONTEXT_UI = "context_ui"
    VOLUME_INFO = "volume_info"
    SCREEN_IMAGE = "screen_image"
    IS_3D = "is_3d"


class LG_PROTOCOL(object):
    """Supported LG TV protcols."""

    HDCP = "hdcp"
    ROAP = "roap"


class LgNetCastClient(object):
    """LG NetCast TV client using the ROAP or HDCP protocol."""

    HEADER = {"Content-Type": "application/atom+xml"}
    XML = '<?xml version="1.0" encoding="utf-8"?>'
    KEY = XML + "<auth><type>AuthKeyReq</type></auth>"
    AUTH = XML + "<auth><type>%s</type><value>%s</value></auth>"
    COMMAND = XML + "<command><session>%s</session><type>%s</type>%s</command>"

    def __init__(self, host, access_token, protocol=LG_PROTOCOL.ROAP):
        """Initialize the LG TV client."""
        self.url = "http://%s:%s/%s/api/" % (host, DEFAULT_PORT, protocol)
        self.access_token = access_token
        self.protocol = protocol
        self._session = None

    def __enter__(self):
        """Context manager method to support with statement."""
        self._session = self._get_session_id()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        """Context manager method to support with statement."""
        self._session = None

    def send_command(self, command):
        """Send remote control commands to the TV."""
        message = self.COMMAND % (
            self._session,
            LG_HANDLE_KEY_INPUT,
            "<value>%s</value>" % command,
        )
        self._send_to_tv("command", message)

    def query_device_info(self):
        """Get model information about the TV."""
        # We're using UDAP to retrieve this information, which requires a specific User-Agent.
        # As self.HEADER is a class variable, we will make a copy to ensure we don't interfere
        # with other instances 
        try:
            self.HEADER = {**self.HEADER, "User-Agent": "UDAP/2.0"}
            response = self._send_to_tv("data", payload={"target": "rootservice.xml"})
        finally:
            # Remove the instance version of self.HEADER to restore original functionality
            del self.HEADER 
        if response.status_code != requests.codes.ok:
            return None
        data = response.text
        tree = ElementTree.fromstring(data.encode("utf-8"))

        return {
            "uuid": tree.findtext("device/uuid"),
            "model_name": tree.findtext("device/modelName"),
            "friendly_name": tree.findtext("device/friendlyName")
        }

    def change_input_source(self, type, index):
        """Send change input source command to the TV."""
        message = self.COMMAND % (
            self._session,
            LG_CHANGE_INPUT_SOURCE,
            "<inputSourceType>%d</inputSourceType><inputSourceIdx>%d</inputSourceIdx>" % (type, index),
        )
        self._send_to_tv("command", message)

    def change_channel(self, channel):
        """Send change channel command to the TV."""
        message = self.COMMAND % (
            self._session,
            LG_HANDLE_CHANNEL_CHANGE,
            ElementTree.tostring(channel, encoding="utf-8"),
        )
        self._send_to_tv("command", message)

    def query_data(self, query):
        """Query status information from the TV."""
        response = self._send_to_tv("data", payload={"target": query})
        if response.status_code == requests.codes.ok:
            data = response.text
            tree = ElementTree.fromstring(data.encode("utf-8"))
            data_list = []
            for data in tree.iter("data"):
                data_list.append(data)
            return data_list

    def get_volume(self):
        """Get volume info from the TV."""
        volume_info = self.query_data("volume_info")
        if volume_info:
            volume_info = volume_info[0]
            volume = float(volume_info.find("level").text)
            muted = volume_info.find("mute").text == "true"
            return volume, muted

    def set_volume(self, volume):
        """Try to set a specific volume level."""
        for _ in range(2):
            current_volume, _ = self.get_volume()
            self._set_volume_diff(volume, current_volume)

    def _set_volume_diff(self, volume, current_volume):
        """Simulate setting a specific volume level based on the difference."""
        volume_difference = volume - current_volume
        if volume_difference != 0:
            command = (
                LG_COMMAND.VOLUME_UP
                if volume_difference > 0
                else LG_COMMAND.VOLUME_DOWN
            )
            for _ in range(abs(int(volume_difference))):
                self.send_command(command)
                time.sleep(0.45)

    def _get_session_id(self):
        """Get the session key for the TV connection.

        If a pair key is defined the session id is requested otherwise display
        the pair key on TV.
        """
        if not self.access_token:
            self._display_pair_key()
            raise AccessTokenError("No access token specified to create session.")
        message = self.AUTH % ("AuthReq", self.access_token)
        response = self._send_to_tv("auth", message)
        if response.status_code != requests.codes.ok:
            raise SessionIdError("Can not get session id from TV.")
        data = response.text
        tree = ElementTree.XML(data)
        session = tree.find("session").text
        return session

    def _display_pair_key(self):
        """Send message to display the pair key on TV screen."""
        self._send_to_tv("auth", self.KEY)

    def _send_to_tv(self, message_type, message=None, payload=None):
        """Send message of given type to the tv."""
        if message_type != "command" and self.protocol == LG_PROTOCOL.HDCP:
            message_type = "dtv_wifirc"
        url = "%s%s" % (self.url, message_type)
        if message:
            response = requests.post(
                url, data=message, headers=self.HEADER, timeout=DEFAULT_TIMEOUT
            )
        else:
            response = requests.get(
                url, params=payload, headers=self.HEADER, timeout=DEFAULT_TIMEOUT
            )
        return response


class LgNetCastError(Exception):
    """Base class for all exceptions in this module."""


class AccessTokenError(LgNetCastError):
    """No access token specified to create session."""


class SessionIdError(LgNetCastError):
    """No session id could be retrieved from TV."""
