#!/usr/bin/env python3
# Copyright 2004-present Facebook. All Rights Reserved.

# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

"""Common functions used across the UI tabs.

The UI shares several common functions across its tabs. Unlike dep_util, this file
contains functions that specifically reference elements in the tab. This means, if
further extension of the UI is pursued, this file should be reserved for common
functions that are *explicitly* tied to the UI and dep_util for functions that could
be used in contexts outside the UI.
"""

import collections
import datetime
import glob
import os
import shutil
import subprocess
import sys

from PyQt5 import QtCore, QtWidgets

dir_scripts = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
dir_root = os.path.dirname(dir_scripts)
sys.path.append(dir_root)
sys.path.append(os.path.join(dir_scripts, "aws"))
sys.path.append(os.path.join(dir_scripts, "render"))
sys.path.append(os.path.join(dir_scripts, "util"))

import dep_util
import glog_check as glog
import scripts.render.config as config
from log_reader import LogReader
from scripts.aws.create import (
    get_render_pid,
    get_staging_info,
    has_render_flag,
    run_ssh_command,
)
from scripts.aws.util import AWSUtil
from scripts.render.network import LAN
from scripts.util.system_util import (
    get_flags,
    get_flags_from_flagfile,
    image_type_paths,
    run_command,
)
from slider_image_thresholds import SliderWidget

script_dir = os.path.dirname(os.path.realpath(__file__))
scripts_dir = os.path.abspath(os.path.join(script_dir, os.pardir))
dep_dir = os.path.join(scripts_dir, os.pardir)
dep_bin_dir = os.path.join(dep_dir, "build", "bin")
dep_res_dir = os.path.join(dep_dir, "res")
dep_flags_dir = os.path.join(dep_res_dir, "flags")
os.makedirs(dep_flags_dir, exist_ok=True)

source_root = os.path.join(dep_dir, "source")
depth_est_src = os.path.join(source_root, "depth_estimation")
render_src = os.path.join(source_root, "render")
render_scripts = os.path.join(scripts_dir, "render")

type_color_var = "color_variance"
type_fg_mask = "fg_mask"

threshold_sliders = {
    # attr: type, printed name, slider index, max value, default value
    "noise": [type_color_var, "Noise variance", 1, 1.5e-3, 4e-5],
    "detail": [type_color_var, "Detail variance", 2, 2e-2, 1e-3],
    "blur": [type_fg_mask, "Blur radius", 1, 20, 2],
    "closing": [type_fg_mask, "Closing size", 2, 20, 4],
    "thresh": [type_fg_mask, "Threshold", 3, 1, 3e-2],
}


def init(parent):
    """Sets up all the UI global internals (logs, data, and flags) and any
    tab specific components.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
    """
    parent.is_refreshing_data = True
    parent.initialize_paths()
    parent.set_default_top_level_paths()
    parent.setup_logs()
    parent.setup_data()
    parent.setup_flags()
    if "retrieve_missing_flagfiles" in dir(parent):
        parent.retrieve_missing_flagfiles()
    if "add_default_flags" in dir(parent):
        parent.add_default_flags()
    if "setup_thresholds" in dir(parent):
        parent.setup_thresholds()
    if "add_data_type_validators" in dir(parent):
        parent.add_data_type_validators()
    if "setup_farm" in dir(parent):
        parent.setup_farm()
    if "update_run_button_text" in dir(parent):
        parent.update_run_button_text()
    parent.is_refreshing_data = False


def setup_aws_config(parent):
    """Sets up the configuration of the Kubernetes cluster.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
    """
    if parent.parent.is_aws:
        create_flagfile = os.path.join(
            parent.path_flags, parent.app_name_to_flagfile[parent.app_aws_create]
        )
        if os.path.exists(create_flagfile):
            create_flags = get_flags_from_flagfile(create_flagfile)
            if "cluster_size" in create_flags:
                spin_num_workers = getattr(
                    parent.dlg, f"spin_{parent.tag}_farm_num_workers", None
                )
                spin_num_workers.setValue(int(create_flags["cluster_size"]))
            if "instance_type" in create_flags:
                dd_ec2 = getattr(parent.dlg, f"dd_{parent.tag}_farm_ec2", None)
                dd_ec2.setCurrentText(create_flags["instance_type"])


def setup_farm(parent):
    """Sets up the UI to interact with a LAN cluster.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
    """
    initialize_farm_groupbox(parent)
    ip_begin, _ = parent.parent.ui_flags.master.rsplit(".", 1)
    parent.lan = LAN(f"{ip_begin}.255")


def get_tooltip(parent, app_name):
    """Gets the help tooltip display of a binary.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
        app_name (str): Name of the binary.

    Returns:
        str: Help from the binary.
    """
    dir = scripts_dir if app_name.endswith(".py") else dep_bin_dir
    tooltip = dep_util.get_tooltip(os.path.join(dir, app_name))
    if not tooltip:
        parent.log_reader.log_warning(f"Cannot get tooltip for: {app_name}")
    return tooltip


def initialize_paths(parent):
    """Initializes paths for scripts and flags depending on the specified tab.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
    """
    tag = parent.tag
    parent.app_name_to_flagfile = {}

    if tag in ["bg", "depth", "export"]:
        parent.app_name = "render/render.py"
        if tag in ["depth", "export"]:
            parent.app_aws_clean = "aws/clean.py"
            parent.app_aws_create = "aws/create.py"
            parent.app_name_to_flagfile[parent.app_aws_clean] = "clean.flags"

    if tag == "calibrate":
        parent.app_name = "Calibration"
        parent.flagfile_basename = "calibration.flags"
    elif tag == "bg":
        parent.flagfile_basename = "render_background.flags"
    elif tag == "depth":
        parent.flagfile_basename = "render_depth.flags"
        parent.app_name_to_flagfile[parent.app_aws_create] = "aws_create_video.flags"
    elif tag == "export":
        parent.flagfile_basename = "render_export.flags"
        parent.app_name_to_flagfile[parent.app_aws_create] = "aws_create_export.flags"
        parent.app_aws_download_meshes = "aws/download_meshes.py"
        parent.app_name_to_flagfile[
            parent.app_aws_download_meshes
        ] = "download_meshes.flags"

    parent.app_name_to_flagfile[parent.app_name] = parent.flagfile_basename
    parent.tooltip = get_tooltip(parent, parent.app_name)
    parent.is_refreshing_data = False
    parent.is_process_killed = False
    parent.threshs_tooltip = "Click and drag to pan, scroll to zoom in and out"
    parent.script_dir = script_dir


def setup_logs(parent):
    """Sets up logging system for dialog on the specified tab.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.

    Returns:
        LogReader: Reader configured for the current tab.
    """
    tag = parent.tag
    qt_text_edit = getattr(parent.dlg, f"text_{tag}_log", None)
    qt_tab_widget = getattr(parent.dlg, f"w_{tag}_preview", None)
    tab_idx = qt_tab_widget.count() - 1  # log is always the last tab
    ts = dep_util.get_timestamp("%Y%m%d%H%M%S.%f")
    name = parent.__class__.__name__
    log_file = os.path.join(parent.path_logs, f"{name}_{ts}")
    log_reader = LogReader(qt_text_edit, parent, log_file)
    log_reader.set_tab_widget(qt_tab_widget, tab_idx)
    return log_reader


