/*
ToolsMenu - Lets you add tools to the Workbench Tools menu
Copyright (C) 2015  Kim Fastrup Larsen

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

The author can be contacted on <kimflarsen@hotmail.com>
*/

#define INTUI_V36_NAMES_ONLY

#include <dos/dosextens.h>
#include <dos/dostags.h>
#include <exec/memory.h>
#include <intuition/gadgetclass.h>
#include <intuition/intuition.h>
#include <intuition/intuitionbase.h>
#include <libraries/gadtools.h>
#include <workbench/startup.h>

#include <clib/alib_protos.h>
#include <clib/commodities_protos.h>
#include <clib/dos_protos.h>
#include <clib/exec_protos.h>
#include <clib/gadtools_protos.h>
#include <clib/icon_protos.h>
#include <clib/intuition_protos.h>
#include <clib/locale_protos.h>
#include <clib/utility_protos.h>
#include <clib/wb_protos.h>

#include <pragmas/commodities_pragmas.h>
#include <pragmas/dos_pragmas.h>
#include <pragmas/exec_pragmas.h>
#include <pragmas/gadtools_pragmas.h>
#include <pragmas/icon_pragmas.h>
#include <pragmas/intuition_pragmas.h>
#include <pragmas/locale_pragmas.h>
#include <pragmas/utility_pragmas.h>
#include <pragmas/wb_pragmas.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "catalog.h"
#include "toolrun.h"
#include "util.h"

#define APP_NAME "ToolsMenu"
#define VERSION "1.4"
#define DATE "23.4.2015"
#define COPYRIGHT "Copyright © 2015 Kim Fastrup Larsen"
#define TEMPLATE "CX_PRIORITY/N/K,CX_POPKEY/K,CX_POPUP/K"
#define CATALOG "toolsmenu.catalog"
#define TITLE APP_NAME " " VERSION
#define ERRORMSG_MAX_LENGTH 80
#define NOT_SET 0xffffffffU
#define DEF_BEGIN '\xab' /* Left double angle quote */
#define DEF_END '\xbb' /* Right double angle quote */
#define MAX_PATH_LENGTH 127

/* We will maintain a list of Tools as the main global state of this
   commodity. The name of each Tool (ie. menu item) will be stored in
   node.ln_Name. */
typedef struct {
    struct Node node;
    char *path;
    struct AppMenuItem *menu_item;
} Tool;

extern struct Library *SysBase;
extern struct Library *DOSBase;

struct Library *CxBase;
struct Library *GadToolsBase;
struct Library *IconBase;
struct Library *IntuitionBase;
struct Library *LocaleBase;
struct Library *UtilityBase;
struct Library *WorkbenchBase;

/* Version string */
char *version = "\0$VER: " TITLE " (" DATE ")";

/* Data */
static struct IntuiText itexts[3] = {
    { 0, 0, JAM2, 0, 0, NULL, NULL, itexts + 1 },
    { 0, 0, JAM2, 0, 0, NULL, NULL, itexts + 2 },
    { 0, 0, JAM2, 0, 0, NULL, NULL, NULL }
};
static WORD zoom[4];

/* Settings */
static int cx_priority;
static BOOL cx_popup = TRUE;
static char *cx_popkey = "control alt t";

/* Resources */
static UBYTE **tool_types;
static struct RDArgs *rdargs;
static struct MsgPort *broker_port;
static CxObj *broker;
static struct MsgPort *wb_port;
static struct Menu *menus;
static struct Screen *screen;
static struct DrawInfo *draw_info;
static APTR visual_info;
static struct Gadget *gadgets;
static struct Window *window;
static struct AppWindow *app_window;
static struct Window *about_req;

/* Global state */
static struct List *tools;
static ULONG tools_size;
static Tool *current;
static BOOL enabled = TRUE;

/* Other globals */
static BOOL started_from_wb;
static union {
    char *name;
    struct WBArg *wb_arg;
} start_arg;
static ULONG all_signals = SIGBREAKF_CTRL_C | SIGBREAKF_CTRL_F;
static ULONG reply_signal, broker_signal, wb_signal, window_signal,
 about_req_signal;
static ULONG left_edge, top_edge = NOT_SET;
static struct Gadget *lv_gad, *sel_gad, *new_gad, *del_gad, *path_gad;
static struct TextAttr *font;
static WORD fontw, fonth;
static WORD padx, pady, labh, strh, buth;
static WORD listw, listh, toolw;

static void v33_message(char *s)
{
    static struct TextAttr topaz60 = {
        "topaz.font", 9, FS_NORMAL, FPF_DESIGNED | FPF_ROMFONT
    };
    static struct IntuiText text = {
        0, 1, JAM1, 15, 6, &topaz60, NULL, NULL
    };
    static struct IntuiText gadget_text = {
        AUTOFRONTPEN, AUTOBACKPEN, AUTODRAWMODE, AUTOLEFTEDGE,
        AUTOTOPEDGE, AUTOITEXTFONT, NULL, AUTONEXTTEXT
    };

    text.IText = s;
    gadget_text.IText = get_string(MSG_GAD_CONTINUE);
    AutoRequest(NULL, &text, NULL, &gadget_text, 0, 0,
     IntuiTextLength(&text) + 30 + 26, 9 + 35 + 13);
}

