from collections import deque
import threading

import errors
import modes
import console
import mi_interface as mi
from config import settings
from command_completer import CommandCompleter
from util import format_error, format_warning, format_info, repr_str_dict

session_manager = None
selected_sessions = None
sessions_history = None

history_length = int(settings['sessions']['history_length'])

def init_debugging_mode(mgr, msgs):
    global selected_sessions
    global session_manager
    global sessions_history

    session_manager = mgr
    sessions_history = {}

    oldest_session = mgr.get_oldest()
    if oldest_session:
        selected_sessions = [oldest_session.tag]

    for tag in session_manager.list_session_tags():
        sessions_history[tag] = deque(maxlen = history_length)
        sessions_history[tag].append(msgs[tag])

    return modes.debugging, None

def _ls_out():
    ls_out = []
    for s_i in session_manager.list_sessions():
        if not s_i.tag in selected_sessions:
            ls_head = s_i.tag
        else:
            ls_head = '(*) {}'.format(s_i.tag)
        ls_out.append('%6s) %s:%d' % (
            ls_head,
            s_i.hostname,
            s_i.meta,
        ))
    return '\n'.join(ls_out)

def list_sessions(args):
    '''Lists all active sessions.'''
    # Verify command syntax
    if len(args) != 0:
        raise errors.BadArgsError(
            'ls', 'This command does not accept arguments'
        )

    pretty_session_list = _ls_out()
    if not pretty_session_list:
        return modes.debugging, 'ls: Empty. No sessions are alive.'
    return modes.debugging, 'Sessions:\n' + pretty_session_list

def select_session_complete(args):
    '''readline library tab completion for session selection in debugging
    mode'''
    if not args:
        return ['all', 'none'] + session_manager.list_session_tags()

    # If 'all' or 'none' then no further arguments are meaningful
    if 'all' in args or 'none' in args:
        return []

    if 'all'.startswith(args[0]):
        return ['all']

    if 'none'.startswith(args[0]):
        return ['none']

    return [
        tag for tag in session_manager.list_session_tags() if not tag in args
    ]

def select_session(args):
    '''Mark the provided sessions as selected'''
    # Verify command syntax
    if not args:
        raise errors.BadArgsError(
            'select', 'select all | none | <session id>[ <session id>[ ...]]'
        )

    if args:
        if args[0] == 'all':
            global selected_sessions
            selected_sessions = sorted(session_manager.list_session_tags())

            return modes.debugging, 'Selected session(s) #{0}\n{1}'.format(
                ', '.join(selected_sessions), _ls_out()
            )
        elif args[0] == 'none':
            global selected_sessions
            selected_sessions = []

            return modes.debugging, 'No sessions selected'.format(
                ', '.join(selected_sessions), _ls_out()
            )

    non_existing_sessions = [
        id for id in args if not session_manager.exists(id)
    ]
    if non_existing_sessions:
        raise errors.BadArgsError(
            'select', 'Session(s) {0} do not exist.'.
            format(', '.join(non_existing_sessions))
        )

    global selected_sessions
    selected_sessions = args
    return modes.debugging, 'Selected session(s) #{0}\n{1}'.format(
        ', '.join(selected_sessions), _ls_out()
    )

def _kill_all():
    for s_i in session_manager.list_sessions():
        if s_i.is_alive():
            s_i.query('-gdb-exit')
    session_manager.kill_all()

    global session_manager
    global sessions_history
    session_manager = None
    sessions_history = None

def end_sessions(args):
    # Verify command syntax
    if len(args) != 0:
        raise errors.BadArgsError(
            'ls', 'This command does not accept arguments'
        )

    _kill_all()
    return modes.offline, None

def quit(args):
    '''Exit scimitar'''
    # Verify command syntax
    if len(args) != 0:
        raise errors.BadArgsError(
            'ls', 'This command does not accept arguments'
        )

    _kill_all()
    return modes.quit, None

def _ensure_sessions_selected(cmd):
    '''Verifies that there are sessions selected at this moment.'''
    if not selected_sessions:
        raise errors.CommandFailedError(
            cmd,
            'No session(s) selected. Debugging mode failed to start. (Maybe init_debugging_mode() was not called?)'
        )

def _ensure_valid_sessions_selected(cmd):
    '''Verifies if all selected sessions are still alive'''
    non_existing_sessions = [
        id for id in selected_sessions if not session_manager.exists(id)
    ]
    if non_existing_sessions:
        raise errors.BadArgsError(
            cmd, 'Cannot proceed. Dead session(s): {0}.'.
            format(', '.format(non_existing_sessions))
        )

def message_history(args):
    '''Prints the selected sessions' history records at index args[0]'''
    _ensure_sessions_selected('history')

    index = -1
    if len(args) == 1:
        try:
            index = -int(args[0])
        except ValueError:
            raise errors.BadArgsError(
                'history', 'history[ <index>]'
            )

    if index > history_length:
        raise errors.BadArgsError(
            'history', 'Selected record does not exist in the history'
        )

    results = []
    for tag in selected_sessions:
        # Output header
        results.append('~~~ Scimitar - Session: {} ~~~'.format(tag))
        try:
            ind_rec, cout, tout, lout = sessions_history[tag][index]
            if ind_rec:
                # Check the type of indicator
                if ind_rec[0] == mi.indicator_error:
                    results.append(format_error(ind_rec[1]))
                elif ind_rec[0] == mi.indicator_exit:
                    session_manager.remove(tag)
                    sessions_history.remove(tag)
                    results.append(format_error('Session {} died.', tag))
                else:
                    results.append(cout)
            else:
                results.append(''.join([cout, tout, lout]))
        except IndexError:
            results.append(format_error('Scimitar: Record does not exist'))
    return modes.debugging, '\n'.join(results)

