#!/usr/bin/env python3

import asyncio
import collections
import datetime
import glob
import html
import json
import os
import re
import time
import traceback
from enum import Enum, IntEnum

import ntpath

from async_hsm import (Ahsm, Event, Framework, Signal, TimeEvent, state)
from experiments.IDriver.DBWriter.AioInfluxDBWriter import AioInfluxDBWriter
from experiments.LOLogger.LOLoggerClient import LOLoggerClient
from experiments.state_machine.back_end.pigss_payloads import (PigletRequestPayload, PlanError, SystemConfiguration)

log = LOLoggerClient(client_name="PigssController", verbose=True)

PLAN_FILE_DIR = "/tmp/plan_files"
if not os.path.isdir(PLAN_FILE_DIR):
    os.makedirs(PLAN_FILE_DIR, 0o755)

class UiStatus(str, Enum):
    DISABLED = "DISABLED"
    READY = "READY"
    AVAILABLE = "AVAILABLE"
    ACTIVE = "ACTIVE"
    CLEAN = "CLEAN"
    REFERENCE = "REFERENCE"

class PlanPanelType(IntEnum):
    NONE = 0
    PLAN = 1
    LOAD = 2
    SAVE = 3
    EDIT = 4

def setbits(mask):
    """Return the bits set in the integer `mask` """
    result = []
    bit = 0
    while mask > 0:
        if mask % 2:
            result.append(bit)
        mask >>= 1
        bit += 1
    return result