static void check_system_version(void)
{
    if (SysBase->lib_Version < 37 || DOSBase->lib_Version < 37) {
        if ((IntuitionBase = OpenLibrary("intuition.library", 33)) != NULL) {
            v33_message(get_string(MSG_ERR_KICKSTART));
            CloseLibrary(IntuitionBase);
        }
        exit(RETURN_FAIL);
    }
}

static void clean_up_about_req(void)
{
    FreeSysRequest(about_req);
    about_req = NULL;
    all_signals &= ~about_req_signal;
    about_req_signal = 0;
}

static void clean_up_gui(void)
{
    if (about_req != NULL)
        clean_up_about_req();
    if (app_window != NULL) {
        RemoveAppWindow(app_window);
        app_window = NULL;
    }
    if (window != NULL) {
        ClearMenuStrip(window);
        left_edge = window->LeftEdge;
        top_edge = window->TopEdge;
        CloseWindow(window);
        window = NULL;
        all_signals &= ~window_signal;
        window_signal = 0;
    }
    FreeGadgets(gadgets);
    gadgets = NULL;
    FreeVisualInfo(visual_info);
    visual_info = NULL;
    if (screen != NULL) {
        FreeScreenDrawInfo(screen, draw_info);
        draw_info = NULL;
        UnlockPubScreen(NULL, screen);
        screen = NULL;
    }
}

static void enable_tool(Tool *t)
{
    if (t->menu_item == NULL)
        t->menu_item = AddAppMenuItemA(0, (ULONG) t, t->node.ln_Name,
         wb_port, TAG_DONE);
}

static void disable_tool(Tool *t)
{
    if (t->menu_item != NULL) {
        RemoveAppMenuItem(t->menu_item);
        t->menu_item = NULL;
    }
}

static void free_tool(Tool *t)
{
    disable_tool(t);
    FreeVec(t->node.ln_Name);
    FreeVec(t->path);
    FreeMem(t, sizeof *t);
}

static void free_tools(void)
{
    for_all(tools, free_tool);
    FreeMem(tools, sizeof *tools);
}

static void delete_port(struct MsgPort *port)
{
    struct Message *msg;

    if (port != NULL) {
        while ((msg = GetMsg(port)) != NULL)
            ReplyMsg(msg);
        DeleteMsgPort(port);
    }
}

static void clean_up_broker(void)
{
    if (broker != NULL)
        DeleteCxObjAll(broker);
    delete_port(broker_port);
}

static void close_libraries(void)
{
    CloseLibrary(UtilityBase);
    CloseLibrary(WorkbenchBase);
    CloseLibrary(GadToolsBase);
    CloseLibrary(CxBase);
    CloseLibrary(IconBase);
    CloseLibrary(IntuitionBase);
}

/* Quit, unless we still have tools running. In case we do, clean up as much
   as possible (almost everything), then wait for the stragglers before
   REALLY quitting. */
static void clean_exit(int return_code)
{
    clean_up_gui();
    if (tools != NULL)
        free_tools();
    FreeMenus(menus);
    clean_up_broker();
    if (rdargs != NULL)
        FreeArgs(rdargs);
    if (tool_types != NULL)
        ArgArrayDone();
    /* clean_up_tool_runner() won't return until all tools have quit. */
    clean_up_tool_runner();
    delete_port(wb_port);
    close_libraries();
    clean_up_catalog();
    CloseLibrary(LocaleBase);
    exit(return_code);
}

static void message(char *s)
{
    static struct EasyStruct easy;

    if (s != NULL && IntuitionBase != NULL) {
        easy.es_StructSize = sizeof easy;
        easy.es_Title = APP_NAME;
        easy.es_TextFormat = s;
        easy.es_GadgetFormat = get_string(MSG_GAD_CONTINUE);
        EasyRequest(NULL, &easy, NULL);
    }
}

static void fail_s(ULONG id, const char *s)
{
    char buffer[ERRORMSG_MAX_LENGTH];
    char *msg;

    msg = get_string(id);
    if (s != NULL) {
        if (msg == NULL || strlen(msg) + strlen(s) > ERRORMSG_MAX_LENGTH)
            clean_exit(RETURN_FAIL);
        sprintf(buffer, msg, s);
        msg = buffer;
    }
    message(msg);
    clean_exit(RETURN_FAIL);
}

static void fail(ULONG id)
{
    fail_s(id, NULL);
}

static struct Library *open_library(STRPTR name)
{
    struct Library *base;

    if ((base = OpenLibrary(name, 37)) == NULL)
        fail_s(MSG_ERR_OPEN, name);
    return base;
}

