#!/usr/bin/python
# -*- coding: utf-8 -*-

"""
=========================================================
PCA example with Iris Data-set
=========================================================

Principal Component Analysis applied to the Iris dataset.

See `here <https://en.wikipedia.org/wiki/Iris_flower_data_set>`_ for more
information on this dataset.

"""
print(__doc__)

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

from sklearn import decomposition
from sklearn import datasets

np.random.seed(5)
# load data
iris = datasets.load_iris()
XX = iris.data
y = iris.target

# run PCA
pca = decomposition.PCA(n_components=3)
pca.fit(XX)
X = pca.transform(XX)

# Plot Principle Components
plt.figure(1)
objects = ['PC-1', 'PC-2', 'PC-3']
variance_percentage = pca.explained_variance_ratio_
y_pos = np.arange(len(objects))
plt.bar(y_pos, variance_percentage, align='center', alpha=0.5)
plt.xticks(y_pos, objects)
plt.ylabel('Explained variance in percent')
plt.title('Explained variance by different principle components')
plt.show()

# Plot Projection onto new (2-D) Feature Space
plt.figure(2)
pc1 = [x[0] for x in X]
pc2 = [x[1] for x in X]
y = np.choose(y, [1, 2, 0]).astype(np.float)
plt.scatter(pc1, pc2, c=y, cmap=plt.cm.cool)
plt.title('Projection onto new Feature Space')
plt.ylabel('Principle Component 2 (PC-2)')
plt.xlabel('Principle Component 1 (PC-1)')
plt.show()


import frappe
from frappe_notification import (
    NotificationClientNotFound,
    InvalidRequest,
    get_active_notification_client)
from typing import Optional
from frappe_notification.utils.cursor_paginator import CursorPaginator, \
    CursorPaginatorExecutionArgs, CursorPaginatorSortBy, CursorPaginatorSortByField, \
    CursorPaginatorSortByDirection

class NotificationLogsFilters(frappe._dict):
    channel: Optional[str]
    channel_id: Optional[str]
    user_identifier: Optional[str]

class NotificationLogsSortByEnum(CursorPaginatorSortByField):
    CREATION = "outbox.creation"

class NotificationLogSortBy(CursorPaginatorSortBy):
    field: NotificationLogsSortByEnum

class GetNotificationLogsExecutionArgs(CursorPaginatorExecutionArgs):
    before: Optional[str]
    after: Optional[str]
    first: Optional[int]
    last: Optional[int]
    sort_by: Optional[NotificationLogSortBy]
    filters: NotificationLogsFilters

def get_notification_logs(args: GetNotificationLogsExecutionArgs):
    filters = args.pop('filters', None)

    # TODO: Find a better way to resolve these values..
    sort_by = args.pop("sort_by", None)
    if sort_by:
        sort_by = NotificationLogSortBy({
            "direction": CursorPaginatorSortByDirection[sort_by.get("direction")],
            "field": NotificationLogsSortByEnum[sort_by.get("field")]
        })
        args["sort_by"] = sort_by

    r = CursorPaginator(
        doctype="Notification Outbox",
        skip_process_filters=True,
        count_resolver=get_notification_logs_count_resolver,
        node_resolver=get_notification_logs_node_resolver,
        default_sorting_fields=["outbox.creation"],
        default_sorting_direction="desc",
        extra_args={"filters": filters}
    )
    return r.execute(args)

def get_notification_logs_count_resolver(paginator: CursorPaginator, filters):
    client = get_active_notification_client()
    if not client:
        raise NotificationClientNotFound()
    notification_filters = paginator.extra_args.get("filters") if paginator.extra_args else None
    conditions = get_notifications_logs_filters(NotificationLogsFilters(notification_filters or {}))

    return frappe.db.sql(f"""
    SELECT
        count(distinct(outbox.name))
    FROM
        `tabNotification Outbox` outbox
    JOIN `tabNotification Outbox Recipient Item` recipient_item
        ON recipient_item.parent = outbox.name AND recipient_item.parenttype = 'Notification Outbox'
    WHERE
        outbox.notification_client = %(client)s
        AND outbox.docstatus = 1
        {conditions}
        AND recipient_item.status = 'Success'
        """, {
        "client": client
    }, as_list=1, debug=0)[0][0]

def get_notification_logs_node_resolver(paginator: CursorPaginator, filters, fields, sorting_fields,
                                        sort_dir, limit):
    client = get_active_notification_client()
    if not client:
        raise NotificationClientNotFound()

    extra_sorting_fields = f", {', '.join(sorting_fields)}"
    order_by = ', '.join([f'{x} {sort_dir}' for x in sorting_fields])

    notification_filters = paginator.extra_args.get("filters") if paginator.extra_args else None
    conditions = get_notifications_logs_filters(NotificationLogsFilters(notification_filters or {}),
                                                filters)

    return frappe.db.sql(f"""
    SELECT
        outbox.name as outbox,
        recipient_item.name as outbox_recipient_row,
        outbox.subject,
        outbox.content,
        recipient_item.time_sent,
        recipient_item.user_identifier,
        recipient_item.channel,
        recipient_item.channel_id,
        recipient_item.seen
        {extra_sorting_fields}
    FROM
        `tabNotification Outbox` outbox
    JOIN `tabNotification Outbox Recipient Item` recipient_item
        ON recipient_item.parent = outbox.name AND recipient_item.parenttype = 'Notification Outbox'
    WHERE
        outbox.notification_client = %(client)s
        AND outbox.docstatus = 1
        {conditions}
        AND recipient_item.status = "Success"
    ORDER BY {order_by}
    LIMIT %(limit_page_length)s
    """, {
        "client": client,
        "limit_page_length": limit
    }, as_dict=1, debug=0)

def get_notifications_logs_filters(filters: NotificationLogsFilters, cursor_filters=None):
    channel = filters.channel
    channel_id = filters.channel_id
    user_identifier = filters.user_identifier

    if not ((channel and channel_id) or user_identifier):
        raise InvalidRequest(message=frappe._(
            "Please specify either (channel, channel_id) or user_identifier"
        ))

    conditions = []

    if cursor_filters:
        conditions.extend(cursor_filters)

    if channel:
        conditions.append(f"recipient_item.channel = '{channel}'")

    if channel_id:
        conditions.append(f"recipient_item.channel_id = '{channel_id}'")

    if user_identifier:
        conditions.append(f"recipient_item.user_identifier = '{user_identifier}'")

    return f' AND {" AND ".join(conditions)}' if len(conditions) else ""


from django.urls import path, include
from rest_framework.routers import DefaultRouter
from apps.asistencia.views import (
    NovedadListView,
    AprendizViewSet,
    AsistenciaViewSet,
    NovedadAprendizView,
    NovedadAcceptanceView,
    InstructorViewSet,
    VerAsistenciasInstructorViewSet,
    UpdateAsistenciaView,
)

# Crear un enrutador para las vistas basadas en ViewSets
router = DefaultRouter()
# Usuarios autenticados de cualquier tipo (Aprendiz, Instructor, Coordinación o Bienestar).
# Los aprendices solo verán sus propias novedades y datos de asistencia.
# Los instructores podrán ver novedades y actualizar datos de asistencia de sus aprendices.
# Coordinación y Bienestar podrán editar novedades.
router.register(r"novedades", NovedadListView, basename="novedades")
# Usuarios autenticados con el tipo de usuario "Aprendiz" podrán acceder y actualizar sus propios datos.
router.register(r"aprendices", AprendizViewSet, basename="aprendices")
# Usuarios autenticados con el tipo de usuario "Aprendiz" podrán acceder a sus propias asistencias.
router.register(r"asistencias", AsistenciaViewSet, basename="asistencias")
# Usuarios autenticados con el tipo de usuario "Instructor" podrán acceder a las novedades de sus aprendices.
router.register(
    r"novedades-aprendices", NovedadAprendizView, basename="novedades-aprendices"
)
# Usuarios autenticados con el tipo de usuario "Coordinación" o "Bienestar" podrán acceder y actualizar novedades para su aceptación.
router.register(
    r"novedades-acceptation", NovedadAcceptanceView, basename="novedades-acceptation"
)
# Usuarios Instructores pueden ver sus datos y actualizarlos
router.register(r"instructores", InstructorViewSet, basename="instructores")
# # Usuarios Instructores pueden ver sus datos y actualizarlos

# vista para obtener las asistencias de los aprendices
router.register(
    r"ver_asistencias/(?P<instructor_id>\d+)",
    VerAsistenciasInstructorViewSet,
    basename="ver-asistencias",
)

# vista para actualizar la asistencia
# router.register(r'actualizar_asistencia/<int:pk>/', UpdateAsistenciaView, basename='actualizar-asistencia')

urlpatterns = [
    path("", include(router.urls)),
    path(
        "actualizar_asistencia/<int:pk>/",
        UpdateAsistenciaView.as_view(),
        name="actualizar-asistencia",
    ),
]

urlpatterns += router.urls


import gym
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
from mountain_car_RBF_q_learning import FeatureTransformer

class HiddenLayer:
    def __init__(self, M1, M2, f = tf.nn.tanh, use_bias = True, zeros = False):
        if zeros:
            W = np.zeros((M1, M2)).astype(np.float32)
            self.W = tf.Variable(W)
        else:
            self.W = tf.Variable(tf.random_normal(shape=(M1, M2)))

        self.params = [self.W]

        self.use_bias = use_bias
        if use_bias:
            self.b = tf.Variable(np.zeros(M2).astype(np.float32))
            self.params.append(self.b)
        self.f = f

    def forward(self, X):
        if self.use_bias:
            a = tf.matmul(X, self.W) + self.b
        else:
            a = tf.matmul(X, self.W)
        return self.f(a)