class PigssController(Ahsm):
    num_chans_per_bank = 8

    def __init__(self, farm=None):
        super().__init__()
        self.farm = farm
        self.error_list = collections.deque(maxlen=32)
        self.status = {}
        self.all_banks = []
        self.plan = {
            "max_steps": 32,
            "panel_to_show": int(PlanPanelType.NONE),
            "current_step": 1,
            "looping": False,
            "focus": {
                "row": 1,
                "column": 1
            },
            # "last_step": 1,
            # "steps": {
            #     1: {
            #         "duration": 60,
            #         "reference": 0,
            #         "banks": {
            #             3: {
            #                 "chan_mask": 0,
            #                 "clean": 0
            #             },
            #             4: {
            #                 "chan_mask": 0,
            #                 "clean": 1
            #             }
            #         }
            #     }
            # },
            "last_step": 0,
            "steps": {},
            "num_plan_files": 0,
            "plan_files": {},
            "plan_filename": "",
            "bank_names": {
                "1": {
                    "name": "Bank 1",
                    "channels": {
                        "1": "Ch. 1",
                        "2": "Ch. 2",
                        "3": "Ch. 3",
                        "4": "Ch. 4",
                        "5": "Ch. 5",
                        "6": "Ch. 6",
                        "7": "Ch. 7",
                        "8": "Ch. 8"
                    }
                },
                "2": {
                    "name": "Bank 2",
                    "channels": {
                        "1": "Ch. 1",
                        "2": "Ch. 2",
                        "3": "Ch. 3",
                        "4": "Ch. 4",
                        "5": "Ch. 5",
                        "6": "Ch. 6",
                        "7": "Ch. 7",
                        "8": "Ch. 8"
                    }
                },
                "3": {
                    "name": "Bank 3",
                    "channels": {
                        "1": "Ch. 1",
                        "2": "Ch. 2",
                        "3": "Ch. 3",
                        "4": "Ch. 4",
                        "5": "Ch. 5",
                        "6": "Ch. 6",
                        "7": "Ch. 7",
                        "8": "Ch. 8"
                    }
                },
                "4": {
                    "name": "Bank 4",
                    "channels": {
                        "1": "Ch. 1",
                        "2": "Ch. 2",
                        "3": "Ch. 3",
                        "4": "Ch. 4",
                        "5": "Ch. 5",
                        "6": "Ch. 6",
                        "7": "Ch. 7",
                        "8": "Ch. 8"
                    }
                }
            },
            "available_ports": {
                "1": 0,
                "2": 0,
                "3": 0,
                "4": 0
            }
        }
        self.modal_info = {
            "show": False,
            "html": "
Example Modal Dialog

Test message
",
            "num_buttons": 2,
            "buttons": {
                1: {
                    "caption": "OK",
                    "className": "btn btn-success btn-large",
                    "response": "modal_ok"
                },
                2: {
                    "caption": "Cancel",
                    "className": "btn btn-danger btn-large",
                    "response": "modal_close"
                }
            }
        }
        self.send_queue = None
        self.receive_queue = None
        db_config = self.farm.config.get_time_series_database()
        self.db_writer = AioInfluxDBWriter(address=db_config["server"], db_port=db_config["port"], db_name=db_config["name"])

    def set_queues(self, send_queue, receive_queue):
        self.send_queue = send_queue
        self.receive_queue = receive_queue

    def get_available_ports(self):
        return self.plan["available_ports"]

    def get_bank_names(self):
        return self.plan["bank_names"]

    def get_plan_filenames(self):
        # Get list of plan filenames and update the entry in self.plan. This also
        #  informs any clients connected via a web socket of the list
        filenames = sorted(glob.glob(os.path.join(PLAN_FILE_DIR, "*.pln")))
        self.set_plan(["plan_files"], {i + 1: os.path.splitext(ntpath.basename(name))[0] for i, name in enumerate(filenames)})
        self.set_plan(["num_plan_files"], len(filenames))
        return filenames

    async def save_port_history(self):
        data = [{
            "measurement": "port_history",
            "tags": {},
            "fields": {
                "available_ports": json.dumps(self.get_available_ports()),
                "bank_names": json.dumps(self.get_bank_names())
            },
            "time": time.time_ns()
        }]
        await self.db_writer.write_data(data)

    async def process_receive_queue_task(self):
        event_by_element = dict(standby=Signal.BTN_STANDBY,
                                identify=Signal.BTN_IDENTIFY,
                                reference=Signal.BTN_REFERENCE,
                                modal_close=Signal.MODAL_CLOSE,
                                modal_ok=Signal.MODAL_OK,
                                plan=Signal.BTN_PLAN,
                                plan_cancel=Signal.BTN_PLAN_CANCEL,
                                plan_delete=Signal.BTN_PLAN_DELETE,
                                plan_insert=Signal.BTN_PLAN_INSERT,
                                plan_load=Signal.BTN_PLAN_LOAD,
                                plan_load_cancel=Signal.BTN_PLAN_LOAD_CANCEL,
                                plan_load_filename=Signal.PLAN_LOAD_FILENAME,
                                plan_ok=Signal.BTN_PLAN_OK,
                                plan_panel=Signal.PLAN_PANEL_UPDATE,
                                plan_save=Signal.BTN_PLAN_SAVE,
                                plan_save_cancel=Signal.BTN_PLAN_SAVE_CANCEL,
                                plan_save_filename=Signal.PLAN_SAVE_FILENAME,
                                plan_delete_filename=Signal.BTN_PLAN_DELETE_FILENAME,
                                plan_save_ok=Signal.BTN_PLAN_SAVE_OK,
                                clean=Signal.BTN_CLEAN,
                                run=Signal.BTN_RUN,
                                channel=Signal.BTN_CHANNEL,
                                edit=Signal.BTN_EDIT,
                                edit_panel=Signal.CHANGE_NAME_BANK,
                                edit_cancel=Signal.EDIT_CANCEL,
                                edit_save=Signal.EDIT_SAVE,
                                plan_loop=Signal.BTN_PLAN_LOOP,
                                plan_run=Signal.BTN_PLAN_RUN)
        while True:
            try:
                msg = json.loads(await self.receive_queue.get())
                # log.info(f"Received from socket {msg}")
                Framework.publish(Event(event_by_element[msg["element"]], msg))
            except KeyError:
                log.warning(f"Cannot find event associated with socket message {msg['element']} - ignoring")

    def get_modal_info(self):
        return self.modal_info

    def get_status(self):
        return self.status

    def get_plan(self):
        return self.plan

    def modify_value_in_nested_dict(self, target, path, value=None):
        """Modify a portion of the "target" which is a nested dictionary. The
        specified "path" is a list of strings which navigate through the levels
        of the dictionary. For example, if "target" is the dictionary
        {
            "identify": READY,
            "clean": {1: DISABLED, 2: READY},
            "channel": {
                1: {1: READY, 2: READY},
                2: {1: ACTIVE, 2: DISABLED}
            }
        }
        we may specify paths such as ["identify"], ["clean", 2], ["channel", 2, 1]
        and ["channel", 1] to refer to specific elements of the target that we wish
        to replace by "value".

        If the path does not yet exist in the target, it is created and set to the
        specified value.

        The function modifies the target in place and returns a "shadow" dictionary
        containing the portion of the target that was modified. For example with the
        above target, path=["channel", 1] and value={4: DISABLED, 5:ACTIVE} the returned
        dictionary is {"channel": {1: {4: DISABLED, 5: ACTIVE}}}.
        """
        if path:
            d = target
            shadow = {}
            o = shadow
            for p in path[:-1]:
                o[p] = {}
                if p not in d:
                    d[p] = {}
                d = d[p]
                o = o[p]
            d[path[-1]] = value
            o[path[-1]] = value
            return shadow
        else:
            target.clear()
            target.update(value)
            return value

    def set_modal_info(self, path, value):
        """Set the portion of self.modal_info specified by `path` to the given `value`.
        A shadow dictionary containing the change is sent via a websocket to inform the UI
        of the change in the plan.
        """
        shadow = self.modify_value_in_nested_dict(self.modal_info, path, value)
        # log.info(f"Setting plan {path} to {value}, shadow is {shadow}")
        self.run_async(self.send_queue.put(json.dumps({"modal_info": shadow})))

    def set_plan(self, path, value):
        """Set the portion of self.plan specified by `path` to the given `value`.
        A shadow dictionary containing the change is sent via a websocket to inform the UI
        of the change in the plan.
        """
        shadow = self.modify_value_in_nested_dict(self.plan, path, value)
        # log.info(f"Setting plan {path} to {value}, shadow is {shadow}")
        self.run_async(self.send_queue.put(json.dumps({"plan": shadow})))

    def set_status(self, path, value):
        """Set the status of the element specified by `path` to the given `value`.
        The self.status attribute is modified in place and a shadow dictionary containing
        the change is sent via a websocket to inform the UI of the change of status.
        """
        shadow = self.modify_value_in_nested_dict(self.status, path, value)
        # log.info(f"Setting status of {path} to {value}, shadow is {shadow}")
        self.run_async(self.send_queue.put(json.dumps({"uistatus": shadow})))

    def set_reference(self, value):
        if self.reference_active != value:
            self.reference_active = value
            Framework.publish(Event(Signal.SET_REFERENCE, value))

    def plan_panel_update(self, msg):
        """Handle change of focus and edits in the duration column of the plan panel"""
        if "focus" in msg:
            data = msg["focus"]
            row = data["row"]
            column = data["column"]
            if row <= self.plan["last_step"] or (row == self.plan["last_step"] + 1 and column == 1):
                self.set_plan(["focus"], {"row": row, "column": column})
            else:
                self.set_plan(["focus"], self.plan["focus"])
        elif "duration" in msg:
            row = msg["row"]
            if row <= self.plan["last_step"]:
                try:
                    duration = int(msg["duration"]) if msg["duration"] else 0
                    self.set_plan(["steps", row, "duration"], duration)
                except ValueError:
                    pass
        elif "current_step" in msg:
            row = msg["current_step"]
            if row <= self.plan["max_steps"]:
                self.set_plan(["current_step"], row)

    def add_to_plan(self, bank_config, reference):
        row = self.plan["focus"]["row"]
        column = self.plan["focus"]["column"]
        if column == 2:
            row += 1
        if row <= self.plan["last_step"]:
            duration = self.plan["steps"][row]["duration"]
        else:
            duration = 0
        self.set_plan(["steps", row], {"banks": bank_config, "reference": reference, "duration": duration})
        if self.plan["last_step"] < row:
            self.set_plan(["last_step"], row)
        self.set_plan(["focus"], {"row": row, "column": 2})

    def add_channel_to_plan(self, msg):
        """Handle a channel button press, adding the bank and channel to the plan
        at the location of the focussed row"""
        bank = msg["bank"]
        channel = msg["channel"]
        bank_config = {b: {"clean": 0, "chan_mask": 0} for b in self.all_banks}
        bank_config[bank]["chan_mask"] = 1 << (channel - 1)
        self.add_to_plan(bank_config, 0)

    def add_bank_to_clean_to_plan(self, bank):
        """Handle a clean button press in a bank. Add to plan at the location of the focussed row"""
        bank_config = {b: {"clean": 0, "chan_mask": 0} for b in self.all_banks}
        bank_config[bank]["clean"] = 1
        self.add_to_plan(bank_config, 0)

    def add_reference_to_plan(self):
        """Handle a reference button press in a bank. Add to plan at the location of the focussed row"""
        bank_config = {b: {"clean": 0, "chan_mask": 0} for b in self.all_banks}
        self.add_to_plan(bank_config, 1)

    def plan_row_delete(self, msg):
        """Delete a row from the plan at the focussed row, moving up the remaining entries"""
        row = self.plan["focus"]["row"]
        column = self.plan["focus"]["column"]
        num_steps = self.plan["last_step"]
        if num_steps > 0 and row <= num_steps:
            for r in range(row, self.plan["last_step"]):
                s = self.plan["steps"][r + 1]
                self.set_plan(["steps", r], s.copy())
            self.set_plan(["last_step"], num_steps - 1)
        if row == 1:
            column = 1
        else:
            row = row - 1
        self.set_plan(["focus"], {"row": row, "column": column})

    def plan_row_insert(self, msg):
        """Insert a row into the plan at the focussed row, moving down the remaining entries"""
        row = self.plan["focus"]["row"]
        column = self.plan["focus"]["column"]
        num_steps = self.plan["last_step"]
        if num_steps < self.plan["max_steps"] and row <= num_steps:
            for r in range(self.plan["last_step"], row - 1, -1):
                s = self.plan["steps"][r]
                self.set_plan(["steps", r + 1], s.copy())
            self.set_plan(["steps", row], {
                "banks": {b: {
                    "clean": 0,
                    "chan_mask": 0
                }
                          for b in self.all_banks},
                "reference": 0,
                "duration": 0
            })
            self.set_plan(["last_step"], num_steps + 1)
        self.set_plan(["focus"], {"row": row, "column": column})

    def validate_plan(self, check_avail=True):
        """Check that there are no errors in the plan. If an error is present,
        return the row and column of the first error and a string describing
        the problem"""
        if self.plan["last_step"] <= 0:
            return PlanError(True, f"Plan is empty", 1, 1)
        if not 1 <= self.plan["current_step"] <= self.plan["last_step"]:
            return PlanError(True, f"Pending step must be between 1 and {self.plan['last_step']}", 1, 1)
        for i in range(self.plan["last_step"]):
            row = i + 1
            s = self.plan["steps"][row]
            if not "duration" in s or not "reference" in s or not "banks" in s:
                return PlanError(True, f"Malformed data at step {row}", row, 1)
            if not s["duration"] > 0:
                return PlanError(True, f"Invalid duration at step {row}", row, 2)
            for bank in s["banks"]:
                if not bank in self.all_banks:
                    return PlanError(True, f"Invalid bank at step {row}", row, 1)
                bank_config = s["banks"][bank]
                if not "chan_mask" in bank_config or not "clean" in bank_config:
                    return PlanError(True, f"Invalid data for bank {bank} in step {row}", row, 1)
                if not 0 <= bank_config["chan_mask"] < 256:
                    return PlanError(True, f"Invalid channel selection for bank {bank} in step {row}", row, 1)
                if check_avail:
                    for j in setbits(bank_config["chan_mask"]):
                        if self.status["channel"][bank][j + 1] != UiStatus.READY:
                            return PlanError(True, f"Unavailable port at step {row}", row, 1)
        return PlanError(False)

    def save_plan_to_file(self):
        fname = os.path.join(PLAN_FILE_DIR, self.plan["plan_filename"] + ".pln")
        plan = {}
        for i in range(self.plan["last_step"]):
            row = i + 1
            s = self.plan["steps"][row]
            # Not strictly necessary to convert row to a string here, but is here to remind the
            # reader that serializing a dictionary via JSON turns all keys into strings
            plan[str(row)] = s
        with open(fname, "w") as fp:
            json.dump({"plan": plan, "bank_names": self.plan["bank_names"]}, fp, indent=4)

    def load_plan_from_file(self):
        fname = os.path.join(PLAN_FILE_DIR, self.plan["plan_filename"] + ".pln")
        with open(fname, "r") as fp:
            data = json.load(fp)
            plan = data["plan"]
            bank_names = data["bank_names"]
        assert isinstance(plan, dict), "Plan should be a dictionary"
        steps = {}
        last_step = len(plan)
        for i in range(last_step):
            # Note: Serializing through JSON turns all dictionary keys into strings. We
            #  need to turn bank numbers and plan steps back into integers for compatibility
            #  with the rest of the code
            row = i + 1
            assert str(row) in plan, f"Plan is missing step {row}"
            step = plan[str(row)]
            assert "banks" in step, f"Plan row {row} is missing 'banks' key"
            assert "reference" in step, f"Plan row {row} is missing 'reference' key"
            assert "duration" in step, f"Plan row {row} is missing 'duration' key"
            steps[row] = {
                "banks": {int(bank_str): step["banks"][bank_str]
                          for bank_str in step["banks"]},
                "reference": step["reference"],
                "duration": step["duration"]
            }
        self.set_plan(["steps"], steps)
        self.set_plan(["last_step"], last_step)
        self.set_plan(["focus"], {"row": last_step + 1, "column": 1})
        self.set_plan(["bank_names"], bank_names)

    def get_current_step_from_focus(self):
        step = self.plan["focus"]["row"]
        column = self.plan["focus"]["column"]
        if column == 2:
            step = step + 1
        if step > self.plan["last_step"]:
            step = step - self.plan["last_step"]
        return step

    def handle_error_signal(self, epoch_time, payload):
        self.error_list.append({"time": epoch_time, "payload": payload, "framework": Framework.get_info()})

    def change_name_bank(self, msg):
        if "bank_names" in msg:
            banks = msg["bank_names"]
            b1 = banks["1"]["name"]
            b2 = banks["2"]["name"]
            b3 = banks["3"]["name"]
            b4 = banks["4"]["name"]
            channels1 = banks["1"]["channels"]
            channels2 = banks["2"]["channels"]
            channels3 = banks["3"]["channels"]
            channels4 = banks["4"]["channels"]

            self.set_plan(
                ["bank_names"], {
                    "1": {
                        "name": b1,
                        "channels": channels1
                    },
                    "2": {
                        "name": b2,
                        "channels": channels2
                    },
                    "3": {
                        "name": b3,
                        "channels": channels3
                    },
                    "4": {
                        "name": b4,
                        "channels": channels4
                    }
                })

    @state
    def _initial(self, e):
        self.publish_errors = True
        self.bank = None
        self.bank_to_update = None
        self.banks_to_process = []
        self.channel = None
        self.plan_error = None
        self.plan_step_te = TimeEvent("PLAN_STEP_TIMER")
        self.plan_step_timer_target = 0
        self.filename_to_delete = ""
        self.state_after_delete = self._plan_load

        # Keyed by bank. Its values are the masks corresponding to active channels
        # e.g. {1: 0, 2:64, 3:0, 4:0} represents channel active 7 in bank 2
        self.chan_active = {1: 0, 2: 0, 3: 0, 4: 0}
        self.clean_active = {1: 0, 2: 0, 3: 0, 4: 0}
        self.reference_active = None
        Signal.register("PC_SEND")
        Signal.register("PLAN_LOAD_SUCCESSFUL")
        Signal.register("PLAN_LOAD_FAILED")
        Signal.register("PLAN_SAVE_SUCCESSFUL")
        Signal.register("PLAN_SAVE_FAILED")
        Signal.register("SET_REFERENCE")
        Framework.subscribe("PIGLET_REQUEST", self)
        Framework.subscribe("PIGLET_STATUS", self)
        Framework.subscribe("PIGLET_RESPONSE", self)
        Framework.subscribe("PIGLET_SEQUENCE", self)
        Framework.subscribe("PIGLET_SEQUENCE_COMPLETE", self)
        Framework.subscribe("PC_ERROR", self)
        Framework.subscribe("PC_TIMEOUT", self)
        Framework.subscribe("PC_RESPONSE", self)
        Framework.subscribe("BTN_STANDBY", self)
        Framework.subscribe("BTN_IDENTIFY", self)
        Framework.subscribe("BTN_PLAN", self)
        Framework.subscribe("BTN_PLAN_CANCEL", self)
        Framework.subscribe("BTN_PLAN_DELETE", self)
        Framework.subscribe("BTN_PLAN_DELETE_FILENAME", self)
        Framework.subscribe("BTN_PLAN_INSERT", self)
        Framework.subscribe("BTN_PLAN_LOAD", self)
        Framework.subscribe("BTN_PLAN_LOAD_CANCEL", self)
        Framework.subscribe("BTN_PLAN_OK", self)
        Framework.subscribe("BTN_PLAN_SAVE", self)
        Framework.subscribe("BTN_PLAN_SAVE_CANCEL", self)
        Framework.subscribe("BTN_PLAN_SAVE_OK", self)
        Framework.subscribe("PLAN_LOAD_FILENAME", self)
        Framework.subscribe("PLAN_PANEL_UPDATE", self)
        Framework.subscribe("PLAN_SAVE_FILENAME", self)
        Framework.subscribe("BTN_RUN", self)
        Framework.subscribe("BTN_REFERENCE", self)
        Framework.subscribe("BTN_CLEAN", self)
        Framework.subscribe("BTN_CHANNEL", self)
        Framework.subscribe("MODAL_CLOSE", self)
        Framework.subscribe("MODAL_OK", self)
        Framework.subscribe("SYSTEM_CONFIGURE", self)
        Framework.subscribe("TERMINATE", self)
        Framework.subscribe("ERROR", self)
        Framework.subscribe("BTN_EDIT", self)
        Framework.subscribe("CHANGE_NAME_BANK", self)
        Framework.subscribe("EDIT_CANCEL", self)
        Framework.subscribe("EDIT_SAVE", self)
        Framework.subscribe("BTN_PLAN_LOOP", self)
        Framework.subscribe("BTN_PLAN_RUN", self)
        self.te = TimeEvent("UI_TIMEOUT")
        return self.tran(self._operational)

    @state
    def _configure(self, e):
        sig = e.signal
        if sig == Signal.SYSTEM_CONFIGURE:
            payload = e.value
            self.all_banks = payload.bank_list
            self.run_async(self.save_port_history())
            return self.tran(self._operational)
        elif sig == Signal.TERMINATE:
            self.run_async(self.db_writer.close_connection())
            return self.tran(self._exit)
        return self.super(self.top)

    @state
    def _operational(self, e):
        sig = e.signal
        if sig == Signal.ENTRY:
            self.set_status(["standby"], UiStatus.READY)
            self.set_status(["identify"], UiStatus.READY)
            self.set_status(["run"], UiStatus.DISABLED)
            self.set_status(["plan"], UiStatus.DISABLED)
            self.set_status(["plan_run"], UiStatus.DISABLED)
            self.set_status(["plan_loop"], UiStatus.DISABLED)
            self.set_status(["reference"], UiStatus.READY)
            for bank in self.all_banks:
                # Use 1-origin for numbering banks and channels
                self.set_status(["clean", bank], UiStatus.READY)
                self.set_status(["bank", bank], UiStatus.READY)
                for j in range(self.num_chans_per_bank):
                    self.set_status(["channel", bank, j + 1], UiStatus.DISABLED)
            return self.handled(e)
        elif sig == Signal.INIT:
            return self.tran(self._standby)
        elif sig == Signal.MODAL_CLOSE:
            self.set_modal_info(["show"], False)
            return self.handled(e)
        elif sig == Signal.BTN_STANDBY:
            return self.tran(self._standby)
        elif sig == Signal.BTN_IDENTIFY:
            return self.tran(self._identify)
        elif sig == Signal.BTN_RUN:
            if self.status["run"] != UiStatus.DISABLED:
                return self.tran(self._run)
        elif sig == Signal.BTN_PLAN:
            if self.status["plan"] != UiStatus.DISABLED:
                return self.tran(self._plan)
        elif sig == Signal.BTN_PLAN_RUN:
            if self.status["plan_run"] != UiStatus.DISABLED:
                return self.tran(self._run_plan)
        elif sig == Signal.BTN_PLAN_LOOP:
            if self.status["plan_loop"] != UiStatus.DISABLED:
                return self.tran(self._loop_plan)
        elif sig == Signal.BTN_REFERENCE:
            if self.status["reference"] != UiStatus.DISABLED:
                return self.tran(self._reference)
        elif sig == Signal.BTN_CLEAN:
            if self.status["clean"][e.value["bank"]] != UiStatus.DISABLED:
                self.bank = e.value["bank"]
                return self.tran(self._clean)
        elif sig == Signal.BTN_EDIT:
            return self.tran(self._edit)
        elif sig == Signal.ERROR:
            payload = e.value
            self.handle_error_signal(time.time(), payload)
            return self.handled(e)
        return self.super(self._configure)

    @state
    def _standby(self, e):
        sig = e.signal
        if sig == Signal.EXIT:
            self.set_status(["standby"], UiStatus.READY)
            return self.handled(e)
        elif sig == Signal.INIT:
            return self.tran(self._standby1)
        elif sig == Signal.BTN_STANDBY:
            return self.handled(e)
        return self.super(self._operational)

    @state
    def _standby1(self, e):
        sig = e.signal
        if sig == Signal.ENTRY:
            Framework.publish(Event(Signal.PIGLET_REQUEST, PigletRequestPayload("STANDBY", self.all_banks)))
            return self.handled(e)
        elif sig == Signal.PIGLET_RESPONSE:
            return self.tran(self._standby2)
        return self.super(self._standby)

    @state
    def _standby2(self, e):
        sig = e.signal
        if sig == Signal.ENTRY:
            self.set_status(["standby"], UiStatus.ACTIVE)
            return self.handled(e)
        return self.super(self._standby)

    @state
    def _reference(self, e):
        sig = e.signal
        if sig == Signal.EXIT:
            self.set_status(["reference"], UiStatus.READY)
            self.set_reference(0)
            for bank in self.all_banks:
                self.set_status(["bank", bank], UiStatus.READY)
            return self.handled(e)
        elif sig == Signal.INIT:
            return self.tran(self._reference1)
        elif sig == Signal.BTN_REFERENCE:
            return self.handled(e)
        return self.super(self._operational)

    @state
    def _reference1(self, e):
        sig = e.signal
        if sig == Signal.ENTRY:
            # TODO: Activate reference valve on Numato
            Framework.publish(Event(Signal.PIGLET_REQUEST, PigletRequestPayload("STANDBY", self.all_banks)))
            return self.handled(e)
        elif sig == Signal.PIGLET_RESPONSE:
            return self.tran(self._reference2)
        return self.super(self._reference)

    @state
    def _reference2(self, e):
        sig = e.signal
        if sig == Signal.ENTRY:
            self.set_status(["reference"], UiStatus.ACTIVE)
            self.set_reference(1)
            for bank in self.all_banks:
                # Use 1-origin for numbering banks and channels
                self.set_status(["bank", bank], UiStatus.REFERENCE)

            return self.handled(e)
        return self.super(self._reference)

    @state
    def _clean(self, e):
        sig = e.signal
        if sig == Signal.EXIT:
            Framework.publish(Event(Signal.PIGLET_REQUEST, PigletRequestPayload("STANDBY", self.all_banks)))
            for bank in self.all_banks:
                # Use 1-origin for numbering banks and channels
                self.set_status(["clean", bank], UiStatus.READY)
                self.set_status(["bank", bank], UiStatus.READY)
                self.clean_active[bank] = 0
                #for j in range(self.num_chans_per_bank):
                #    if self.get_status()["channel"][bank][j + 1] == UiStatus.CLEAN:
                #        self.set_status(["channel", bank, j + 1], UiStatus.READY)
            return self.handled(e)
        elif sig == Signal.INIT:
            return self.tran(self._clean1)
        """
        elif sig == Signal.BTN_CLEAN:  # and self.get_status()["clean"][e.value["bank"]] == UiStatus.CLEAN:
            self.bank = e.value["bank"]
            if self.get_status()["clean"][self.bank] == UiStatus.CLEAN:
                return self.handled(e)
            else:
                return self.tran(self._clean1)
        """
        return self.super(self._operational)

    @state
    def _clean1(self, e):
        sig = e.signal
        if sig == Signal.ENTRY:
            Framework.publish(Event(Signal.PIGLET_REQUEST, PigletRequestPayload("CLEAN", [self.bank])))
            self.clean_active[self.bank] = 0
            return self.handled(e)
        elif sig == Signal.PIGLET_RESPONSE:
            return self.tran(self._clean2)
        return self.super(self._clean)

    @state
    def _clean2(self, e):
        sig = e.signal
        if sig == Signal.ENTRY:
            # Use 1-origin for numbering banks and channels
            self.set_status(["clean", self.bank], UiStatus.CLEAN)
            self.set_status(["bank", self.bank], UiStatus.CLEAN)
            #for j in range(self.num_chans_per_bank):
            #    if self.get_status()["channel"][self.bank][j + 1] == UiStatus.READY:
            #        self.set_status(["channel", self.bank, j + 1], UiStatus.CLEAN)

            return self.handled(e)
        return self.super(self._clean)

    @state
    def _identify(self, e):
        sig = e.signal
        if sig == Signal.ENTRY:
            self.set_status(["identify"], UiStatus.ACTIVE)
            self.set_status(["standby"], UiStatus.DISABLED)
            self.set_status(["run"], UiStatus.DISABLED)
            self.set_status(["plan"], UiStatus.DISABLED)
            self.set_status(["plan_run"], UiStatus.DISABLED)
            self.set_status(["plan_loop"], UiStatus.DISABLED)
            self.set_status(["reference"], UiStatus.DISABLED)
            for bank in self.all_banks:
                self.set_status(["clean", bank], UiStatus.DISABLED)
            self.banks_to_process = self.all_banks.copy()
            self.bank = self.banks_to_process.pop(0)
            return self.handled(e)
        elif sig == Signal.EXIT:
            self.set_status(["identify"], UiStatus.READY)
            self.set_status(["standby"], UiStatus.READY)
            self.set_status(["run"], UiStatus.READY)
            self.set_status(["plan"], UiStatus.READY)
            self.set_status(["reference"], UiStatus.READY)
            for bank in self.all_banks:
                # Use 1-origin for numbering banks and channels
                self.set_status(["clean", bank], UiStatus.READY)
                self.set_status(["bank", bank], UiStatus.READY)
            return self.handled(e)
        elif sig == Signal.INIT:
            return self.tran(self._identify1)
        elif sig == Signal.BTN_IDENTIFY:
            return self.handled(e)
        return self.super(self._operational)

    @state
    def _identify1(self, e):
        sig = e.signal
        if sig == Signal.ENTRY:
            Framework.publish(Event(Signal.PIGLET_REQUEST, PigletRequestPayload("IDENTIFY", [self.bank])))
            return self.handled(e)
        elif sig == Signal.PIGLET_RESPONSE:
            return self.tran(self._identify2)
        return self.super(self._identify)

    @state
    def _identify2(self, e):
        sig = e.signal
        if sig == Signal.ENTRY:
            self.set_status(["bank", self.bank], UiStatus.ACTIVE)
            return self.handled(e)
        elif sig == Signal.PIGLET_STATUS:
            # log.info(f"In identify2: {msg['status'][self.bank-1]['OPSTATE']}")
            if e.value[self.bank]['OPSTATE'].startswith('ident'):
                return self.tran(self._identify3)
        return self.super(self._identify)

    @state
    def _identify3(self, e):
        sig = e.signal
        if sig == Signal.PIGLET_STATUS:
            # log.info(f"In identify3: {msg['status'][self.bank-1]['OPSTATE']}")
            if e.value[self.bank]['OPSTATE'] == 'standby':
                return self.tran(self._identify4)
        return self.super(self._identify)

    @state
    def _identify4(self, e):
        sig = e.signal
        if sig == Signal.ENTRY:
            Framework.publish(Event(Signal.PIGLET_REQUEST, PigletRequestPayload("ACTIVECH?", [self.bank])))
            return self.handled(e)
        elif sig == Signal.PIGLET_RESPONSE:
            msg = int(e.value[self.bank])  # Integer representing available channels
            self.set_plan(["available_ports", str(self.bank)], msg)
            active = [1 if int(c) else 0 for c in reversed(format(msg, '08b'))]
            # Update the states of the channel buttons in the bank
            for i, stat in enumerate(active):
                self.set_status(["channel", self.bank, i + 1], UiStatus.AVAILABLE if stat else UiStatus.DISABLED)
            self.set_status(["bank", self.bank], UiStatus.READY)
            # Go to the next bank, and continue identification if the bank is present
            if self.banks_to_process:
                self.bank = self.banks_to_process.pop(0)
                return self.tran(self._identify1)
            else:
                # Otherwise, enable run and plan buttons and go back to standby since identification is complete
                self.set_status(["run"], UiStatus.READY)
                self.set_status(["plan"], UiStatus.READY)
                # Write out bank name and port availability information to database
                self.run_async(self.save_port_history())
                return self.tran(self._operational)
        return self.super(self._identify)

    @state
    def _plan(self, e):
        sig = e.signal
        if sig == Signal.ENTRY:
            for bank in self.all_banks:
                for j in range(self.num_chans_per_bank):
                    if self.status["channel"][bank][j + 1] == UiStatus.AVAILABLE:
                        self.set_status(["channel", bank, j + 1], UiStatus.READY)
            return self.handled(e)
        elif sig == Signal.EXIT:
            for bank in self.all_banks:
                for j in range(self.num_chans_per_bank):
                    if self.status["channel"][bank][j + 1] == UiStatus.READY:
                        self.set_status(["channel", bank, j + 1], UiStatus.AVAILABLE)
            self.set_plan(["panel_to_show"], int(PlanPanelType.NONE))
            return self.handled(e)
        elif sig == Signal.INIT:
            return self.tran(self._plan_plan)
        elif sig == Signal.BTN_PLAN:
            return self.handled(e)
        return self.super(self._operational)

    @state
    def _plan_delete_file(self, e):
        sig = e.signal
        if sig == Signal.ENTRY:
            msg = f"Delete file {self.filename_to_delete}?"
            self.set_modal_info(
                [], {
                    "show": True,
                    "html": f"
Confirm file deletion

{msg}
",
                    "num_buttons": 2,
                    "buttons": {
                        1: {
                            "caption": "OK",
                            "className": "btn btn-success btn-large",
                            "response": "modal_ok"
                        },
                        2: {
                            "caption": "Cancel",
                            "className": "btn btn-danger btn-large",
                            "response": "modal_close"
                        }
                    }
                })
            return self.handled(e)
        elif sig == Signal.EXIT:
            self.set_modal_info(["show"], False)
            return self.handled(e)
        elif sig == Signal.MODAL_OK:
            fname = os.path.join(PLAN_FILE_DIR, self.filename_to_delete + ".pln")
            os.remove(fname)
            self.get_plan_filenames()
            return self.tran(self.state_after_delete)
        elif sig == Signal.MODAL_CLOSE:
            return self.tran(self.state_after_delete)
        return self.super(self._plan)

    @state
    def _plan_plan(self, e):
        sig = e.signal
        if sig == Signal.ENTRY:
            self.set_plan(["panel_to_show"], int(PlanPanelType.PLAN))
            return self.handled(e)
        elif sig == Signal.BTN_PLAN_OK:
            self.plan_error = self.validate_plan(check_avail=True)
            if not self.plan_error.error:
                # TODO: Modify next line when we have step selection by radio button
                # self.set_plan(["current_step"], self.get_current_step_from_focus())
                self.set_status(["plan_run"], UiStatus.READY)
                self.set_status(["plan_loop"], UiStatus.READY)
                return self.tran(self._operational)
            else:
                self.set_status(["plan_run"], UiStatus.DISABLED)
                self.set_status(["plan_loop"], UiStatus.DISABLED)
                return self.tran(self._plan_plan1)
        elif sig == Signal.BTN_PLAN_CANCEL:
            return self.tran(self._operational)
        elif sig == Signal.BTN_PLAN_DELETE:
            self.plan_row_delete(e.value)
            return self.handled(e)
        elif sig == Signal.BTN_PLAN_INSERT:
            self.plan_row_insert(e.value)
            return self.handled(e)
        elif sig == Signal.PLAN_PANEL_UPDATE:
            self.plan_panel_update(e.value)
            return self.handled(e)
        elif sig == Signal.BTN_CHANNEL:
            self.add_channel_to_plan(e.value)
            return self.handled(e)
        elif sig == Signal.BTN_CLEAN:
            self.add_bank_to_clean_to_plan(e.value["bank"])
            return self.handled(e)
        elif sig == Signal.BTN_REFERENCE:
            self.add_reference_to_plan()
            return self.handled(e)
        elif sig == Signal.BTN_PLAN_SAVE:
            self.plan_error = self.validate_plan(check_avail=False)
            if not self.plan_error.error:
                return self.tran(self._plan_save)
            else:
                return self.tran(self._plan_plan1)
        elif sig == Signal.BTN_PLAN_LOAD:
            return self.tran(self._plan_load)
        return self.super(self._plan)

    @state
    def _plan_file(self, e):
        sig = e.signal
        if sig == Signal.BTN_PLAN_DELETE_FILENAME:
            self.filename_to_delete = e.value["name"]
            return self.tran(self._plan_delete_file)
        return self.super(self._plan)

    @state
    def _plan_load(self, e):
        sig = e.signal
        if sig == Signal.ENTRY:
            self.set_plan(["panel_to_show"], int(PlanPanelType.LOAD))
            self.state_after_delete = self._plan_load
            return self.handled(e)
        elif sig == Signal.BTN_PLAN_LOAD_CANCEL:
            return self.tran(self._plan)
        elif sig == Signal.PLAN_LOAD_FILENAME:
            self.set_plan(["plan_filename"], e.value["name"])
            return self.tran(self._plan_load1)
        return self.super(self._plan_file)

    @state
    def _plan_load1(self, e):
        sig = e.signal
        if sig == Signal.ENTRY:
            try:
                self.load_plan_from_file()
                self.postFIFO(Event(Signal.PLAN_LOAD_SUCCESSFUL, None))
            except Exception:
                self.postFIFO(Event(Signal.PLAN_LOAD_FAILED, traceback.format_exc()))
            return self.handled(e)
        elif sig == Signal.PLAN_LOAD_SUCCESSFUL:
            self.run_async(self.save_port_history())
            return self.tran(self._plan)
        elif sig == Signal.PLAN_LOAD_FAILED:
            self.plan_error = PlanError(True, f'
{html.escape(e.value)}
')
            return self.tran(self._plan_load11)
        return self.super(self._plan_load)

    @state
    def _plan_load11(self, e):
        sig = e.signal
        if sig == Signal.ENTRY:
            self.set_modal_info([], {
                "show": True,
                "html": f"
Plan load error

{self.plan_error.message}
",
                "num_buttons": 0
            })
            return self.handled(e)
        elif sig == Signal.EXIT:
            self.set_modal_info(["show"], False)
            return self.handled(e)
        elif sig == Signal.MODAL_CLOSE:
            return self.tran(self._plan_load)
        return self.super(self._plan_load1)

    @state
    def _plan_save(self, e):
        sig = e.signal
        if sig == Signal.ENTRY:
            self.set_plan(["panel_to_show"], int(PlanPanelType.SAVE))
            self.state_after_delete = self._plan_save
            return self.handled(e)
        elif sig == Signal.BTN_PLAN_SAVE_CANCEL:
            return self.tran(self._plan)
        elif sig == Signal.PLAN_SAVE_FILENAME:
            # Remove non alphanumeric characters
            self.set_plan(["plan_filename"], re.sub(r"\W", "", e.value["name"]))
            return self.handled(e)
        elif sig == Signal.BTN_PLAN_SAVE_OK:
            fname = os.path.join(PLAN_FILE_DIR, self.plan["plan_filename"] + ".pln")
            if os.path.isfile(fname):
                return self.tran(self._plan_save1)
            else:
                return self.tran(self._plan_save2)
        return self.super(self._plan_file)

    @state
    def _plan_save1(self, e):
        sig = e.signal
        if sig == Signal.ENTRY:
            msg = "File exists. Overwrite?"
            self.set_modal_info(
                [], {
                    "show": True,
                    "html": f"
Confirm file overwrite

{msg}
",
                    "num_buttons": 2,
                    "buttons": {
                        1: {
                            "caption": "OK",
                            "className": "btn btn-success btn-large",
                            "response": "modal_ok"
                        },
                        2: {
                            "caption": "Cancel",
                            "className": "btn btn-danger btn-large",
                            "response": "modal_close"
                        }
                    }
                })
            return self.handled(e)
        elif sig == Signal.EXIT:
            self.set_modal_info(["show"], False)
            return self.handled(e)
        elif sig == Signal.MODAL_OK:
            return self.tran(self._plan_save2)
        elif sig == Signal.MODAL_CLOSE:
            return self.tran(self._plan_save)
        return self.super(self._plan_save)

    @state
    def _plan_save2(self, e):
        sig = e.signal
        if sig == Signal.ENTRY:
            try:
                self.save_plan_to_file()
                self.postFIFO(Event(Signal.PLAN_SAVE_SUCCESSFUL, None))
            except Exception:
                self.postFIFO(Event(Signal.PLAN_SAVE_FAILED, traceback.format_exc()))
            return self.handled(e)
        elif sig == Signal.PLAN_SAVE_SUCCESSFUL:
            self.get_plan_filenames()
            return self.tran(self._plan)
        elif sig == Signal.PLAN_SAVE_FAILED:
            self.plan_error = PlanError(True, f'
{html.escape(e.value)}
')
            return self.tran(self._plan_save21)
        return self.super(self._plan_save)

    @state
    def _plan_save21(self, e):
        sig = e.signal
        if sig == Signal.ENTRY:
            self.set_modal_info([], {
                "show": True,
                "html": f"
Plan save error

{self.plan_error.message}
",
                "num_buttons": 0
            })
            return self.handled(e)
        elif sig == Signal.EXIT:
            self.set_modal_info(["show"], False)
            return self.handled(e)
        elif sig == Signal.MODAL_CLOSE:
            return self.tran(self._plan_save)
        return self.super(self._plan_save2)

    @state
    def _plan_plan1(self, e):
        sig = e.signal
        if sig == Signal.ENTRY:
            self.set_modal_info([], {
                "show": True,
                "html": f"
Plan error

{self.plan_error.message}
",
                "num_buttons": 0
            })
            return self.handled(e)
        elif sig == Signal.EXIT:
            self.set_modal_info(["show"], False)
            self.set_plan(["focus"], {"row": self.plan_error.row, "column": self.plan_error.column})
            return self.handled(e)
        elif sig == Signal.MODAL_CLOSE:
            return self.tran(self._plan)
        return self.super(self._plan_plan)

    # @state
    # def _plan_plan2(self, e):
    #     sig = e.signal
    #     if sig == Signal.ENTRY:
    #         msg = "Loop" if self.plan['looping'] else "Run"
    #         msg += f" plan starting at step {self.plan['current_step']}"
    #         self.set_modal_info(
    #             [], {
    #                 "show": True,
    #                 "html": f"
Confirm Plan

{msg}
",
    #                 "num_buttons": 2,
    #                 "buttons": {
    #                     1: {
    #                         "caption": "OK",
    #                         "className": "btn btn-success btn-large",
    #                         "response": "modal_ok"
    #                     },
    #                     2: {
    #                         "caption": "Cancel",
    #                         "className": "btn btn-danger btn-large",
    #                         "response": "modal_close"
    #                     }
    #                 }
    #             })
    #         return self.handled(e)
    #     elif sig == Signal.EXIT:
    #         self.set_modal_info(["show"], False)
    #         return self.handled(e)
    #     elif sig == Signal.MODAL_OK:
    #         return self.tran(self._run_plan)
    #     elif sig == Signal.MODAL_CLOSE:
    #         return self.tran(self._plan)
    #     return self.super(self._plan_plan)

    @state
    def _sampling(self, e):
        sig = e.signal
        if sig == Signal.ENTRY:
            for bank in self.all_banks:
                self.chan_active[bank] = 0
            self.piglet_commands = [("CHANSET 0", self.all_banks)]
            self.postLIFO(Event(Signal.PIGLET_SEQUENCE, None))
            return self.handled(e)
        elif sig == Signal.EXIT:
            self.set_status(["run"], UiStatus.READY)
            self.set_status(["plan"], UiStatus.READY)
            self.set_status(["run_plan"], UiStatus.READY)
            self.set_status(["loop_plan"], UiStatus.READY)
            for bank in self.all_banks:
                if not self.status["bank"][bank] == UiStatus.READY:
                    self.set_status(["bank", bank], UiStatus.READY)
                if not self.status["clean"][bank] == UiStatus.READY:
                    self.set_status(["clean", bank], UiStatus.READY)
                for j in range(self.num_chans_per_bank):
                    if self.status["channel"][bank][j + 1] in [UiStatus.READY, UiStatus.ACTIVE]:
                        self.set_status(["channel", bank, j + 1], UiStatus.AVAILABLE)
            Framework.publish(Event(Signal.PIGLET_REQUEST, PigletRequestPayload("STANDBY", self.all_banks)))
            return self.handled(e)
        elif sig == Signal.BTN_RUN:
            return self.tran(self._run1)
        elif sig == Signal.BTN_PLAN_RUN:
            return self.tran(self._run_plan1)
        elif sig == Signal.BTN_PLAN_LOOP:
            return self.tran(self._loop_plan1)
        elif sig in (Signal.PIGLET_SEQUENCE, Signal.PIGLET_RESPONSE):
            if self.piglet_commands:
                cmd, banks = self.piglet_commands.pop()
                Framework.publish(Event(Signal.PIGLET_REQUEST, PigletRequestPayload(cmd, banks)))
            else:
                self.postFIFO(Event(Signal.PIGLET_SEQUENCE_COMPLETE, None))
            return self.handled(e)
        return self.super(self._operational)

    @state
    def _run(self, e):
        sig = e.signal
        if sig == Signal.EXIT:
            self.set_status(["run"], UiStatus.READY)
            return self.handled(e)
        elif sig == Signal.BTN_RUN:
            return self.handled(e)
        elif sig == Signal.PIGLET_SEQUENCE_COMPLETE:
            return self.tran(self._run1)
        return self.super(self._sampling)

    @state
    def _run1(self, e):
        sig = e.signal
        if sig == Signal.ENTRY:
            for bank in self.all_banks:
                for j in range(self.num_chans_per_bank):
                    if self.status["channel"][bank][j + 1] == UiStatus.AVAILABLE:
                        self.set_status(["channel", bank, j + 1], UiStatus.READY)
            return self.handled(e)
        elif sig == Signal.EXIT:
            for bank in self.all_banks:
                for j in range(self.num_chans_per_bank):
                    if self.status["channel"][bank][j + 1] == UiStatus.READY:
                        self.set_status(["channel", bank, j + 1], UiStatus.AVAILABLE)
            return self.handled(e)
        elif sig == Signal.INIT:
            return self.tran(self._run11)
        return self.super(self._run)

    @state
    def _run11(self, e):
        sig = e.signal
        if sig == Signal.ENTRY:
            self.set_status(["run"], UiStatus.ACTIVE)
            return self.handled(e)
        elif sig == Signal.BTN_CHANNEL:
            if self.status["channel"][e.value["bank"]][e.value["channel"]] == UiStatus.READY:
                self.banks_to_process = self.all_banks.copy()
                self.bank_to_update = self.banks_to_process.pop(0)
                self.bank = e.value["bank"]
                self.channel = e.value["channel"]
                # print(f"\nBTN_CHANNEL: {self.bank} {self.channel}")
                mask = 1 << (self.channel - 1)
                # For this version, we can only have one active channel, so
                #  replace the currently active channel with the selected one
                for bank in self.all_banks:
                    # Turn off ACTIVE states in UI for active channels
                    for j in setbits(self.chan_active[bank]):
                        self.set_status(["channel", bank, j + 1], UiStatus.READY)
                    # Replace with the selected channel
                    if bank == self.bank:
                        self.chan_active[bank] = mask
                    else:
                        self.chan_active[bank] = 0
                return self.tran(self._run111)
        return self.super(self._run1)

    @state
    def _run111(self, e):
        sig = e.signal
        if sig == Signal.ENTRY:
            mask = self.chan_active[self.bank_to_update]
            Framework.publish(Event(Signal.PIGLET_REQUEST, PigletRequestPayload(f"CHANSET {mask}", [self.bank_to_update])))
            for j in setbits(mask):
                self.set_status(["channel", self.bank_to_update, j + 1], UiStatus.ACTIVE)
            return self.handled(e)
        elif sig == Signal.PIGLET_RESPONSE:
            if self.banks_to_process:
                self.bank_to_update = self.banks_to_process.pop(0)
                return self.tran(self._run111)
            else:
                return self.handled(e)
        return self.super(self._run11)

    @state
    def _run_plan(self, e):
        sig = e.signal
        if sig == Signal.EXIT:
            self.set_status(["plan_run"], UiStatus.READY)
            return self.handled(e)
        elif sig == Signal.BTN_PLAN_RUN:
            return self.handled(e)
        elif sig == Signal.PIGLET_SEQUENCE_COMPLETE:
            return self.tran(self._run_plan1)
        return self.super(self._sampling)

    @state
    def _run_plan1(self, e):
        sig = e.signal
        if sig == Signal.ENTRY:
            msg = f"Run plan starting at step {self.plan['current_step']}"
            self.set_modal_info(
                [], {
                    "show": True,
                    "html": f"
Confirm Run Plan

{msg}
",
                    "num_buttons": 2,
                    "buttons": {
                        1: {
                            "caption": "OK",
                            "className": "btn btn-success btn-large",
                            "response": "modal_ok"
                        },
                        2: {
                            "caption": "Cancel",
                            "className": "btn btn-danger btn-large",
                            "response": "modal_close"
                        }
                    }
                })
            return self.handled(e)
        elif sig == Signal.EXIT:
            self.set_modal_info(["show"], False)
            return self.handled(e)
        elif sig == Signal.MODAL_OK:
            self.plan_step_timer_target = asyncio.get_event_loop().time()
            return self.tran(self._run_plan2)
        elif sig == Signal.MODAL_CLOSE:
            return self.tran(self._operational)
        return self.super(self._run_plan)

    @state
    def _run_plan2(self, e):
        sig = e.signal
        if sig == Signal.ENTRY:
            self.set_status(["plan_run"], UiStatus.ACTIVE)
            current_step = self.plan["current_step"]
            self.plan_step_timer_target += self.plan["steps"][current_step]["duration"]
            self.plan_step_te.postAt(self, self.plan_step_timer_target)
            return self.handled(e)
        elif sig == Signal.EXIT:
            self.plan_step_te.disarm()
            return self.handled(e)
        elif sig == Signal.INIT:
            return self.tran(self._run_plan21)
        elif sig == Signal.PLAN_STEP_TIMER:
            current_step = self.plan["current_step"]
            last_step = self.plan["last_step"]
            current_step += 1
            if current_step > last_step:
                current_step -= last_step
                self.set_plan(["current_step"], current_step)
                # All steps done
                return self.tran(self._operational)
            else:
                self.set_plan(["current_step"], current_step)
                return self.tran(self._run_plan2)
        return self.super(self._run_plan)

    @state
    def _run_plan21(self, e):
        sig = e.signal
        if sig == Signal.ENTRY:
            self.banks_to_process = self.all_banks.copy()
            self.bank_to_update = self.banks_to_process.pop(0)
            current_step = self.plan["current_step"]
            bank_config = self.plan["steps"][current_step]["banks"]
            self.set_reference(self.plan["steps"][current_step]["reference"])
            for bank in self.all_banks:
                self.clean_active[bank] = bank_config[bank]["clean"]
                self.chan_active[bank] = bank_config[bank]["chan_mask"]
            return self.handled(e)
        elif sig == Signal.INIT:
            return self.tran(self._run_plan211)
        elif sig == Signal.PIGLET_STATUS:
            # Update the channel button states on the UI from SOLENOID_VALVES in piglet status
            result = {}
            for bank in self.all_banks:
                if self.reference_active:
                    self.set_status(["clean", bank], UiStatus.READY)
                    self.set_status(["bank", bank], UiStatus.REFERENCE)
                elif e.value[bank]['OPSTATE'] == "clean":
                    self.set_status(["clean", bank], UiStatus.CLEAN)
                    self.set_status(["bank", bank], UiStatus.CLEAN)
                else:
                    self.set_status(["clean", bank], UiStatus.READY)
                    self.set_status(["bank", bank], UiStatus.READY)

                mask = e.value[bank]['SOLENOID_VALVES']
                sel = setbits(mask)
                chan_status = self.status["channel"][bank].copy()
                for j in range(self.num_chans_per_bank):
                    current = chan_status[j + 1]
                    if current != UiStatus.DISABLED:
                        if j in sel:
                            if current != UiStatus.ACTIVE:
                                chan_status[j + 1] = UiStatus.ACTIVE
                        else:
                            if current != UiStatus.AVAILABLE:
                                chan_status[j + 1] = UiStatus.AVAILABLE
                result[bank] = chan_status
            self.set_status(["channel"], result)
            return self.handled(e)
        return self.super(self._run_plan2)

    @state
    def _run_plan211(self, e):
        sig = e.signal
        if sig == Signal.ENTRY:
            if self.clean_active[self.bank_to_update] != 0:
                Framework.publish(Event(Signal.PIGLET_REQUEST, PigletRequestPayload(f"CLEAN", [self.bank_to_update])))
            else:
                mask = self.chan_active[self.bank_to_update]
                Framework.publish(Event(Signal.PIGLET_REQUEST, PigletRequestPayload(f"CHANSET {mask}", [self.bank_to_update])))
            return self.handled(e)
        elif sig == Signal.PIGLET_RESPONSE:
            if self.banks_to_process:
                self.bank_to_update = self.banks_to_process.pop(0)
                return self.tran(self._run_plan211)
            else:
                return self.handled(e)
        return self.super(self._run_plan21)

    @state
    def _loop_plan(self, e):
        sig = e.signal
        if sig == Signal.EXIT:
            self.set_status(["plan_loop"], UiStatus.READY)
            return self.handled(e)
        elif sig == Signal.BTN_PLAN_LOOP:
            return self.handled(e)
        elif sig == Signal.PIGLET_SEQUENCE_COMPLETE:
            return self.tran(self._loop_plan1)
        return self.super(self._sampling)

    @state
    def _loop_plan1(self, e):
        sig = e.signal
        if sig == Signal.ENTRY:
            msg = f"Loop plan starting at step {self.plan['current_step']}"
            self.set_modal_info(
                [], {
                    "show": True,
                    "html": f"
Confirm Loop Plan

{msg}
",
                    "num_buttons": 2,
                    "buttons": {
                        1: {
                            "caption": "OK",
                            "className": "btn btn-success btn-large",
                            "response": "modal_ok"
                        },
                        2: {
                            "caption": "Cancel",
                            "className": "btn btn-danger btn-large",
                            "response": "modal_close"
                        }
                    }
                })
            return self.handled(e)
        elif sig == Signal.EXIT:
            self.set_modal_info(["show"], False)
            return self.handled(e)
        elif sig == Signal.MODAL_OK:
            self.plan_step_timer_target = asyncio.get_event_loop().time()
            return self.tran(self._loop_plan2)
        elif sig == Signal.MODAL_CLOSE:
            return self.tran(self._operational)
        return self.super(self._loop_plan)

    @state
    def _loop_plan2(self, e):
        sig = e.signal
        if sig == Signal.ENTRY:
            self.set_status(["plan_loop"], UiStatus.ACTIVE)
            current_step = self.plan["current_step"]
            self.plan_step_timer_target += self.plan["steps"][current_step]["duration"]
            self.plan_step_te.postAt(self, self.plan_step_timer_target)
            return self.handled(e)
        elif sig == Signal.EXIT:
            self.plan_step_te.disarm()
            return self.handled(e)
        elif sig == Signal.INIT:
            return self.tran(self._loop_plan21)
        elif sig == Signal.PLAN_STEP_TIMER:
            current_step = self.plan["current_step"]
            last_step = self.plan["last_step"]
            current_step += 1
            if current_step > last_step:
                current_step -= last_step
            self.set_plan(["current_step"], current_step)
            return self.tran(self._loop_plan2)
        return self.super(self._loop_plan)

    @state
    def _loop_plan21(self, e):
        sig = e.signal
        if sig == Signal.ENTRY:
            self.banks_to_process = self.all_banks.copy()
            self.bank_to_update = self.banks_to_process.pop(0)
            current_step = self.plan["current_step"]
            bank_config = self.plan["steps"][current_step]["banks"]
            self.set_reference(self.plan["steps"][current_step]["reference"])
            for bank in self.all_banks:
                self.clean_active[bank] = bank_config[bank]["clean"]
                self.chan_active[bank] = bank_config[bank]["chan_mask"]
            return self.handled(e)
        elif sig == Signal.INIT:
            return self.tran(self._loop_plan211)
        elif sig == Signal.PIGLET_STATUS:
            # Update the channel button states on the UI from SOLENOID_VALVES in piglet status
            result = {}
            for bank in self.all_banks:
                if self.reference_active:
                    self.set_status(["clean", bank], UiStatus.READY)
                    self.set_status(["bank", bank], UiStatus.REFERENCE)
                elif e.value[bank]['OPSTATE'] == "clean":
                    self.set_status(["clean", bank], UiStatus.CLEAN)
                    self.set_status(["bank", bank], UiStatus.CLEAN)
                else:
                    self.set_status(["clean", bank], UiStatus.READY)
                    self.set_status(["bank", bank], UiStatus.READY)

                mask = e.value[bank]['SOLENOID_VALVES']
                sel = setbits(mask)
                chan_status = self.status["channel"][bank].copy()
                for j in range(self.num_chans_per_bank):
                    current = chan_status[j + 1]
                    if current != UiStatus.DISABLED:
                        if j in sel:
                            if current != UiStatus.ACTIVE:
                                chan_status[j + 1] = UiStatus.ACTIVE
                        else:
                            if current != UiStatus.AVAILABLE:
                                chan_status[j + 1] = UiStatus.AVAILABLE
                result[bank] = chan_status
            self.set_status(["channel"], result)
            return self.handled(e)
        return self.super(self._loop_plan2)

    @state
    def _loop_plan211(self, e):
        sig = e.signal
        if sig == Signal.ENTRY:
            if self.clean_active[self.bank_to_update] != 0:
                Framework.publish(Event(Signal.PIGLET_REQUEST, PigletRequestPayload(f"CLEAN", [self.bank_to_update])))
            else:
                mask = self.chan_active[self.bank_to_update]
                Framework.publish(Event(Signal.PIGLET_REQUEST, PigletRequestPayload(f"CHANSET {mask}", [self.bank_to_update])))
            return self.handled(e)
        elif sig == Signal.PIGLET_RESPONSE:
            if self.banks_to_process:
                self.bank_to_update = self.banks_to_process.pop(0)
                return self.tran(self._loop_plan211)
            else:
                return self.handled(e)
        return self.super(self._loop_plan21)

    @state
    def _edit(self, e):
        sig = e.signal
        if sig == Signal.INIT:
            return self.tran(self._edit_edit)
        elif sig == Signal.EXIT:
            self.set_plan(["panel_to_show"], int(PlanPanelType.NONE))
            return self.handled(e)
        elif sig == Signal.BTN_EDIT:
            return self.handled(e)
        return self.super(self._operational)

    @state
    def _edit_edit(self, e):
        sig = e.signal
        if sig == Signal.ENTRY:
            self.set_plan(["panel_to_show"], int(PlanPanelType.EDIT))
            return self.handled(e)
        # elif sig == Signal.CHANGE_NAME_BANK:
        #     self.change_name_bank(e.value)
        #     return self.tran(self._operational)
        elif sig == Signal.EDIT_CANCEL:
            self.set_plan(["panel_to_show"], int(PlanPanelType.NONE))
            return self.tran(self._operational)
        elif sig == Signal.EDIT_SAVE:
            self.change_name_bank(e.value)
            self.run_async(self.save_port_history())
            self.set_plan(["panel_to_show"], int(PlanPanelType.NONE))
            return self.tran(self._operational)
        return self.super(self._edit)

if __name__ == "__main__":
    # Uncomment this line to get a visual execution trace (to demonstrate debugging)
    # from async_hsm.SimpleSpy import SimpleSpy
    # Spy.enable_spy(SimpleSpy)

    pc = PigssController()
    pc.start(1)

    event = Event(Signal.SYSTEM_CONFIGURE, SystemConfiguration(bank_list=[1, 3, 4]))
    Framework.publish(event)
    event = Event(Signal.BTN_STANDBY, None)
    Framework.publish(event)
    event = Event(Signal.TERMINATE, None)
    Framework.publish(event)

    run_forever()


import os
from datetime import datetime

# Posts
print("POST CHECKS")

# Episode Number in Title
problem = False
for post_name in os.listdir("_posts/"):
    num = int(post_name[:-3].split('-')[-1])
    with open("_posts/" + post_name) as post:
        for line in post:
            if "title:" in line and num != 110:
                if int(line.split()[2][:-1]) != num:
                    problem = True
print (("❌" if problem else "✅") + " - Episode Number in Title")

# Episode Number in Link to Website (Text)
problem = False
for post_name in os.listdir("_posts/"):
    num = int(post_name[:-3].split('-')[-1])
    with open("_posts/" + post_name) as post:
        idx = 3 if num < 115 else 4
        for line in post:
            if "Link to Episode" in line:
                if int(line.split()[idx]) != num:
                    problem = True
print (("❌" if problem else "✅") + " - Episode Number in Link to Website (Text)")

# Date in Link to Website (Link)
problem = False
for post_name in os.listdir("_posts/"):
    date = post_name[:10]
    with open("_posts/" + post_name) as post:
        idx = 3 if num < 115 else 4
        for line in post:
            if "Link to Episode" in line:
                if '-'.join(line.split('/')[3:6]) != date:
                    problem = True
print (("❌" if problem else "✅") + " - Date in Link to Website (Link)")

# Date in Release Date
problem = False
for post_name in os.listdir("_posts/"):
    date = post_name[:10]
    with open("_posts/" + post_name) as post:
        idx = 3 if num < 115 else 4
        for line in post:
            if "Date Released:" in line:
                if line.strip().split()[-1] != date:
                    problem = True
print (("❌" if problem else "✅") + " - Date in Release Date")

# Discussion Link Issue Number
problem = False
for post_name in os.listdir("_posts/"):
    num = int(post_name[:-3].split('-')[-1])
    with open("_posts/" + post_name) as post:
        for line in post:
            if "Discuss this episode" in line:
                # Ep 115 started with Issue 5 (so subtract 110)
                if num - 110 + (num > 116) + (num > 151) != int(line[:-2].split('/')[-1]):
                    problem = True
print (("❌" if problem else "✅") + " - Discussion Link Issue Number")

# Dates Differ by 7 Days
problem = False
dates = []
for post_name in os.listdir("_posts/"):
    date = post_name[:10]
    dates.append(datetime.strptime(date, "%Y-%m-%d"))
dates.sort()
for a, b in zip(dates[:-1], dates[1:]):
    if (b - a).days != 7:
        problem = True
print (("❌" if problem else "✅") + " - Dates Differ by 7 Days")

# Episodes
print("EPISODES CHECKS")

# Number, Title & Link in Episodes.md
problem_title      = False
problem_date       = False
problem_link_date  = False
problem_link_num   = False
for post_name in os.listdir("_posts/"):
    num  = int(post_name[:-3].split('-')[-1])
    date = post_name[:10]
    with open("_posts/" + post_name) as post:
        for line in post:
            if "title:" in line:
                title = ' '.join(line.split("\"")[-2].split()[2:])
    with open("pages/episodes.md") as file:
        for n, line in enumerate(file):
            if n > 10:
                data = line.split("|")
                if len(data) > 3:
                    other_num   = int(data[1].strip())
                    other_date  = data[-2].strip()
                    other_title = data[2].split("]")[0].strip()[1:].replace('`', '')
                    link_num    = int(data[2].split(']')[1].split("/")[6].strip()[8:-6])
                    link_date   = '-'.join(data[2].split(']')[1].split("/")[3:6])

                    if num == other_num:
                        if date != other_date:
                            problem_date = True
                        if title != other_title and 'Ben Deane' not in title:
                            print(title)
                            print(other_title)
                            problem_title = True
                        if date != link_date:
                            problem_link_date = True
                        if num != link_num:
                            problem_link_num = True

print (("❌" if problem_date      else "✅") + " - Episode Date")
print (("❌" if problem_title     else "✅") + " - Episode Title")
print (("❌" if problem_link_date else "✅") + " - Episode Date in Link")
print (("❌" if problem_link_num  else "✅") + " - Episode Number in Link")


from pydantic import BaseModel, ValidationError, validator
from typing import Optional

class Box(BaseModel):
    id: int
    weight: str
    height: int

    @validator('*')
    def ageAndIdValidator(cls, value):
        if(value < 1):
            raise ValueError('id, weight and height must > 0')
        return value

try:
    b1 = Box(id = 1, weight = 0, height = 0)
    print(b1)
except ValidationError as e:
    print(e.json())


#!/usr/bin/env python3

import asyncio
import re

from i3ipc.aio import Connection

OUTPUT_MAIN = "eDP-1"
OUTPUT_IGNORE = ("xroot-0", "VIRTUAL-1", "DP-1", "HDMI-2")  # Outputs to ignore

async def get_focused_monitor(i3, primary, secondary):
    workspaces = await i3.get_workspaces()

    for workspace in workspaces:
        if workspace.name == primary and workspace.focused is True:
            return primary
        elif workspace.name == secondary and workspace.focused is True:
            return secondary

    return ""

async def get_temporary_workspace(i3):
    workspaces = await i3.get_workspaces()
    numbered_workspaces = filter(lambda w: w.name[0].isdigit(), workspaces)
    numbers = list(
        map(
            lambda w: int(re.search(r"^[0-9]+", w.name).group(0)),
            numbered_workspaces,
        )
    )

    for i in range(1, max(numbers) + 2):
        if i not in numbers:
            return i

async def swap_workspace_names(i3, primary, secondary):
    """Swap workspaces names"""

    tmp_workspace = await get_temporary_workspace(i3)

    await i3.command(
        "rename workspace {old} to {new}".format(old=primary, new=tmp_workspace)
    )

    await i3.command(
        "rename workspace {old} to {new}".format(
            old=secondary,
            new=primary,
        )
    )

    await i3.command(
        "rename workspace {old} to {new}".format(
            old=tmp_workspace, new=secondary
        )
    )

async def swap_workspaces_outputs(i3, primary, secondary):
    """Swap workspaces from one monitor to another"""

    await i3.command("focus output {output}".format(output=primary.name))
    await i3.command(
        "move workspace to output {output}".format(output=secondary.name)
    )

    await i3.command("focus output {output}".format(output=secondary.name))
    await i3.command(
        "move workspace to output {output}".format(output=primary.name)
    )

    # Show both workspaces again on screen and focus the one which used to be
    # "secondary"
    await i3.command(
        "workspace {name}".format(name=secondary.current_workspace)
    )
    await i3.command("workspace {name}".format(name=primary.current_workspace))

async def main():
    i3 = await Connection().connect()

    outputs = await i3.get_outputs()

    primary_output = None
    secondary_output = None

    for output in outputs:
        print(output.name)
        if output.name == OUTPUT_MAIN:
            primary_output = output
        elif output.name in OUTPUT_IGNORE:
            continue
        else:
            secondary_output = output

    if secondary_output is None:
        return

    focused = await get_focused_monitor(
        i3, primary_output.current_workspace, secondary_output.current_workspace
    )

    # Swap variable names if needed
    if focused != primary_output.current_workspace:
        tmp = primary_output
        primary_output = secondary_output
        secondary_output = tmp

    await swap_workspace_names(
        i3, primary_output.current_workspace, secondary_output.current_workspace
    )

    await swap_workspaces_outputs(i3, primary_output, secondary_output)

if __name__ == "__main__":
    asyncio.run(main())


from dataclasses import dataclass, field
from typing import List, Optional
from nova.ausgabe_status_typ import AusgabeStatusTyp
from nova.freizeit_beleg_parameter import FreizeitBelegParameter
from nova.vertriebs_response import VertriebsResponse

__NAMESPACE__ = "http://nova.voev.ch/services/v14/vertrieb"

@dataclass
class LeseFreizeitBelegStatusVertriebsResponse(VertriebsResponse):
    """Die Belegantwort ist das Antwortsobjekt des 4.

    Klangs. Sie enthält für jede übermittelte LeistungId ein Objekt
    DruckDaten in dem alle zum Drucken notwendige Druckattribute
    enthalten sind.
    """
    leistung: List["LeseFreizeitBelegStatusVertriebsResponse.Leistung"] = field(
        default_factory=list,
        metadata={
            "type": "Element",
            "namespace": "http://nova.voev.ch/services/v14/vertrieb",
        }
    )

    @dataclass
    class Leistung:
        leistungs_id: Optional[int] = field(
            default=None,
            metadata={
                "name": "leistungsId",
                "type": "Element",
                "namespace": "http://nova.voev.ch/services/v14/vertrieb",
                "required": True,
                "min_inclusive": 0,
            }
        )
        traeger_medium_code: Optional[str] = field(
            default=None,
            metadata={
                "name": "traegerMediumCode",
                "type": "Element",
                "namespace": "http://nova.voev.ch/services/v14/vertrieb",
                "required": True,
                "min_length": 0,
                "max_length": 50,
            }
        )
        ausgabe_status: Optional[AusgabeStatusTyp] = field(
            default=None,
            metadata={
                "name": "ausgabeStatus",
                "type": "Element",
                "namespace": "http://nova.voev.ch/services/v14/vertrieb",
                "required": True,
            }
        )
        beleg_parameter: List[FreizeitBelegParameter] = field(
            default_factory=list,
            metadata={
                "name": "belegParameter",
                "type": "Element",
                "namespace": "http://nova.voev.ch/services/v14/vertrieb",
            }
        )


from mbot.core.plugins import plugin, PluginCommandContext, PluginCommandResponse,PluginMeta
from . import plexst
from mbot.openapi import mbot_api
from mbot.core.params import ArgSchema, ArgType

import logging
_LOGGER = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)

plugins_name = '「PLEX 工具箱」'

def get_enum_data():
    """
    返回一个包含name和value的枚举数据，在前端页面会呈现为下拉列表框；
    value就是你最终你能接收到的变量值
    """
    _LOGGER.info(f'{plugins_name}开始获取媒体库')
    libtable=plexst.get_library()
    return libtable

@plugin.command(name='select_data', title='Plex整理全库', desc='自动翻译标签&&拼音排序&&添加TOP250标签', icon='HourglassFull',run_in_background=True)
def select_data(ctx: PluginCommandContext,
                library: ArgSchema(ArgType.Enum, '选择需要整理的媒体库', '', enum_values=get_enum_data,multi_value=True),
                sortoutNum: ArgSchema(ArgType.String, '整理数量，示例：50，表示只整理最新的50条，留空整理全部', '', default_value='ALL', required=False)):

    # plexst.config['library']=library
    plexst.process_all(library,sortoutNum)
    user_list = list(filter(lambda x: x.role == 1, mbot_api.user.list()))
    if user_list:
        for user in user_list:
            mbot_api.notify.send_system_message(user.uid, '手动运行 Plex 媒体库整理',
                                                '翻译标签 && 拼音排序 && 添加TOP250标签完毕')
    return PluginCommandResponse(True, f'手动运行 Plex 媒体库整理完成')

# @plugin.command(name='plexcollection', title='Plex整理合集首字母', desc='自动整理合集首字母', icon='HourglassFull',run_in_background=True)
# def echo_c(ctx: PluginCommandContext):
#     plexst.process_collections()
#     user_list = list(filter(lambda x: x.role == 1, mbot_api.user.list()))
#     if user_list:
#         for user in user_list:
#             mbot_api.notify.send_system_message(user.uid, 'Plex整理合集首字母',
#                                                 '自动整理合集首字母')
#     return PluginCommandResponse(True, f'Plex合集整理完成')


import itertools

import numpy as np
import pandas as pd
import networkx as nx

from sklearn.neighbors import KDTree

import sqr.core.network
import sqr.core.scoring

from sqr.core.config import years, years_num, years, years_hh, years_pers, minimum_cols

def check_pop(df, idxs, base_years):
    '''
    Checks whether population constraints are satisfied for the set
    of indices in the DataFrame, in the years specified.

    Parameters
    -----------
    df: pd.DataFrame
        This dataframe must contain cell level population data.

    idxs: 1-d array
        Contains indices for subsample of dataframe.

    base_years: 1-d array
        Contains years as strings.
    '''

    check = True
    for label, threshold in  [('hh', 50),('pers', 100)]:
        check_years = [f'{c}_{label}' for c in base_years]
        count = \
            df.loc[idxs, check_years]\
                .dropna(how='all',axis=1)\
                .sum(axis=0)\
                .min()
        check &= (count >=  threshold)
    return check

def cell_expansion(i, unassigned, df, G, max_group_attempts,
                   year_cols, check_connectivity):
    '''
    This function begins with a single individual and tries
    to attach it to neighboring cells. This collection
    is evaluated repeatedly. If it's value exceeds 100
    the operation terminates succesfully with a novel group.

    Note that the search is limited to neighbors as specified
    in the graph neighboring tile cells.
    '''

    # initialize group
    current_group = np.array([i])
    current_group_neighbors = np.array([i])
    temp_unassigned = np.setdiff1d(unassigned, [i])
    group_attempts = 0

    while True:

        # choose next cell
        i = np.random.choice(current_group_neighbors)
        feasible_neighbors = np.intersect1d(temp_unassigned, list(G.neighbors(i)))
        if feasible_neighbors.size > 0:
            j = np.random.choice(feasible_neighbors)
        else:
            current_group_neighbors = np.setdiff1d(current_group_neighbors, [i])

            if group_attempts >= max_group_attempts:
                return False, unassigned, None

            if current_group_neighbors.size==0:
                return False, unassigned, None

            group_attempts += 1
            continue

        next_group = np.union1d(current_group,[j])

        temp_unassigned = np.setdiff1d(temp_unassigned, [j])

        if check_pop(df, next_group, base_years = year_cols): #Rettet

            if check_connectivity:
                # check if null observation is necessary, otherwise remove
                if len(next_group) > 1:
                    group_df = df.loc[next_group]

                    if (group_df[minimum_cols].isnull().min(1)).sum() > 0:
                        next_group, removal = remove_null(next_group, df=df, G=G)
                        temp_unassigned = np.union1d(temp_unassigned, removal)

            return True, temp_unassigned, next_group

        else:
            current_group = next_group

            current_group_neighbors = np.union1d(current_group_neighbors, [j])

            if group_attempts >= max_group_attempts:
                return False, unassigned, None

            group_attempts += 1

def remove_null(group, df, G):
    '''
    This function attempts to remove all null entries
    if they are not critical for connecting the group
    '''

    sub_G = G.subgraph(group)
    group_df = df.loc[group]

    empty = group_df[minimum_cols].isnull().min(1)
    X_e = group_df.loc[empty, ['e','n']].values
    X_ne = group_df.loc[~empty, ['e','n']].values

    tree = KDTree(X_ne)
    dists = [4,3,2,1]
    has_neighbors = [list(map(lambda l: len(l)>0, tree.query_radius(X_e, d))) for d in dists]

    sorted_neighbors = \
        pd.DataFrame(np.array(has_neighbors).T, group_df[empty].index, dists)\
            .sort_values(by=dists)
    removal = []

    for idx_null in sorted_neighbors.index:
        g_n = list(set(group)-set([idx_null]))
        if sqr.core.network.group_connect(g_n, sub_G):
            removal += [idx_null]
            group = np.setdiff1d(group,[idx_null])

    return group, removal

def assign_selfsufficient(partition, df, unassigned_pop): #Rettet
    '''
    Remove cells with at least 100 inhabitants. These become
    groups with a single unique member, i.e. themselves.
    '''

    # check if cell is self-sufficient
    check_pers = df[minimum_cols[0]]>=100
    check_hh = df[minimum_cols[1]]>=50

    selfsufficient = df[check_pers & check_hh].index

    unassigned_selfsuff = np.intersect1d(selfsufficient, unassigned_pop)
    partition += [[cell] for cell in unassigned_selfsuff]
    unassigned_pop = np.setdiff1d(unassigned_pop, unassigned_selfsuff)

    return partition, unassigned_pop

def assign_remaining(partition, unassigned_pop, df, G, year_cols):
    '''
    All unassigned cells are attempted to be merged with other
    close cells.
    '''

    for i in unassigned_pop:

        optimals = {}
        neighbor_groups = [g for g in partition if len(set(G.neighbors(i)) & set(g))>0]

        for group in neighbor_groups:
            if check_pop(df, np.union1d(group,[i]), base_years=year_cols):
                optimals[partition.index(group)] = \
                    sqr.core.scoring.group_collapse(group, [i], df)

        optimals = pd.Series(optimals)

        if optimals.max()>0:
            partition[int(optimals.idxmax())] += [i]
            unassigned_pop = np.setdiff1d(unassigned_pop, [i])

    return partition, unassigned_pop


import boto3
from dotenv import load_dotenv
import os
from src.common.excel import export_to_excel

from progress.bar import FillingSquaresBar

load_dotenv()

region = os.getenv('AWS_REGION')

ami_name = []
ami_id = []
ami_origem = []
ami_visibilidade = []
ami_owner = []
ami_state = []
ami_creation = []
ami_architecture = []
ami_platform = []
ami_root_device_name = []
ami_root_device_type = []
ami_block_device = []
ami_description = []
ami_imagetype = []
ami_kernelid = []
ami_ramdiskid = []
ami_product = []
ami_virtualization = []
ami_usageoperation = []
ami_volumesize = []

def run():
    ec2 = boto3.client('ec2', region_name=region)

    response = ec2.describe_images(Owners=['self'])

    bar1 = FillingSquaresBar('AMI - Imagens')

    bar1.max = len(response['Images']) + 1

    for ami in response['Images']:
        bar1.next()
        try:
            ami_name.append(ami['Name'])
        except:
            ami_name.append("-")
        ami_id.append(ami['ImageId'])
        ami_origem.append(ami['ImageLocation'])
        vis_validate = str(ami['Public'])
        if vis_validate == "False":
            ami_visibilidade.append('Private')
        else:
            ami_visibilidade.append('Public')
        ami_owner.append(ami['OwnerId'])
        ami_state.append(ami['State'])
        ami_creation.append(ami['CreationDate'])
        ami_architecture.append(ami['Architecture'])
        ami_platform.append(ami['PlatformDetails'])
        ami_root_device_name.append(ami['RootDeviceName'])
        ami_root_device_type.append(ami['RootDeviceType'])
        try:
            ami_description.append(ami['Description'])
        except:
            ami_description.append("-")

        device_name = ami['BlockDeviceMappings'][0]['DeviceName']
        snapshot = ami['BlockDeviceMappings'][0]['Ebs']['SnapshotId']
        volumesize = ami['BlockDeviceMappings'][0]['Ebs']['VolumeSize']
        ami_volumesize.append(volumesize)
        volumetype = ami['BlockDeviceMappings'][0]['Ebs']['VolumeType']
        encrypted = ami['BlockDeviceMappings'][0]['Ebs']['Encrypted']
        blockdevice = f'{device_name}={snapshot}:{volumesize}:{encrypted}:{volumetype}'
        ami_block_device.append(blockdevice)
        ami_imagetype.append(ami['ImageType'])
        ami_virtualization.append(ami['VirtualizationType'])
        ami_usageoperation.append(ami['UsageOperation'])

    bar1.next()
    bar1.finish()

    bar2 = FillingSquaresBar('AMI - Atributos')
    bar2.max = len(ami_id) + 1

    for id in ami_id:
        bar2.next()

        response = ec2.describe_image_attribute(Attribute='kernel', ImageId=id)
        if len(response['KernelId']) > 0:
            try:
                ami_kernelid.append(response['KernelId'][0]['Value'])
            except:
                ami_kernelid.append('-')
        else:
            ami_kernelid.append('-')
        response = ec2.describe_image_attribute(
            Attribute='ramdisk', ImageId=id)
        if len(response['RamdiskId']) > 0:
            try:
                ami_ramdiskid.append(response['RamdiskId'][0]['Value'])
            except:
                ami_ramdiskid.append('-')
        else:
            ami_ramdiskid.append('-')
        response = ec2.describe_image_attribute(
            Attribute='productCodes', ImageId=id)
        if len(response['ProductCodes']) > 0:
            try:
                ami_product.append(response['ProductCodeId'][0]['Value'])
            except:
                ami_product.append('-')
        else:
            ami_product.append('-')

    bar2.next()
    bar2.finish()

    ami_dict = {"Nome da AMI": ami_name,
                "ID da AMI": ami_id,
                "Origem": ami_origem,
                "Proprietário": ami_owner,
                "Visibilidade": ami_visibilidade,
                "Status": ami_state,
                "Data de criação": ami_creation,
                "Código de produto": ami_product,
                "Arquitetura": ami_architecture,
                "Descrição": ami_description,
                "Plataforma": ami_platform,
                "Nome de dispositivo raiz": ami_root_device_name,
                "Tipo de dispositivo raiz": ami_root_device_type,
                "Dispositivo de blocos": ami_block_device,
                "Tipo de Imagem": ami_imagetype,
                "ID de kernel": ami_kernelid,
                "ID do disco RAM": ami_ramdiskid,
                "Tamanho da imagem": ami_volumesize,
                "Virtualização": ami_virtualization,
                "Operação de uso": ami_usageoperation}

    export_to_excel(ami_dict, 'ami')


def lstwordlength(A):
    d = dict()
    d['X'] = 10
    d['M'] = 1000
    d['C'] = 100
    d['L'] = 50
    d['D'] = 500
    d['I'] = 1
    d['V'] = 5

    x = len(A) - 1
    num = 0
    prev = 0
    while x >= 0:
        temp = d.get(A[x])
        if temp < prev:
            num = num - temp
        else:
            num = num + temp
        prev = temp
        x = x - 1
    print(num)
i = input()
lstwordlength(i)


import re

if __name__ == "__main__":
    example = """    [D]
[N] [C]
[Z] [M] [P]
 1   2   3

move 1 from 2 to 1
move 3 from 1 to 3
move 2 from 2 to 1
move 1 from 1 to 2"""

    example_input = example.split("\n")

    with open("input/day5.txt") as input:
        problem_input = [i.rstrip() for i in input.readlines()]

    actual_input = problem_input

    num_stacks = (len(actual_input[0]) + 1) // 4

    stacks = []

    for _ in range(num_stacks):
        stacks.append([])

    highest_stack = 8  # barf :)

    stack_lines = actual_input[0:highest_stack]

    for line in stack_lines:
        for s in range(len(stacks)):
            crate = line[s * 4 + 1]
            if crate != " ":
                stacks[s].append(crate)

    # deep copy for part 2
    part_2_stacks = []
    for s in stacks:
        part_2_stacks.append(s[:])

    instructions = actual_input[highest_stack + 2 :]
    instruction_matcher = re.compile(r"move (\d+) from (\d) to (\d)")

    for line in instructions:
        instruction_match = instruction_matcher.search(line)

        (num_to_move, target_stack, dest_stack) = (
            int(g) for g in instruction_match.groups()
        )

        for _ in range(num_to_move):
            crate = stacks[target_stack - 1].pop(0)
            stacks[dest_stack - 1].insert(0, crate)

        chunk_to_move = part_2_stacks[target_stack - 1][0:num_to_move]
        del part_2_stacks[target_stack - 1][0:num_to_move]
        part_2_stacks[dest_stack - 1] = chunk_to_move + part_2_stacks[dest_stack - 1]

    print("part 1:", "".join(s[0] for s in stacks))
    print("part 2:", "".join(s[0] for s in part_2_stacks))


from Photovoltaics import *
import pandas as pd
from scipy.interpolate import interp1d
import numpy as np
import urllib.request
import matplotlib.pyplot as plt
from datetime import datetime
volt_dir = "Voltdata_functioningDays.csv"
solar_dir = "pyranometer_functioningDays.csv"
temp_dir = "temp_functioningDays.csv"
dasT_dir = "DAST_functioningDays.csv"
current_dir = "current_functioningDays.csv"
weather_dir = "WeatherStationIrradiance_functioningDays.csv"
volt_df = pd.read_csv(volt_dir)
solar_df = pd.read_csv(solar_dir)
temp_df = pd.read_csv(temp_dir)
dasT_df = pd.read_csv(temp_dir)
current_df = pd.read_csv(current_dir)
weather_df = pd.read_csv(weather_dir)

volts = volt_df.iloc[:, 2]
das_flux = solar_df.iloc[:, 2]
temp = temp_df.iloc[:, 2]
dasT = temp_df.iloc[:, 2]
current = current_df.iloc[:, 2]
temp0 = 25
current_cal = current + 0.2*(temp - temp0)
station_flux = weather_df.iloc[:, 1]
pv = Photovoltaics(38.7, 9.42, 32.1, 8.92, 285, 1.00, 60)
pvarray = Photovoltaics(5 * 38.7, 9.42, 5 * 32.1, 8.92, 5 * 285, 1.00, 60 * 5)

t_das = pd.Series(index=range(temp.shape[0]))
for i, t_das_str in enumerate(temp.iloc[:, 1]):
	t_das[i] = datetime.strptime(t_das_str + " -0000", "%Y-%m-%d %H:%M:%S %z")
station_flux_interp = interp1d()

tstart_str = weather_df.iloc[0, 0] + " -0500"
tstop_str = weather_df.iloc[-1, 0] + " -0500"
tstart = datetime.strptime(tstart_str, "%m/%d/%Y %H:%M %z")
tstop = datetime.strptime(tstop_str, "%m/%d/%Y %H:%M %z")
temp_15 = pd.Series(index=range(station_flux.shape[0]))
current_15 = pd.Series(index=range(station_flux.shape[0]))
volt_15 = pd.Series(index=range(station_flux.shape[0]))
das_flux_15 = pd.Series(index=range(station_flux.shape[0]))

j = -1
k = 0
for i in range(int((temp.shape[0]-8))):
	t = temp_df.iloc[i, 1]
	utc = datetime.strptime(t + " -0000", "%Y-%m-%d %H:%M:%S %z")
	if utc == tstart:
		j = 0
		k = i
	if j > -1:
		t = temp_df.iloc[k, 1]
		tnow = datetime.strptime(t + " -0000", "%Y-%m-%d %H:%M:%S %z")
		if tnow >= tstop or k + 8 > temp.shape[0]:
			break
		temp_15[j] = np.nanmean(temp[np.array(range(k - 7, k + 8))])
		current_15[j] = np.nanmean(current_cal[np.array(range(k - 7, k + 8))])
		volt_15[j] = np.nanmean(volts[np.array(range(k - 7, k + 8))])
		das_flux_15[j] = np.nanmean(das_flux[np.array(range(k - 0, k + 1))])
		j = j + 1
		k = k + 15

# plt.plot(das_flux, weather_df[:, 1])
mpp_model = pd.Series(index=range(station_flux.shape[0]))
for i in range(temp_15.shape[0]):
	vmp, imp = pvarray.mpp(temp_15[i], station_flux[i])
	mpp_model[i] = vmp*imp
plt.scatter(station_flux, current_15*volt_15)
plt.scatter(station_flux, mpp_model)
plt.show()