def gdb_exec(cmd):
    '''Runs the provided user command cmd on all selected sessions.'''

    class RemoteCommandExecutingThread(threading.Thread):
        '''This thread type is responsible for running commands on terminal'''

        def __init__(self, term, cmd):
            super(RemoteCommandExecutingThread, self).__init__()
            self.term = term
            self.cmd = cmd
            self.error = None
            self.result = None

        def run(self):
            '''Start the execution'''
            try:
                self._run()
            except Exception as e:
                self.error = e

        def _run(self):
            # Send the command
            gdb_response = self.term.query(self.cmd)
            # In case GDB dies
            if gdb_response in (r'^exit', r'^kill'):
                raise console.SessionDiedError
            else:
                self.result = mi.parse(gdb_response)

        def report(self):
            if self.error:
                raise self.error
            return self.result

    def _sanitize_gdb_command(cmd):
        '''Explicitly tell GDB that this is not an MI command following. Tries to
        differentiate between control signals and commands.'''
        if cmd and not repr_str_dict.has_key(cmd[0]):
            return ['-interpreter-exec', 'console'] + cmd
        return cmd

    def _parallel_exec_async(cmd, tag, target_sessions):
        '''Start running the provided cmd per each target session in separate
        thread '''
        tasks = {}
        for tag in target_sessions:
            # Get the session
            cs = session_manager.get(tag)
            exctr = RemoteCommandExecutingThread(cs, ' '.join(cmd))
            exctr.start()
            tasks[tag] = exctr
        return tasks

    def _collect_exec_results(tasks):
        '''Wait until threads finish and then collect the results.'''
        # If sessions died during execution collect them
        session_casualties = []
        # Results
        results = []

        # Go through the results
        for tag, task in tasks.iteritems():
            task.join()
            try:
                mi_response = task.report()
                # Add it to message history stash
                sessions_history[tag].append(mi_response)
                # Output header
                results.append('~~~ Scimitar - Session: {} ~~~'.format(tag))
                ind_rec, cout, tout, lout = mi_response
                # Check the type of indicator
                # Error
                if ind_rec[0] == mi.indicator_error:
                    results.append(format_error(ind_rec[1]))
                # Exit
                elif ind_rec[0] == mi.indicator_exit:
                    session_manager.remove(tag)
                    sessions_history.remove(tag)
                    results.append(format_error('Session {} died.', tag))
                # Connected, Success, etc
                else:
                    results.append(cout)
            # When the session is dead
            except console.SessionDiedError:
                # It is not a session we can work with in future
                session_manager.remove(tag)
                sessions_history.remove(tag)
                selected_sessions.remove(tag)
                # Add this session to casualty list
                session_casualties += [tag]
        return results, session_casualties

    #
    # gdb_exec starts here
    #

    # Ensure there are selected sessions
    _ensure_sessions_selected('gdb:cmd(?)')
    # Make sure selected sessions are valid
    _ensure_valid_sessions_selected('gdb:cmd(?)')

    # GDB launch command
    cmd = _sanitize_gdb_command(cmd)

    # Threads that run the command
    tasks = _parallel_exec_async(cmd, tag, selected_sessions)

    # Wait for all commands to finish
    session_casualties, results = _collect_exec_results(tasks)

    # In case we had dead sessions the command has essentially failed.
    if session_casualties:
        results.append(
            format_error(
                'Session(s) {} died.', ', '.join(session_casualties)
            )
        )
    return modes.debugging, '\n'.join(results)

def debug(args):
    import pdb
    pdb.set_trace()
    return modes.debugging, None

commands = {
    'ls': (list_sessions, None),
    'select': (select_session, select_session_complete),
    'debug': (debug, None), # HACK: For debugging only
    'history': (message_history, None),
    'end': (end_sessions, None),
    'quit': (quit, None),
}

def process(cmd, args):
    if cmd in commands:
        return commands[cmd][0](args)
    else:
        return gdb_exec([cmd] + (args or []))
    raise errors.CommandImplementationIncompleteError

class DebugSessionCommandCompleter(CommandCompleter):

    def _complete_command(self):
        return commands.keys()

    def _complete_command_arguments(self, cmd, args):
        if commands.has_key(cmd) and commands[cmd][1]:
            return commands[cmd][1](args)

# vim: :ai:sw=4:ts=4:sts=4:et:ft=python:fo=corqj2:sm:tw=79:


"""
ZMMemory
=====================
Wrapper to access SHM for Monitor status
"""

import mmap
import struct
from collections import namedtuple
import os
from pyzm.helpers.Base import Base
import pyzm.helpers.globals as g

"""
shared_data => { type=>'SharedData', seq=>$mem_seq++, contents=> {
    size             => { type=>'uint32', seq=>$mem_seq++ }, I
    last_write_index => { type=>'uint32', seq=>$mem_seq++ }, I
    last_read_index  => { type=>'uint32', seq=>$mem_seq++ }, I
    state            => { type=>'uint32', seq=>$mem_seq++ }, I
    last_event       => { type=>'uint64', seq=>$mem_seq++ }, Q
    action           => { type=>'uint32', seq=>$mem_seq++ }, I
    brightness       => { type=>'int32', seq=>$mem_seq++ },  i
    hue              => { type=>'int32', seq=>$mem_seq++ },  i
    colour           => { type=>'int32', seq=>$mem_seq++ },  i
    contrast         => { type=>'int32', seq=>$mem_seq++ },  i
    alarm_x          => { type=>'int32', seq=>$mem_seq++ },  i
    alarm_y          => { type=>'int32', seq=>$mem_seq++ },  i
    valid            => { type=>'uint8', seq=>$mem_seq++ },  ?
    active           => { type=>'uint8', seq=>$mem_seq++ },  ?
    signal           => { type=>'uint8', seq=>$mem_seq++ },  ?
    format           => { type=>'uint8', seq=>$mem_seq++ },  ?
    imagesize        => { type=>'uint32', seq=>$mem_seq++ }, I
    epadding1        => { type=>'uint32', seq=>$mem_seq++ },  I
    startup_time     => { type=>'time_t64', seq=>$mem_seq++ },  Q
    last_write_time  => { type=>'time_t64', seq=>$mem_seq++ },  Q
    last_read_time   => { type=>'time_t64', seq=>$mem_seq++ },  Q
    control_state    => { type=>'uint8[256]', seq=>$mem_seq++ }, s256
    alarm_cause      => { type=>'int8[256]', seq=>$mem_seq++ }, s256
"""

"""
    trigger_data => { type=>'TriggerData', seq=>$mem_seq++, 'contents'=> {
    size             => { type=>'uint32', seq=>$mem_seq++ }, I
    trigger_state    => { type=>'uint32', seq=>$mem_seq++ }, I
    trigger_score    => { type=>'uint32', seq=>$mem_seq++ }, I
    padding          => { type=>'uint32', seq=>$mem_seq++ }, I
    trigger_cause    => { type=>'int8[32]', seq=>$mem_seq++ }, s32
    trigger_text     => { type=>'int8[256]', seq=>$mem_seq++ }, s256
    trigger_showtext => { type=>'int8[256]', seq=>$mem_seq++ }, s256
  }
"""