def setup_flagfile_tab(parent):
    """Sets up the flags according to the corresponding flagfile on the specified tab.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
    """
    tag = parent.tag
    dlg = parent.dlg
    qt_text_edit = getattr(dlg, f"text_{tag}_flagfile_edit", None)
    qt_btn_save = getattr(dlg, f"btn_{tag}_flagfile_save", None)
    qt_text_edit.textChanged.connect(parent.on_changed_flagfile_edit)
    qt_btn_save.clicked.connect(parent.save_flag_file)
    qt_btn_save.setEnabled(False)


def setup_file_explorer(parent):
    """Creates the file explorer rooted  on the specified tab.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
    """
    dlg = parent.dlg
    parent.fs_tree = dlg.tree_file_explorer
    path = parent.path_project
    parent.fs_model, parent.fs_tree = dep_util.setup_file_explorer(parent.fs_tree, path)
    parent.fs_tree.clicked.connect(lambda: preview_file(parent))


def preview_file(parent):
    """Displays the file and its label on the specified tab.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
    """
    dlg = parent.dlg
    frame = dlg.label_preview_image
    label = dlg.label_preview_path
    project = parent.path_project
    prefix = f"{project}/"
    dep_util.preview_file(parent.fs_model, parent.fs_tree, frame, label, prefix)


def switch_ui_elements_for_processing(parent, gb, state):
    """Switches element interaction when processing on the specified tab.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
        gb (QtWidgets.QGroupBox): Group box for the tab.
        state (str): Identifier of the callback state.
    """
    # Buttons
    parent.update_buttons(gb)

    # Switch all other sections, except the file explorer
    dlg = parent.dlg
    for gbi in dlg.findChildren(QtWidgets.QGroupBox):
        if gbi != gb and not gbi.objectName().endswith("_file_explorer"):
            gbi.setEnabled(state)

    # Switch current group box elements
    prefixes = ["cb_", "dd_", "val_", "label_"]
    dep_util.switch_objects_prefix(gb, prefixes, state)

    # Switch tabs that are not image preview or log
    for w in dlg.findChildren(QtWidgets.QWidget):
        name = w.objectName()
        ignore = name.endswith("_preview") or name.endswith("_log")
        if name.startswith("tab_") and not ignore:
            w.setEnabled(state)

    # Switch other sections
    for s in parent.parent.sections:
        if s != parent:
            dep_util.set_tab_enabled(parent.dlg.w_steps, s.tag, state)


def cancel_process(parent):
    """Stops a running process on the specified tab.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
    """
    running_render = False  # Render has to be explicitly killed since it runs detached
    if parent.is_farm and parent.is_aws:
        processes = parent.log_reader.get_processes()
        for process in processes:
            if process == "run_aws_create" or process.startswith("run_export"):
                running_render = True

    if running_render:
        aws_util = AWSUtil(
            parent.path_aws_credentials, region_name=parent.parent.aws_util.region_name
        )
        _, ip_staging = get_staging_info(aws_util, parent.path_aws_ip_file)
        if ip_staging:
            render_pid = get_render_pid(parent.path_aws_key_fn, ip_staging)
            if render_pid is not None:
                run_ssh_command(
                    parent.path_aws_key_fn, ip_staging, f"kill -9 {render_pid}"
                )

    parent.log_reader.kill_all_processes()
    parent.is_process_killed = True

    if "reset_run_button_text" in dir(parent):
        parent.reset_run_button_text()


def is_cloud_running_process(parent):
    """Checks if a render process is being run on the cloud"""
    key_fn = parent.path_aws_key_fn
    if not parent.is_aws or not parent.is_farm or not os.path.isfile(key_fn):
        return False

    aws_util = AWSUtil(
        parent.path_aws_credentials, region_name=parent.parent.aws_util.region_name
    )
    _, ip_staging = get_staging_info(
        aws_util, parent.path_aws_ip_file, start_instance=False
    )
    if not ip_staging:
        return False

    tag = parent.tag
    if tag not in ["depth", "export"]:
        return False

    flag = "run_depth_estimation"
    value = tag == "depth"
    return has_render_flag(key_fn, ip_staging, flag, value)


def sync_with_s3(parent, gb, subdirs):
    """Synchronizes data from the local directory to S3.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
        gb (QtWidgets.QGroupBox): Group box for the tab.
        subdirs (list[str]): Local path to be synced.
    """
    run_silently = not parent.parent.ui_flags.verbose
    cmds = []

    parent.log_reader.log_notice(f"Syncing frames with S3...")

    for subdir in subdirs:
        local = os.path.join(config.DOCKER_INPUT_ROOT, subdir)
        remote = os.path.join(parent.parent.ui_flags.project_root, subdir)

        if "_levels" in subdir:
            locals = [
                os.path.join(local, f"level_{l}") for l in range(len(config.WIDTHS))
            ]
        else:
            locals = [local]

        # Tar frames
        tar_app_path = os.path.join(scripts_dir, "util", "tar_frame.py")
        for local_i in locals:
            frames = dep_util.get_frame_list(local_i)
            if not frames:
                if not run_silently:
                    print(glog.yellow(f"No frames found for S3 syncing in {local_i}"))
                continue
            for frame in frames:
                cmds.append(f"python3.7 {tar_app_path} --src={local_i} --frame={frame}")
        cmds.append(f"aws s3 sync {local} {remote} --exclude '*' --include '*.tar'")

    p_id = f"sync_results_s3_{parent.tag}"
    cmd_and = " && ".join(cmds)
    cmd = f'/bin/sh -c "{cmd_and}"'
    start_process(parent, cmd, gb, p_id, run_silently)


def on_process_finished(parent, p_id):
    """Callback event handler for a process completing on the specified tab.

    Args:
        p_id (str): PID of completed process.
    """
    if not p_id or p_id.startswith("run"):
        parent.log_reader.remove_processes()
    else:
        parent.log_reader.remove_process(p_id)
    parent.refresh_data()

    if p_id.startswith("run") and "_export_" not in p_id:
        if "update_frame_names" in dir(parent):
            parent.update_frame_names()

        if "sync_with_s3" in dir(parent) and not parent.is_process_killed:
            if parent.parent.is_aws:
                parent.sync_with_s3()

    if len(parent.log_reader.get_processes()) == 0:
        # Re-enable UI elements
        switch_ui_elements_for_processing(parent, parent.log_reader.gb, True)

    # We may have data to enable other tabs
    if p_id.startswith("run"):
        [s.refresh_data() for s in parent.parent.sections if s != parent]

    if "update_run_button_text" in dir(parent):
        parent.update_run_button_text()

    parent.is_process_killed = False


