/*
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 COPYRIGHT "Copyright © 2015 Kim Fastrup Larsen"
#define VERSION "1.1"
#define DATE "19.1.2015"
#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

/* This will hold the information we need to later open
   ToolsMenu's icon for reading and writing tool definitions. */
typedef struct {
    BOOL started_from_wb;
    union {
        char *name;
        struct WBArg *wb_arg;
    } arg;
} Startarg;

/* 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 TextAttr topaz60 = {
    "topaz.font", 9, FS_NORMAL, FPF_DESIGNED | FPF_ROMFONT
};
static struct TextAttr topaz80 = {
    "topaz.font", 8, FS_NORMAL, FPF_DESIGNED | FPF_ROMFONT
};
static struct IntuiText itexts[3] = {
    { 1, 0, JAM2, 0, 0, &topaz80, NULL, itexts + 1 },
    { 1, 0, JAM2, 0, 9, &topaz80, NULL, itexts + 2 },
    { 1, 0, JAM2, 0, 18, &topaz80, NULL, NULL }
};
static WORD zoom[4] = { 0, 0, 200, 0 };

/* 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 *menu;
static struct Screen *screen;
static APTR visual_info;
static struct Gadget *gadgets;
static struct Window *window;
static BOOL menu_strip_set;
static struct AppWindow *app_window;

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

/* Other globals */
static Startarg start_arg;
static ULONG all_signals = SIGBREAKF_CTRL_C | SIGBREAKF_CTRL_F;
static ULONG reply_signal, broker_signal, wb_signal, window_signal;
static ULONG left_edge, top_edge = NOT_SET;
static struct Gadget *lv_gad, *sel_gad, *new_gad, *del_gad, *path_gad;

static void v33_message(char *s)
{
    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_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_KICKSTART_2_REQUIRED));
            CloseLibrary(IntuitionBase);
        }
        exit(RETURN_FAIL);
    }
}

static void clean_up_gui(void)
{
    if (app_window != NULL) {
        RemoveAppWindow(app_window);
        app_window = NULL;
    }
    if (menu_strip_set) {
        ClearMenuStrip(window);
        menu_strip_set = FALSE;
    }
    if (window != NULL) {
        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) {
        UnlockPubScreen(NULL, screen);
        screen = NULL;
    }
}

static void free_tool(Tool *t)
{
    if (t->menu_item != NULL)
        RemoveAppMenuItem(t->menu_item);
    FreeVec(t->node.ln_Name);
    FreeVec(t->path);
    FreeMem(t, sizeof *t);
}

static void free_tools(void)
{
    struct Node *n, *next;

    n = tools->lh_Head;
    while ((next = n->ln_Succ) != NULL) {
        free_tool((Tool *) n);
        n = next;
    }
    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);
    clean_up_catalog();
    CloseLibrary(LocaleBase);
}