class ZMMemory(Base):

    def __init__(self,api=None, path='/dev/shm', mid=None):
        self.api = api

        self.alarm_state_stages = {
        'STATE_IDLE':0,
        'STATE_PREALARM':1,
        'STATE_ALARM':2,
        'STATE_ALERT':3,
        'STATE_TAPE':4,
        'ACTION_GET':5,
        'ACTION_SET':6,
        'ACTION_RELOAD':7,
        'ACTION_SUSPEND':8,
        'ACTION_RESUME':9,
        'TRIGGER_CANCEL':10,
        'TRIGGER_ON':11,
        'TRIGGER_OFF':12
        }
        self.fhandle = None
        self.mhandle = None

        if not mid:
            raise ValueError ('No monitor specified')
        self.fname = path+'/zm.mmap.'+str(mid)
        self.reload()

    def reload(self):
        """Reloads monitor information. Call after you get
        an invalid memory report

        Raises:
            ValueError: if no monitor is provided
        """
        self.close()
        self.fhandle = open(self.fname, "r+b")
        sz = os.path.getsize(self.fname)
        if not sz:
            raise ValueError ('Invalid size: {} of {}'.format(sz, self.fname))

        self.mhandle = mmap.mmap(self.fhandle.fileno(), 0, access=mmap.ACCESS_READ)
        self.sd = None
        self.td = None
        self._read()

    def is_valid(self):
        """True if the memory handle is valid

        Returns:
            bool: True if memory handle is valid
        """
        try:
            d = self._read()
            return not d['shared_data']['size']==0
        except Exception as e:
            g.logger.Error('Memory: {}'.format(e))
            return False

    def is_alarmed(self):
        """True if monitor is currently alarmed

        Returns:
            bool: True if monitor is currently alarmed
        """

        d = self._read()
        return int(d['shared_data']['state']) == self.alarm_state_stages['STATE_ALARM']

    def alarm_state(self):
        """Returns alarm state

        Returns:
            dict: as below::

                {
                    'id': int # state id
                    'state': string # name of state
                }

        """

        d = self._read()
        return {
            'id': d['shared_data']['state'],
            'state': list(self.alarm_state_stages.keys())[list(self.alarm_state_stages.values()).index(int( d['shared_data']['state']))]
        }

    def last_event(self):
        """Returns last event ID

        Returns:
            int: last event id
        """
        d = self._read()
        return d['shared_data']['last_event']

    def cause(self):
        """Returns alarm and trigger cause as applicable

        Returns:
            dict: as below::

                {
                    'alarm_cause': string
                    'trigger_cause': string
                }

        """
        d=self._read()
        return {
            'alarm_cause': d['shared_data'].get('alarm_cause'), # May not be there
            'trigger_cause': d['shared_data'].get('trigger_cause'),
        }

    def trigger(self):
        """Returns trigger information

        Returns:
            dict: as below::

                {
                    'trigger_text': string,
                    'trigger_showtext': string,
                    'trigger_cause': string,
                    'trigger:state' {
                        'id': int,
                        'state': string
                    }
                }
        """

        d=self._read()
        return {
            'trigger_text': d['trigger_data'].get('trigger_text'),
            'trigger_showtext': d['trigger_data'].get('trigger_showtext'),
            'trigger_cause': d['trigger_data'].get('trigger_cause'),
            'trigger_state': {
                'id':d['trigger_data'].get('trigger_state'),
                'state': d['trigger_data']['trigger_state']
            }

        }

    def _read(self):
        self.mhandle.seek(0)
        SharedData = namedtuple('SharedData', 'size last_write_index last_read_index state last_event action brightness hue color contrast alarm_x alarm_y valid active signal format imagesize epadding1 startup_time last_write_time last_read_time control_state alarm_cause')
        s = SharedData._make(struct.unpack('IIIIQIiiiiii????IIQQQ256s256s',self.mhandle.read(600)))
        TriggerData = namedtuple('TriggerData', 'size trigger_state trigger_score padding trigger_cause trigger_text trigger_showtext')
        t = TriggerData._make(struct.unpack('IIII32s256s256s', self.mhandle.read(560)))
        self.sd = s._asdict()
        self.td = t._asdict()

        self.sd['alarm_cause'] = self.sd['alarm_cause'].split(b'\0',1)[0].decode()
        self.sd['control_state'] = self.sd['control_state'].split(b'\0',1)[0].decode()
        self.td['trigger_cause'] = self.td['trigger_cause'].split(b'\0',1)[0].decode()
        self.td['trigger_text'] = self.td['trigger_text'].split(b'\0',1)[0].decode()
        self.td['trigger_showtext'] = self.td['trigger_showtext'].split(b'\0',1)[0].decode()
        return { 'shared_data':self.sd, 'trigger_data':self.td}

    def get(self):
        """returns raw shared and trigger data as a dict

        Returns:
            dict: raw shared and trigger data ::

                {
                    'shared_data': dict, # of shared data,
                    'trigger_data': dict # trigger data
                }
        """
        return self._read()

    def get_shared_data(self):
        """Returns just the shared data

        Returns:
            dict: shared data
        """
        return self._read()['shared_data']

    def get_trigger_data(self):
        """Returns just the trigger data

        Returns:
            dict: trigger data
        """
        return self._read()['trigger_data']

    def close(self):
        """Closes the handle
        """
        try:
            if self.mhandle: self.mhandle.close()
            if self.fhandle: self.fhandle.close()
        except Exception as e:
            pass


from common.device_command import CommandUtil
from common.log import Log
from common.utils import Utils

import os
import time
from enum import Enum
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtTest import *
from com.dtmilano.android.viewclient import ViewClient
# from com.dtmilano.android.adb.adbclient import AdbClient

ONBOARDING_TIMEOUT = 600
REMOVING_TIMEOUT = 120
# start onboarding
ONBOARDING_ADD_BUTTON = 0
ONBOARDING_ADDITIONAL_ADD_BUTTON = 1
ONBOARDING_MANUAL_PAIRING_CODE_BOTTON = 2
ONBOARDING_MANUAL_PAIRING_CODE_TEXTBOX = 3
ONBOARDING_MANUAL_PAIRING_CODE_INPUT_DONE = 4
ONBOARDING_CONTINUE_BUTTON = 5
ONBOARDING_NO_MATTER_CERT_DEVICE_SKIP_BUTTON = 6
ONBOARDING_MATTER_DEVICE_NAME_CHANGE = 7
ONBOARDING_MATTER_DEVICE_NAME_CHANGE_DONE = 8
ONBOARDING_DONE_BACK_TO_FIRST_SCREEN = 9
# remove device
REMOVE_START = 30
REMOVE_BUTTON = 31
REMOVE_DONE = 32
# error capture
ERROR_CHECK_LAYOUT = 40
# words
ERROR_OCCUR_KOR = 90
ERROR_OCCUR_ENG = 91
ADD_DEVICE_KOR = 92
ADD_DEVICE_ENG = 93
REMOVE_BUTTON_KOR = 94
REMOVE_BUTTON_ENG = 95

class STOnboardingResult():
    ONBOARDING_FAILURE = 0
    ONBOARDING_SUCCESS = 1
    REMOVING_FAILURE = 2
    REMOVING_SUCCESS = 3
    REMOVED_PHONE = 4

    result_message = {
        ONBOARDING_FAILURE : "Onboarding failed",
        ONBOARDING_SUCCESS : "Onboarding success",
        REMOVING_FAILURE : "Removing failed",
        REMOVING_SUCCESS : "Removing success",
        REMOVED_PHONE : "Removed phone"
    }

    @classmethod
    def is_onboarding_result(cls, r):
        if r == cls.ONBOARDING_FAILURE or r == cls.ONBOARDING_SUCCESS:
            return True
        return False

    @classmethod
    def is_removing_result(cls, r):
        if r == cls.REMOVING_FAILURE or r == cls.REMOVING_SUCCESS:
            return True
        return False

    @classmethod
    def get_result_msg(cls, result):
        return cls.result_message.get(result, "")

class AutoDeviceState():
    IDLE = 0
    CONNECTING_PHONE = 1
    ONBOARDING = 2
    REMOVING = 3