def populate_dropdown(parent, gb, dd):
    """Populates a dropdown on the specified tab.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
        gb (QtWidgets.QGroupBox): Group box for the tab.
        dd (QtWidgets.QComboBox): Dropdown UI element.
    """
    project = parent.parent.path_project
    t = dep_util.remove_prefix(gb.objectName(), "gb_")
    dd_prev_text = dd.currentText() if dd.count() > 0 else ""
    tag = dep_util.remove_prefix(dd.objectName(), f"dd_{t}_")
    ps = parent.get_files(tag)
    dep_util.populate_dropdown(dd, ps, f"{project}/")
    dep_util.update_qt_dropdown(dd, dd_prev_text, add_if_missing=False)


def populate_dropdowns(parent, gb, dd_first=None):
    """Populates the dropdowns on the specified tab.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
        gb (QtWidgets.QGroupBox): Group box for the tab.
        dd_first (list[QtWidgets.QGroupBox], optional): Dropdowns to populate first.
    """
    if not dd_first:
        dd_first = []
    for dd in dd_first:
        populate_dropdown(parent, gb, dd)
    for dd in gb.findChildren(QtWidgets.QComboBox):
        if dd not in dd_first:
            populate_dropdown(parent, gb, dd)


def refresh_data(parent):
    """Updates UI elements to be in sync with data on disk on the specified tab.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
    """
    tag = parent.tag
    dlg = parent.dlg
    tab = getattr(dlg, f"t_{tag}", None)

    if tag in ["bg", "depth", "export"]:
        parent.path_rig_json = get_calibrated_rig_json(parent)

    if tag == "depth":
        parent.update_bg_checkbox()

    # This locks the dropdown callbacks while we re-populate them
    parent.is_refreshing_data = True
    for gb in tab.findChildren(QtWidgets.QGroupBox):
        gb.setEnabled(True)
        parent.populate_dropdowns(gb)
        parent.update_buttons(gb)
    if "flagfile_fn" in dir(parent):
        sync_data_and_flagfile(parent, parent.flagfile_fn)
    parent.disable_tab_if_no_data()
    parent.is_refreshing_data = False


def update_flagfile_edit(parent, flagfile_fn, switch_to_flag_tab=False):
    """Updates the edit box for the flagfile on the specified tab.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
        flagfile_fn (str): Name of the flagfile.
        switch_to_flag_tab (bool, optional): Whether or not to switch tabs after updating.
    """
    if not os.path.isfile(flagfile_fn):
        return

    tag = parent.tag
    dlg = parent.dlg
    text = getattr(dlg, f"text_{tag}_flagfile_edit", None)
    preview = getattr(dlg, f"w_{tag}_preview", None)

    text.setPlainText(open(flagfile_fn).read())
    if switch_to_flag_tab:
        preview.setCurrentIndex(1)


def update_data_or_flags(
    parent, flagfile_fn, flagfile_from_data, switch_to_flag_tab=False
):
    """Updates the flagfile from the UI elements or vice versa on the specified tab.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
        flagfile_fn (str): Name of the flagfile.
        flagfile_from_data (bool): Whether to load the flagfile from the data (True) or
            vice versa (False).
        switch_to_flag_tab (bool, optional): Whether or not to switch tabs after updating.
    """
    if not flagfile_fn:
        return

    flags = get_flags_from_flagfile(flagfile_fn)
    if flagfile_from_data:
        parent.update_flags_from_data(flags)
    else:
        parent.update_data_from_flags(flags)

    if flagfile_from_data:
        # Overwrite flag file
        sorted_flags = collections.OrderedDict(sorted(flags.items()))
        dep_util.write_flagfile(flagfile_fn, sorted_flags)

        # Refresh flagfile edit window
        parent.update_flagfile_edit(flagfile_fn, switch_to_flag_tab)


def sync_data_and_flagfile(
    parent, flagfile_fn, set_label=True, switch_to_flag_tab=False
):
    """Synchronizes displayed UI elements and contents of the flagfile.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
        flagfile_fn (str): Name of the flagfile.
        set_label (bool, optional): Whether or not to update the flagfile label in the UI.
        switch_to_flag_tab (bool, optional): Whether or not to switch tabs after updating.
    """
    tag = parent.tag
    dlg = parent.dlg
    label = getattr(dlg, f"label_{tag}_flagfile_path", None)

    flagfile = os.path.basename(flagfile_fn)
    label.setText(flagfile)

    # flag file to data first, then data to flag file for missing info
    flagfile_from_data = False
    parent.update_data_or_flags(flagfile_fn, flagfile_from_data, switch_to_flag_tab)
    parent.update_data_or_flags(flagfile_fn, not flagfile_from_data, switch_to_flag_tab)


def disable_tab_if_no_data(parent, btn_run):
    """Prevents navigation to the tab if the required data is not present on the specified tab.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
        btn_run (QtWidgets.QPushButton): UI button for tab switch.
    """
    if not btn_run.isEnabled():
        dep_util.set_tab_enabled(parent.dlg.w_steps, parent.tag, enabled=False)


def setup_project(parent, mkdirs=False):
    """Retrieves any missing flagfiles and sets the default flags on the specified tab.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
        mkdirs (bool, optional): Whether or not to make the defined directories.
    """
    parent.is_refreshing_data = True
    parent.log_reader.log_header()
    parent.refresh_data()
    parent.is_refreshing_data = False


def save_flag_file(parent, flagfile_fn):
    """Saves flagfile from the UI to disk on the specified tab.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
        flagfile_fn (str): Name of the flagfile.
    """
    if not os.path.isfile(flagfile_fn):
        return

    tag = parent.tag
    dlg = parent.dlg
    text_edit = getattr(dlg, f"text_{tag}_flagfile_edit", None)
    btn_save = getattr(dlg, f"btn_{tag}_flagfile_save", None)

    with open(flagfile_fn, "w") as f:
        f.write(text_edit.toPlainText())
    f.close()

    # Disable save button
    btn_save.setEnabled(False)

    # Update corresponding groupbox
    flagfile_from_data = False  # flagfile to data
    parent.update_data_or_flags(flagfile_fn, flagfile_from_data)


def update_flagfile(parent, flagfile_fn):
    """Updates the edit box for the flagfile on the specified tab.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
        flagfile_fn (str): Name of the flagfile.
    """
    parent.update_data_or_flags(flagfile_fn, flagfile_from_data=True)


def retrieve_missing_flagfiles(parent):
    """Copies the missing flagfiles to project for local modification on the specified tab.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
    """
    tag = parent.tag
    if tag == "calibrate":
        ff_base = "calibration.flags"
    elif tag in ["bg", "depth", "export"]:
        ff_base = "render.flags"

    ffs_expected = [[ff_base, parent.flagfile_fn]]
    if tag in ["depth", "export"]:
        ff_aws_create = os.path.join(
            parent.path_flags, parent.app_name_to_flagfile[parent.app_aws_create]
        )
        ffs_expected.append(["aws_create.flags", ff_aws_create])
    for ff_src_rel, ff_dst_abs in ffs_expected:
        if not os.path.isfile(ff_dst_abs):
            ff_src_abs = os.path.join(dep_flags_dir, ff_src_rel)
            os.makedirs(os.path.dirname(ff_dst_abs), exist_ok=True)
            shutil.copyfile(ff_src_abs, ff_dst_abs)
            update_flagfile(parent, ff_dst_abs)