static void open_libraries(void)
{
    IntuitionBase = open_library("intuition.library");
    IconBase = open_library("icon.library");
    CxBase = open_library("commodities.library");
    GadToolsBase = open_library("gadtools.library");
    WorkbenchBase = open_library("workbench.library");
    UtilityBase = open_library("utility.library");
}

static void get_wb_args(int argc, char **argv)
{
    started_from_wb = TRUE;
    start_arg.wb_arg = ((struct WBStartup *) argv)->sm_ArgList;
    if ((tool_types = ArgArrayInit(argc, argv)) == NULL)
        fail(MSG_ERR_ARG_ARRAY);
    cx_priority = ArgInt(tool_types, "CX_PRIORITY", cx_priority);
    cx_popup = Stricmp(ArgString(tool_types, "CX_POPUP",
     cx_popup ? "YES" : "NO"), "NO") != 0;
    cx_popkey = ArgString(tool_types, "CX_POPKEY", cx_popkey);
}

static void get_shell_args(int argc, char **argv)
{
    LONG args[3];

    started_from_wb = FALSE;
    start_arg.name = argv[0];
    args[0] = cx_priority;
    args[1] = (LONG) cx_popkey;
    args[2] = cx_popup ? (LONG) "YES" : (LONG) "NO";
    if ((rdargs = ReadArgs(TEMPLATE, args, NULL)) == NULL) {
        PrintFault(IoErr(), argv[0]);
        clean_exit(RETURN_ERROR);
    }
    cx_priority = args[0];
    cx_popkey = (STRPTR) args[1];
    cx_popup = Stricmp((char *) args[2], "YES") == 0;
}

static void create_port(struct MsgPort **port, ULONG *signal)
{
    if ((*port = CreateMsgPort()) == NULL)
        fail(MSG_ERR_MEMORY);
    all_signals |= (*signal = 1UL << (*port)->mp_SigBit);
}

static void set_up_broker(void)
{
    struct NewBroker new_broker;
    LONG error;
    CxObj *filter;

    create_port(&broker_port, &broker_signal);
    memset(&new_broker, 0, sizeof new_broker);
    new_broker.nb_Version = NB_VERSION;
    new_broker.nb_Name = APP_NAME;
    new_broker.nb_Title = TITLE;
    new_broker.nb_Descr = COPYRIGHT;
    new_broker.nb_Unique = NBU_UNIQUE | NBU_NOTIFY;
    new_broker.nb_Flags = COF_SHOW_HIDE;
    new_broker.nb_Pri = cx_priority;
    new_broker.nb_Port = broker_port;
    if ((broker = CxBroker(&new_broker, &error)) == NULL)
        if (error == CBERR_DUP)
            clean_exit(RETURN_OK);
        else
            fail(MSG_ERR_MEMORY);
    if ((filter = CxFilter(cx_popkey)) == NULL)
        fail(MSG_ERR_MEMORY);
    AttachCxObj(broker, filter);
    AttachCxObj(filter, CxSender(broker_port, 0));
    AttachCxObj(filter, CxTranslate(NULL));
    if ((error = CxObjError(filter)) != 0)
        fail((error == COERR_BADFILTER) ? MSG_ERR_HOTKEY : MSG_ERR_MEMORY);
    ActivateCxObj(broker, 1);
}

static void set_up_menus(void)
{
    static struct NewMenu nm[] = {
        { NM_TITLE, NULL, NULL, 0, 0, NULL },       /* Project          */
        {  NM_ITEM, NULL, "S",  0, 0, NULL },       /*  Save Menu Items */
        {  NM_ITEM, NM_BARLABEL, NULL, 0, 0, NULL },/*  --------------- */
        {  NM_ITEM, NULL, "H",  0, 0, NULL },       /*  Hide            */
        {  NM_ITEM, NULL, NULL,  0, 0, NULL },      /*  About...        */
        {  NM_ITEM, NULL, "Q",  0, 0, NULL },       /*  Quit            */
        {   NM_END, NULL, NULL, 0, 0, NULL }
    };

    nm[0].nm_Label = get_string(MSG_PROJECT_MENU);
    nm[1].nm_Label = get_string(MSG_PROJECT_SAVE);
    nm[3].nm_Label = get_string(MSG_PROJECT_HIDE);
    nm[4].nm_Label = get_string(MSG_PROJECT_ABOUT);
    nm[5].nm_Label = get_string(MSG_PROJECT_QUIT);
    if ((menus = CreateMenus(nm, TAG_DONE)) == NULL)
        fail(MSG_ERR_MEMORY);
}

static void set_up_tools(void)
{
    if ((tools = AllocMem(sizeof *tools, MEMF_PUBLIC)) == NULL)
        fail(MSG_ERR_MEMORY);
    NewList(tools);
}