/* 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(menu);
    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();
    exit(return_code);
}

static void message(char *s)
{
    static struct EasyStruct easy = { sizeof(struct EasyStruct), 0 };

    if (s != NULL && IntuitionBase != NULL) {
        easy.es_Title = APP_NAME;
        easy.es_TextFormat = s;
        easy.es_GadgetFormat = get_string(MSG_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)
{
    LocaleBase = OpenLibrary("locale.library", 38);
    set_up_catalog(CATALOG);
    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)
{
    start_arg.started_from_wb = TRUE;
    start_arg.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];

    start_arg.started_from_wb = FALSE;
    start_arg.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 = { NB_VERSION, 0 };
    LONG error;
    CxObj *filter;

    create_port(&broker_port, &broker_signal);
    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_menu(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, "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_QUIT);
    if ((menu = 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)
{
    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_menu();
    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 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 + screen->WBorTop + screen->Font->ta_YSize + 1;
    ng->ng_Width = width;
    ng->ng_Height = height;
    ng->ng_GadgetText = gadget_text;
    ng->ng_Flags = flags;
}

static BOOL create_gadgets(void)
{
    static struct NewGadget ng;
    struct Gadget *g = CreateContext(&gadgets);

    ng.ng_TextAttr = &topaz80;
    ng.ng_VisualInfo = visual_info;
    make_newgadget(&ng,8,16,220,58,get_string(MSG_GADGET_MENU_ITEMS),0);
    g = lv_gad = CreateGadget(LISTVIEW_KIND, g, &ng,
     GTLV_Labels, tools,
     GTLV_ScrollWidth, 18,
     LAYOUTA_Spacing, 1,
     TAG_DONE);
    make_newgadget(&ng, 8, 74, 220, 14, NULL, 0);
    g = sel_gad = CreateGadget(STRING_KIND, g, &ng,
     GA_Disabled, TRUE,
     TAG_DONE);
    make_newgadget(&ng, 8, 88, 110, 14, get_string(MSG_GADGET_NEW), 0);
    g = new_gad = CreateGadget(BUTTON_KIND, g, &ng, TAG_DONE);
    make_newgadget(&ng,118,88,110,14,get_string(MSG_GADGET_DELETE),0);
    g = del_gad = CreateGadget(BUTTON_KIND, g, &ng, GA_Disabled, TRUE,
     TAG_DONE);
    make_newgadget(&ng, 236, 31, 280, 14, get_string(MSG_GADGET_TOOL),
     PLACETEXT_ABOVE);
    g = path_gad = CreateGadget(STRING_KIND, g, &ng,
     GTST_MaxChars, MAX_PATH_LENGTH,
     GA_Disabled, TRUE,
     TAG_DONE);
    return 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[3] = screen->WBorTop + screen->Font->ta_YSize + 1;
    ScreenToFront(screen);
    window = OpenWindowTags(NULL,
     WA_Left, left_edge,
     WA_Top, top_edge,
     WA_InnerWidth, 524,
     WA_InnerHeight, 107,
     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)
{
    struct DrawInfo *draw_info;
    int i;

    draw_info = GetScreenDrawInfo(screen);
    for (i = 0; i < sizeof itexts / sizeof *itexts; ++i) {
        if (draw_info != NULL) {
            itexts[i].FrontPen = draw_info->dri_Pens[TEXTPEN];
            itexts[i].BackPen = draw_info->dri_Pens[BACKGROUNDPEN];
        }
        itexts[i].LeftEdge = (280 - 8 * strlen(itexts[i].IText)) / 2;
    }
    if (draw_info != NULL)
        FreeScreenDrawInfo(screen, draw_info);
}

static void render(void)
{
    PrintIText(window->RPort, itexts, window->BorderLeft + 236,
     window->BorderTop + 62);
}

static void set_up_gui(void)
{
    if ((screen = LockPubScreen(NULL)) == NULL)
        goto gui_failed;
    if (top_edge == NOT_SET)
        top_edge = screen->BarHeight + 1;
    if ((visual_info = GetVisualInfo(screen, TAG_DONE)) == NULL)
        goto gui_failed;
    if (!create_gadgets())
        goto gui_failed;
    if (!open_window())
        goto gui_failed;
    if (!LayoutMenus(menu,visual_info,GTMN_NewLookMenus,TRUE,TAG_DONE))
        goto gui_failed;
    SetMenuStrip(window, menu);
    menu_strip_set = TRUE;
    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 UBYTE *get_buffer(const struct Gadget *g)
{
    return ((struct StringInfo *) g->SpecialInfo)->Buffer;
}

static Tool *get_tool(UWORD index)
{
    struct Node *n;
    UWORD i;

    for (i = 0, n = tools->lh_Head; n->ln_Succ != NULL; ++i, n = n->ln_Succ)
        if (i == index)
            return (Tool *) n;
    return NULL;
}

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

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)
        add_menu_item(t);
    return t;
create_tool_failed:
    free_tool(t);
    return NULL;
}

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

    if ((t = create_tool(name, path)) != NULL) {
        AddTail(tools, &t->node);
        ++tools_size;
    }
    return 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 (start_arg.started_from_wb) {
        old_dir = CurrentDir(start_arg.arg.wb_arg->wa_Lock);
        name = start_arg.arg.wb_arg->wa_Name;
    } else
        name = start_arg.arg.name;
    if ((disk_obj = GetDiskObject(name)) != NULL) {
        (*f)(name, disk_obj);
        FreeDiskObject(disk_obj);
    }
    if (start_arg.started_from_wb)
        CurrentDir(old_dir);
}

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

    for ( ; *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';
            add_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;
    int i = 0;

    for (n = tools->lh_Head; n->ln_Succ != NULL; n = n->ln_Succ)
        if ((tt[i++] = 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 disable_gadgets(void)
{
    GT_SetGadgetAttrs(sel_gad, window, NULL,
     GA_Disabled, TRUE,
     GTST_String, "",
     TAG_DONE);
    GT_SetGadgetAttrs(del_gad, window, NULL,
     GA_Disabled, TRUE,
     TAG_DONE);
    GT_SetGadgetAttrs(path_gad, window, NULL,
     GA_Disabled, TRUE,
     GTST_String, "",
     TAG_DONE);
}

static void enable_gadgets(void)
{
    GT_SetGadgetAttrs(sel_gad, window, NULL,
     GA_Disabled, FALSE,
     GTST_String, current->node.ln_Name,
     TAG_DONE);
    GT_SetGadgetAttrs(del_gad, window, NULL,
     GA_Disabled, FALSE,
     TAG_DONE);
    GT_SetGadgetAttrs(path_gad, window, NULL,
     GA_Disabled, FALSE,
     GTST_String, 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 = get_buffer(sel_gad);
    Tool *t = NULL;

    if (current == NULL)
        return;
    if (*buffer != 0) {
        if ((t = create_tool(buffer, get_buffer(path_gad))) == NULL)
            return;
        Insert(tools, &t->node, &current->node);
        ++tools_size;
    }
    Remove(&current->node);
    --tools_size;
    free_tool(current);
    if ((current = t) == NULL)
        disable_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;
    if (current == NULL)
        disable_gadgets();
    else {
        enable_gadgets();
        ActivateGadget(sel_gad, window, NULL);
    }
}

static void disable(void)
{
    struct Node *n;

    for (n = tools->lh_Head; n->ln_Succ != NULL; n = n->ln_Succ)
        if (((Tool *) n)->menu_item != NULL) {
            RemoveAppMenuItem(((Tool *) n)->menu_item);
            ((Tool *) n)->menu_item = NULL;
        }
    enabled = FALSE;
}

static void enable(void)
{
    struct Node *n;

    for (n = tools->lh_Head; n->ln_Succ != NULL; n = n->ln_Succ)
        if (((Tool *) n)->menu_item == NULL)
            add_menu_item((Tool *) n);
    enabled = TRUE;
}

static void appear(void)
{
    if (window == NULL) {
        set_up_gui();
    } else {
        ScreenToFront(window->WScreen);
        WindowToFront(window);
        if (window->Height == zoom[3])
            ZipWindow(window);
        ActivateWindow(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 quit(void)
{
    clean_exit(RETURN_OK);
}

static void select(UWORD index)
{
    set_current(get_tool(index));
}

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

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

static BOOL tool_in_list(const Tool *t)
{
    struct Node *node;

    for (node = tools->lh_Head; node->ln_Succ != NULL; node = node->ln_Succ)
        if ((Tool *) node == t)
            return TRUE;
    return FALSE;
}

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(add_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' &&
         (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 in the WBStartup
   message to the tool that we are going to run. 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' || !tool_in_list(t) ||
     (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: /* Quit */
                quit();
                break;
            }
        }
        code = ItemAddress(menu, 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 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 & 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();
}
