"""ODIN data live view adapter.

This module implements an odin-control adapter capable of rendering odin-data live
view images to users.

Created on 8th October 2018

:author: Adam Neaves, STFC Application Engineering Gruop
"""

import logging
import re
from collections import OrderedDict
import numpy as np
import cv2
from tornado.escape import json_decode

from odin_data.ipc_tornado_channel import IpcTornadoChannel
from odin_data.ipc_channel import IpcChannelException
from odin.adapters.adapter import ApiAdapter, ApiAdapterResponse, response_types
from odin.adapters.parameter_tree import ParameterTree, ParameterTreeError
from odin.util import convert_unicode_to_string

ENDPOINTS_CONFIG_NAME = 'live_view_endpoints'
COLORMAP_CONFIG_NAME = 'default_colormap'

DEFAULT_ENDPOINT = 'tcp://127.0.0.1:5020'
DEFAULT_COLORMAP = "Jet"


class LiveViewAdapter(ApiAdapter):
    """Live view adapter class.

    This class implements the live view adapter for odin-control.
    """

    def __init__(self, **kwargs):
        """
        Initialise the adapter.

        Creates a LiveViewer Object that handles the major logic of the adapter.

        :param kwargs: Key Word arguments given from the configuration file,
        which is copied into the options dictionary.
        """
        logging.debug("Live View Adapter init called")
        super(LiveViewAdapter, self).__init__(**kwargs)

        if self.options.get(ENDPOINTS_CONFIG_NAME, False):
            endpoints = [x.strip() for x in self.options.get(ENDPOINTS_CONFIG_NAME, "").split(',')]
        else:
            logging.debug("Setting default endpoint of '%s'", DEFAULT_ENDPOINT)
            endpoints = [DEFAULT_ENDPOINT]

        if self.options.get(COLORMAP_CONFIG_NAME, False):
            default_colormap = self.options.get(COLORMAP_CONFIG_NAME, "")
        else:
            default_colormap = "Jet"

        self.live_viewer = LiveViewer(endpoints, default_colormap)

    @response_types('application/json', 'image/*', 'image/webp', default='application/json')
    def get(self, path, request):
        """
        Handle a HTTP GET request from a client, passing this to the Live Viewer object.

        :param path: The path to the resource requested by the GET request
        :param request: Additional request parameters
        :return: The requested resource, or an error message and code if the request was invalid.
        """
        try:
            response, content_type, status = self.live_viewer.get(path, request)
        except ParameterTreeError as param_error:
            response = {'response': 'LiveViewAdapter GET error: {}'.format(param_error)}
            content_type = 'application/json'
            status = 400

        return ApiAdapterResponse(response, content_type=content_type, status_code=status)

    @response_types('application/json', default='application/json')
    def put(self, path, request):
        """
        Handle a HTTP PUT request from a client, passing it to the Live Viewer Object.

        :param path: path to the resource
        :param request: request object containing data to PUT to the resource
        :return: the requested resource after changing, or an error message and code if invalid
        """
        logging.debug("REQUEST: %s", request.body)
        try:
            data = json_decode(request.body)
            self.live_viewer.set(path, data)
            response, content_type, status = self.live_viewer.get(path)

        except ParameterTreeError as param_error:
            response = {'response': 'LiveViewAdapter PUT error: {}'.format(param_error)}
            content_type = 'application/json'
            status = 400

        return ApiAdapterResponse(response, content_type=content_type, status_code=status)

    def cleanup(self):
        """Clean up the adapter on shutdown. Calls the Live View object's cleanup method."""
        self.live_viewer.cleanup()


class LiveViewer(object):
    """
    Live viewer main class.

    This class handles the major logic of the adapter, including generation of the images from data.
    """

    def __init__(self, endpoints, default_colormap):
        """
        Initialise the LiveViewer object.

        This method creates the IPC channel used to receive images from odin-data and
        assigns a callback method that is called when data arrives at the channel.
        It also initialises the Parameter tree used for HTTP GET and SET requests.
        :param endpoints: the endpoint address that the IPC channel subscribes to.
        """
        logging.debug("Initialising LiveViewer")

        self.img_data = np.arange(0, 1024, 1).reshape(32, 32)
        self.clip_min = None
        self.clip_max = None
        self.header = {}
        self.endpoints = endpoints
        self.ipc_channels = []
        for endpoint in self.endpoints:
            try:
                tmp_channel = SubSocket(self, endpoint)
                self.ipc_channels.append(tmp_channel)
                logging.debug("Subscribed to endpoint: %s", tmp_channel.endpoint)
            except IpcChannelException as chan_error:
                logging.warning("Unable to subscribe to %s: %s", endpoint, chan_error)

        logging.debug("Connected to %d endpoints", len(self.ipc_channels))

        if not self.ipc_channels:
            logging.warning(
                "Warning: No subscriptions made. Check the configuration file for valid endpoints")

        # Define a list of available cv2 colormaps
        self.cv2_colormaps = {
            "Autumn": cv2.COLORMAP_AUTUMN,
            "Bone": cv2.COLORMAP_BONE,
            "Jet": cv2.COLORMAP_JET,
            "Winter": cv2.COLORMAP_WINTER,
            "Rainbow": cv2.COLORMAP_RAINBOW,
            "Ocean": cv2.COLORMAP_OCEAN,
            "Summer": cv2.COLORMAP_SUMMER,
            "Spring": cv2.COLORMAP_SPRING,
            "Cool": cv2.COLORMAP_COOL,
            "HSV": cv2.COLORMAP_HSV,
            "Pink": cv2.COLORMAP_PINK,
            "Hot": cv2.COLORMAP_HOT,
            "Parula": cv2.COLORMAP_PARULA
        }

        # Build a sorted list of colormap options mapping readable name to lowercase option
        self.colormap_options = OrderedDict()
        for colormap_name in sorted(self.cv2_colormaps.keys()):
            self.colormap_options[colormap_name.lower()] = colormap_name

        # Set the selected colormap to the default
        if default_colormap.lower() in self.colormap_options:
            self.selected_colormap = default_colormap.lower()
        else:
            self.selected_colormap = "jet"

        self.rendered_image = self.render_image()

        self.param_tree = ParameterTree({
            "name": "Live View Adapter",
            "endpoints": (self.get_channel_endpoints, None),
            "frame": (lambda: self.header, None),
            "colormap_options": self.colormap_options,
            "colormap_selected": (self.get_selected_colormap, self.set_selected_colormap),
            "data_min_max": (lambda: [int(self.img_data.min()), int(self.img_data.max())], None),
            "frame_counts": (self.get_channel_counts, self.set_channel_counts),
            "clip_range": (lambda: [self.clip_min, self.clip_max], self.set_clip)
        })

    def get(self, path, _request=None):
        """
        Handle a HTTP get request.

        Checks if the request is for the image or another resource, and responds accordingly.
        :param path: the URI path to the resource requested
        :param request: Additional request parameters.
        :return: the requested resource,or an error message and code, if the request is invalid.
        """
        path_elems = re.split('[/?#]', path)
        if path_elems[0] == 'image':
            if self.img_data is not None:
                response = self.rendered_image
                content_type = 'image/png'