static void set_up(int argc, char **argv)
{
    LocaleBase = OpenLibrary("locale.library", 38);
    set_up_catalog(CATALOG);
    open_libraries();
    create_port(&wb_port, &wb_signal);
    if ((reply_signal = set_up_tool_runner()) == 0)
        fail(MSG_ERR_MEMORY);
    all_signals |= reply_signal;
    if (argc == 0)
        get_wb_args(argc, argv);
    else
        get_shell_args(argc, argv);
    set_up_broker();
    if (IntuitionBase->lib_Version >= 39)
        zoom[0] = zoom[1] = ~0;
    set_up_menus();
    itexts[0].IText = get_string(MSG_INFO_1);
    itexts[1].IText = get_string(MSG_INFO_2);
    itexts[2].IText = get_string(MSG_INFO_3);
    set_up_tools();
}

static UWORD window_border_top(void)
{
    return (UWORD) (screen->WBorTop + screen->Font->ta_YSize + 1);
}

static void make_newgadget(struct NewGadget *ng, WORD left_edge,
 WORD top_edge, WORD width, WORD height, UBYTE *gadget_text, ULONG flags)
{
    ng->ng_LeftEdge = left_edge + screen->WBorLeft;
    ng->ng_TopEdge = top_edge + window_border_top();
    ng->ng_Width = width;
    ng->ng_Height = height;
    ng->ng_GadgetText = gadget_text;
    ng->ng_Flags = flags;
}

static BOOL create_gadgets(void)
{
    struct NewGadget ng;
    struct Gadget *g = CreateContext(&gadgets);
    WORD butw = listw / 2;
    WORD top;

    ng.ng_TextAttr = font;
    ng.ng_VisualInfo = visual_info;
    make_newgadget(&ng, padx, top = pady + labh, listw, listh,
     get_string(MSG_GAD_MENU_ITEMS), 0);
    g = lv_gad = CreateGadget(LISTVIEW_KIND, g, &ng,
     GTLV_Labels, tools,
     GTLV_ScrollWidth, fontw + 10,
     LAYOUTA_Spacing, 1,
     TAG_DONE);
    make_newgadget(&ng, padx, top += listh, listw, strh, NULL, 0);
    g = sel_gad = CreateGadget(STRING_KIND, g, &ng,
     GA_Disabled, TRUE,
     TAG_DONE);
    make_newgadget(&ng, padx, top += strh, butw, buth,
     get_string(MSG_GAD_NEW), 0);
    g = new_gad = CreateGadget(BUTTON_KIND, g, &ng,
     TAG_DONE);
    make_newgadget(&ng, padx + butw, top, butw, buth,
     get_string(MSG_GAD_DELETE), 0);
    g = del_gad = CreateGadget(BUTTON_KIND, g, &ng,
     GA_Disabled, TRUE,
     TAG_DONE);
    make_newgadget(&ng, 2 * padx + listw, 2 * (pady + labh), toolw, strh,
     get_string(MSG_GAD_TOOL), PLACETEXT_ABOVE);
    g = path_gad = CreateGadget(STRING_KIND, g, &ng,
     GTST_MaxChars, MAX_PATH_LENGTH,
     GA_Disabled, TRUE,
     TAG_DONE);
    return (BOOL) (g != NULL);
}

static BOOL open_window(void)
{
    static char window_title_buf[64];
    char *window_title = get_string(MSG_WINDOW_TITLE);

    if (strlen(window_title) + strlen(APP_NAME) + strlen(cx_popkey) > 64)
        window_title = APP_NAME;
    else {
        sprintf(window_title_buf, window_title, APP_NAME, cx_popkey);
        window_title = window_title_buf;
    }
    zoom[2] = 25 * fontw;
    zoom[3] = window_border_top();
    ScreenToFront(screen);
    window = OpenWindowTags(NULL,
     WA_Left, left_edge,
     WA_Top, top_edge,
     WA_InnerWidth, 3 * padx + listw + toolw,
     WA_InnerHeight, 2 * pady + labh + listh + strh + buth,
     WA_IDCMP, IDCMP_CLOSEWINDOW | IDCMP_MENUPICK | IDCMP_REFRESHWINDOW |
      LISTVIEWIDCMP | STRINGIDCMP | BUTTONIDCMP,
     WA_Flags, WFLG_DRAGBAR | WFLG_DEPTHGADGET | WFLG_CLOSEGADGET |
      WFLG_ACTIVATE | WFLG_SIMPLE_REFRESH,
     WA_PubScreen, screen,
     WA_Zoom, zoom,
     WA_Gadgets, gadgets,
     WA_Title, window_title,
     WA_AutoAdjust, TRUE,
     WA_NewLookMenus, TRUE,
     TAG_DONE);
    if (window == NULL)
        return FALSE;
    all_signals |= (window_signal = 1UL << window->UserPort->mp_SigBit);
    GT_RefreshWindow(window, NULL);
    return TRUE;
}