# approximates pi(a | s)
class PolicyModel:
    def __init__(self, ft, D, hidden_layer_sizes_mean=[], hidden_layer_sizes_std=[]):

        # save inputs for copy
        self.ft = ft
        self.D = D
        self.hidden_layer_sizes_mean = hidden_layer_sizes_mean
        self.hidden_layer_sizes_std = hidden_layer_sizes_std

        ### model the mean ###
        self.mean_layers = []
        M1 = D
        for M2 in hidden_layer_sizes_mean:
            layer = HiddenLayer(M1, M2)
            self.mean_layers.append(layer)
            M1 = M2

        # final layer
        layer = HiddenLayer(M1, 1, lambda x:x, use_bias=False, zeros=True)
        self.mean_layers.append(layer)

        ### model the standard deviation ###
        self.std_layers = []
        M1 = D
        for M2 in hidden_layer_sizes_std:
            layer = HiddenLayer(M1, M2)
            self.std_layers.append(layer)

        # final layer
        layer = HiddenLayer(M1, 1, tf.nn.softplus, use_bias=False, zeros=False)
        self.std_layers.append(layer)

        # gather params
        self.params = []
        for layer in (self.mean_layers + self.std_layers):
            self.params += layer.params

        # inputs and targets
        self.X = tf.placeholder(tf.float32, shape=(None, D), name = 'X')
        self.actions = tf.placeholder(tf.float32, shape=(None,), name ='actions')
        self.advantages = tf.placeholder(tf.float32, shape=(None,), name='advantages')

        def get_ouput(layers):
            Z = self.X
            for layer in layers:
                Z = layer.forward(Z)
            return tf.reshape(Z, [-1])

        # calculate output and cost
        mean = get_ouput(self.mean_layers)
        std = get_ouput(self.std_layers) + 1e-4 # smoothing
        norm = tf.contrib.distributions.Normal(mean, std)
        self.predict_op = tf.clip_by_value(norm.sample(), -1, 1)

    def set_session(self, session):
        self.session = session

    # Since we need to save the best params but initialize the
    # current params we're testing, we no longer can use
    # global_variables_initializer
    def init_vars(self):
        init_op = tf.variables_initializer(self.params)
        self.session.run(init_op)

    def predict(self, X):
        X = np.atleast_2d(X)
        X = self.ft.transform(X)
        return self.session.run(self.predict_op, feed_dict={self.X:X})

    def sample_action(self, X):
        p = self.predict(X)[0]
        return p

    def copy_from(self, other):
        # Collect all the operations
        ops = []
        my_params = self.params
        other_params = other.params
        for p, q in zip(my_params, other_params):
            actual = self.session.run(q)
            op = p.assign(actual)
            ops.append(op)
        # Now run them all
        self.session.run(ops)

    def copy(self):
        # Create a new model with the same constants
        clone = PolicyModel(self.ft, self.D, self.hidden_layer_sizes_mean, self.hidden_layer_sizes_std)
        clone.set_session(self.session)
        clone.init_vars()
        # Now copy all the variables
        clone.copy_from(self)
        return clone

    def perturb_params(self):
        ops = []
        for p in self.params:
            v = self.session.run(p)
            noise = np.random.randn(*v.shape) / np.sqrt(v.shape[0]) * 5.0
            # With a 0.1 probability, start completely from scratch
            if np.random.random() < 0.1:
                op = p.assign(noise)
            else:
                op = p.assign(v + noise)
            ops.append(op)
        self.session.run(ops)

def play_one(env, pmodel):
    observation = env.reset()
    done = False
    total_reward = 0
    iters = 0

    while not done:
        action = pmodel.sample_action(observation)
        # oddly the mountain car environment requires the action to be in
        # an object where the actual action is stored in object[0]
        observation, reward, done, info = env.step([action])

        total_reward += reward
        iters += 1
    return total_reward

def play_multiple_episodes(env, T, pmodel, print_iters=False):
    total_rewards = np.empty(T)

    for t in range(T):
        total_rewards[t] = play_one(env, pmodel)

        if print_iters:
            print(t, 'avg so far:', total_rewards[:t+1].mean())

    avg_total_rewards = total_rewards.mean()
    print('avg total rewards:', avg_total_rewards)
    return avg_total_rewards

def random_search(env, pmodel):
    total_rewards = []
    best_avg_total_reward = float('-inf')
    best_pmodel = pmodel
    num_episodes_per_param_test = 3
    for t in range(100):
        tmp_model = best_pmodel.copy()
        tmp_model.perturb_params()

        avg_total_rewards = play_multiple_episodes(env, num_episodes_per_param_test, tmp_model)
        total_rewards.append(avg_total_rewards)

        if avg_total_rewards > best_avg_total_reward:
            best_pmodel = tmp_model
            best_avg_total_reward = avg_total_rewards

    return total_rewards, best_pmodel

def main():
    env = gym.make('MountainCarContinuous-v0')
    ft = FeatureTransformer(env, n_components=100)
    D = ft.dimension
    pmodel = PolicyModel(ft, D, [], [])
    session = tf.InteractiveSession()
    pmodel.set_session(session)
    pmodel.init_vars()

    total_rewards, pmodel = random_search(env, pmodel)
    print('max reward:', np.max(total_rewards))

    # play 100 episodes and check the average
    avg_totalrewards = play_multiple_episodes(env, 100, pmodel, print_iters=True)
    print('avg reward over 100 episodes with best models:', avg_totalrewards)

    plt.plot(total_rewards)
    plt.title('Rewards')
    plt.show()

main()


## 구름 알고리즘 챌린지 7주차 3. 구름이의 여행 2

from collections import deque
# from collections import defaultdict
node, line, k = map(int, input().split())

graph = [ [] for _ in range(node+1)]

for i in range(line):
    a, b = map(int, input().split())
    graph[a].append(b)

check = [0]*(node+1)

def BFS(idx):
    global check
    queue = deque()
    queue.append(idx)
    while queue:
        idx = queue.popleft()
        for i in graph[idx]:
            if check[i] == 0:
                check[i] = check[idx]+1
                if i == k:
                    return check[k]
                else:
                    queue.append(i)
    return -1

## 4 -> 5 -> 3 -> 1 -> 4
print(BFS(k))


# This application note shows how to extract detections from the
# measurements
# This appnote only works with one TX antenna enabled.
# First the range-Doppler maps are calculated for all the enabled channels
# and in the next step a detection process is carried out and then the
# angles for the detections are calculated and returned as list of
# detections to matlab.

# (1) Connect to RadarLog
# (2) Enable Supply
# (3) Configure RX
# (4) Configure TX
# (5) Start Measurements

#--------------------------------------------------------------------------
# Include all necessary packages
#--------------------------------------------------------------------------
import sys, os
sys.path.append("../")

import  src.cmd_modules.Rbk2Adf24Tx2Rx8 as Rbk2Adf24Tx2Rx8
import  time as time
import  numpy
from    numpy import *

from    pyqtgraph.Qt import QtGui, QtCore
import  pyqtgraph as pg

#--------------------------------------------------------------------------
# Configure Script
#--------------------------------------------------------------------------
Disp_DT = 1
# Display Video (only possible if camera is enabled in RadServe)
Disp_Video = 0

if Disp_DT > 0:
    App = QtGui.QApplication([])

    viewWin = QtGui.QMainWindow()
    view = pg.GraphicsLayoutWidget()
    viewWin.setCentralWidget(view)
    viewWin.show()
    viewWin.setWindowTitle('Detections')
    viewPlot = view.addPlot()
    viewPlot.setXRange(-150, 150, padding=0)
    viewPlot.setYRange(0, 200, padding=0)
    viewImg = pg.ScatterPlotItem(size=10, pen=None, pxMode=True)
    viewPlot.addItem(viewImg)

#--------------------------------------------------------------------------
# Setup Connection
#--------------------------------------------------------------------------
Brd = Rbk2Adf24Tx2Rx8.Rbk2Adf24Tx2Rx8('RadServe', '127.0.0.1', 8000, '192.168.1.1')

# Reset board: resets timing unit in case a measurement has been configured
# previously
Brd.BrdRst()
# Enable RF power supply and wait for supply
Brd.BrdPwrEna()

#--------------------------------------------------------------------------
# Load Calibration Data
#--------------------------------------------------------------------------
dCalCfg = dict()
dCalCfg['Mask'] = 1
dCalCfg['Len'] = 16
CalData = Brd.BrdGetCalData(dCalCfg)

# Configure Board as Master and set ADC clock to 40 MHz
# Brd.BrdSetRole('Ms', 20e6); Configure to 20 MHz sampling clock
# Brd.BrdSetRole('Ms', 40e6);
Brd.BrdSetRole('Ms', 40e6)

# Display status of board: Just an auxiliary information
Brd.BrdDispSts()
Brd.BrdDispInf()

#--------------------------------------------------------------------------
# Configure Receiver and Transmitters (static setup with single Tx turned on)
#--------------------------------------------------------------------------
Brd.RfRxEna()
Brd.RfTxEna(1, 80)

#--------------------------------------------------------------------------
# Configure AFE5801
#--------------------------------------------------------------------------
Brd.Set('AfeLowNoise',0)
# Enable/Disable internal DC coupling
Brd.Set('AfeIntDcCoupling',1)
# Ramp pattern can be used to test communication: sample data is replaced
# by linarly increasing ramp
Brd.Set('AfePatRamp','Off')
# Set AfeGain in dB (-5 - 30 dB); The closest available value is configured
Brd.Set('AfeGaindB', 20)

if Disp_Video > 0:
    # Configure RadServe to output video on TCP/IP connection
    Brd.CfgRadServe('AddVideoToDataPort',1);

#--------------------------------------------------------------------------
# Configure Up-Chirp
#--------------------------------------------------------------------------
dCfg = dict()
dCfg['fStrt'] = 24e9
dCfg['fStop'] = 24.2e9
dCfg['TRampUp'] = 256e-6
dCfg['TInt'] = 80e-3
dCfg['N'] = 1024
dCfg['IniEve'] = 0
dCfg['IniTim'] = 100e-3
dCfg['Np'] = 64
dCfg['Tp'] = 300e-6

Brd.Set('DmaMult', dCfg['Np'])
Brd.ConSet('Mult', dCfg['Np'])

#--------------------------------------------------------------------------
# Use FPGA triggered measurement mode:
# FPGA generates timing and triggers RCC1010
# The timing is generated in multiples of ADC clock cycles. The default
# clock is set to 20 MHz.
#--------------------------------------------------------------------------
Brd.RfMeas('ExtTrigUp',dCfg)

#--------------------------------------------------------------------------
# Read actual configuration
#--------------------------------------------------------------------------
NrChn = Brd.Get('NrChn')
N = Brd.Get('N')
fs = Brd.Get('fs')

#--------------------------------------------------------------------------
# Configure Range Profile
#--------------------------------------------------------------------------
# Set the fft size for the range profiles
Brd.Computation.SetParam('Range_NFFT', dCfg['N'] * 2)
# Set the minimum and maximum range for returning the range profiles
Brd.Computation.SetParam('Range_RMin', 20)
Brd.Computation.SetParam('Range_RMax', 200)

#--------------------------------------------------------------------------
# Configure Velocity FFT
#--------------------------------------------------------------------------
# Configure Doppler FFT
Brd.Computation.SetParam('Vel_NFFT', pow(2, ceil(log2(dCfg['Np']))))

#--------------------------------------------------------------------------
# Configure Detector (Local Maxima in Range-Doppler map)
#--------------------------------------------------------------------------
Brd.Computation.SetParam('Thres_Old', 0.95)
Brd.Computation.SetParam('Thres_Mult', 2)
# Brd.Computation.SetParam('Thres_Range1', 70)
# Brd.Computation.SetParam('Thres_Mult2', 2)
# Brd.Computation.SetParam('Thres_Range2', 80)

#--------------------------------------------------------------------------
# Configure Angular FFT
#--------------------------------------------------------------------------
# Angular FFT
Brd.Computation.SetParam('Ang_NFFT', 256)
Brd.Computation.SetParam('Ang_Interpolate', 1)

Brd.Computation.SetType('DetectionList')

for Cycles in range(0, 10000):

    Detections        =   Brd.BrdGetData(1)

    viewImg.clear()
    tar = []
    for Idx in range(size(Detections)):

        x = Detections[Idx]['Range'] * math.sin(Detections[Idx]['Ang'])
        y = Detections[Idx]['Range'] * math.cos(Detections[Idx]['Ang'])
        if (Detections[Idx]['Vel'] < 0):
            if (Detections[Idx]['Vel'] < -0.5):
                tar.append({'pos': (x,y), 'data': 1, 'brush': pg.mkBrush(255,0,0,255), 'size': 7})
            else:
                tar.append({'pos': (x,y), 'data': 1, 'brush': pg.mkBrush(255,0,0,255), 'size': 7})
        else:
            if (Detections[Idx]['Vel'] > 0.5):
                tar.append({'pos': (x,y), 'data': 1, 'brush': pg.mkBrush(0,0,255,255), 'size': 7})
            else:
                tar.append({'pos': (x,y), 'data': 1, 'brush': pg.mkBrush(0,0,255,255), 'size': 7})

    viewImg.addPoints(tar)
    pg.QtGui.QApplication.processEvents()