## Dialog class ##
class simpleDlg(QDialog):

    ## Initialize Dialog ##
    def __init__(self, title, content):
        super(simpleDlg, self).__init__()
        self.app = QErrorMessage()
        self.app.showMessage(content)
        self.app.setWindowModality(Qt.WindowModal)
        self.app.setWindowTitle(title)
        self.app.exec()

    def no_device_dialog():
        simpleDlg("No device is selected",
                                "Check the device for onboarding")
        Log.print("Error: No device is selected for onboarding.")

    def one_device_dialog():
        simpleDlg("Wrong input",
                "Only one device can use Auto onboarding repeat test!!")
        Log.print("only one device can use Auto onboarding repeat test!!")

    def no_phone_dialog():
        err = simpleDlg("Error",
                "Can't work auto-onboarding because the phone is not connected")
        err.setWindowModality(Qt.NonModal)
        Log.print("Can't work auto-onboarding because the phone is not connected")

## Automation of devices ##
class autoDevice(QThread):
    update_onboarding_state = pyqtSignal(int, str, str)

    ## Initialize autodevice ##
    def __init__(self, parent):
        super().__init__()
        self.parent = parent
        self.device = None
        self.serialno = None
        self.vc = None
        self.running = False
        self.device_name = None
        self.comport = None
        self.device_num = None
        self.is_request = dict()
        self.debug = False
        # Log.print("autoDevice")

    ## Run autodevice ##
    def run(self):
        self.running = True
        self.step = AutoDeviceState.IDLE
        self.is_request.clear()
        while self.running:
            if self.step == AutoDeviceState.CONNECTING_PHONE:
                if self.try_connect_phone():
                    self.step = AutoDeviceState.ONBOARDING
                    continue
                self.step = AutoDeviceState.IDLE
            elif self.step == AutoDeviceState.ONBOARDING:
                self.auto_onboarding_device()
                self.step = AutoDeviceState.IDLE
            elif self.step == AutoDeviceState.REMOVING:
                self.auto_remove_device()
                self.step = AutoDeviceState.IDLE
            QTest.qWait(1000)

    ## Check device available for auto-onboarding ##
    def check_auto_available(self):
        if Utils.check_connectable():
            return True
        else:
            simpleDlg.no_phone_dialog()
            return False

    ## Check device running state ##
    def is_running(self):
        return self.running

    ## Try to connect to phone ##
    def try_connect_phone(self):
        result = False
        if self.check_auto_available():
            try:
                (self.device, self.serialno) = ViewClient.connectToDeviceOrExit()
                self.vc = ViewClient(self.device, self.serialno)
                self.device.reconnect = True
            except RuntimeError as e:
                Log.print("connect error", e)
            else:
                Log.print("adb is connected")
                result = True
        return result

    ## Check device connection state ##
    def is_connected(self):
        if self.device and self.vc:
            return True
        else:
            return False

    ## Request onboarding ##
    def request_onboarding(self, comport, device_num, code, device_id):
        if self.step != AutoDeviceState.IDLE:
            Log.print(f'working on something else  step : {self.step}')
            return False
        if True in self.is_request.values():
            Log.print(f'working on something else is_request {self.is_request}')
            return False
        self.is_request[device_num] = True
        self.comport = comport
        self.device_num = device_num
        self.device_id = device_id
        self.pairing_code = code
        if not self.is_connected():
            self.step = AutoDeviceState.CONNECTING_PHONE
        else:
            self.step = AutoDeviceState.ONBOARDING
        return True

    ## Update onboarding status of device ##
    def auto_onboarding_device(self):
        self.device_name = f"{CommandUtil.get_device_type_by_device_id(self.device_id)}-{self.device_num}"
        self.count = 0
        if self.auto_onboarding():
            Log.print("onboarding success!")
            self.update_onboarding_state.emit(
                STOnboardingResult.ONBOARDING_SUCCESS, self.comport, self.device_num)
            self.is_request[self.device_num] = False
        else:
            if not self.is_connected():
                Log.print("onboarding failed due to adb problem")
            else:
                Log.print("onboarding failed..")
                self.update_onboarding_state.emit(
                    STOnboardingResult.ONBOARDING_FAILURE, self.comport, self.device_num)
            if self.device_num in self.is_request:
                del self.is_request[self.device_num]

    ## auto onboarding ##
    def auto_onboarding(self):
        start_time = time.time()
        stair = ONBOARDING_ADD_BUTTON
        while self.running:
            # Log.print(f"auto_onboarding step {self.step} device_num {self.device_num}")
            if True not in self.is_request.values():
                return False
            obj = self.get_obj(str(stair))
            if obj and stair == ONBOARDING_ADDITIONAL_ADD_BUTTON:
                if obj.getText() in [self.get_smartthings_view_id(ADD_DEVICE_KOR),
                                     self.get_smartthings_view_id(ADD_DEVICE_ENG)]:
                    if self.debug == True:
                        Log.print(
                            f"stair::ONBOARDING_ADDITIONAL_ADD_BUTTON, obj={obj}")
                    obj.touch()
                    stair += 1
            elif not obj and stair == ONBOARDING_ADDITIONAL_ADD_BUTTON:
                obj = self.get_obj(str(stair), True)
                if obj:
                    if self.debug == True:
                        Log.print(
                            f"stair::ONBOARDING_ADDITIONAL_ADD_BUTTON, obj={obj}")
                    obj.touch()
                    stair += 1
                else:
                    obj = self.get_obj(
                        str(ONBOARDING_MANUAL_PAIRING_CODE_BOTTON))
                    if obj:
                        if self.debug == True:
                            Log.print(
                                f"stair::ONBOARDING_MANUAL_PAIRING_CODE_BOTTON, obj={obj}")
                        obj.touch()
                        stair += 2
            elif obj and stair == ONBOARDING_MANUAL_PAIRING_CODE_TEXTBOX:
                if self.debug == True:
                    Log.print(
                        f"stair::ONBOARDING_MANUAL_PAIRING_CODE_TEXTBOX, obj={obj}")
                obj.type(self.pairing_code)
                stair += 1
            elif obj and stair == ONBOARDING_MATTER_DEVICE_NAME_CHANGE:
                if self.debug == True:
                    Log.print(
                        f"stair::ONBOARDING_MATTER_DEVICE_NAME_CHANGE, obj={obj}")
                obj.setText(self.device_name)
                stair += 1
            elif obj and stair == ONBOARDING_MATTER_DEVICE_NAME_CHANGE_DONE:
                if self.debug == True:
                    Log.print(
                        f"stair::ONBOARDING_MATTER_DEVICE_NAME_CHANGE_DONE, obj={obj}")
                if self.device.isKeyboardShown():
                    self.device.press("BACK")
                    if self.debug == True:
                        Log.print(
                            f"stair::ONBOARDING_MATTER_DEVICE_NAME_CHANGE_DONE, keyboard down")
                    QTest.qWait(2000)
                obj.touch()
                QTest.qWait(12000)
                stair += 1
            elif stair == ONBOARDING_DONE_BACK_TO_FIRST_SCREEN:
                if self.debug == True:
                    Log.print("stair::ONBOARDING_DONE_BACK_TO_FIRST_SCREEN")
                obj = self.get_obj("text:" + self.device_name, True)
                if obj or self.count >= 3:
                    if self.debug == True:
                        Log.print(f"obj={obj}")
                    self.vc.device.press("BACK")
                    QTest.qWait(3000)
                    if self.debug == True:
                        Log.print("press BACK")
                    return True
                else:
                    if self.debug == True:
                        Log.print(
                            f"stair::ONBOARDING_DONE_BACK_TO_FIRST_SCREEN {self.device_name} not found")
                    self.count += 1
            elif obj:
                if self.debug == True:
                    Log.print(f"stair::{stair}, obj={obj}")
                obj.touch()
                QTest.qWait(1000)
                stair += 1
            elif not obj:
                if stair == ONBOARDING_MATTER_DEVICE_NAME_CHANGE:
                    if self.debug == True:
                        Log.print("waiting download plugin in SmartThings...")
                    self.view_dump(5)
                else:
                    if self.debug == True:
                        Log.print(f"not found stair::{stair}'s obj")
                    self.view_dump()
            if True not in self.is_request.values():
                return False
            # screenshot when error occurs
            err = self.get_obj(str(ERROR_CHECK_LAYOUT))
            if err:
                if err.getText() in [self.get_smartthings_view_id(ERROR_OCCUR_KOR),
                                     self.get_smartthings_view_id(ERROR_OCCUR_ENG)]:
                    if self.debug == True:
                        Log.print(err)
                    self.screenshot()
                    err2 = self.get_obj(
                        str(ONBOARDING_MATTER_DEVICE_NAME_CHANGE_DONE))
                    if err2:
                        err2.touch()
                    QTest.qWait(1000)
                    self.vc.device.press("BACK")
                    if self.debug == True:
                        Log.print("press BACK")
                    return False

            # screenshot when too much delay occurred during onboarding and return error
            now_time = time.time()
            if now_time - start_time > ONBOARDING_TIMEOUT:
                Log.print(
                    f"Too much time {ONBOARDING_TIMEOUT}s goes on for onboarding... it is failed!")
                self.vc.device.press("BACK")
                while self.running:
                    ti = self.get_obj(str(ERROR_CHECK_LAYOUT), True)
                    if ti:
                        self.screenshot()
                        if self.debug == True:
                            Log.print(ti)
                        ti.touch()
                        break
                QTest.qWait(1000)
                self.vc.device.press("BACK")
                QTest.qWait(1000)
                if self.debug == True:
                    Log.print("press BACK")
                return False

    ## Disconnect device ##
    def disconnect_device(self, device_num):
        if device_num in self.is_request:
            del self.is_request[device_num]
            # self.step = 0

    ## Remove request ##
    def request_remove(self, comport, device_num, device_id):
        if self.step != AutoDeviceState.IDLE:
            Log.print(f'working on something else  step : {self.is_request}')
            return
        if True in self.is_request.values():
            Log.print(f'working on something else is_request : {self.is_request}')
            return
        if not device_num in self.is_request:
            return
        self.is_request[device_num] = True
        self.device_num = device_num
        self.comport = comport
        self.device_id = device_id
        self.step = AutoDeviceState.REMOVING

    ## Check remove status of device ##
    def auto_remove_device(self):
        self.device_name = f"{CommandUtil.get_device_type_by_device_id(self.device_id)}-{self.device_num}"
        if self.remove_device():
            Log.print("removing success!")
            self.update_onboarding_state.emit(
                STOnboardingResult.REMOVING_SUCCESS, self.comport, self.device_num)
            self.vc.sleep(1)
        else:
            Log.print("removing failed!")
            self.update_onboarding_state.emit(
                STOnboardingResult.REMOVING_FAILURE, self.comport, self.device_num)
            self.vc.sleep(1)
        if self.device_num in self.is_request:
            del self.is_request[self.device_num]

    ## Remove device ##
    def remove_device(self):
        Log.print(f"{self.device_name} will be removed ...")
        start_time = time.time()
        stair = REMOVE_START
        while self.running:
            if stair == REMOVE_START:
                obj = self.get_obj("text:" + self.device_name, True)
                if obj:
                    if self.debug == True:
                        Log.print(obj)
                    (x, y) = obj.getCenter()
                    self.device.drag((x, y), (x, y), 2000, 1)
                    stair = stair + 1
                else:
                    Log.print(f"can't find {self.device_name}")
                    if time.time() - start_time > REMOVING_TIMEOUT:
                        Log.print(f"can't not find {self.device_name} anymore")
                        return False
                    continue
            elif stair >= REMOVE_BUTTON and stair <= REMOVE_DONE:
                self.view_dump()
                if stair == REMOVE_BUTTON:
                    obj = self.get_obj("text:" + str(REMOVE_BUTTON_KOR)
                                       ) or self.get_obj("text:" + str(REMOVE_BUTTON_ENG))
                else:
                    obj = self.get_obj(str(stair))
                if obj:
                    if self.debug == True:
                        Log.print(obj)
                    obj.touch()
                    stair = stair + 1
                else:
                    if stair == REMOVE_BUTTON:
                        Log.print(f"can't not find edit button")
                    elif stair == REMOVE_DONE:
                        Log.print(f"can't find remove device button")
                    continue
            else:
                Log.print("exit removing device")
                return True

    ## Take screen shot ##
    def screenshot(self):
        screenshot_path = Utils.get_screenshot_path()
        if not os.path.isdir(screenshot_path):
            os.mkdir(screenshot_path)
        self.view_dump()
        self.device.takeSnapshot(reconnect=True).save(
            f"{screenshot_path}error_{self.device_name}_{time.strftime('%Y_%m_%d-%H_%M_%S')}.png", "PNG")
        Log.print("error screenshot done")

    ## Get object ##
    def get_obj(self, key, request_dump=False):
        if not self.vc:
            return None
        if request_dump:
            self.view_dump()
        if key.startswith("text:"):
            text = key[len("text:"):]
            if text.isdecimal():
                return self.vc.findViewWithText(self.get_smartthings_view_id(int(text)))
            else:
                return self.vc.findViewWithText(text)
        else:
            return self.vc.findViewById(self.get_smartthings_view_id(int(key)))

    ## View dump ##
    def view_dump(self, delay=1):
        try:
            self.vc.dump(window=-1, sleep=delay)
        except:
            pass

    ## Get smartthings view id ##
    def get_smartthings_view_id(self, key):
        type = {
            ONBOARDING_ADD_BUTTON: "com.samsung.android.oneconnect:id/add_menu_button",
            ONBOARDING_ADDITIONAL_ADD_BUTTON: "com.samsung.android.oneconnect:id/title",
            ONBOARDING_MANUAL_PAIRING_CODE_BOTTON: "com.samsung.android.oneconnect:id/enter_setup_code_button",
            ONBOARDING_MANUAL_PAIRING_CODE_TEXTBOX: "com.samsung.android.oneconnect:id/add_device_item_serial_edit_text",
            ONBOARDING_MANUAL_PAIRING_CODE_INPUT_DONE: "com.samsung.android.oneconnect:id/menu_done",
            ONBOARDING_CONTINUE_BUTTON: "com.samsung.android.oneconnect:id/onboarding_item_main_button",
            ONBOARDING_NO_MATTER_CERT_DEVICE_SKIP_BUTTON: "android:id/button1",
            ONBOARDING_MATTER_DEVICE_NAME_CHANGE: "com.samsung.android.oneconnect:id/onboarding_success_card_detail_view_editor",
            ONBOARDING_MATTER_DEVICE_NAME_CHANGE_DONE: "com.samsung.android.oneconnect:id/onboarding_item_positive_navigation_text",
            ONBOARDING_DONE_BACK_TO_FIRST_SCREEN: "android.widget.Button",\
            # REMOVE_BUTTON:"id/no_id/5",\
            REMOVE_BUTTON_ENG: "Remove",\
            REMOVE_DONE: "android:id/button1",\
            REMOVE_BUTTON_KOR: "삭제",\
            ERROR_CHECK_LAYOUT: "com.samsung.android.oneconnect:id/onboarding_step_description_layout",\
            ERROR_OCCUR_KOR: "오류가 있어요",\
            ERROR_OCCUR_ENG: "Something went wrong",\
            ADD_DEVICE_KOR: "기기 추가",\
            ADD_DEVICE_ENG: "Add device"
        }.get(key, key)
        return type

    ## Phone removed ##
    def removed_phone(self):
        del self.device
        del self.vc
        self.device = None
        self.vc = None

    ## Stop Onboarding ##
    def stop(self):
        if not self.running:
            return
        self.removed_phone()
        self.running = False
        self.update_onboarding_state.emit(
            STOnboardingResult.REMOVED_PHONE, self.comport, self.device_num)
        self.quit()
        self.wait(1000)