static void set_up_texts(void)
{
    int i;

    for (i = 0; i < sizeof itexts / sizeof *itexts; ++i) {
        itexts[i].FrontPen = draw_info->dri_Pens[TEXTPEN];
        itexts[i].BackPen = draw_info->dri_Pens[BACKGROUNDPEN];
        itexts[i].ITextFont = font;
        itexts[i].LeftEdge = (toolw - IntuiTextLength(&itexts[i])) / 2;
        itexts[i].TopEdge = i * (fonth + 1);
    }
}

static void render(void)
{
    PrintIText(window->RPort, itexts, window->BorderLeft + 2*padx + listw,
     window->BorderTop + 2 * (pady + labh) + strh + 2 * fonth);
}

static void set_up_gui(void)
{
    if ((screen = LockPubScreen(NULL)) == NULL)
        goto gui_failed;
    if ((draw_info = GetScreenDrawInfo(screen)) == NULL)
        goto gui_failed;
    font = screen->Font;
    fontw = draw_info->dri_Font->tf_XSize;
    fonth = font->ta_YSize;
    padx = fontw;
    pady = fonth / 2;
    labh = fonth + 4;
    strh = buth = fonth + 6;
    listw = 26 * fontw + 12;
    listh = (fonth + 1) * 6 + 4;
    toolw = 35 * fontw;
    if ((visual_info = GetVisualInfo(screen, TAG_DONE)) == NULL)
        goto gui_failed;
    if (!LayoutMenus(menus, visual_info, GTMN_NewLookMenus, TRUE, TAG_DONE))
        goto gui_failed;
    if (!create_gadgets())
        goto gui_failed;
    if (top_edge == NOT_SET)
        top_edge = screen->BarHeight + 1;
    if (!open_window())
        goto gui_failed;
    SetMenuStrip(window, menus);
    if ((app_window = AddAppWindow(0, 0, window, wb_port, TAG_DONE)) == NULL)
        goto gui_failed;
    set_up_texts();
    render();
    return;
gui_failed:
    clean_up_gui();
}

static void busy(BOOL go_busy)
{
    if (IntuitionBase->lib_Version >= 39)
        if (go_busy)
            SetWindowPointer(window,
             WA_BusyPointer, TRUE,
             WA_PointerDelay, TRUE,
             TAG_DONE);
        else
            SetWindowPointer(window, TAG_DONE);
}

static void activate_window(struct Window *w)
{
    ScreenToFront(w->WScreen);
    WindowToFront(w);
    ActivateWindow(w);
}

static UBYTE *string_buffer(const struct Gadget *g)
{
    return ((struct StringInfo *) g->SpecialInfo)->Buffer;
}

static Tool *create_tool(const char *name, const char *path)
{
    Tool *t;

    if ((t = AllocMem(sizeof *t, MEMF_PUBLIC | MEMF_CLEAR)) == NULL)
        return NULL;
    if ((t->node.ln_Name = copy_of(name)) == NULL)
        goto create_tool_failed;
    if ((t->path = copy_of(path)) == NULL)
        goto create_tool_failed;
    if (enabled)
        enable_tool(t);
    return t;
create_tool_failed:
    free_tool(t);
    return NULL;
}

static Tool *add_tool(const char *name, const char *path, struct Node *pred)
{
    Tool *t;

    if ((t = create_tool(name, path)) != NULL) {
        Insert(tools, &t->node, pred);
        ++tools_size;
    }
    return t;
}

static Tool *append_tool(const char *name, const char *path)
{
    return add_tool(name, path, tools->lh_TailPred);
}

static void remove_tool(Tool *t)
{
    Remove(&t->node);
    --tools_size;
    free_tool(t);
}

/* Handles current directory and ToolsMenu's icon resources while calling
   another function. */
static void with_prog_icon(void (*f)(char *, struct DiskObject *))
{
    struct DiskObject *disk_obj;
    BPTR old_dir = NULL; /* Initialize only to suppress warning */
    char *name;

    if (started_from_wb) {
        old_dir = CurrentDir(start_arg.wb_arg->wa_Lock);
        name = start_arg.wb_arg->wa_Name;
    } else
        name = start_arg.name;
    if ((disk_obj = GetDiskObject(name)) != NULL) {
        (*f)(name, disk_obj);
        FreeDiskObject(disk_obj);
    }
    if (started_from_wb)
        CurrentDir(old_dir);
}

static void read_definitions(char *name, struct DiskObject *disk_obj)
{
    char **tt, *def_end;

    for (tt = disk_obj->do_ToolTypes; *tt != NULL; ++tt)
        /* Identify the tool types that are menu item definitions and create
           menu items from them. */
        if (**tt == DEF_BEGIN && (def_end = strchr(*tt, DEF_END)) != NULL &&
         *(def_end + 1) == ' ') {
            /* Temporarily insert terminator to facilitate string copy. */
            *def_end = '\0';
            append_tool(*tt + 1, def_end + 2);
            /* Restore original char to leave string "untouched". */
            *def_end = DEF_END;
        }
}