Brd.BrdRst()
Brd.BrdPwrDi()

del Brd

App.quit()


from django.shortcuts import render
from django.http import JsonResponse
from ..models import Type, Category

def getTypeByCategory(request):
    category_id = request.GET['category_id']
    category = Category.objects.get(pk=category_id)
    types = Type.objects.filter(category=category)
    res = []

    for t in types:
        res.append((t.id, t.name))
    return JsonResponse({'types': res})


from __future__ import absolute_import, division, print_function

# Copyright 2010 University of California
# All rights reserved

# This is an approximate mimic of the NSSegmentedControl in Cocoa.  The
# Apple control has many more styles than this, but the basic functionality
# is the same, with three selection modes (any, one, or none).  Either
# labels, bitmaps, or both are supported.

# TODO:
#   - Windows bugs
#   - additional styles?

import wx
from six.moves import range

SEGBTN_ROUNDED_CORNERS = 1
SEGBTN_VERTICAL = 2
SEGBTN_HORIZONTAL = 4
SEGBTN_INVERT_BITMAP = 8
SEGBTN_HIGHLIGHT = 16
SEGBTN_RADIO_ALLOW_NONE = 32

SEGBTN_DEFAULT_STYLE = SEGBTN_HORIZONTAL|SEGBTN_ROUNDED_CORNERS

if wx.Platform == '__WXMAC__' :
  _DEFAULT_PADDING = 8
elif wx.Platform == '__WXGTK__' :
  _DEFAULT_PADDING = 6
else :
  _DEFAULT_PADDING = 6

class SegmentedControl(wx.Control):
  def __init__(self,
                parent,
                id=wx.ID_ANY,
                pos=wx.DefaultPosition,
                size=wx.DefaultSize,
                style=SEGBTN_DEFAULT_STYLE,
                name=wx.ButtonNameStr,
                border=0,
                pad=_DEFAULT_PADDING):
    wx.Control.__init__(self, parent, id, pos=pos, size=size,
      style=wx.NO_BORDER,
      name=name)
    self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM)
    self.InheritAttributes()
    self.segments = []
    self.values = [] # for radio and toggle versions
    self._clicked_segment = None
    self._padding = _DEFAULT_PADDING
    self._border = border
    self._style = style
    self._border_color = (0, 0, 0)
    if wx.Platform == '__WXGTK__' :
      self.SetFont(wx.Font(12, wx.FONTFAMILY_DEFAULT, wx.NORMAL, wx.NORMAL))

    # Event Handlers
    self.Bind(wx.EVT_PAINT, lambda evt: self.__DrawButtons())
    self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnErase)
    #self.Bind(wx.EVT_SET_FOCUS, self.OnFocus)
    #self.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus)
    self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
    self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
    #self.Bind(wx.EVT_LEFT_DCLICK, lambda evt: self.ToggleState())
    self.Bind(wx.EVT_ENTER_WINDOW, self.OnEnter)
    self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeave)
    self.Bind(wx.EVT_MOTION, self.OnMotion)

  def AddSegment(self, label="", bitmap=None):
    if label == "" and bitmap is None :
      raise RuntimeError("Must specify either a label or a bitmap or both "+
        "for SegmentControl.AddSegment().")
    self.segments.append((label, bitmap))
    self.values.append(False)

  def InsertSegment(self, index, label="", bitmap=None):
    if label == "" and bitmap is None :
      raise RuntimeError("Must specify either a label or a bitmap or both "+
        "for SegmentControl.InsertSegment().")
    self.segments.insert(index, (label, bitmap))
    self.values.append(True)

  def Realize(self):
    self.SetSize(self.DoGetBestSize())

  def SetBorderColor(self, color):
    self._border_color = color

  def DoGetBestSize(self):
    dc = wx.ClientDC(self)
    dc.SetFont(self.GetFont())
    n_seg = len(self.segments)
    buf = 1
    if n_seg == 0 :
      (w, h) = dc.GetTextExtent("XXX")
      w += self._padding * 2
      h += self._padding * 2
    else :
      txt_w, txt_h = self.__OverallSegmentContentSize(dc)
      seg_w = txt_w + (2 * self._padding)
      seg_h = txt_h + (2 * self._padding)
      if self._style & SEGBTN_VERTICAL :
        w = seg_w + 2
        h = 1 + ((seg_h + buf) * n_seg)
      else :
        w = 1 + ((seg_w + buf) * n_seg)
        h = seg_h + 2
    w += (self._border * 2)
    h += (self._border * 2)
    return (w, h)

  def __OverallSegmentContentSize(self, dc):
    max_h = 0
    max_w = 0
    for (label, bitmap) in self.segments :
      (text_w, text_h) = (0, 0)
      (bmp_w, bmp_h) = (0, 0)
      if label != "" :
        (text_w, text_h) = dc.GetTextExtent(label)
      if bitmap is not None :
        (bmp_w, bmp_h) = bitmap.GetSize()
      if label != "" and bitmap is not None :
        seg_w = max(text_w, bmp_w)
        seg_h = text_h + bmp_h + 2
      elif label != "" :
        (seg_w, seg_h) = (text_w, text_h)
      else :
        (seg_w, seg_h) = (bmp_w, bmp_h)
      if seg_h > max_h :
        max_h = seg_h
      if seg_w > max_w :
        max_w = seg_w
    return (max_w, max_h)

  def __DrawButtons(self):
    dc = wx.AutoBufferedPaintDCFactory(self)
    if wx.Platform == '__WXMSW__' :
      dc.SetBackground(wx.Brush(self.GetParent().GetBackgroundColour()))
      dc.Clear()
    gc = wx.GCDC(dc)
    gc.SetBackgroundMode(wx.TRANSPARENT)
    gc.SetFont(self.GetFont())

    (w, h) = self.GetSize() #DoGetBestSize()
    w -= (self._border * 2)
    h -= (self._border * 2)
    n_seg = len(self.segments)
    rgc = gc.GetGraphicsContext()
    if self._style & SEGBTN_VERTICAL :
      # XXX a smaller gradient looks better for vertical controls
      gradient_brush = rgc.CreateLinearGradientBrush(0, 0, w, 0, (240,240,240),
        (200,200,200))
    else :
      gradient_brush = rgc.CreateLinearGradientBrush(0, 0, 0, h, (250,250,250),
        (185,185,185))
    rgc.SetBrush(gradient_brush)
    border_pen = wx.Pen(self._border_color, 1)
    # XXX wx.GCDC on GTK draws thin black lines as thicker dark grey lines
    # due to antialiasing.
    if wx.Platform == '__WXGTK__' :
      gc.SetPen(wx.TRANSPARENT_PEN)
      dc.SetPen(border_pen)
      dc.SetBrush(wx.TRANSPARENT_BRUSH)
    else :
      gc.SetPen(border_pen)
    if self._style & SEGBTN_ROUNDED_CORNERS :
      gc.DrawRoundedRectangle(self._border, self._border, w, h, 3)
      if wx.Platform == '__WXGTK__' :
        dc.DrawRoundedRectangle(self._border, self._border, w, h, 3)
    else :
      gc.DrawRectangle(self._border, self._border, w, h)
      if wx.Platform == '__WXGTK__' :
        dc.DrawRectangle(self._border, self._border, w, h)
    gc.SetPen(wx.TRANSPARENT_PEN)
    gc.SetBrush(wx.TRANSPARENT_BRUSH)
    if n_seg == 0 :
      gc.SetTextForeground('red')
      gc.DrawText("XXX", self._padding, self._padding)
    else :
      gc.SetTextForeground('black')
      gc.SetPen(wx.Pen('black', 1)) #BLACK_PEN)
      buf = 1
      (txt_w, txt_h) = self.__OverallSegmentContentSize(gc)
      if self._style & SEGBTN_VERTICAL :
        seg_w = w - 2
        seg_h = ((h - 1) / n_seg) - buf
      else :
        seg_w = ((w - 1) / n_seg) - buf
        seg_h = h -  2
      current_x = 1 + self._border
      current_y = 1 + self._border
      for i, (label, bitmap) in enumerate(self.segments):
        gc.SetPen(border_pen)
        dc.SetPen(border_pen)
        (txt_x, txt_y) = (self._padding + 1, self._padding + 1)
        if self._style & SEGBTN_VERTICAL :
          if i > 0 :
            dc.DrawLine(int(self._border), int(current_y), int(w + self._border - 1),
              int(current_y))
            current_y += 1
          txt_y += current_y
          txt_x += self._border
        else :
          if i > 0 :
            dc.DrawLine(int(current_x), int(self._border + 1), int(current_x),
              int(h + self._border - 1))
            current_x += 1
          txt_x += current_x
          txt_y += self._border
        if ((self.HasCapture() and i == self._clicked_segment) or
             self.values[i] == True):
          xo = current_x + (seg_w / 2)
          yo = current_y + (seg_h / 2)
          radius  = max(seg_w, seg_h) - 1
          brush = rgc.CreateRadialGradientBrush(xo, yo, xo, yo, radius,
            (220,220,220), (120,120,120))
          rgc.SetBrush(brush)
          gc.SetPen(wx.TRANSPARENT_PEN)
          if self._style & SEGBTN_ROUNDED_CORNERS and (i == 0 or i == n_seg):
            gc.DrawRoundedRectangle(int(current_x), int(current_y), int(seg_w), int(seg_h), 1)
          else :
            gc.DrawRectangle(int(current_x), int(current_y), int(seg_w), int(seg_h))
          #gc.DrawRectangle(current_x, current_y, total_w, total_h)
        rgc.SetBrush(gradient_brush)
        gc.SetBrush(wx.TRANSPARENT_BRUSH)
        gc.SetPen(wx.BLACK_PEN)
        label_w, label_h = gc.GetTextExtent(label)
        if bitmap is not None :
          (img_w, img_h) = bitmap.GetSize()
          img_x, img_y = txt_x, txt_y
          if label != "" :
            if wx.Platform == '__WXGTK__' :
              img_y -= 4
            extra_h_space = seg_w - img_w - (self._padding * 2)
            extra_v_space = seg_w - img_h - label_h - (self._padding * 2)
          else :
            extra_h_space = seg_w - img_w - (self._padding * 2)
            extra_v_space = seg_h - img_h - (self._padding * 2)
          if extra_h_space > 0 :
            img_x += extra_h_space / 2
          if extra_v_space > 0 :
            img_y += extra_v_space / 2
          gc.DrawBitmap(bitmap, img_x, img_y, bitmap.GetMask() != None)
          txt_y += img_h
          if (wx.Platform == '__WXMSW__'):
            txt_y -= 2
        if label != "" :
          #print seg_w, label_w
          extra_w = seg_w - label_w - (self._padding*2)
          if extra_w > 0 :
            txt_x += extra_w / 2
          extra_h = seg_h - label_h - (self._padding*2)
          if extra_h > 0 :
            txt_y += extra_h / 2
          gc.DrawText(label, int(txt_x), int(txt_y))
        if self._style & SEGBTN_VERTICAL :
          current_y += seg_h
        else :
          current_x += seg_w

  def HandleClick(self):
    pass

  def HitTest(self, event):
    x, y = event.GetPositionTuple()
    dc = wx.ClientDC(self)
    n_seg = len(self.segments)
    buf = 1
    (w, h) = self.DoGetBestSize()
    if self._style & SEGBTN_VERTICAL :
      seg_w = w - 2
      seg_h = ((h - 1) / n_seg) - buf
    else :
      seg_w = ((w - 1) / n_seg) - buf
      seg_h = h -  2
    if n_seg == 0 :
      return None
    elif (x < 1 or x > (w - 1)) or (y < 1 or y > (h-1)):
      return None
    elif self._style & SEGBTN_VERTICAL :
      current_y = 1
      for i in range(n_seg):
        max_y = current_y + seg_h
        if y > current_y and y < max_y :
          return i
        current_y = max_y
    else :
      current_x = 1
      for i in range(n_seg):
        max_x = current_x + seg_w -1
        if x >= current_x and x < max_x :
          return i
        current_x = max_x + 2
    return None

  def OnLeftDown(self, event):
    segment_id = self.HitTest(event)
    self._have_mouse = True
    self._clicked_segment = segment_id
    self.CaptureMouse()
    self.SetFocus()
    self.Refresh()

  def OnLeftUp(self, event):
    if self.HasCapture():
      self.ReleaseMouse()
    segment_id = self.HitTest(event)
    if segment_id is not None and segment_id == self._clicked_segment :
      self.HandleClick()
    self._clicked_segment = None
    self.Refresh()

  def OnEnter(self, event):
    if event.LeftDown():
      self.CaptureMouse()
      self.Refresh()

  def OnLeave(self, event):
    pass
    if self.HasCapture():
      self.ReleaseMouse()
      self.Refresh()