def add_default_flags(parent):
    """Retrieves the default flags to the local flagfile on the specified tab from
    either the source or scripts binaries.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
    """
    default_flags = {}

    tag = parent.tag
    if tag in ["bg", "depth"]:
        default_flags.update(
            {
                os.path.join(depth_est_src, "DerpCLI.cpp"): {
                    "max_depth_m",
                    "min_depth_m",
                    "resolution",
                    "var_high_thresh",
                    "var_noise_floor",
                }
            }
        )

    if tag == "depth":
        default_flags.update(
            {
                os.path.join(render_scripts, "setup.py"): {"do_temporal_filter"},
                os.path.join(depth_est_src, "TemporalBilateralFilter.cpp"): {
                    "time_radius"
                },
                os.path.join(render_src, "GenerateForegroundMasks.cpp"): {
                    "blur_radius",
                    "morph_closing_size",
                    "threshold",
                },
            }
        )
    elif tag == "export":
        default_flags.update(
            {
                os.path.join(render_src, "SimpleMeshRenderer.cpp"): {"width"},
                os.path.join(render_src, "ConvertToBinary.cpp"): {"output_formats"},
            }
        )

    flagfile_fn = os.path.join(parent.path_flags, parent.flagfile_basename)
    flags = get_flags_from_flagfile(flagfile_fn)
    for source in default_flags:
        if os.path.isfile(source):
            source_flags = get_flags(source)
        else:
            source_flags
        desired_flags = default_flags[source]
        for source_flag in source_flags:
            flag_name = source_flag["name"]

            # Only add the default flag if not already present in current flags
            if flag_name in desired_flags:
                if flag_name not in flags or flags[flag_name] == "":
                    flags[flag_name] = source_flag["default"]

    # Add run flags
    if tag == "bg":
        flags["run_generate_foreground_masks"] = False
        flags["run_precompute_resizes"] = True
        flags["run_depth_estimation"] = True
        flags["run_convert_to_binary"] = False
        flags["run_fusion"] = False
        flags["run_simple_mesh_renderer"] = False
        flags["use_foreground_masks"] = False
    elif tag == "depth":
        flags["run_depth_estimation"] = True
        flags["run_precompute_resizes"] = True
        flags["run_precompute_resizes_foreground"] = True
        flags["run_convert_to_binary"] = False
        flags["run_fusion"] = False
        flags["run_simple_mesh_renderer"] = False
    elif tag == "export":
        flags["run_generate_foreground_masks"] = False
        flags["run_precompute_resizes"] = False
        flags["run_precompute_resizes_foreground"] = False
        flags["run_depth_estimation"] = False

    # Overwrite flag file
    sorted_flags = collections.OrderedDict(sorted(flags.items()))
    dep_util.write_flagfile(flagfile_fn, sorted_flags)


def get_calibrated_rig_json(parent):
    """Finds calibrated rig in the project.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.

    Returns:
        str: Name of the calibrated rig (assumes the rig contains "_calibrated.json").
    """
    has_log_reader = "log_reader" in dir(parent)
    ps = dep_util.get_files_ext(parent.path_rigs, "json", "calibrated")
    if len(ps) == 0:
        if has_log_reader:
            parent.log_reader.log_warning(f"No rig files found in {parent.path_rigs}")
        return ""
    if len(ps) > 1:
        ps_str = "\n".join(ps)
        if has_log_reader:
            parent.log_reader.log_warning(
                f"Too many rig files found in {parent.path_rigs}:\n{ps_str}"
            )
        return ""
    return ps[0]


def update_run_button_text(parent, btn):
    """Updates the text of the Run button depending on the existance of a process
    running on the cloud
    """
    text_run_btn = "Run"
    if is_cloud_running_process(parent):
        text_run_btn = "Re-attach"
    btn.setText(text_run_btn)


def update_buttons(parent, gb, ignore=None):
    """Enables buttons and dropdowns according to whether or not data is present on the specified tab.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
        gb (QtWidgets.QGroupBox): Group box for the tab.
        ignore (list[QtWidgets.QGroupBox], optional): Buttons to not update.

    Returns:
        tuple[bool, bool, bool]: Whether or not the UI is currently running a process and if it
            has all its dropdowns.
    """
    if not ignore:
        ignore = []

    has_all_dropdowns = True
    for dd in gb.findChildren(QtWidgets.QComboBox):
        if not dd.currentText() and dd not in ignore:
            has_all_dropdowns = False
            break

    has_all_values = True
    for v in gb.findChildren(QtWidgets.QLineEdit):
        if v.objectName() and not v.text() and v not in ignore:
            has_all_values = False
            break

    is_running = parent.log_reader.is_running()
    for btn in gb.findChildren(QtWidgets.QPushButton):
        btn_name = btn.objectName()
        if btn in ignore:
            continue
        if btn_name.endswith("_run"):
            btn.setEnabled(not is_running and has_all_dropdowns and has_all_values)
        elif btn_name.endswith("_cancel"):
            btn.setEnabled(is_running)
        elif btn_name.endswith("_threshs"):
            btn.setEnabled(not is_running and has_all_dropdowns)
        elif btn_name.endswith("_view"):
            btn.setEnabled(not is_running)
        elif btn_name.endswith("_download_meshes"):
            btn.setEnabled(not is_running)
    return is_running, has_all_dropdowns, is_running


def on_changed_dropdown(parent, gb, dd):
    """Callback event handler for changed dropdown on the specified tab.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
        gb (QtWidgets.QGroupBox): Group box for the tab.
        dd (QtWidgets.QComboBox): Dropdown UI element.
    """
    if not parent.is_refreshing_data:
        name = dd.objectName()
        if not name.endswith(
            "_farm_ec2"
        ):  # farm_ec2 dropdowns are not used in flagfile
            parent.update_flagfile(parent.flagfile_fn)

        # Check if we need to update the threshold image
        if name.endswith(("_camera", "_frame_bg", "_first")):
            # Check if we are already in a threshold tab, else default to color variance
            tag = parent.tag
            tab_widget = getattr(parent.dlg, f"w_{tag}_preview", None)
            tab_idx = tab_widget.currentIndex()
            if tab_widget.widget(tab_idx).objectName().endswith("_fg_mask"):
                type = type_fg_mask
            else:
                type = type_color_var

            if "run_thresholds" in dir(parent):
                parent.run_thresholds(type)


def on_changed_line_edit(parent, gb, le):
    """Callback event handler for changed line edit on the specified tab.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
        gb (QtWidgets.QGroupBox): Group box for the tab.
        le (_): Ignore
    """
    if not parent.is_refreshing_data:
        parent.update_buttons(gb)
        parent.update_flagfile(parent.flagfile_fn)