/* Counts the number of tool types that aren't menu item definitions. */
static ULONG non_defs(const char **tt)
{
    ULONG result = 0;

    for ( ; *tt != NULL; ++tt)
        if (**tt != DEF_BEGIN)
            ++result;
    return result;
}

/* Create a tool type for a menu item definition. */
static char *create_def(const Tool *t)
{
    char *def;
    size_t name_len = strlen(t->node.ln_Name);

    /* Allocate space for name, path, two delimiters, a space, and
       string terminator */
    def = AllocVec(1 + name_len + 2 + strlen(t->path) + 1, 0);
    if (def != NULL) {
        def[0] = DEF_BEGIN;
        strcpy(def + 1, t->node.ln_Name);
        def[name_len + 1] = DEF_END;
        def[name_len + 2] = ' ';
        strcpy(def + name_len + 3, t->path);
    }
    return def;
}

/* Create a tool type for every menu item in the tools list. */
static BOOL create_defs(char **tt)
{
    struct Node *n;

    for (n = tools->lh_Head; n->ln_Succ != NULL; n = n->ln_Succ)
        if ((*(tt++) = create_def((Tool *) n)) == NULL)
            return FALSE;
    return TRUE;
}

/* Make a straight copy of those tool types that are not menu item
   definitions. */
static BOOL copy_non_defs(char **new_tt, const char **old_tt)
{
    for ( ; *old_tt != NULL; ++old_tt)
        if (**old_tt != DEF_BEGIN)
            if ((*(new_tt++) = copy_of(*old_tt)) == NULL)
                return FALSE;
    return TRUE;
}

static void free_tool_types(char **tt)
{
    char **i;

    for (i = tt ; *i != NULL; ++i)
        FreeVec(*i);
    FreeVec(tt);
}

static void write_definitions(char *name, struct DiskObject *disk_obj)
{
    ULONG size = tools_size + non_defs(disk_obj->do_ToolTypes) + 1;
    char **old_tool_types = disk_obj->do_ToolTypes;
    char **new_tool_types;

    new_tool_types = AllocVec(sizeof(char *) * size, MEMF_CLEAR);
    if (new_tool_types == NULL)
        return;
    if (!create_defs(new_tool_types))
        goto write_definitions_done;
    if (!copy_non_defs(new_tool_types + tools_size, old_tool_types))
        goto write_definitions_done;
    disk_obj->do_ToolTypes = new_tool_types;
    PutDiskObject(name, disk_obj);
    disk_obj->do_ToolTypes = old_tool_types;
write_definitions_done:
    free_tool_types(new_tool_types);
}

static void update_gadgets(void)
{
    BOOL disable = (current == NULL);

    GT_SetGadgetAttrs(sel_gad, window, NULL,
     GA_Disabled, disable,
     GTST_String, disable ? "" : current->node.ln_Name,
     TAG_DONE);
    GT_SetGadgetAttrs(del_gad, window, NULL,
     GA_Disabled, disable,
     TAG_DONE);
    GT_SetGadgetAttrs(path_gad, window, NULL,
     GA_Disabled, disable,
     GTST_String, disable ? "" : current->path,
     TAG_DONE);
}

/* This is called every time the user interacts with the GUI in some way, to
   update the currently selected menu item definition. It actually creates a
   new definition and then deletes the old one, to simplify low memory
   failure scenarios. If the name gadget is blank, it will simply delete the
   currently selected definition. */
static void update_current(void)
{
    UBYTE *buffer = string_buffer(sel_gad);
    Tool *t = NULL;

    if (current == NULL)
        return;
    if (*buffer != 0 &&
     (t = add_tool(buffer, string_buffer(path_gad), &current->node)) == NULL)
        return;
    remove_tool(current);
    if ((current = t) == NULL)
        update_gadgets();
}

/* Called every time we handle a GUI interaction to temporarily suspend the
   listview so we can update the list, and to update the currenly selected
   menu item. */
static void begin_update(void)
{
    GT_SetGadgetAttrs(lv_gad, window, NULL, GTLV_Labels, ~0L, TAG_DONE);
    update_current();
}

/* Called after handling every GUI interaction to resume normal listview
   operations. */
static void end_update(void)
{
    GT_SetGadgetAttrs(lv_gad, window, NULL, GTLV_Labels, tools, TAG_DONE);
}

static void set_current(Tool *t)
{
    current = t;
    update_gadgets();
    if (current != NULL)
        ActivateGadget(sel_gad, window, NULL);
}

static void disable(void)
{
    for_all(tools, disable_tool);
    enabled = FALSE;
}

static void enable(void)
{
    for_all(tools, enable_tool);
    enabled = TRUE;
}

static void appear(void)
{
    if (window == NULL)
        set_up_gui();
    else {
        activate_window(window);
        if (window->Height == zoom[3])
            ZipWindow(window);
    }
}