#      self._clicked_segment = None

  def OnMotion(self, event):
    return
    if not self.IsEnabled():
      return
    x,y = event.GetPositionTuple()
    w,h = self.GetClientSizeTuple()
    if (x<0 or y<0 or x>=w or y>=h) and self.HasCapture():
      #self._clicked_segment = None
      print(-3)
      self.ReleaseMouse()
      self.Refresh()
      return
    elif event.LeftDown():
      print(-2)
      self.CaptureMouse()
      self.Refresh()
      return
    event.Skip()

  def OnErase(self, event):
    pass

  def OnFocus(self, event):
    pass

  def OnKillFocus(self, event):
    pass

class SegmentedButtonControl(SegmentedControl):
  def HandleClick(self):
    bevt = wx.CommandEvent(wx.wxEVT_COMMAND_BUTTON_CLICKED, self.GetId())
    bevt.SetEventObject(self)
    bevt.SetString(self.GetLabel())
    bevt.SetClientData(self._clicked_segment)
    wx.PostEvent(self.GetParent(), bevt)
    self._clicked_segment = None

class SegmentedRadioControl(SegmentedControl):
  def HandleClick(self):
    index = self._clicked_segment
    assert index >= 0
    if self.values[index] == True and self._style & SEGBTN_RADIO_ALLOW_NONE :
      self.values[index] = False
    elif not self.values[index] :
      self.SetSelection(index)
    bevt = wx.CommandEvent(wx.wxEVT_COMMAND_RADIOBUTTON_SELECTED, self.GetId())
    bevt.SetEventObject(self)
    bevt.SetString(self.GetLabel())
    bevt.SetClientData(self.GetSelection())
    wx.PostEvent(self.GetParent(), bevt)
    self._clicked_segment = None

  def GetSelection(self):
    assert self.values.count(True) <= 1
    if True in self.values :
      return self.values.index(True)
    return -1

  def SetSelection(self, index):
    self.values = [False] * len(self.segments)
    self.values[index] = True

class SegmentedToggleControl(SegmentedControl):
  def HandleClick(self):
    index = self._clicked_segment
    assert index >= 0
    old_value = self.values[index]
    self.values[index] = not old_value
    bevt = wx.CommandEvent(wx.wxEVT_COMMAND_TOGGLEBUTTON_CLICKED, self.GetId())
    bevt.SetEventObject(self)
    bevt.SetString(self.GetLabel())
    bevt.SetClientData(self._clicked_segment)
    wx.PostEvent(self.GetParent(), bevt)
    self._clicked_segment = None

  def GetValue(self, index):
    return self.values[index]

  def SetValue(self, index, value):
    self.values[index] = value

#-----------------------------------------------------------------------
if __name__ == "__main__" :
  def OnButton(evt):
    print("Button clicked: %d" % evt.GetClientData())
  def OnRadio(evt):
    print("Radio button selection: %d" % evt.GetClientData())
  def OnToggle(evt):
    index = evt.GetClientData()
    print("Toggle button clicked: %d, %s" % (index,
      evt.GetEventObject().GetValue(index)))
  try :
    from wxtbx import bitmaps
  except ImportError :
    bitmaps = None
  app = wx.App(0)
  frame = wx.Frame(None, -1, "Test frame")
  toolbar = frame.CreateToolBar()
  panel = wx.Panel(frame, -1, size=(640,480))
  sizer = wx.BoxSizer(wx.HORIZONTAL)
  panel.SetSizer(sizer)
  v_sizer = wx.BoxSizer(wx.VERTICAL)
  btn = SegmentedButtonControl(panel,
    style=SEGBTN_HORIZONTAL) #|SEGBTN_AUTOSIZE)
  btn.AddSegment("Prev")
  btn.AddSegment("Home")
  btn.AddSegment("Search")
  btn.AddSegment("Help")
  btn.AddSegment("Next")
  frame.Bind(wx.EVT_BUTTON, OnButton, btn)
  v_sizer.Add(wx.StaticText(panel, -1, "Standard buttons"), 0, wx.ALL, 5)
  v_sizer.Add(btn, 0, wx.ALL, 5)
  sizer.Add(v_sizer)
  if True :
    tbtn = SegmentedButtonControl(toolbar,
      style=SEGBTN_HORIZONTAL|SEGBTN_ROUNDED_CORNERS,
      border=4)
    tbtn.AddSegment("Prev")
    tbtn.AddSegment("Home")
    tbtn.AddSegment("Search")
    tbtn.AddSegment("Help")
    tbtn.AddSegment("Next")
    tbtn.Realize()
    (w,h) = tbtn.GetSize()
    toolbar.SetToolBitmapSize((h,h))
    frame.Bind(wx.EVT_BUTTON, OnButton, tbtn)
    toolbar.AddControl(tbtn)
    #toolbar.AddControl(wx.Button(toolbar, -1, "Hello"))
    btn2 = SegmentedRadioControl(panel)
    btn2.AddSegment("Rotate")
    btn2.AddSegment("Pan")
    btn2.AddSegment("Zoom")
    btn2.SetSelection(0)
    frame.Bind(wx.EVT_RADIOBUTTON, OnRadio, btn2)
    v_sizer.Add(wx.StaticText(panel, -1, "Radio buttons"), 0, wx.ALL, 5)
    v_sizer.Add(btn2, 0, wx.ALL, 5)
    btn3 = SegmentedToggleControl(panel)
    btn3.AddSegment("Maps")
    btn3.AddSegment("Models")
    btn3.AddSegment("Selection")
    btn3.Realize()
    frame.Bind(wx.EVT_TOGGLEBUTTON, OnToggle, btn3)
    v_sizer.Add(wx.StaticText(panel, -1, "Toggle buttons"), 0, wx.ALL, 5)
    v_sizer.Add(btn3, 0, wx.ALL, 5)
    if (bitmaps is not None) and (bitmaps.icon_lib is not None):
      bmp1 = bitmaps.fetch_icon_bitmap("actions", "1leftarrow", 16)
      bmp2 = bitmaps.fetch_icon_bitmap("actions", "gohome", 16)
      bmp3 = bitmaps.fetch_icon_bitmap("actions", "viewmag", 16)
      bmp4 = bitmaps.fetch_icon_bitmap("actions", "1rightarrow", 16)
      btn4 = SegmentedButtonControl(panel)
      btn4.AddSegment(bitmap=bmp1)
      btn4.AddSegment(bitmap=bmp2)
      btn4.AddSegment(bitmap=bmp3)
      btn4.AddSegment(bitmap=bmp4)
      v_sizer.Add(wx.StaticText(panel, -1, "Bitmap buttons"), 0, wx.ALL, 5)
      v_sizer.Add(btn4, 0, wx.ALL, 5)
      frame.Bind(wx.EVT_BUTTON, OnButton, btn4)
      btn5 = SegmentedButtonControl(panel,
        style=SEGBTN_ROUNDED_CORNERS)
      btn5.AddSegment(label="Back", bitmap=bmp1)
      btn5.AddSegment(label="Home", bitmap=bmp2)
      btn5.AddSegment(label="Search", bitmap=bmp3)
      btn5.AddSegment(label="Next", bitmap=bmp4)
      btn5.SetFont(wx.Font(9, wx.FONTFAMILY_DEFAULT, wx.NORMAL, wx.NORMAL))
      v_sizer.Add(wx.StaticText(panel, -1, "Bitmaps and text"), 0, wx.ALL, 5)
      v_sizer.Add(btn5, 0, wx.ALL, 5)
      frame.Bind(wx.EVT_BUTTON, OnButton, btn5)
    vbtn1 = SegmentedButtonControl(panel,
      style=SEGBTN_VERTICAL)
    vbtn1.AddSegment("Up")
    vbtn1.AddSegment("Home")
    vbtn1.AddSegment("Search")
    vbtn1.AddSegment("Down")
    frame.Bind(wx.EVT_BUTTON, OnButton, vbtn1)
    sizer.Add(vbtn1, 0, wx.ALL, 20)
    vbtn2 = SegmentedButtonControl(panel,
      style=SEGBTN_VERTICAL|SEGBTN_ROUNDED_CORNERS,
      border=8)
    vbtn2.AddSegment("Up")
    vbtn2.AddSegment("Home")
    vbtn2.AddSegment("Search")
    vbtn2.AddSegment("Down")
    frame.Bind(wx.EVT_BUTTON, OnButton, vbtn2)
    sizer.Add(vbtn2, 0, wx.ALL, 20)
  sizer.Layout()
  sizer.Fit(panel)
  frame.Fit()
  toolbar.Realize()
  frame.Show()
  app.MainLoop()


# -*- coding: utf-8 -*-

"""
Created on Jul 29, 2021

@author: Frank Feuerbacher

"""

from enum import auto, Enum
import sys
import threading

import xbmc
from xbmcgui import (Control, ControlImage, ControlButton, ControlEdit,
                     ControlGroup, ControlLabel, ControlList, ControlTextBox,
                     ControlSpin, ControlSlider, ControlProgress, ControlFadeLabel,
                     ControlRadioButton)

from common.imports import *
from common.constants import Constants
from common.flexible_timer import FlexibleTimer
from common.imports import *
from common.exceptions import AbortException
from common.logger import LazyLogger, Trace
from common.messages import Messages
from common.monitor import Monitor
from common.settings import Settings
from frontend.abstract_dialog_state import BaseDialogStateMgr, DialogState
from frontend.text_to_speech import TTS
from player.my_player import MyPlayer

module_logger = LazyLogger.get_addon_module_logger(file_path=__file__)

class Glue:
    _dialog = None

    @staticmethod
    def set_dialog(dialog: ForwardRef('TrailerDialog')) -> None:
        Glue._dialog = dialog

    @staticmethod
    def get_dialog() -> ForwardRef('TrailerDialog'):
        return Glue._dialog

    @staticmethod
    def get_player() -> MyPlayer:
        return Glue._dialog.get_player()