from celery import Celery
from django.core.mail import send_mail
from django.conf import settings
from django.template import loader
import os
# import django
# os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dailyfresh.settings')
# django.setup()
from goods.models import GoodsType, IndexGoodsBanner, IndexPromotioBanner, IndexTypeGoodsBanner

# 创建一个celery的实力对象
app = Celery('celery_tasks.tasks', broker='redis://192.168.72.136:6379/8')

# 定义任务函数
@app.task
def send_register_active_email(to_email, username, token):
    '''发送激活邮件'''
    subject = '天天生鲜欢迎信息'
    message = ''
    html_message = '''
%s,欢迎注册会员

请点击下面链接激活

            http://127.0.0.1:8000/user/active/%s
            ''' % (username, token, token)
    sender = settings.DEFAULT_FROM_EMAIL
    recrice = [to_email]
    send_mail(subject, message, sender, recrice, html_message=html_message)

@app.task
def generate_static_index_html():
    '''生产首页静态页面'''
    # 获取首页商品的分类信息
    types = GoodsType.objects.all()

    # 获取轮播商品信息
    goods_banners = IndexGoodsBanner.objects.all().order_by('index')

    # 获取首页促销活动信息
    promotion_banners = IndexPromotioBanner.objects.all().order_by('index')

    # 获取首页分类商品展示信息
    for type in types:
        # 获取type种类商品的图片展示信息
        image_banners = IndexTypeGoodsBanner.objects.filter(type=type, display_type=1)
        # 获取文字展示信息
        title_banners = IndexTypeGoodsBanner.objects.filter(type=type, display_type=0)
        # 动态增加属性
        type.image_banners = image_banners
        type.title_banners = title_banners

    # 获取购物车商品数目
    cart_count = 0

    # 组织上下文
    context = {
        'types': types,
        'goods_banners': goods_banners,
        'promotion_banners': promotion_banners,
        'cart_count': cart_count
    }
    # 加载模板
    temp = loader.get_template('static_index.html')
    # 模板渲染
    static_index_html = temp.render(context)
    # 生产静态文件
    save_path = os.path.join(settings.BASE_DIR, 'static/index.html')
    with open(save_path, 'w') as f:
        f.write(static_index_html)