static void disappear(void)
{
    current = NULL;
    clean_up_gui();
}

static void save_menu_items(void)
{
    busy(TRUE);
    with_prog_icon(write_definitions);
    busy(FALSE);
}

static void about(void)
{
    static struct EasyStruct ez = {
        sizeof(struct EasyStruct), 0, APP_NAME, TITLE "\n" COPYRIGHT, NULL
    };

    if (about_req != NULL)
        activate_window(about_req);
    else {
        ez.es_GadgetFormat = get_string(MSG_GAD_CONTINUE);
        if ((about_req = BuildEasyRequest(window, &ez, 0)) != NULL)
            all_signals |= (about_req_signal = 1UL <<
             about_req->UserPort->mp_SigBit);
    }
}

static void quit(void)
{
    clean_exit(RETURN_OK);
}

static void select(UWORD index)
{
    set_current((Tool *) node_at(tools, index));
}

static void new_tool(void)
{
    set_current(append_tool("", ""));
}

static void delete_tool(void)
{
    GT_SetGadgetAttrs(sel_gad, window, NULL,
     GTST_String, "",
     TAG_DONE);
    update_current();
}

static void handle_broker_command(ULONG msg_id)
{
    switch (msg_id) {
    case CXCMD_DISABLE:
        ActivateCxObj(broker, 0);
        disable();
        break;
    case CXCMD_ENABLE:
        ActivateCxObj(broker, 1);
        enable();
        break;
    case CXCMD_KILL:
        quit();
        break;
    case CXCMD_UNIQUE:
    case CXCMD_APPEAR:
        appear();
        break;
    case CXCMD_DISAPPEAR:
        disappear();
        break;
    }
}

static void handle_broker_signal(void)
{
    CxMsg *msg;
    ULONG msg_type, msg_id;

    while ((msg = (CxMsg *) GetMsg(broker_port)) != NULL) {
        msg_type = CxMsgType(msg);
        msg_id = CxMsgID(msg);
        ReplyMsg((struct Message *) msg);
        switch (msg_type) {
        case CXM_IEVENT:
            /* Must be Hot Key event */
            appear();
            break;
        case CXM_COMMAND:
            handle_broker_command(msg_id);
            break;
        }
    }
}

static void add_tool_from_icon(char *name)
{
    char path[MAX_PATH_LENGTH + 1];
    BPTR lock;

    if ((lock = Lock(name, ACCESS_READ)) == NULL)
        return;
    if (NameFromLock(lock, path, MAX_PATH_LENGTH + 1))
        set_current(append_tool(name, path));
    UnLock(lock);
}

static void handle_app_window_msg(const struct AppMessage *msg)
{
    struct WBArg *arg;
    struct WBArg *end = msg->am_ArgList + msg->am_NumArgs;
    BPTR old_dir;
    struct DiskObject *disk_obj;

    appear();
    begin_update();
    for (arg = msg->am_ArgList; arg != end; ++arg) {
        old_dir = CurrentDir(arg->wa_Lock);
        /* Check that name isn't NULL and isn't empty, and that the icon is
           a Workbench tool. */
        if (arg->wa_Name != NULL && *arg->wa_Name != '\0')
            if ((disk_obj = GetDiskObject(arg->wa_Name)) != NULL) {
                if (disk_obj->do_Type == WBTOOL)
                    add_tool_from_icon(arg->wa_Name);
                FreeDiskObject(disk_obj);
            }
        CurrentDir(old_dir);
    }
    end_update();
}

static void free_args(struct WBArg *arg_list, LONG num_args)
{
    FreeVec(arg_list[0].wa_Name);
    if (arg_list[0].wa_Lock != NULL)
        UnLock(arg_list[0].wa_Lock);
    FreeMem(arg_list, sizeof(struct WBArg) * num_args);
}

/* Creates the directory locks and tool names to pass to run_tool(). It
   actually only creates the first element, ie. the home directory and name
   of the tool itself, the rest are copied from the AppMessage that
   workbench.library sent us. */
static struct WBArg *create_args(char *path, const struct WBArg *arg_list,
 LONG num_args)
{
    struct WBArg *result;
    size_t args_size;
    UBYTE t;
    UBYTE *file_part;

    /* Create an array one element bigger than the one received from
       workbench.library, so we can put the tool itself at the front. */
    args_size = sizeof(struct WBArg) * num_args;
    result = AllocMem(args_size+sizeof(struct WBArg),MEMF_PUBLIC|MEMF_CLEAR);
    if (result == NULL)
        return NULL;
    /* Temporarily separate the file part and directory part of the tool's
       path with a string terminator. */
    t = *(file_part = FilePart(path));
    *file_part = '\0';
    /* Create the tool's directory lock. */
    result[0].wa_Lock = Lock(path, ACCESS_READ);
    /* Restore original string. */
    *file_part = t;
    if (result[0].wa_Lock == NULL)
        goto create_args_failed;
    /* Copy the tool's name. */
    if ((result[0].wa_Name = copy_of(file_part)) == NULL)
        goto create_args_failed;
    /* The rest of the array is just a copy of the AppMessage array. */
    if (args_size > 0)
        memcpy(result + 1, arg_list, args_size);
    return result;
create_args_failed:
    free_args(result, num_args + 1);
    return NULL;
}