def setup_groupbox(gb, callbacks):
    """Sets up callbacks for any groupboxes on the specified tab.

    Args:
        gb (QtWidgets.QGroupBox): Group box for the tab.
        callbacks (dict[QtWidgets.QGroupBox, func : QEvent -> _]): Callbacks for the UI elements.
    """
    if gb.isCheckable() and gb in callbacks:
        gb.toggled.connect(callbacks[gb])


def setup_checkboxes(gb, callbacks):
    """Sets up callbacks for any checkboxes on the specified tab.

    Args:
        gb (QtWidgets.QGroupBox): Group box for the tab.
        callbacks (dict[QtWidgets.QGroupBox, func : QEvent -> _]): Callbacks for the UI elements.
    """
    for cb in gb.findChildren(QtWidgets.QCheckBox):
        if cb in callbacks:
            cb.stateChanged.connect(callbacks[cb])


def setup_dropdowns(parent, gb):
    """Sets up callbacks for any dropdowns on the specified tab.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
        gb (QtWidgets.QComboBox): Group box for the tab.
    """
    if "on_changed_dropdown" in dir(parent):
        for dd in gb.findChildren(QtWidgets.QComboBox):
            dd.currentTextChanged.connect(
                lambda state, y=gb, z=dd: parent.on_changed_dropdown(y, z)
            )
            dd.activated.connect(
                lambda state, y=gb, z=dd: parent.on_changed_dropdown(y, z)
            )


def setup_lineedits(parent, gb):
    """Sets up callbacks for any line edits on the specified tab.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
        gb (QtWidgets.QGroupBox): Group box for the tab.
    """
    if "on_changed_line_edit" in dir(parent):
        for le in gb.findChildren(QtWidgets.QLineEdit):
            le.textChanged.connect(
                lambda state, y=gb, z=le: parent.on_changed_line_edit(y, z)
            )


def setup_buttons(parent, gb, callbacks):
    """Sets up callbacks for any buttons on the specified tab.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
        gb (QtWidgets.QGroupBox): Group box for the tab.
        callbacks (dict[QtWidgets.QPushButton, func : QEvent -> _]): Callbacks for the UI elements.
    """
    for btn in gb.findChildren(QtWidgets.QPushButton):
        if btn in callbacks:
            callback = callbacks[btn]
        else:
            name = btn.objectName()
            callback = None
            if name.endswith("_refresh"):
                callback = parent.refresh
            elif name.endswith("_run"):
                callback = parent.run_process
            elif name.endswith("_cancel"):
                callback = parent.cancel_process
            elif name.endswith("_threshs"):
                callback = parent.run_thresholds
            elif name.endswith("_logs"):
                callback = parent.get_logs
            else:
                parent.log_reader.log_error(f"Cannot setup button {name}")

        if callback:
            btn.clicked.connect(callback)


def on_changed_preview(parent):
    """Callback event handler for changed image previews on the specified tab.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
    """
    tag = parent.tag
    tab_widget = getattr(parent.dlg, f"w_{tag}_preview", None)
    tab_idx = tab_widget.currentIndex()
    tab_name = tab_widget.widget(tab_idx).objectName()
    if "_threshs_" in tab_name:
        if tab_name.endswith("_fg_mask"):
            type = type_fg_mask
        else:
            type = type_color_var

        if not parent.is_refreshing_data:
            parent.run_thresholds(type)


def setup_preview(parent):
    """Creates preview window in the UI and connects a callback on the specified tab.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
    """
    tag = parent.tag
    dlg = parent.dlg
    btn_log_clear = getattr(dlg, f"btn_{tag}_log_clear", None)
    text_log = getattr(dlg, f"text_{tag}_log", None)
    preview = getattr(dlg, f"w_{tag}_preview", None)
    btn_log_clear.clicked.connect(lambda: text_log.clear())
    preview.setCurrentIndex(0)

    if "on_changed_preview" in dir(parent):
        preview.currentChanged.connect(parent.on_changed_preview)


def setup_data(parent, callbacks=None):
    """Sets up callbacks and initial UI element statuses on the specified tab.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
        callbacks (dict[QtWidgets.QGroupBox, func : QEvent -> _]): Callbacks for the UI elements.
    """
    tag = parent.tag
    dlg = parent.dlg
    tab = getattr(dlg, f"t_{tag}", None)

    if not callbacks:
        callbacks = {}

    for gb in tab.findChildren(QtWidgets.QGroupBox):
        setup_groupbox(gb, callbacks)
        setup_checkboxes(gb, callbacks)
        setup_dropdowns(parent, gb)
        setup_lineedits(parent, gb)
        setup_buttons(parent, gb, callbacks)

    # Preview tabs
    setup_preview(parent)


def update_noise_detail(parent, noise, detail):
    """Updates noise/detail thresholds interaction on the specified tab.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
        noise (float): Noise threshold.
        detail (float): Detail threshold.
    """
    # Modify flagfile
    parent.update_data_or_flags(
        parent.flagfile_fn, flagfile_from_data=True, switch_to_flag_tab=False
    )

    # Update flagfile edit window
    parent.update_flagfile_edit(parent.flagfile_fn, switch_to_flag_tab=False)


def update_fg_masks_thresholds(parent, blur, closing, thresh):
    """Updates thresholds and display for the foreground masking on the specified tab.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
        blur (int, optional): Gaussian blur radius.
        closing (int, optional): Closure (for sealing holes).
        thresh (int, optional): Threshold applied to segment foreground and background
    """
    # Modify flagfile
    parent.update_data_or_flags(
        parent.flagfile_fn, flagfile_from_data=True, switch_to_flag_tab=False
    )

    # Update flagfile edit window
    parent.update_flagfile_edit(parent.flagfile_fn, switch_to_flag_tab=False)


def log_missing_image(parent, path_color, cam_id, frame):
    """Prints a warning if an image cannot be located.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
        path_color (str): Path to the directory with color images.
        cam_id (str): Name of the camera.
        frame (str): Name of the frame (0-padded, six digits).
    """
    parent.log_reader.log_warning(f"Cannot find frame {cam_id}/{frame} in {path_color}")


def update_thresholds_color_variance(parent, path_color, labels=None):
    """Updates the displayed thresholds for color variance on the specified tab.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
        path_color (str): Path to the directory with color images.
        labels (list[str], optional): Labels used to filter UI elements to update.
    """
    labels = labels if labels is not None else ("_frame_bg", "_first")
    dlg = parent.dlg
    for dd in parent.dlg.findChildren(QtWidgets.QComboBox):
        name = dd.objectName()
        if name.endswith(labels):
            frame = dd.currentText()
        elif name.endswith("_camera"):
            cam_id = dd.currentText()
    image_path = dep_util.get_level_image_path(path_color, cam_id, frame)
    if not image_path:
        log_missing_image(parent, path_color, cam_id, frame)
        return

    tag = parent.tag
    w_image = getattr(dlg, f"w_{tag}_threshs_image_{type_color_var}", None)

    # Foreground masks are generated at the finest level of the pyramid
    res = max(config.WIDTHS)
    w_image.color_var.set_image(image_path, res)

    noise = float(parent.slider_noise.get_label_text())
    detail = float(parent.slider_detail.get_label_text())
    project = parent.parent.path_project
    fn = dep_util.remove_prefix(image_path, f"{project}/")
    getattr(dlg, f"label_{tag}_threshs_filename_{type_color_var}", None).setText(fn)

    # Force update
    w_image.update_thresholds(noise=noise, detail=detail)