#using conda install -c conda-forge tqdm command
from tqdm.auto import tqdm
import torch
from torch import nn
def train(train_loader,model_d,optimizer,device):
    # set model to training mode
    model_d.train()
    total_correct_number = 0
    train_loss_list = []
    # iterate through the dataloader
    for data , label in tqdm(train_loader):
      # move data to device (cpu/cuda)
      data_d , label_d = data.to(device), label.to(device)
      # forward pass (compute output tensor)
      pred = (model_d(data_d))
      # get the index of the class with the highest probability
      max_prob_values, max_prob_indexs = torch.max(pred, dim = 1)
      correct_number = (max_prob_indexs.cpu() == label_d.cpu()).sum().item()
      total_correct_number += correct_number
      # compute loss
      loss = model_d.cal_loss(pred , label_d)
      # compute gradient (backpropagation)
      loss.backward()
      # Clip the gradient norms for stable training.
      nn.utils.clip_grad_norm_(model_d.parameters(), max_norm=10)
      # update model with optimizer
      optimizer.step()
      # set optimizer gradient to zero
      optimizer.zero_grad()
      train_loss_list.append(loss.detach().cpu().item())
    acc = total_correct_number/len(train_loader.dataset)
    return acc , train_loss_list


import sqlite3
import requests
from bs4 import BeautifulSoup
from steampy.client import SteamClient
from config import *

def decorator_open_data_base(func):
    def open_data_base():
        con = sqlite3.connect("guns.db")
        mouse = con.cursor()
        func(mouse)
        con.close()

    return open_data_base

def market(func):
    def result(*args, **kwargs):
        x = requests.get(func(*args, **kwargs))
        if dict(x.content).get("success"):
            print('Успешно')
        else:
            print(x.content)

    return result

@decorator_open_data_base
def cost_update(mouse, current_cost_of_weapons, current_weapons, current_box):
    for i in current_weapons:
        url = f'https://market.csgo.com/?t=all&search={current_weapons[i]}&sd=desc'
        response = requests.get(url)
        soup = BeautifulSoup(response.text, 'lxml')
        tags = soup.find_all("a", {"class": "item"})
        item_prices = tags[i].find('a', class_="price").text.strip()
        if item_prices is not None:
            current_cost_of_weapons.append(sorted(item_prices, reverse=False)[0])
            sql = """UPDATE guns SET nickname = %s WHERE id = %s"""
            mouse.execute(sql, (current_weapons[i], current_cost_of_weapons[i]))

@decorator_open_data_base
def find_weapons(mouse, current_box):
    result = mouse.execute("""SELECT * FROM guns
                WHERE box = ?""", ()).fetchall(current_box)
    return result

@market
def sale(id_weapon, price):
    return f'https://market.csgo.com/api/v2/add-to-sale?key=[your_secret_key]&id=[{id_weapon}]&price=[{price}]&cur=[RUB]'