/* This function is called whenever a tool we started quits. At this point,
   we reply to the workbench.library so it can free the arguments that we
   borrowed. Called indirectly by clean_up_tool_runner(), so we must not
   clean up anything needed by reply_app_msg() until after the call to
   clean_up_tool_runner(). */
static void reply_app_msg(struct WBArg *arg_list, LONG num_args,
 struct AppMessage *app_message)
{
    if (arg_list != NULL)
        free_args(arg_list, num_args);
    ReplyMsg((struct Message *) app_message);
}

static void handle_app_menu_msg(struct AppMessage *msg)
{
    Tool *t = (Tool *) msg->am_UserData;
    struct WBArg *args = NULL;
    LONG num_args = msg->am_NumArgs + 1;

    /* Check that the path isn't empty and that tool hasn't been removed
       prior to handling this event, then create the Workbench arguments and
       run the tool. */
    if (*t->path == '\0' || !in_list(tools, &t->node) ||
     (args = create_args(t->path,msg->am_ArgList,msg->am_NumArgs)) == NULL ||
     !run_tool(args, num_args, reply_app_msg, msg))
        /* Failed to run, reply to the AppMessage right away. */
        reply_app_msg(args, num_args, msg);
}

static void handle_wb_signal(void)
{
    struct AppMessage *msg;

    while ((msg = (struct AppMessage *) GetMsg(wb_port)) != NULL) {
        if (msg->am_Type == AMTYPE_APPWINDOW) {
            handle_app_window_msg(msg);
            ReplyMsg((struct Message *) msg);
        } else
            handle_app_menu_msg(msg);
    }
}

static BOOL handle_menupick(UWORD code)
{
    BOOL hide = FALSE;

    begin_update();
    while (code != MENUNULL) {
        switch (MENUNUM(code)) {
        case 0: /* Project menu */
            switch (ITEMNUM(code)) {
            case 0: /* Save Menu Items */
                save_menu_items();
                break;
            case 2: /* Hide */
                hide = TRUE;
                break;
            case 3: /* About */
                about();
                break;
            case 4: /* Quit */
                quit();
                break;
            }
        }
        code = ItemAddress(menus, code)->NextSelect;
    }
    end_update();
    return hide;
}

static void handle_closewindow(void)
{
    begin_update();
    end_update();
}

static void handle_refreshwindow(void)
{
    GT_BeginRefresh(window);
    render();
    GT_EndRefresh(window, TRUE);
}

static void handle_gadgetup(const struct Gadget *g, UWORD code)
{
    begin_update();
    if (g == lv_gad)
        select(code);
    else if (g == new_gad)
        new_tool();
    else if (g == del_gad)
        delete_tool();
    end_update();
}

static void handle_window_signal(void)
{
    BOOL hide = FALSE;
    struct IntuiMessage *msg;
    ULONG class;
    UWORD code;
    APTR iaddress;

    while ((msg = GT_GetIMsg(window->UserPort)) != NULL) {
        class = msg->Class;
        code = msg->Code;
        iaddress = msg->IAddress;
        GT_ReplyIMsg(msg);
        switch (class) {
        case IDCMP_MENUPICK:
            if (handle_menupick(code))
                hide = TRUE;
            break;
        case IDCMP_CLOSEWINDOW:
            handle_closewindow();
            hide = TRUE;
            break;
        case IDCMP_REFRESHWINDOW:
            handle_refreshwindow();
            break;
        case IDCMP_GADGETUP:
            handle_gadgetup(iaddress, code);
            break;
        }
    }
    if (hide)
        disappear();
}

static void handle_about_req_signal(void)
{
    if (SysReqHandler(about_req, NULL, FALSE) != -2)
        clean_up_about_req();
}

static void main_loop(void)
{
    ULONG signals;

    for (;;) {
        signals = Wait(all_signals);
        if ((signals & broker_signal) != 0)
            handle_broker_signal();
        if ((signals & wb_signal) != 0)
            handle_wb_signal();
        if ((signals & reply_signal) != 0)
            handle_tool_reply();
        if ((signals & window_signal) != 0)
            handle_window_signal();
        if ((signals & about_req_signal) != 0)
            handle_about_req_signal();
        if ((signals & SIGBREAKF_CTRL_C) != 0)
            quit();
        if ((signals & SIGBREAKF_CTRL_F) != 0)
            appear();
    }
}

int main(int argc, char **argv)
{
    check_system_version();
    set_up(argc, argv);
    with_prog_icon(read_definitions);
    if (cx_popup)
        appear();
    main_loop();
}