def update_thresholds_fg_mask(parent, paths_color):
    """Updates thresholds and display for the foreground masking using values from UI
    on the specified tab."

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
        paths_color (list[str]): Paths to the directory with color images.
    """
    dlg = parent.dlg
    frames = [None] * 2
    for dd in parent.dlg.findChildren(QtWidgets.QComboBox):
        name = dd.objectName()
        if name.endswith("_frame_bg"):
            frames[0] = dd.currentText()
        elif name.endswith("_first"):
            frames[1] = dd.currentText()
        elif name.endswith("_camera"):
            cam_id = dd.currentText()

    bg_image_path = dep_util.get_level_image_path(paths_color[0], cam_id, frames[0])
    if not bg_image_path:
        log_missing_image(parent, paths_color[0], cam_id, frames[0])
        return

    fg_image_path = dep_util.get_level_image_path(paths_color[1], cam_id, frames[1])
    if not fg_image_path:
        log_missing_image(parent, paths_color[1], cam_id, frames[1])
        return

    tag = parent.tag
    w_image = getattr(dlg, f"w_{tag}_threshs_image_{type_fg_mask}", None)

    # Foreground masks are generated at the finest level of the pyramid
    res = max(config.WIDTHS)
    w_image.fg_mask.set_images(bg_image_path, fg_image_path, res)

    blur = float(parent.slider_blur.get_label_text())
    closing = float(parent.slider_closing.get_label_text())
    thresh = float(parent.slider_thresh.get_label_text())

    project = parent.parent.path_project
    fn_bg = dep_util.remove_prefix(bg_image_path, f"{project}/")
    fn_fg = dep_util.remove_prefix(fg_image_path, f"{project}/")
    getattr(dlg, f"label_{tag}_threshs_filename_{type_fg_mask}", None).setText(
        f"{fn_bg} vs {fn_fg}"
    )

    # Force update
    w_image.update_thresholds(blur=blur, closing=closing, thresh=thresh)


def run_thresholds_after_wait(parent, type):
    """Computes the threshold and displays after a delay on the specified tab.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
        type (Union[ColorVariance, ForegroundMask]): Instance where thresholds
            can be run.
    """
    # Apply flag file values in case it had unsaved changes
    parent.save_flag_file()

    tag = parent.tag
    dlg = parent.dlg
    label = getattr(dlg, f"label_{tag}_threshs_tooltip_{type}", None)
    label.setToolTip(parent.threshs_tooltip)
    getattr(dlg, f"w_{tag}_threshs_image_{type}", None).set_zoom_level(0)

    if type == type_color_var:
        parent.setup_thresholds_color_variance()
        parent.update_thresholds_color_variance()
    elif type == type_fg_mask:
        parent.setup_thresholds_fg_masks()
        parent.update_thresholds_fg_mask()


def run_thresholds(parent, type):
    """Runs thresholding based on values in the UI and update UI display on the specified tab.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
        type (Union[ColorVariance, ForegroundMask]): Instance where thresholds are run.
    """
    tag = parent.tag
    tab_widget = getattr(parent.dlg, f"w_{tag}_preview", None)
    dep_util.switch_tab(tab_widget, f"_threshs_{type}")

    # HACK: if we try to draw on a widget too quickly after switching tabs the resulting image
    # does not span all the way to the width of the widget. We can wait a few milliseconds to
    # let the UI "settle"
    parent.timer = QtCore.QTimer(parent.parent)
    parent.timer.timeout.connect(lambda: parent.run_thresholds_after_wait(type))
    parent.timer.setSingleShot(True)
    parent.timer.start(10)  # 10ms


def output_has_images(output_dirs):
    """Whether or not outputs already have results.

    Args:
        output_dirs (list[str]): List of directories where outputs will be saved.

    Returns:
        bool: Whether or not the output directories all have at least one valid file.
    """
    for d in output_dirs:
        if dep_util.get_first_file_path(d):
            return True
    return False


def run_process_check_existing_output(parent, gb, app_name, flagfile_fn, p_id):
    """Run terminal process and raise on failure.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
        gb (QtWidgets.QGroupBox): Group box for the tab.
        app_name (str): Name of the binary being executed.
        flagfile_fn (str): Name of the flagfile.
        p_id (str): PID name of the process to be run.
    """
    tag = parent.tag
    cb_recompute = getattr(parent.dlg, f"cb_{tag}_recompute", None)
    if cb_recompute is not None:
        needs_rename = cb_recompute.isChecked()
        if needs_rename:
            # Rename current output directories using timestamp and create new empty ones
            ts = dep_util.get_timestamp()
            for d in parent.output_dirs:
                if not os.path.isdir(d):
                    continue
                d_dst = f"{d}_{ts}"
                parent.log_reader.log_notice(
                    f"Saving copy of {d} to {d_dst} before re-computing"
                )
                shutil.move(d, d_dst)
                os.makedirs(d, exist_ok=True)
        run_process(parent, gb, app_name, flagfile_fn, p_id, not needs_rename)


def start_process(parent, cmd, gb, p_id, run_silently=False):
    """Runs a terminal process and disables UI element interaction.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
        cmd (str): Command to run in the terminal.
        gb (QtWidgets.QGroupBox): Group box for the tab.
        p_id (str): PID name of the process being started.
    """
    if not run_silently:
        parent.log_reader.log(f"CMD: {cmd}")
    parent.log_reader.gb = gb
    parent.log_reader.setup_process(p_id)
    parent.log_reader.start_process(p_id, cmd)

    # Switch to log tab
    tag = parent.tag
    tab_widget = getattr(parent.dlg, f"w_{tag}_preview", None)
    dep_util.switch_tab(tab_widget, "_log")

    # Disable UI elements
    parent.switch_ui_elements_for_processing(False)


def run_process(
    parent, gb, app_name=None, flagfile_fn=None, p_id="run", overwrite=False
):
    """Runs an application on the terminal, using the associated flagfile.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
        gb (QtWidgets.QGroupBox): Group box for the tab.
        app_name (str, optional): Name of the binary being executed.
        flagfile_fn (str, optional): Name of the flagfile to supply to the binary. this
            will default to the flagfile associated with the binary if unspecified.
        p_id (str, optional): PID name of the process being started.
        overwrite (bool, optional): Whether or not to overwrite the local flagfile on disk.
    """
    # Apply flag file values in case it had unsaved changes
    parent.save_flag_file()

    if not app_name:
        app_name = parent.app_name
    is_py_script = app_name.endswith(".py")

    dir = scripts_dir if is_py_script else dep_bin_dir
    app_path = os.path.join(dir, app_name)
    if not os.path.isfile(app_path):
        parent.log_reader.log_warning(f"App doesn't exist: {app_path}")
        return

    if not flagfile_fn:
        flagfile_fn = parent.flagfile_fn

    if output_has_images(parent.output_dirs) and not overwrite:
        run_process_check_existing_output(parent, gb, app_name, flagfile_fn, p_id)
        return

    cmd = f'{app_path} --flagfile="{flagfile_fn}"'
    if is_py_script:
        cmd = f"python3.7 -u {cmd}"

    start_process(parent, cmd, gb, p_id)