@market
def buy(id_weapon, price):
    return f'https://market.csgo.com/api/v2/buy?key=[your_secret_key]&id=[{id_weapon}]&price=[{price}]'

def accept_contract():
    steam_client = SteamClient(MY_API_KEY)
    steam_client.login(MY_USERNAME, MY_PASSWORD, 'steam_guard.json')
    steam_client.accept_trade_offer(trade_id)

# class weapon:
#     def __init__(self, name, quality):
#         self.quality = quality
#         self.cost = 0
#         self.name = name
#
#     def change(self, cost):
#         self.cost = cost
#
#
# class contract:
#     def __init__(self, weapons):
#         all = [self.gun1 = weapon()
#         self.gun2 = weapon()
#         self.gun3 = weapon()
#         self.gun4 = weapon()
#         self.gun5 = weapon()
#         self.gun6 = weapon()
#         self.gun7 = weapon()
#         self.gun8 = weapon()
#         self.gun9 = weapon()
#         self.gun10 = weapon()]
#
#     def change(self):
#         for i in sorted(weapons, key=lambda x: x[0]):
#             weapon(i[0]).change(i[1])


r"""Plotting helpers."""

__all__ = ['nice_rc', 'corner', 'mark_point', 'coverage_plot']

import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np

from numpy import ndarray as Array
from typing import *

def nice_rc(latex: bool = False) -> Dict[str, Any]:
    r"""Returns a dictionary of runtime configuration (rc) settings for nicer
    :mod:`matplotlib` plots. The settings include 12pt font size, higher DPI,
    tight layout, transparent background, etc.

    Arguments:
        latex: Whether to use LaTeX typesetting or not.

    Example:
        >>> plt.rcParams.update(nice_rc())
    """

    rc = {
        'axes.axisbelow': True,
        'axes.linewidth': 0.8,
        'figure.dpi': 150,
        'figure.figsize': (6.4, 4.8),
        'font.size': 12.0,
        'legend.fontsize': 'x-small',
        'lines.linewidth': 1.0,
        'lines.markersize': 3.0,
        'savefig.bbox': 'tight',
        'savefig.pad_inches': 0.05,
        'savefig.transparent': True,
        'xtick.labelsize': 'x-small',
        'xtick.major.width': 0.8,
        'ytick.labelsize': 'x-small',
        'ytick.major.width': 0.8,
    }

    if latex:
        rc.update({
            'font.family': ['serif'],
            'font.serif': ['Computer Modern'],
            'text.usetex': True,
        })

    return rc

class LinearAlphaColormap(mpl.colors.LinearSegmentedColormap):
    r"""Linear segmented transparency colormap.

    Arguments:
        color: A color.
        levels: A sequence of levels dividing the domain into segments.
        alpha: The transparency range.
        name: A name for the colormap.
    """

    def __new__(
        self,
        color: Union[str, tuple],
        levels: Array = None,
        alpha: Tuple[float, float] = (0.0, 1.0),
        name: str = None,
    ):
        if name is None:
            if type(color) is str:
                name = f'alpha_{color}'
            else:
                name = f'alpha_{hash(color)}'

        if levels is None:
            levels = [0.0, 1.0]

        levels = np.asarray(levels)
        levels = (levels - levels.min()) / (levels.max() - levels.min())
        alpha = np.linspace(*alpha, len(levels))

        rgb = mpl.colors.to_rgb(color)
        colors = sorted((l, rgb + (a,)) for l, a in zip(levels, alpha))

        return mpl.colors.LinearSegmentedColormap.from_list(
            name=name,
            colors=colors,
        )

def gaussian_blur(img: Array, sigma: float = 1.0) -> Array:
    r"""Applies a Gaussian blur to an image.

    Arguments:
        img: An image array.
        sigma: The standard deviation of the Gaussian kernel.

    Returns:
        The blurred image.

    Example:
        >>> img = np.random.rand(128, 128)
        >>> gaussian_blur(img, sigma=2.0)
        array([...])
    """

    size = 2 * int(3 * sigma) + 1

    k = np.arange(size) - size / 2
    k = np.exp(-(k**2) / (2 * sigma**2))
    k = k / np.sum(k)

    smooth = lambda x: np.convolve(x, k, mode='same')

    for i in range(len(img.shape)):
        img = np.apply_along_axis(smooth, i, img)

    return img

def credible_levels(hist: Array, creds: Array) -> Array:
    r"""Returns the levels of credibility region contours.

    Arguments:
        hist: An histogram.
        creds: The region credibilities.
    """

    x = np.sort(hist, axis=None)[::-1]
    cdf = np.cumsum(x)
    idx = np.searchsorted(cdf, creds * cdf[-1])

    return x[idx]