class ControlId(Enum):

    def __init__(self, control_id: int) -> None:
        self._control_id: int = control_id

    def get_control_id(self) -> int:
        clz = type(self)
        return self._control_id

    def get_control(self) -> Control:
        clz = type(self)
        control = Glue.get_dialog().getControl(self.get_control_id())
        return control

    def get_label_control(self) -> ControlLabel:
        return self.get_control()

    SHOW_TRAILER = 38028
    SHOW_DETAILS = 38001
    SHOW_TRAILER_TITLE = 38029
    SHOW_TRAILER_NOTIFICATION = 38030
    SHOW_DETAILS_NOTIFICATION = 38024
    PLAYING_TITLE = 38021
    DETAIL_TITLE = 38003
    DIRECTOR_LABEL = 38025

class VisibleFields(Enum):
    NOTIFICATION = auto()
    TITLE = auto()

    # Either Movie details are displayed or the trailer is played
    SHOW_TRAILER = auto()
    SHOW_DETAILS = auto()
    SHOW_CURTAIN = auto()   # Show a open/close curtain
    NEW_TRAILER = auto()
    OPAQUE = auto()
    SHUTDOWN = auto()

class TimerState(Enum):
    IDLE = auto()
    STARTING = auto()
    WAITING = auto()
    CLEANUP_IN_PROGRESS = auto()
    CLEANUP_FINISHED = auto()

class DialogStateMgrAccess:
    _dialog_state_mgr: BaseDialogStateMgr = None
    _logger: LazyLogger = None

    @classmethod
    def class_init(cls):
        if cls._logger is None:
            cls._logger = module_logger.getChild(cls.__name__)

    @classmethod
    def get_dialog_state_mgr(cls) -> BaseDialogStateMgr:
        if DialogStateMgrAccess._dialog_state_mgr is None:
            DialogStateMgrAccess._dialog_state_mgr = BaseDialogStateMgr.get_instance()
        return DialogStateMgrAccess._dialog_state_mgr

class BaseTimer:

    CANCELLING: Tuple[ForwardRef('TimerState')] = (
        TimerState.CLEANUP_FINISHED,
        TimerState.CLEANUP_IN_PROGRESS
    )

    # All inheriting classes must override these class variables or
    # they will clobber each other

    _logger: LazyLogger = None

    # All changes to state must be accessed using _lock_cv
    # Variables declared here, but not instantiated or initialized
    # Initialization left to implementing classes

    _lock_cv: threading.Condition = None # threading.Condition()
    _busy_event: threading.Event = None # threading.Event()
    _cancel_event: threading.Event = None # threading.Event()
    _timer_state: TimerState = None # TimerState.IDLE
    _stop_called: bool = None

    _display_seconds: float = None
    _timer_name: str = None
    _timer: FlexibleTimer = None

    # function passed to FlexibleTimer, which it will call once the timer
    # expires Normally (after _display_seconds have elapsed). This function is NOT
    # called when cancelled; instead _cancel_callback is called.

    _callback_on_stop: Callable[[], None] = None
    _cancel_callback: Callable[[], None] = None
    _cancel_stop_on_play: bool = None
    _cancel_msg: str = None
    _debug_label: str = None   # For debug logs

    @classmethod
    def class_init(cls):
        if cls._logger is None:
            cls._logger = module_logger.getChild(cls.__name__)

    @classmethod
    def is_busy(cls) -> bool:
        return cls._busy_event.is_set()

    @classmethod
    def is_canceled(cls) -> bool:
        return cls._cancel_event.is_set()

    @classmethod
    def _start(cls) -> None:
        """
        :return:
        """
        try:
            with cls._lock_cv:
                try:
                    if not cls._busy_event.is_set():
                        cls._logger.error(f'_busy_event is NOT set. debug_label '
                                          f'{cls._debug_label}')
                        cls._cancel_event.clear()  # Just in case
                        return

                    if cls._logger.isEnabledFor(LazyLogger.DEBUG_EXTRA_VERBOSE):
                        cls._logger.debug_extra_verbose(f'debug_label: {cls._debug_label} '
                                                        f'display_seconds: '
                                                        f'{cls._display_seconds} '
                                                        f'start_action: {cls.start_action}',
                                                        trace=Trace.TRACE_UI_CONTROLLER)
                finally:
                    cls._lock_cv.notify_all()

            with cls._lock_cv:
                try:
                    if cls._incorrect_state(TimerState.IDLE):
                        return

                    cls._logger.debug_extra_verbose(f'Setting _timer_state to: STARTING '
                                                    f'debug_label: {cls._debug_label}',
                                                    trace=Trace.TRACE_UI_CONTROLLER)
                    cls._timer_state = TimerState.STARTING
                finally:
                    cls._lock_cv.notify_all()

            with cls._lock_cv:
                try:
                    if cls._incorrect_state(TimerState.STARTING):
                        return

                    cls.start_action()
                finally:
                    cls._lock_cv.notify_all()

            # Start Timer thread which, unless canceled first,
            # either kill a Trailer that is running too long, or stop
            # displaying Notification after a configured amount of time.

            with cls._lock_cv:
                try:
                    run_start: bool = True
                    if cls._incorrect_state(TimerState.STARTING):
                        run_start = False
                        return

                    if not (DialogStateMgrAccess.get_dialog_state_mgr()
                                .is_random_trailers_play_state(
                                DialogState.USER_REQUESTED_EXIT)):
                        cls._timer = FlexibleTimer(cls._display_seconds,
                                                   cls._stop,
                                                   cls._debug_label,
                                                   kwargs={'callback':
                                                           cls._callback_on_stop})
                        cls._timer.setName(cls._timer_name)
                        cls._logger.debug(f'debug_label: {cls._debug_label} '
                                          f'Starting timeout in '
                                          f'{cls._display_seconds} seconds',
                                          trace=Trace.TRACE_UI_CONTROLLER)
                        cls._timer_state = TimerState.WAITING
                finally:
                    if run_start:
                        cls._timer.start()
                    cls._lock_cv.notify_all()

            with cls._lock_cv:
                if cls._incorrect_state(TimerState.WAITING):
                    return
                cls._lock_cv.notify_all()

            if cls._logger.isEnabledFor(LazyLogger.DISABLED):
                cls._logger.debug_extra_verbose(f'STATE: {cls._timer_state}',
                                                trace=Trace.TRACE_UI_CONTROLLER)

            return

        except AbortException:
            reraise(*sys.exc_info())

        except Exception:
            cls._logger.exception()

    @classmethod
    def _incorrect_state(cls, timerstate: TimerState) -> bool:
        if cls._timer_state != timerstate:
            cls._logger.error(f'debug_label: {cls._debug_label} TimerState should be '
                              f'{timerstate} not: '
                              f'{cls._timer_state}. CANCELING LOCK',
                              trace=Trace.TRACE_UI_CONTROLLER)
            cls._logger.dump_stack(heading=f'TimerState should be {timerstate} not: '
                                           f'{cls._timer_state}. CANCELING LOCK')
            cls.cancel()
            return True
        return False

    @classmethod
    def _stop(cls,
              callback: Callable[[], None] = None,
              called_early: bool = False,
              stop_play: bool = False
              ) -> None:
        """
        TODO: FIX
            This code doesn't itself stop any notification display. It
            relies on other code, such as the trailer player to set detail display
            to invisible.

            This method does unblock any wait on the details to be finished
            displaying.

        :param called_early: Called as result of user action instead of
                             expired timer
        :param callback: Callback to inform completion of task. Called
             IFF not called_early, otherwise, callback from
             method used to cancel early is used.
        :param stop_play: Passed to stop_action so that stop_acton can call
                          Player.stop instead of player.pause.
        :return:
        """

        try:
            with cls._lock_cv:
                try:
                    cls._stop_called = True
                    if not cls._busy_event.is_set():
                        cls._logger.error(f'debug_label: {cls._debug_label} '
                                          f'_busy_event is NOT set. Returning')
                        return
                finally:
                    cls._lock_cv.notify_all()

            cls._logger.debug(f'debug_label: {cls._debug_label} stop_play: {stop_play}')
            with cls._lock_cv:
                try:
                    if cls._incorrect_state(TimerState.WAITING):
                        return

                    cls._timer_state = TimerState.CLEANUP_IN_PROGRESS
                    if cls._logger.isEnabledFor(LazyLogger.DEBUG_EXTRA_VERBOSE):
                        cls._logger.debug_extra_verbose(f'debug_label: {cls._debug_label} '
                                                        f'Setting _timer_state to: '
                                                        f'{cls._timer_state}',
                                                        trace=Trace.TRACE_UI_CONTROLLER)
                finally:
                    cls._lock_cv.notify_all()

            if cls._logger.isEnabledFor(LazyLogger.DEBUG_EXTRA_VERBOSE):
                cls._logger.debug_extra_verbose(f'debug_label: {cls._debug_label} '
                                                f'Stopping',
                                                trace=Trace.TRACE_UI_CONTROLLER)

            with cls._lock_cv:
                try:
                    if cls._incorrect_state(TimerState.CLEANUP_IN_PROGRESS):
                        return

                    cls.stop_action(called_early=called_early, stop_play=stop_play)
                    # Clear
                    TrailerStatus.clear_notification_msg()
                    Glue.get_dialog().update_notification_labels(text=None)
                finally:
                    cls._lock_cv.notify_all()

            with cls._lock_cv:
                try:
                    if cls._incorrect_state(TimerState.CLEANUP_IN_PROGRESS):
                        return

                    if called_early:
                        # Cleanup script will be called next

                        if not cls._cancel_event.is_set():
                            cls._logger.error(f'debug_label: {cls._debug_label} '
                                              f'cancel_event is NOT set.')
                        next_state = TimerState.CLEANUP_FINISHED
                    else:
                        cls._timer = None
                        next_state = TimerState.IDLE

                    cls._timer_state = next_state
                    if cls._logger.isEnabledFor(LazyLogger.DEBUG_EXTRA_VERBOSE):
                        cls._logger.debug_extra_verbose(f'debug_label: {cls._debug_label} '
                                                        f'Setting _timer_state to: '
                                                        f'{cls._timer_state}',
                                                        trace=Trace.TRACE_UI_CONTROLLER)

                    if not called_early:
                        if callback is not None:
                            # Terminating without being canceled.
                            cls._logger.debug(f'debug_label: {cls._debug_label} '
                                              f'Calling callback: {callback}')
                            callback()

                        cls._stop_called = False
                        cls._timer = None
                        cls._logger.debug(f'busy_event.clear()')
                        cls._busy_event.clear()
                    else:
                        # When canceled, the cancel method will call
                        # _cleanup, which will reset:
                        # busy_event.clear() & cancel_event.clear(), stop_called
                        pass
                finally:
                    cls._lock_cv.notify_all()
        except AbortException:
            raise sys.exc_info()

        except Exception as e:
            cls._logger.exception(msg='')

        if cls._logger.isEnabledFor(LazyLogger.DEBUG_EXTRA_VERBOSE):
            cls._logger.debug_extra_verbose(f'debug_label: {cls._debug_label} exit',
                                            trace=Trace.TRACE_UI_CONTROLLER)

    @classmethod
    def cancel(cls, reason: str = '', stop_play: bool = False,
               cancel_callback: Callable[[], None] = None,
               kwargs: Dict[Union[str, Enum], Any] = None) -> bool:
        """
        # Starts the cancelation of this timer. The _config method waits for
        # the cancel to complete before proceeding

        :param kwargs: Kwargs for callable
        :param reason: Displyable comment for debug
        :param stop_play: When cancelling trailer playback. If True, STOP
                          playback, otherwise, pause. Passed to stop_action.
        :param cancel_callback: Callback to pass along to stop_action. This function
                         will be called after the cancel is complete. Note that
                         the Timer's callback for normal timer expiration is NOT
                         called. See FlexibleTimer
        :return: True if cancel initiated, callback called and cancellation
                 complete. At this point _busy_event and _cancel_event are not set,
                 stop_called is False.

                 Otherwise, False, because it was
                 not necessary.
        """
        try:
            cls._logger.debug(f'reason: {reason} stop_play: {stop_play} '
                              f'cancel_callback: {cancel_callback}')
            with cls._lock_cv:
                try:
                    if cls._stop_called:
                        cls._logger.debug(f'debug_label: {cls._debug_label} '
                                          f'Too late to cancel, already stopped')
                        return False

                    # If not busy, then finished. No cancel needed.

                    if not cls._busy_event.is_set():
                        cls._logger.error(f'debug_label: {cls._debug_label}'
                                          f' _busy_event is NOT set')

                        # Just to make sure
                        cls._cancel_event.clear()
                        return False

                    if cls._cancel_event.is_set():
                        cls._logger.debug(f'debug_label: {cls._debug_label} '
                                          f'Already Marked to be Canceled, waiting for'
                                          f'cancellation to complete')

                        # Wait is in Finally block

                        return False

                    if cls._timer_state != TimerState.WAITING:
                        cls._logger.info(f'debug_label: {cls._debug_label} '
                                         f'TimerState should be WAITING not: '
                                         f'{cls._timer_state}',
                                         trace=Trace.TRACE_UI_CONTROLLER)
                        return False

                    cls._cancel_event.set()

                except AbortException:
                    reraise(*sys.exc_info())

                except Exception:
                    cls._logger.exception()

                finally:
                    cls._lock_cv.notify_all()

            if cls._logger.isEnabledFor(LazyLogger.DISABLED):
                cls._logger.debug(f'debug_label: {cls._debug_label} Canceling for '
                                  f'{reason}',
                                  trace=[Trace.TRACE_UI_CONTROLLER])

            # Running timer early (before timeout)
            cls._logger.debug(f'debug_label: {cls._debug_label} '
                              f'Calling stop early, stop_play: {stop_play}')
            cls._timer.run_now(kwargs={'stop_play': stop_play})
            cls._timer = None

            if kwargs is None:
                kwargs = {}

            kwargs['stop_play'] = stop_play
            cls._cleanup(cancel_callback=cancel_callback, kwargs=kwargs)
            #
            # _cleanup takes care of resetting:
            # _cancel_event, _busy_event, _stop_called
            #
            if cls._logger.isEnabledFor(LazyLogger.DEBUG_EXTRA_VERBOSE):
                cls._logger.debug_extra_verbose(f'debug_label: {cls._debug_label} '
                                                f'exit cancel_event.is_set: '
                                                f'{cls._cancel_event.is_set()}',
                                                trace=Trace.TRACE_UI_CONTROLLER)

        except AbortException:
            reraise(*sys.exc_info())

        except Exception:
            cls._logger.exception()

        finally:
            i: int = 0
            while cls._busy_event.is_set():
                i += 1
                # Every 5 seconds log msg
                if i % 20 == 0:
                    cls._logger.debug(f'Waiting for _busy_event cleared count: {i}')
                Monitor.throw_exception_if_abort_requested(0.25)
        return True

    @classmethod
    def _cleanup(cls, cancel_callback: Callable[[], None] = None, comment: str = '',
                 kwargs: Dict[str, Union[str, bool]] = None):
        cls._logger.debug(f'debug_label: {cls._debug_label} cancel_callback: '
                          f'{cancel_callback} comment: {comment}')
        try:
            while not Monitor.wait_for_abort(0.1):
                # Wait until:
                #  1- _busy_event.is_set() indicating that the allotted time
                #      to show movie details or the trailer has expired
                #      OR
                #      ane external event has canceled the display
                # 2- _display_timer is None
                #
                with cls._lock_cv:
                    try:
                        if not cls._busy_event.is_set():
                            cls._logger.error(f'debug_label: {cls._debug_label} '
                                              f'_busy_event is NOT set')
                            cls._cancel_event.clear()
                            cls._stop_called: bool = False
                            return

                        if not cls._cancel_event.is_set():
                            cls._logger.error(f'debug_label: {cls._debug_label} '
                                              f'_cancel_event is NOT set')
                            cls._stop_called: bool = False
                            return

                        if cls._timer_state == TimerState.CLEANUP_FINISHED:
                            cls._timer_state = TimerState.IDLE
                            if cls._logger.isEnabledFor(LazyLogger.DEBUG_EXTRA_VERBOSE):
                                cls._logger.debug_extra_verbose(f'debug_label: '
                                                                f'{cls._debug_label} '
                                                                f'Setting _timer_state to:'
                                                                f' {cls._timer_state}',
                                                                trace=
                                                                Trace.TRACE_UI_CONTROLLER)
                            if cancel_callback is not None:
                                cls._logger.debug(f'debug_label: {cls._debug_label} '
                                                  f'Calling cancel_callback')
                                if kwargs is None:
                                    kwargs = {}
                                cancel_callback(**kwargs)
                            cls._logger.debug(f'debug_label: {cls._debug_label} '
                                              f'clearing cancel_event')
                            cls._cancel_event.clear()
                            cls._logger.debug(f'busy_event.clear()')
                            cls._busy_event.clear()
                            cls._stop_called: bool = False
                        break
                    finally:
                        cls._lock_cv.notify_all()

        except AbortException:
            reraise(*sys.exc_info())

        except Exception:
            cls._logger.exception()

    @classmethod
    def start_action(cls):
        raise NotImplementedError

    @classmethod
    def stop_action(cls, called_early: bool, stop_play: bool = False):
        raise NotImplementedError

    @classmethod
    def wait_for_idle(cls) -> None:
        while Monitor.throw_exception_if_abort_requested(0.1):
            if cls._timer_state == TimerState.IDLE:
                break