def update_thresholds(parent, gb, type):
    """Updates the displayed thresholds for either color variance or foreground masks.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
        gb (QtWidgets.QGroupBox): Group box for the tab.
        type (Union[ColorVariance, ForegroundMask]): Instance where thresholds
            can be run.
    """
    if type == type_color_var:
        noise = parent.slider_noise.get_label_text()
        detail = parent.slider_detail.get_label_text()
        parent.update_noise_detail(noise, detail)
    elif type == type_fg_mask:
        blur = parent.slider_blur.get_label_text()
        closing = parent.slider_closing.get_label_text()
        thresh = parent.slider_thresh.get_label_text()
        parent.update_fg_masks_thresholds(blur, closing, thresh)

    # Update buttons
    parent.update_buttons(gb)


def on_state_changed_partial_360(parent):
    """Callback event handler for changed "partial coverage" checkbox on the specified tab.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
    """
    if not parent.is_refreshing_data:
        parent.update_flagfile(parent.flagfile_fn)


def on_state_changed_recompute(parent):
    """Callback event handler for changed "recompute" checkbox on the specified tab.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
    """
    if not parent.is_refreshing_data:
        parent.update_flagfile(parent.flagfile_fn)


def on_state_changed_use_bg(parent, gb):
    """Callback event handler for changed "use background" checkbox on the specified tab.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
        gb (QtWidgets.QGroupBox): Group box for the tab.
    """
    if not parent.is_refreshing_data:
        parent.update_buttons(gb)
        parent.update_flagfile(parent.flagfile_fn)


def on_state_changed_farm(parent, state):
    """Callback event handler for changed "AWS" checkbox on the specified tab.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
        state (str): Identifier of the callback state.
    """
    parent.is_farm = state > 0
    if not parent.is_refreshing_data:
        if "update_frame_range_dropdowns" in dir(parent):
            parent.update_frame_range_dropdowns()
        if "update_run_button_text" in dir(parent):
            parent.update_run_button_text()
        parent.update_flagfile(parent.flagfile_fn)


def setup_thresholds(parent, types):
    """Sets necessary thresholds apps on the specified tab.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
        type (Union[ColorVariance, ForegroundMask]): Instance where thresholds
            can be run.
    """
    tag = parent.tag
    dlg = parent.dlg
    for attr in threshold_sliders:
        type, printed, num, max, default = threshold_sliders[attr]
        if type in types:
            name = getattr(dlg, f"label_{tag}_threshs_{num}_name_{type}", None)
            hs = getattr(dlg, f"hs_{tag}_threshs_{num}_{type}", None)
            label = getattr(dlg, f"label_{tag}_threshs_{num}_{type}", None)
            slider = SliderWidget(type, attr, name, printed, hs, label, max, default)
            setattr(parent, f"slider_{attr}", slider)

    for type in types:
        w_image = getattr(dlg, f"w_{tag}_threshs_image_{type}", None)
        w_viewer = getattr(dlg, f"w_{tag}_image_viewer_{type}", None)
        w_image.set_image_viewer(w_viewer)


def setup_thresholds_color_variance(parent):
    """Sets color variance thresholds apps on the specified tab.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
    """
    for slider in [parent.slider_noise, parent.slider_detail]:
        slider.setup(callback=parent.on_changed_slider)


def setup_thresholds_fg_masks(parent):
    """Sets up the default thresholds on foreground masks on the specified tab.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
    """
    for slider in [parent.slider_blur, parent.slider_closing, parent.slider_thresh]:
        slider.setup(callback=parent.on_changed_slider)


def update_data_from_flags(
    parent,
    flags,
    dropdowns=None,
    values=None,
    checkboxes=None,
    labels=None,
    prefix=None,
):
    """Updates UI elements from the flags on the specified tab.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
        flags (dict[str, _]): Flags and their corresponding values.
        dropdowns (list[QtWidgets.QComboBox], optional): Dropdowns in the tab.
        values (dict[QtWidgets.QLineEdit, _], optional): Map from UI elements to values.
        checkboxes (list[QtWidgets.QCheckBox], optional): Checkboxes in the tab.
        labels (list[QtWidgets.QLabel], optional): Labels in the tab.
        prefix (str, optional): Prefix to append to values in the population of tab values.
    """
    if not dropdowns:
        dropdowns = []
    if not values:
        values = []
    if not checkboxes:
        checkboxes = []
    if not labels:
        labels = []

    flagfile = parent.flagfile_basename
    if not prefix:
        prefix = f"{parent.parent.path_project}/"

    for key, dd in dropdowns:
        error = dep_util.update_qt_dropdown_from_flags(flags, key, prefix, dd)
        if error:
            parent.log_reader.log_warning(f"{flagfile}: {error}")

    for key, val in values:
        dep_util.update_qt_lineedit_from_flags(flags, key, prefix, val)

    for key, cb in checkboxes:
        error = dep_util.update_qt_checkbox_from_flags(flags, key, prefix, cb)
        if error:
            parent.log_reader.log_warning(f"{flagfile}: {error}")

    for key, label in labels:
        dep_util.update_qt_label_from_flags(flags, key, prefix, label)


def get_notation(parent, attr):
    """Gets standard format for attribute on the specified tab.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
        attr (str): Name of the attribute.

    Returns:
        str: Format string corresponding to the display notation.
    """
    if attr in ["noise", "detail", "thresh"]:
        notation = "{:.3e}"
    elif attr in ["blur", "closing"]:
        notation = "{:d}"
    else:
        parent.log_reader.log_error(f"Invalid slider attr: {attr}")
    return notation


def on_changed_slider(parent, slider, value):
    """Callback event handler for changes to a slider UI element on the specified tab.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
        slider (QtWidgets.QSlider): Slider UI element.
        value (int/float): Value of the slider element.
    """
    type = slider.type
    attr = slider.attr
    notation = get_notation(parent, attr)
    if notation == "{:d}":
        value = int(value)
    slider.set_label(value, notation)
    tag = parent.tag
    w_image = getattr(parent.dlg, f"w_{tag}_threshs_image_{type}", None)
    if w_image.update_thresholds(**{attr: value}):
        # Update thresholds in flagfile
        parent.update_thresholds(type)