def corner(
    data: Array,
    weights: Array = None,
    domain: Tuple[Array, Array] = None,
    bins: Union[int, Sequence[int]] = 64,
    creds: Sequence[float] = (0.6827, 0.9545, 0.9973),
    color: Union[str, tuple] = None,
    alpha: Tuple[float, float] = (0.0, 0.5),
    legend: str = None,
    labels: Sequence[str] = None,
    smooth: float = 0,
    figure: plt.Figure = None,
    **kwargs,
) -> plt.Figure:
    r"""Displays each 1 or 2-d projection of multi-dimensional data, as a triangular
    matrix of histograms, known as corner plot. For 2-d histograms, highest density
    credibility regions are delimited.

    Arguments:
        data: Multi-dimensional data, either as a table or as a matrix of histograms.
        weights: The importance weights of the data samples.
        domain: A pair of lower and upper domain bounds. If :py:`None`, inferred from data.
        bins: The number(s) of bins per dimension.
        creds: The region credibilities (in :math:`[0, 1]`) to delimit.
        color: The color of histograms. If :py:`None`, use the color cycler instead.
        alpha: The transparency range.
        legend: A legend.
        labels: The dimension labels.
        smooth: The standard deviation of the smoothing kernels.
        figure: A corner plot over which to draw the new one.
        kwargs: Keyword arguments passed to :func:`matplotlib.pyplot.subplots`.

    Returns:
        The figure instance for the corner plot.

    Example:
        >>> data = np.random.randn(2**16, 3)
        >>> labels = [r'$\alpha$', r'$\beta$', r'$\gamma$']
        >>> figure = corner(data, bins=32, labels=labels, figsize=(4.8, 4.8))

    .. image:: ../images/corner.png
        :align: center
        :width: 600
    """

    # Histograms
    if np.asarray(data[0][0]).ndim == 0:
        data = np.asarray(data)
        D = data.shape[-1]

        if type(bins) is int:
            bins = [bins] * D

        if domain is None:
            lower, upper = data.min(axis=0), data.max(axis=0)
        else:
            lower, upper = map(np.asarray, domain)

        bins = [
            np.histogram_bin_edges(data, bins[i], range=(lower[i], upper[i]))
            for i in range(D)
        ]

        hists = np.ndarray((D, D), dtype=object)

        for i in range(D):
            for j in range(i + 1):
                if i == j:
                    hist, _ = np.histogram(
                        data[..., i],
                        bins=bins[i],
                        density=True,
                        weights=weights,
                    )
                else:
                    hist, _, _ = np.histogram2d(
                        data[..., i],
                        data[..., j],
                        bins=(bins[i], bins[j]),
                        density=True,
                        weights=weights,
                    )

                hists[i, j] = hist
    else:
        data = np.asarray(data, dtype=object)
        D = len(data)

        if domain is None:
            lower, upper = np.zeros(D), np.ones(D)
        else:
            lower, upper = map(np.asarray, domain)

        bins = [None] * D
        for i in range(D):
            if data[i, i] is not None:
                bins[i] = np.linspace(lower[i], upper[i], len(data[i, i]) + 1)

        hists = data

    # Figure
    if figure is None:
        kwargs.setdefault('figsize', (6.4, 6.4))

        figure, axes = plt.subplots(
            D,
            D,
            squeeze=False,
            sharex='col',
            gridspec_kw={'wspace': 0.0, 'hspace': 0.0},
            **kwargs,
        )
        new = True
    else:
        axes = np.asarray(figure.axes).reshape(D, D)
        new = False

    # Legend

    ## Quantiles
    creds = np.sort(np.asarray(creds))[::-1]
    creds = np.append(creds, 0)

    if new:
        cmap = LinearAlphaColormap('black', levels=creds, alpha=alpha)

        levels = (creds - creds.min()) / (creds.max() - creds.min())
        levels = (levels[:-1] + levels[1:]) / 2

        for c, l in zip(creds[:-1], levels):
            axes[0, -1].plot(
                [],
                [],
                color=cmap(l),
                linewidth=6,
                solid_capstyle='butt',
                label=r'${:.1f}\,\%$'.format(c * 100),
            )

    ## Color
    lines = axes[0, -1].plot([], [], color=color, solid_capstyle='butt', label=legend)
    color = lines[-1].get_color()

    axes[0, -1].legend(loc='upper right', frameon=False)

    # Plot
    for i in range(D):
        for j in range(D):
            ax = axes[i, j]
            hist = hists[i, j]

            if j > i:
                ax.axis('off')
                continue

            if hist is None:
                continue

            if smooth > 0:
                hist = gaussian_blur(hist, smooth)

            ## Draw
            x, y = bins[j], bins[i]
            x = (x[1:] + x[:-1]) / 2
            y = (y[1:] + y[:-1]) / 2

            if i == j:
                ax.plot(x, hist, color=color)

                _, top = ax.get_ylim()
                top = max(top, hist.max() * 1.0625)

                ax.set_xlim(left=bins[i][0], right=bins[i][-1])
                ax.set_ylim(bottom=0.0, top=top)
            else:
                levels = np.unique(credible_levels(hist, creds))

                cf = ax.contourf(
                    x,
                    y,
                    hist,
                    levels=levels,
                    cmap=LinearAlphaColormap(color, levels, alpha=alpha),
                )
                ax.contour(cf, colors=color)

                if j > 0:
                    ax.sharey(axes[i, j - 1])
                else:
                    ax.set_ylim(bottom=bins[i][0], top=bins[i][-1])

            ## Ticks
            if i == D - 1:
                ax.xaxis.set_major_locator(mpl.ticker.MaxNLocator(3, prune='both'))
                plt.setp(
                    ax.get_xticklabels(),
                    rotation=45.0,
                    horizontalalignment='right',
                    rotation_mode='anchor',
                )
            else:
                ax.xaxis.set_ticks_position('none')

            if i == j:
                ax.set_yticks([])
            elif j == 0:
                ax.yaxis.set_major_locator(mpl.ticker.MaxNLocator(3, prune='both'))
            else:
                ax.yaxis.set_ticks_position('none')

            ## Labels
            if labels is not None:
                if i == D - 1:
                    ax.set_xlabel(labels[j])

                if j == 0 and i != j:
                    ax.set_ylabel(labels[i])

            ax.label_outer()

    figure.align_labels()
    figure.set_layout_engine(layout='tight')

    return figure

def mark_point(
    figure: plt.Figure,
    point: Array,
    color: Union[str, tuple] = 'black',
    linestyle: str = 'dashed',
    marker: str = 's',  # square
    legend: str = None,
) -> None:
    r"""Marks a point on the histograms of a corner plot.

    Arguments:
        figure: The corner plot figure (see :func:`corner`).
        point: The point to mark.
        color: The color of the lines and markers.
        linestyle: The style of the lines.
        marker: The shape of the markers.
        legend: A legend.

    Example:
        >>> mark_point(figure, [0.5, 0.3, -0.7], color='black')

    .. image:: ../images/corner_marked.png
        :align: center
        :width: 600
    """

    D = len(point)
    axes = np.asarray(figure.axes).reshape(D, D)

    # Legend
    if legend is not None:
        axes[0, -1].plot(
            [],
            [],
            color=color,
            linestyle=linestyle,
            marker=marker,
            solid_capstyle='butt',
            label=legend,
        )

        axes[0, -1].legend(loc='upper right', frameon=False)

    # Plot
    for i in range(D):
        for j in range(i + 1):
            ax = axes[i, j]
            ax.axvline(point[j], color=color, linestyle=linestyle)

            if i != j:
                ax.axhline(point[i], color=color, linestyle=linestyle)
                ax.plot(point[j], point[i], color=color, marker=marker)

def coverage_plot(
    levels: Array,
    coverages: Array,
    color: Union[str, tuple] = None,
    legend: str = None,
    figure: plt.Figure = None,
    **kwargs,
) -> plt.Figure:
    r"""Plots the expected coverage at various credible levels.

    Arguments:
        levels: A vector of increasing credible levels.
        coverages: A vector of corresponding expected coverages.
        legend: A legend.
        figure: Another coverage plot over which to draw the new one.
        kwargs: Keyword arguments passed to :func:`matplotlib.pyplot.subplots`.

    Returns:
        The figure instance for the coverage plot.

    Example:
        >>> levels = np.linspace(0, 1, 512) ** 2
        >>> coverages = np.linspace(0, 1, 512)
        >>> figure = coverage_plot(levels, coverages)

    .. image:: ../images/coverage.png
        :align: center
        :width: 400
    """

    levels, coverages = np.asarray(levels), np.asarray(coverages)

    # Figure
    if figure is None:
        kwargs.setdefault('figsize', (3.2, 3.2))

        figure, ax = plt.subplots(**kwargs)
        new = True
    else:
        ax = figure.axes[0]
        new = False

    # Plot
    if new:
        ax.plot([0, 1], [0, 1], color='k', linestyle='--')

    ax.plot(levels, coverages, color=color, label=legend)

    ax.grid(visible=True)
    ax.set_xlabel(r'Credible level')
    ax.set_ylabel(r'Expected coverage')

    if legend is not None:
        ax.legend(loc='upper left')

    figure.set_layout_engine(layout='tight')

    return figure