class NotificationTimer(BaseTimer):
    """
       Manages How long Notifications are displayed

       Takes into account user actions which may cause any of the above
       to be cut short or, in the case of user request to show movie
       details to be untimed, or the pause the playing trailer to stop
       the timer.

   """
    _logger: LazyLogger = None

    # All changes to state must be accessed using _lock_cv
    # Variables declared here, but not instantiated or initialized
    # Initialization left to implementing classes

    _lock_cv: threading.Condition = threading.Condition()
    _busy_event: threading.Event = threading.Event()
    _cancel_event: threading.Event = threading.Event()
    _timer_state: TimerState = TimerState.IDLE

    _display_seconds: float = float(Constants.NOTIFICATION_SECONDS)
    _timer_name: str = None  # Useful for debug logging
    _timer: FlexibleTimer = None

    _notification_msg: str = None
    # _previous_msg: str = None

    # Callback to use when canceled
    _cancel_callback: Callable[[], None] = None
    _cancel_stop_on_play: bool = False

    # Debug msg to use on cancel

    _cancel_msg: str = None
    _stop_called: bool = False
    _debug_label: str = ''

    @classmethod
    def class_init(cls):
        if cls._logger is None:
            cls._logger = module_logger.getChild(cls.__name__)

    @classmethod
    def start_timer(cls, notification_msg: str, debug_label: str = '') -> None:
        with cls._lock_cv:
            cls._config(notification_msg=notification_msg,
                        debug_label=debug_label)
            cls._start()

    @classmethod
    def _config(cls, notification_msg: str, debug_label: str = '') -> None:
        """
        Configure timer for Notification.

        1- Cancels any existent Notification task
        2- Waits for any cancelation to complete
        3- Configures timer for next message:
           Sets message

        :param notification_msg:
        :param debug_label:
        :return:
        """
        with cls._lock_cv:
            try:
                if cls._busy_event.is_set():
                    # Notifications always cancels any previous Notification

                    if cls._timer_state == TimerState.WAITING:
                        cls.cancel(reason=f'Canceling for new Notification: '
                                          f'{debug_label}')

                    while cls._busy_event.is_set():
                        cls._lock_cv.wait(0.0)
                        Monitor.throw_exception_if_abort_requested(timeout=0.2)

                # Cancel complete. Now can proceed to work on new notification

                cls._logger.debug(f'busy_event.set()')
                cls._busy_event.set()
                cls._debug_label = debug_label
                # if notification_msg == cls._previous_msg:
                #    return

                cls._notification_msg = notification_msg
                # cls._previous_msg = notification_msg

            except AbortException:
                reraise(*sys.exc_info())

            except Exception:
                cls._logger.exception()
            finally:
                cls._lock_cv.notify_all()

    @classmethod
    def start_action(cls) -> None:
        cls._logger.debug(f'debug_label: {cls._debug_label} About to notify')
        with cls._lock_cv:
            try:
                if not cls._busy_event.is_set():
                    cls._logger.error(f'debug_label: {cls._debug_label} _busy_event '
                                      f'is NOT set. Returning')
                    return

                Glue.get_dialog().update_notification_labels(text=cls._notification_msg)
                TrailerStatus.set_notification_msg(msg=cls._notification_msg)
            finally:
                cls._lock_cv.notify_all()

    @classmethod
    def stop_action(cls, called_early: bool,
                    stop_play: bool = False):
        cls._logger.debug(f'debug_label: {cls._debug_label} called_early: {called_early} '
                          f'stop_play: {stop_play} '
                          f'About to clear notification')
        with cls._lock_cv:
            try:
                if not cls._busy_event.is_set():
                    cls._logger.error(f'debug_label: {cls._debug_label} _busy_event '
                                      f'is NOT set. Returning')
                    return

                TrailerStatus.clear_notification_msg()
                Glue.get_dialog().update_notification_labels(text=None)
            finally:
                cls._lock_cv.notify_all()

NotificationTimer.class_init()