def initialize_farm_groupbox(parent):
    """Sets up the farm render box for the project path, i.e. AWS is displayed if
    rendering on an S3 project path and LAN if on a SMB drive.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
    """
    tag = parent.tag
    dlg = parent.dlg
    gb_farm = getattr(dlg, f"gb_{tag}_farm", None)
    grid_s3 = getattr(dlg, f"w_{tag}_farm_s3", None)
    grid_lan = getattr(dlg, f"w_{tag}_farm_lan", None)
    parent.is_aws = parent.parent.is_aws
    parent.is_lan = parent.parent.is_lan
    if not parent.is_aws and not parent.is_lan:
        gb_farm.hide()
    elif parent.is_aws:
        grid_lan.hide()
    elif parent.is_lan:
        grid_s3.hide()

    parent.ec2_instance_types_cpu = []
    parent.ec2_instance_types_gpu = []
    if parent.is_aws:
        # Get list of EC2 instances
        client = parent.parent.aws_util.session.client("ec2")
        ts = client._service_model.shape_for("InstanceType").enum
        ts = [t for t in ts if not t.startswith(config.EC2_UNSUPPORTED_TYPES)]
        parent.ec2_instance_types_cpu = [t for t in ts if t.startswith("c")]
        parent.ec2_instance_types_gpu = [t for t in ts if t.startswith(("p", "g"))]

    # Check if flagfile has farm attributes
    flagfile_fn = os.path.join(parent.path_flags, parent.flagfile_basename)
    flags = get_flags_from_flagfile(flagfile_fn)
    parent.is_farm = False
    for farm_attr in ["master", "workers", "cloud"]:
        if flags[farm_attr] != "":
            parent.is_farm = True
            break
    call_force_refreshing(parent, gb_farm.setChecked, parent.is_farm)


def show_resources(parent):
    """Displays resources used in the container.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.

    Returns:
        str: Resources (memory and CPU) being used.
    """
    return run_command("top -b -n 1")


def show_aws_resources(parent):
    """Displays resources used across the AWS cluster.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.

    Returns:
        src: Resources (memory and CPU) being used in the farm.
    """
    return "\n".join(parent.parent.aws_util.ec2_get_running_instances())


def get_aws_workers():
    """Get names of the instances in the AWS farm.

    Returns:
        list[str]: Instances IDs of EC2 instances in the farm.
    """
    with open(config.DOCKER_AWS_WORKERS) as f:
        lines = f.readlines()
    return lines


def set_aws_workers(workers):
    """Sets names of the instances in the AWS farm.

    Args:
        workers (list[str]): Instance IDs of EC2 instances in the farm.
    """
    with open(config.DOCKER_AWS_WORKERS, "w") as f:
        f.writelines([worker.id for worker in workers])


def popup_ec2_dashboard_url(parent):
    """Displays a link to the EC2 dashboard in a popup on the specified tab.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
    """
    region = parent.parent.aws_util.region_name
    prefix = f"{region}." if region else ""
    url = f"https://{prefix}console.aws.amazon.com/ec2#Instances"
    dep_util.popup_message(parent.parent, url, "EC2 Dashboard")


def popup_logs_locations(parent):
    """Displays the path to local logs in a popup on the specified tab.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
    """
    logs = [parent.log_reader.log_file]
    logs_workers = glob.iglob(f"{parent.path_logs}/Worker-*", recursive=False)
    for log in logs_workers:
        ts_log = datetime.datetime.fromtimestamp(os.path.getmtime(log))
        if ts_log > parent.parent.ts_start:
            logs.append(log)
    project = parent.parent.path_project
    logs = [dep_util.remove_prefix(l, f"{project}/") for l in logs]

    dep_util.popup_message(parent.parent, "\n".join(logs), "Logs")


def run_process_aws(parent, gb, p_id=None):
    """Runs the process to create a cluster on AWS and perform the render job.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
        gb (QtWidgets.QGroupBox): Group box for the tab.
    """
    flags = {}
    flags["key_dir"] = os.path.dirname(parent.path_aws_key_fn)
    flags["key_name"] = os.path.splitext(os.path.basename(parent.path_aws_key_fn))[0]
    flags["csv_path"] = parent.path_aws_credentials
    flags["ec2_file"] = parent.path_aws_ip_file
    spin_num_workers = getattr(parent.dlg, f"spin_{parent.tag}_farm_num_workers", None)
    flags["cluster_size"] = int(spin_num_workers.value())
    flags["region"] = parent.parent.aws_util.region_name
    dd_ec2 = getattr(parent.dlg, f"dd_{parent.tag}_farm_ec2", None)
    flags["instance_type"] = dd_ec2.currentText()
    flags["tag"] = parent.tag

    # Overwrite flag file
    app_name = parent.app_aws_create
    flagfile_fn = os.path.join(parent.path_flags, parent.app_name_to_flagfile[app_name])
    dep_util.write_flagfile(flagfile_fn, flags)

    if not p_id:
        p_id = "run_aws_create"
    run_process(parent, gb, app_name, flagfile_fn, p_id)


def on_download_meshes(parent, gb):
    """Downloads meshes from S3. This is a no-op if not an S3 project.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
        gb (QtWidgets.QGroupBox): Group box for the tab.
    """
    if not parent.parent.is_aws:
        return

    subdir = image_type_paths["video_bin"]
    flags = {}
    flags["csv_path"] = parent.path_aws_credentials
    flags["local_dir"] = os.path.join(config.DOCKER_INPUT_ROOT, subdir)
    flags["s3_dir"] = os.path.join(parent.parent.ui_flags.project_root, subdir)
    flags["verbose"] = parent.parent.ui_flags.verbose
    flags["watch"] = True  # NOTE: watchdog sometimes gets stale file handles in Windows

    # Overwrite flag file
    app_name = parent.app_aws_download_meshes
    flagfile_fn = os.path.join(parent.path_flags, parent.app_name_to_flagfile[app_name])
    dep_util.write_flagfile(flagfile_fn, flags)

    p_id = "download_meshes"
    run_process(parent, gb, app_name, flagfile_fn, p_id)


def on_terminate_cluster(parent, gb):
    """Terminates a running AWS cluster. This is a no-op if no cluster is up.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.
        gb (QtWidgets.QGroupBox): Group box for the tab.
    """
    flags = {}
    flags["key_dir"] = os.path.dirname(parent.path_aws_key_fn)
    flags["key_name"] = os.path.splitext(os.path.basename(parent.path_aws_key_fn))[0]
    flags["csv_path"] = parent.path_aws_credentials
    flags["ec2_file"] = parent.path_aws_ip_file
    flags["region"] = parent.parent.aws_util.region_name

    # Overwrite flag file
    flagfile_fn = os.path.join(
        parent.path_flags, parent.app_name_to_flagfile[parent.app_aws_clean]
    )
    dep_util.write_flagfile(flagfile_fn, flags)

    app_name = parent.app_aws_clean
    p_id = "terminate_cluster"
    run_process(parent, gb, app_name, flagfile_fn, p_id)


def get_workers(parent):
    """Finds workers in a LAN farm.

    Args:
        parent (App(QDialog)): Object corresponding to the parent UI element.

    Returns:
        list[str]: IPs of workers in the local farm.
    """
    if parent.parent.ui_flags.master == config.LOCALHOST:
        return []
    else:
        return parent.lan.scan()


def call_force_refreshing(parent, fun, *args):
    already_refreshing = parent.is_refreshing_data
    if not already_refreshing:
        parent.is_refreshing_data = True
    fun(*args)
    if not already_refreshing:
        parent.is_refreshing_data = False