class MovieDetailsTimer(BaseTimer):
    """
    Manages the display of the details for a movie. The UI is handled elsewhere,
    this class simply controls how long the contents are displayed and responds
    to other events that impact the display.

    """
    _logger: LazyLogger = None

    # All changes to state must be accessed using _lock_cv
    # Variables declared here, but not instantiated or initialized
    # Initialization left to implementing classes

    _lock_cv: threading.Condition = threading.Condition()
    _busy_event: threading.Event = threading.Event()
    _cancel_event: threading.Event = threading.Event()
    _timer_state: TimerState = TimerState.IDLE

    _timer: FlexibleTimer = None
    _display_seconds: float = Settings.get_time_to_display_detail_info()
    _timer_name = 'Display Movie Details'
    _scroll_plot: bool = None
    _callback_on_stop: Callable[[], None] = None
    _cancel_callback: Callable[[], None] = None
    _cancel_stop_on_play: bool = False
    _cancel_msg: str = None
    _stop_called: bool = None
    _debug_label: str = ''

    @classmethod
    def class_init(cls):
        if cls._logger is None:
            cls._logger = module_logger.getChild(cls.__name__)

    @classmethod
    def start_timer(cls,
                    scroll_plot: bool,
                    display_seconds: float,
                    debug_label: str = '',
                    callback_on_stop: Callable[[], None] = None) -> None:
        """

        :param scroll_plot:
        :param display_seconds:
        :param debug_label:
        :param callback_on_stop:
        :return:
        """
        with cls._lock_cv:
            cls._config(scroll_plot=scroll_plot,
                        display_seconds=display_seconds,
                        debug_label=debug_label,
                        callback_on_stop=callback_on_stop)
            cls._start()

    @classmethod
    def _config (cls, scroll_plot: bool,
                 display_seconds: float,
                 debug_label: str = '',
                 callback_on_stop: Callable[[], None] = None) -> None:
        cls._debug_label = debug_label
        if cls._logger is None:
            cls._logger = module_logger.getChild(cls.__name__)

        """
        Configure timer for displaying Movie Details.

        1- Cancels any existent DetailsTimer task
        2- Waits for any cancelation to complete
        3- Configures timer for next message:
           Sets message

        :param scroll_plot: Passed through to the UI. Controls whether to scroll
                            the plot or not. For now, set to False for TFH trailers
                            to prevent very verbose boiler-plate cluttering the
                            screen.
        :param debug_label: Used to help during debugging
        :return:
        """

        with cls._lock_cv:
            try:
                if cls._busy_event.is_set():
                    cls._logger.debug_verbose(f'debug_label: {cls._debug_label} '
                                              f'Canceling prior to MovieDetailsTimer'
                                              f' completion')
                    if cls._timer_state == TimerState.WAITING:
                        cls.cancel(reason=f'Canceling for new MovieDetailsTimer: '
                                          f'{debug_label}')

                    while cls._busy_event.is_set():
                        cls._lock_cv.wait(0.0)
                        Monitor.throw_exception_if_abort_requested(timeout=0.2)

                cls._logger.debug(f'busy_event.set()')
                cls._busy_event.set()
                cls._scroll_plot = scroll_plot
                cls._display_seconds = display_seconds
                cls._callback_on_stop = callback_on_stop
            finally:
                cls._lock_cv.notify_all()

    @classmethod
    def start_action(cls) -> None:
        with cls._lock_cv:
            try:
                if not cls._busy_event.is_set():
                    cls._logger.error(f'_debug_label: {cls._debug_label} _busy_event '
                                      f'is NOT set. Returning')
                    return

                cls._logger.debug(f'_debug_label: {cls._debug_label} '
                                  f'About to set_show_details & voice_detail_view')
                TrailerStatus.set_show_details(scroll_plot=cls._scroll_plot)
                Glue.get_dialog().voice_detail_view()
            finally:
                cls._lock_cv.notify_all()

    @classmethod
    def stop_action(cls, called_early: bool,
                    stop_play: bool = False):
        cls._logger.debug(f'_debug_label: {cls._debug_label} '
                          f'called_early: {called_early} stop_play: {stop_play} '
                          f'Calling TTS.stop and TrailerStatus.opaque')
        with cls._lock_cv:
            try:
                if not cls._busy_event.is_set():
                    cls._logger.error(f'_debug_label: {cls._debug_label} _busy_event is '
                                      f'NOT set. Returning')
                    return

                TTS.stop()
                TrailerStatus.opaque()
            finally:
                cls._lock_cv.notify_all()

MovieDetailsTimer.class_init()

class TrailerTimer(BaseTimer):

    _logger: LazyLogger = None

    # All changes to state must be accessed using _lock_cv
    # Variables declared here, but not instantiated or initialized
    # Initialization left to implementing classes

    _lock_cv: threading.Condition = threading.Condition()
    _busy_event: threading.Event = threading.Event()
    _cancel_event: threading.Event = threading.Event()
    _timer_state: TimerState = TimerState.IDLE

    _timer: FlexibleTimer = None
    _display_seconds: float = Settings.get_max_trailer_play_seconds()
    _timer_name: str = 'TrailerTimer'
    _inform_user: bool = None
    _callback_on_stop: Callable[[], None] = None
    _cancel_callback: Callable[[], None] = None
    _cancel_stop_on_play: bool = False
    _cancel_msg: str = None
    _debug_label: str = ''

    @classmethod
    def class_init(cls):
        if cls._logger is None:
            cls._logger = module_logger.getChild(cls.__name__)

    @classmethod
    def start_timer(cls,
                    display_seconds: float,
                    debug_label: str = '',
                    callback_on_stop: Callable[[], None] = None) -> None:
        with cls._lock_cv:
            cls._config(display_seconds=display_seconds,
                        debug_label=debug_label,
                        callback_on_stop=callback_on_stop)
            cls._start()

    @classmethod
    def _config (cls,
                 display_seconds: float,
                 debug_label: str = '',
                 callback_on_stop: Callable[[], None] = None) -> None:
        cls._debug_label = debug_label
        if cls._logger is None:
            cls._logger = module_logger.getChild(cls.__name__)

        with cls._lock_cv:
            try:
                if cls._busy_event.is_set():
                    cls._logger.debug_verbose(f'debug_label: {cls._debug_label} '
                                              f'Canceling previous TrailerTimer')
                if cls._timer_state == TimerState.WAITING:
                    cls.cancel(reason=f'Canceling for new TrailerTimer: '
                                      f'{debug_label}')

                while cls._busy_event.is_set():
                    cls._lock_cv.wait(0.0)
                    Monitor.throw_exception_if_abort_requested(timeout=0.2)

                cls._logger.debug(f'busy_event.set()')
                cls._busy_event.set()
                cls._display_seconds = display_seconds
                cls._callback_on_stop = callback_on_stop
            finally:
                cls._lock_cv.notify_all()

    @classmethod
    def start_action(cls) -> None:
        with cls._lock_cv:
            try:
                if not cls._busy_event.is_set():
                    cls._logger.error(f'_debug_label: {cls._debug_label} _busy_event '
                                      f'is NOT set. Returning')
                    return

                cls._logger.debug(f'_debug_label: {cls._debug_label} Calling '
                                  f'set_show_trailer,'
                                  f' set_playing_trailer_title')
                TrailerStatus.set_show_trailer()

                trailer_dialog: ForwardRef('TrailerDialog') = Glue.get_dialog()
                verbose: bool = cls._logger.isEnabledFor(LazyLogger.DEBUG_EXTRA_VERBOSE)
                title = trailer_dialog.get_title_string(trailer_dialog._movie, verbose)
                trailer_dialog.set_playing_trailer_title_control(title)
            finally:
                cls._lock_cv.notify_all()

    @classmethod
    def stop_action(cls, called_early: bool,
                    stop_play: bool = False):
        """
        TODO: Fix
        Called when:
            1) Trailer has finished playing. The player will be stopped,
               called_early = False, stop_play = False
            2) Trailer has played past the maximim time limit. The player is
               still playing (unless it finished in the time between the timer
               going off and this code). called_early = False, stop_play = False.
            3) User action requires trailer to pause or stop playing:
               In either case called_early = True. The trailer will be stopped
               or paused, depending upon stop_play.

        When callback is not None, then it will be called with the argument
        stop_play. stop_play = (stop_play OR not called_early).

        :param called_early:
        :param stop_play:
        :return:
        """
        # Don't inform user if called due to user action
        cls._logger.debug(f'_debug_label: {cls._debug_label} '
                          f'called_early: {called_early} stop_play: {stop_play}')

        with cls._lock_cv:
            try:
                if not cls._busy_event.is_set():
                    cls._logger.error(f'_debug_label: {cls._debug_label} _busy_event is '
                                      f'NOT set. Returning')
                    return

                if cls._inform_user and not called_early:
                    cls._logger.debug(f'_debug_label {cls._debug_label} '
                                      f'About to notify max_play_time exceeded')
                    NotificationTimer.start_timer(notification_msg=Messages.get_msg(
                            Messages.TRAILER_EXCEEDS_MAX_PLAY_TIME),
                            debug_label=cls._debug_label)

                TrailerStatus.opaque()
                player = Glue.get_player()
                if not called_early:
                    stop_play = True

                if stop_play:
                    cls._logger.debug(f'Title: {cls._debug_label} Stopping player',
                                      trace=Trace.TRACE_UI_CONTROLLER)
                    player.stop()
                else:
                    cls._logger.debug(f'Title: {cls._debug_label} Pausing player',
                                      trace=Trace.TRACE_UI_CONTROLLER)
                    player.pause_play()

                TTS.stop()
            finally:
                cls._lock_cv.notify_all()

TrailerTimer.class_init()

class TrailerStatus:
    """
    Manages the visibility of information based upon changes to data and
    events.

    Note that most controls displayed when movie details is visible are
    handled directly by Trailer_Dialog.update. However, several controls
    that are optionally visible when playing trailers are handled here.

    This class also controls visibility of movie details or the playing
    trailer.
    """

    # There are several fields that are displayed when movie details are shown
    # or the trailer is being played.

    FIELDS_SHOWN_WHEN_TRAILER_PLAYED = [
        VisibleFields.NOTIFICATION,
        VisibleFields.TITLE
    ]

    logger: LazyLogger = None

    # show_title_while_playing: bool = Settings.get_show_movie_title()
    show_notification: bool = False

    show_trailer: bool = False
    scroll_plot: bool = False

    _notification_msg: str = None

    _dialog: ForwardRef('TrailerDialog') = None

    @classmethod
    def class_init(cls, dialog: ForwardRef('TrailerDialog')):
        cls._dialog = dialog
        if cls.logger is None:
            cls.logger = module_logger.getChild(cls.__name__)

    @classmethod
    def set_notification_msg(cls, msg: str = None) -> None:
        cls._notification_msg = msg
        cls.value_changed(VisibleFields.NOTIFICATION)

    @classmethod
    def clear_notification_msg(cls) -> None:
        # Called by stop methods
        cls._notification_msg = None
        cls.show_notification = False
        # TODO test to see if needed
        # cls.value_changed(VisibleFields.NOTIFICATION)

    @classmethod
    def get_notification_msg(cls) -> str:
        return cls._notification_msg

    @classmethod
    def get_show_notification(cls) -> bool:
        return cls.show_notification

    @classmethod
    def set_show_trailer(cls) -> None:
        cls.show_trailer = True
        cls.reset_state()
        cls.value_changed(VisibleFields.SHOW_TRAILER)

    @classmethod
    def set_show_curtain(cls) -> None:
        cls.show_trailer = True
        cls.reset_state()
        cls.value_changed(VisibleFields.SHOW_CURTAIN)

    @classmethod
    def set_show_details(cls, scroll_plot: bool = False) -> None:
        cls.show_trailer = False
        cls.scroll_plot = scroll_plot
        cls.reset_state()
        cls.value_changed(VisibleFields.SHOW_DETAILS)

    @classmethod
    def reset_state(cls, new_trailer: bool = False) -> None:
        cls.show_notification = False
        cls._notification_msg = None
        if new_trailer:
            cls._show_trailer = False

    @classmethod
    def opaque(cls) -> None:
        cls.reset_state()
        cls.value_changed(VisibleFields.OPAQUE)

    @classmethod
    def value_changed(cls, changed_field: VisibleFields) -> None:
        """
            Controls the visible elements of TrailerDialog
        :return:
        """
        # First, determine if we need to shut things down

        shutdown = changed_field == VisibleFields.SHUTDOWN
        try:
            if (DialogStateMgrAccess.get_dialog_state_mgr().
                    is_random_trailers_play_state(
                    minimum_exit_state=DialogState.SHUTDOWN_CUSTOM_PLAYER)):
                shutdown = True

        except AbortException:
            shutdown = True

        commands = []
        if shutdown:
            pass

        elif changed_field in (VisibleFields.SHOW_TRAILER, VisibleFields.SHOW_CURTAIN):
            # Hide entire Movie details screen first
            # Any notifications are canceled

            Glue.get_dialog().set_visibility(False, ControlId.SHOW_DETAILS)
            Glue.get_dialog().set_visibility(False,
                                             ControlId.SHOW_TRAILER_NOTIFICATION)
            if changed_field != VisibleFields.SHOW_CURTAIN:
                show_title_while_playing: bool = Settings.get_show_movie_title()
                if show_title_while_playing:
                    Glue.get_dialog().set_visibility(True,
                                                     ControlId.SHOW_TRAILER_TITLE)

            Glue.get_dialog().set_visibility(True, ControlId.SHOW_TRAILER)

        elif changed_field == VisibleFields.SHOW_DETAILS:
            # Show movie details
            Glue.get_dialog().set_visibility(False, ControlId.SHOW_TRAILER)
            Glue.get_dialog().set_visibility(False,
                                             ControlId.SHOW_DETAILS_NOTIFICATION)
            if cls.scroll_plot:
                commands.append("Skin.SetBool(ScrollPlot)")
            else:
                commands.append("Skin.Reset(ScrollPlot)")

            Glue.get_dialog().set_visibility(True, ControlId.SHOW_DETAILS)

        elif changed_field == VisibleFields.NOTIFICATION:
            visible: bool = cls._notification_msg is not None
            if cls.show_trailer:
                Glue.get_dialog().set_visibility(visible,
                                                 ControlId.SHOW_TRAILER_NOTIFICATION)
            else:
                Glue.get_dialog().set_visibility(visible,
                                                 ControlId.SHOW_DETAILS_NOTIFICATION)

        elif changed_field == VisibleFields.OPAQUE:
            Glue.get_dialog().set_visibility(False, ControlId.SHOW_DETAILS)
            Glue.get_dialog().set_visibility(False, ControlId.SHOW_TRAILER)

        for command in commands:
            if cls.logger.isEnabledFor(LazyLogger.DEBUG):
                cls.logger.debug_extra_verbose(command,
                                               trace=Trace.TRACE_UI_CONTROLLER)
            xbmc.executebuiltin(command, wait=False)


# redis docker build: https://www.runoob.com/docker/docker-install-redis.html
import redis
import json
import os

def createRedisClient(host, port):
    try:
        redisClient = redis.Redis(host=host, port=port)
        pingTest = redisClient.ping()

        if (pingTest == True):
            print("Connected To Redis!")
            return redisClient
        else:
            print("Could not connect to Redis!")
    except Exception as ex:
        print("Could not connect to redis! See Message Below")
        print(ex)
    return False

# cache flow:
# try to get redis cache data
# acquire a lock for data
# try again to get redis cache data
# if not found, use DB
# release lock

# cache flow is used to prevent simultaneously acquire a data

# get cache by transform string back to object

def getRedisValueByKey(key: str):
    output = redisClient.get(key)
    if output == None:
        with redisClient.lock('lock_' + key):
            output = redisClient.get(key)

    if output != None:
        output = json.loads(output)

    return output

# cache object with jsonify string

def setRedisKeyAndValue(key: str, value):
    value = json.dumps(value)
    output = redisClient.set(key, value)
    redisClient.expire(key, 180) # set 3 min data expiration time
    return output

redisClient = createRedisClient('localhost', 6379) if os.getenv("FROM") == 'LOCAL' else createRedisClient('redis', 6379)


#!/usr/bin/env python3
# coding: utf-8

# ===========================================
# OpenFoam vs Fenics (HeatTransfer Problem)
# ===========================================

# Loading the libraries
# ---------------------------------

import numpy as np
import os                                              # to move through directories
import vtk                                             # to deal with .vtu file
from vtk.util.numpy_support import vtk_to_numpy        # to convert values from vtk to numpy
import matplotlib.pyplot as plt                        # to plot

# Input the time folder to load
# -------------------------------------

time = 0.05

# Loading the data from .xy and .vtu
# -------------------------------------

Dir = os.getcwd()

fenics_file = int(time/0.05)
if (fenics_file<10):
    fenics_file = "00000" + str(fenics_file)
elif (fenics_file<100):
    fenics_file = "0000" + str(fenics_file)
elif (fenics_file<1000):
    fenics_file = "000" + str(fenics_file)
elif (fenics_file<10000):
    fenics_file = "00" + str(fenics_file)
elif (fenics_file<100000):
    fenics_file = "0" + str(fenics_file)
elif (fenics_file<1000000):
    fenics_file = str(fenics_file)

# OpenFoam data

os.chdir(Dir + "/postProcessing/singleGraph/" + str(time) + "/")
if (time>0):
    openF_x   = np.loadtxt(open('line_T_g_T_s_p_rho_g.xy','r'))[:, 0]
    openF_x = np.concatenate(([0], openF_x))
    openF_Tg  = np.loadtxt(open('line_T_g_T_s_p_rho_g.xy','r'))[:, 1]
    openF_Tg = np.concatenate(([293], openF_Tg))
    openF_Ts  = np.loadtxt(open('line_T_g_T_s_p_rho_g.xy','r'))[:, 2]
    openF_Ts = np.concatenate(([openF_Ts[0]], openF_Ts))
    openF_P   = np.loadtxt(open('line_T_g_T_s_p_rho_g.xy','r'))[:, 3]
    openF_P = np.concatenate(([100084.375], openF_P))
    openF_rho = np.loadtxt(open('line_T_g_T_s_p_rho_g.xy','r'))[:, 4]
    openF_rho = np.concatenate(([1.1494], openF_rho))
else:
    openF_x   = np.loadtxt(open('line_T_g_T_s_p.xy','r'))[:, 0]
    openF_x   = np.concatenate(([0], openF_x))
    openF_Tg  = np.loadtxt(open('line_T_g_T_s_p.xy','r'))[:, 1]
    openF_Tg  = np.concatenate(([293], openF_Tg))
    openF_Ts  = np.loadtxt(open('line_T_g_T_s_p.xy','r'))[:, 2]
    openF_Ts  = np.concatenate(([openF_Ts[0]], openF_Ts))
    openF_P   = np.loadtxt(open('line_T_g_T_s_p.xy','r'))[:, 3]
    openF_P   = np.concatenate(([openF_P[0]], openF_P))
    openF_rho = np.zeros(len(openF_P))

openF_q   = np.loadtxt(open('line_u_g_q.xy','r'))[:, 4]
openF_q = np.concatenate(([openF_q[0]], openF_q))
openF_v   = np.loadtxt(open('line_u_g_q.xy','r'))[:, 1]
openF_v = np.concatenate(([openF_v[0]], openF_v))

# Fenics data

reader = vtk.vtkXMLUnstructuredGridReader()
os.chdir(Dir + "/Fenics/output/")
reader.SetFileName('T_g'+fenics_file+'.vtu')
reader.Update()
fenics_x  = vtk_to_numpy(reader.GetOutput().GetPoints().GetData())[:,0]
fenics_Tg = vtk_to_numpy(reader.GetOutput().GetPointData().GetScalars())

reader.SetFileName('T_s'+fenics_file+'.vtu')
reader.Update()
fenics_Ts = vtk_to_numpy(reader.GetOutput().GetPointData().GetScalars())

reader.SetFileName('P'+fenics_file+'.vtu')
reader.Update()
fenics_P = vtk_to_numpy(reader.GetOutput().GetPointData().GetScalars())

reader.SetFileName('r'+fenics_file+'.vtu')
reader.Update()
fenics_rho = vtk_to_numpy(reader.GetOutput().GetPointData().GetScalars())

reader.SetFileName('q'+fenics_file+'.vtu')
reader.Update()
fenics_q = vtk_to_numpy(reader.GetOutput().GetPointData().GetScalars())

reader.SetFileName('v'+fenics_file+'.vtu')
reader.Update()
fenics_v = vtk_to_numpy(reader.GetOutput().GetPointData().GetScalars())

# plot the comparisons
# -------------------------------------

if not os.path.isdir(Dir + "/plot/figures"):
    os.mkdir(Dir + "/plot/figures")
os.chdir(Dir + "/plot/figures")

# Temperature

plt.plot(openF_x,openF_Tg,'b', linewidth=1, label="GAS")
plt.plot(openF_x,openF_Ts,'k', linewidth=1, label="SOLID")
plt.plot(fenics_x[0:-1:7],fenics_Tg[0:-1:7],'bo', markersize=7, mfc='none')
plt.plot(fenics_x[0:-1:7],fenics_Ts[0:-1:7],'ko', markersize=7, mfc='none', label="fenics")
plt.plot(openF_x,openF_Ts,'k', linewidth=1, label="OpenFoam")
plt.title("Time t = " + str(time))
plt.xlabel ("x [m]");
plt.ylabel ("Temperature [k]");
plt.legend(loc='lower right', borderaxespad=0., frameon = False)
plt.savefig('temperatures_'+str(time)+'.pdf')
plt.show()

# Pressure

plt.plot(openF_x,openF_P,'k', linewidth=1, label="OpenFoam")
plt.plot(fenics_x[0:-1:7],fenics_P[0:-1:7],'ko', markersize=7, mfc='none', label="fenics")
plt.title("Time t = " + str(time))
plt.xlabel ("x [m]");
plt.ylabel ("Pressure [Pa]");
plt.legend(loc='lower right', borderaxespad=0., frameon = False)
plt.savefig('pressure_'+str(time)+'.pdf')
plt.show()

# Density

plt.plot(openF_x,openF_rho,'k', linewidth=1, label="OpenFoam")
plt.plot(fenics_x[0:-1:7],fenics_rho[0:-1:7],'ko', markersize=7, mfc='none', label="fenics")
plt.title("Time t = " + str(time))
plt.xlabel ("x [m]");
plt.ylabel ("Density [Kg/m^3]");
plt.legend(loc='lower right', borderaxespad=0., frameon = False)
plt.savefig('density_'+str(time)+'.pdf')
plt.show()

# Flow rate

plt.plot(openF_x,openF_q,'k', linewidth=1, label="OpenFoam")
plt.plot(fenics_x[0:-1:7],fenics_q[0:-1:7],'ko', markersize=7, mfc='none', label="fenics")
plt.title("Time t = " + str(time))
plt.xlabel ("x [m]");
plt.ylabel ("Flow rate [Kg/m^2/s]");
plt.legend(loc='lower right', borderaxespad=0., frameon = False)
plt.savefig('flow_'+str(time)+'.pdf')
plt.show()

# Velocity

plt.plot(openF_x,openF_v,'k', linewidth=1, label="OpenFoam")
plt.plot(fenics_x[0:-1:7],fenics_v[0:-1:7],'ko', markersize=7, mfc='none', label="fenics")
plt.title("Time t = " + str(time))
plt.xlabel ("x [m]");
plt.ylabel ("Velocity [m/s]");
plt.legend(loc='lower right', borderaxespad=0., frameon = False)
plt.savefig('velocity_'+str(time)+'.pdf')
plt.show()