/*  Altabber, n. He who performs an `Alt-Tab' action.
**
**  Version 1.5 (1.4.99) İ Mfc.
**
**  Compile with DICE:
**      dcc -mi -mRR -pi Altabber.c NewReadArgs.c
**
**  The "fixed-keys" version of Altabber (without argument/tooltypes parsing)
**  can be obtained with:
**      dcc -mi -mRR -DFIXEDKEYS -pi Altabber.c
**
**  To enable debugging output, simply add "-DDEBUG"
**
*/


#include "Altabber.h"
#ifndef FIXEDKEYS
#include <NewReadArgs.h>
#endif

/// Defines

/* Messages we will receive from the Cx filter: */
#define WINTAB      1   /* Cycle thru windows */
#define SCRTAB      2   /* Cycle thru screens */
#define UPSTROKE    8   /* Select this item */

/* Max string lengths: */
#define MAX_TITLE_LEN   80
#define MAX_HOTKEY_LEN  40

/* Some macros: */
#define CURRENTLY_ACTIVE_SCR    IntuitionBase->ActiveScreen
#define CURRENTLY_ACTIVE_WIN    IntuitionBase->ActiveWindow
#define FIRST_AVAILABLE_SCR     IntuitionBase->FirstScreen
#define FIRST_AVAILABLE_WIN     IntuitionBase->ActiveScreen->FirstWindow

/* Pretty obvious :-) */
#define JAM1 0
#define JAM2 1

/* This is a clearly impossible hotkey */
#define DUMMY_KEY   "Ctrl LAmiga RAmiga NumPad *"

/* A couple of largely used macros: */
#define xalloc(s)   AllocVec(s, 0)
#define xfree(a)    FreeVec(a)

#ifdef DEBUG
#define D(x)    x
#else
#define D(x)
#endif

#define TEMPLATE    "WQ=WIN_QUAL/K,WK=WIN_KEY/K," \
                    "SQ=SCR_QUAL/K,SK=SCR_KEY/K," \
                    "CX_PRIORITY/N/K,"      \
                    "COLOR1/N/K,COLOR2/N/K,"\
                    "AA=AUTOACTIVATE/S" /* Not yet fully functional! */
///

/// Globals

/* The options (either CLI parameters or icon ToolTypes) */
/* are stored in the Opt struct. Some defaults are given here: */
const int def_pri = -5;
const int def_color1 = 1;
const int def_color2 = 2;

struct {
    STRPTR win_qual, win_key;
    STRPTR scr_qual, scr_key;
    int *pri;
    int *color1, *color2;
    int aa; /* Not yet fully functional! */
} Opt = {
    "LAmiga", "Tab",
    "LAmiga", "Shift Tab",
    &def_pri,
    &def_color1, &def_color2,
    0       /* Not yet fully functional! */
};

/* These are extern since we let the compiler care for */
/* opening library, replying WB, etc. */
extern struct IntuitionBase *IntuitionBase;
extern struct WBStartup *_WBMsg;

/* Has our window been opened? */
int Win_open = 0;
int Scr_open = 0;

/* Each item (either scr or win) is stored in a struct */
/* like this. Multiple records are then linked together */
/* in a single chain. */

struct record {
    void            *id;
    unsigned char   title[MAX_TITLE_LEN];
    int             len;
    int             width;
    struct record   *next;
};

struct record *First_item;      /* Head of the chain */
struct record *Special_item;    /* Some magic stuff usad by the OMM */
struct record *Displayed_item;  /* The currently displayed item */
struct record *Active_item;     /* The item which was active when the user */
                                /* pressed the hotkey the first time */

/* Gfx Globals: all the global variable which are related to gfx */
/* (window, rastport, fonts, etc.) are placed in a single global struct */
struct {
    struct Window   *win;
    struct RastPort *rp;
    struct TextFont *font;
    int font_height;
    int font_baseline;
    int max_width;
    int top_corner, left_corner;
} GG = {NULL};

/* OMM (Olivier's Memory Method) */
void *Last_active_win = NULL;
void *Last_active_scr = NULL;

struct MsgPort  *mp;    /* the port where we will receive all the msgs */
ULONG   Sigbits;        /* signals we will wait for */

/* Every Cx must have an Object called a broker: */
CxObj           *Broker;

/* The special filters used to know when the user releases the qualifier key. */
/* They can't be "normal" hotkeys as they imply "upstroke", which the Cx */
/* interface doen't recognize. */
            /*  magic       class        code  cmask qual qmask synonyms*/
IX scr_ix = { IX_VERSION, IECLASS_RAWKEY,  0, 0xffff,  0,   0,   0 };
IX win_ix = { IX_VERSION, IECLASS_RAWKEY,  0, 0xffff,  0,   0,   0 };



///

/// Prototypes

int open_window(void);
void close_window(void);
void display_next(void);

int create_scrlist(void);
int create_winlist(void);
void raise_scr(void);
void raise_win(void);
void dispose_list(void);

void handle(void);

int copy_title(STRPTR, STRPTR);
int qualcode(STRPTR);

CxObj *AttachFilter(STRPTR, ULONG);
int ShowError(STRPTR);
///

#define BROKERVERSION "Altabber 1.5  İ1999 Mfc."
static const STRPTR version = "$VER: Altabber 1.5 (1.4.99)";

/// main()
/*  ŻŻŻŻŻŻ
*/

#ifdef _DCC
__stkargs
#endif
void _main()
{
    int error = 0;

#ifndef FIXEDKEYS
    /* Read the arguments/tooltypes */
    struct NewRDArgs nrda = {
        TEMPLATE,
        NULL,
        NULL,
        (LONG *) &Opt,
        -1,
        TRUE,
        /* other fields = NULL */
    };

    error = NewReadArgs(_WBMsg, &nrda);
#endif

    if (error == 0)
    {
        unsigned char win_hotkey[MAX_HOTKEY_LEN];
        unsigned char scr_hotkey[MAX_HOTKEY_LEN];

#ifdef DEBUG
        Printf("win_qual=%s\n", Opt.win_qual);
        Printf("win_key=%s\n", Opt.win_key);
        Printf("scr_qual=%s\n", Opt.scr_qual);
        Printf("scr_key=%s\n", Opt.scr_key);
        Printf("pri=%ld\n", *Opt.pri);
        Printf("color1,2=%ld,%ld\n", *Opt.color1, *Opt.color2);
        Printf("aa=%ld\n", Opt.aa);
#endif

        /* Try to interpret the options and to build an adequate */
        /* IX filter for every qualifier */
        win_ix.ix_Code = IECODE_UP_PREFIX | qualcode(Opt.win_qual);
        scr_ix.ix_Code = IECODE_UP_PREFIX | qualcode(Opt.scr_qual);

        /* Build the win hotkey as the union of the "_QUAL" option */
        /* and the "_KEY" option */
        strcpy(win_hotkey, Opt.win_qual);
        strcat(win_hotkey, " ");
        strcat(win_hotkey, Opt.win_key);

        /* Build the scr hotkey as the union of the "_QUAL" option */
        /* and the "_KEY" option */
        strcpy(scr_hotkey, Opt.scr_qual);
        strcat(scr_hotkey, " ");
        strcat(scr_hotkey, Opt.scr_key);

#ifdef DEBUG
        Printf("win_hotkey=|%s|\n", win_hotkey);
        Printf("scr_hotkey=|%s|\n", scr_hotkey);
        Printf("win_ix.ix_Code=%lx\n", win_ix.ix_Code);
        Printf("scr_ix.ix_Code=%lx\n", scr_ix.ix_Code);
#endif

        if (mp=CreateMsgPort())
        {
            /* Create the broker */
            struct NewBroker nb = {
                NB_VERSION,
                "Altabber",
                BROKERVERSION,
                "Windoze-like Alt-Tab function",
                NBU_UNIQUE | NBU_NOTIFY,
                0, *Opt.pri, 0, 0
            };

            nb.nb_Port = mp;
            Sigbits = SIGBREAKF_CTRL_C | (1L << mp->mp_SigBit);

            if (Broker=CxBroker(&nb, &error))
            {
                /* Now we start to attach things to the broker. */
                /* First of all the two filters which intercept */
                /* the Amiga+Tab and Amiga+Shift+Tab strokes */
                if (AttachFilter(win_hotkey, WINTAB) &&
                    AttachFilter(scr_hotkey, SCRTAB))
                {
                    /* Add the special filter which intercept */
                    /* the release of the Amiga key. To do this, */
                    /* first build a normal filetr with a dummy hotkey, */
                    /* then replace the dummy hotkey with the actual IX */
                    CxObj *x = AttachFilter(DUMMY_KEY, UPSTROKE);
                    if (x)
                    {
                        SetFilterIX(x, &win_ix);

                        /* Do the same with the scr upstroke filter, */
                        /* but only if it's different from the win one */
                        if (scr_ix.ix_Code != win_ix.ix_Code)
                        {
                            x = AttachFilter(DUMMY_KEY, UPSTROKE);
                            if (x)
                                SetFilterIX(x, &scr_ix);
                        }
                        if (x)
                        {
                            /* If everithing's ok, activate the broker */
                            ActivateCxObj(Broker, 1);

                            handle();
                        }
                        else
                            error = ShowError("couldn't create 2nd upstroke filter.");
                    }
                    else
                        error = ShowError("Couldn't create upstroke filter.");
                }
                else
                    error = ShowError("couldn't create filters.");

                /* Delete the broker (all attached objects get freed as well) */
                DeleteCxObjAll(Broker);

                /* Empty the msg queue by replying to all pending msgs */
                {
                    struct Message *msg;
                    while (msg=GetMsg(mp))
                        ReplyMsg(msg);
                }
            }
            else
            {
                if (error == CBERR_DUP)
                    /* Not a real error: the prg was started twice */
                    /* so the second instance tells the first one to quit */
                    error = 0;
                else
                    error = ShowError("couldn't create broker.");
            }

            DeleteMsgPort(mp);
        }
        else
            error = ShowError("couldn't create message port.");

#ifndef FIXEDKEYS
        NewFreeArgs(&nrda);
#endif
    }
    else
        error = ShowError("invalid arguments.");

    _exit(error);


#ifdef _DCC
    /* Dummy reference (never executed) which forces Dice to import */
    /* the WBMsg handling routines. */
    void _waitwbmsg(void);
    _waitwbmsg();
#endif
}
///

/// AttachFilter()
/*  ŻŻŻŻŻŻŻŻŻŻŻŻŻŻ
** This routine adds a filter to an existing broker.
** Each filter is triggered by the event ginven in the "key" string.
** The filter in turn has attached a Translator (which "eats" the event
** so that it doesn't propagate to lower-priority Cx's) an a Sender
** (which sends a "event" msg to the msg port "mp").
**
** It returns the address of the created filter.
*/

CxObj *AttachFilter(STRPTR key, ULONG event)
{
    CxObj *filter, *sender, *translator;

    if (filter=CxFilter(key))
    {
        AttachCxObj(Broker, filter);

        if (sender=CxSender(mp, event))
        {
            AttachCxObj(filter, sender);

            if (translator=CxTranslate(NULL))
            {
                AttachCxObj(filter, translator);

                if (!CxObjError(filter))
                    return filter;
            }
        }
    }

    return NULL;
}
///

/// handle()
/*  ŻŻŻŻŻŻŻŻ
** This is the kernel of the entire program. It basically consists
** of a classic Wait-GetMsg-ReplyMsg loop where the appropriate
** routines are called according to the type of message received.
*/

void handle(void)
{
    int error = 0;

    /* Main loop: the routines inside will set error to 1 */
    /* if the main loop is to be quitted. */

    while (!error)
    {
        CxMsg *msg;
        ULONG signal = Wait(Sigbits);

        /* We received a signal: either one or more msgs have arrived */
        /* or someone has sent us a CTRL-C */

        while (msg = (CxMsg*) GetMsg(mp))
        {
            /* We received a msg, so keep its type & ID */
            /* and then reply to it */
            ULONG id = CxMsgID(msg);
            ULONG type = CxMsgType(msg);
            ReplyMsg((struct Message *) msg);

            /* Take the appropriate action */
            /* Note that this part could be more optimized */
            switch (type)
            {
                case CXM_IEVENT: /* One of the hotkeys... */

                    switch(id)
                    {
                        case SCRTAB:
                            if (Win_open)
                            {
                                close_window();
                                dispose_list();
                            }

                            if (!Scr_open)
                            {
                                if (create_scrlist())
                                    Scr_open = open_window();
                            }

                            if (Scr_open)
                            {
                                display_next();
                            }
                            break;

                        case WINTAB:
                            if (Scr_open)
                            {
                                close_window();
                                dispose_list();
                            }

                            if (!Win_open)
                            {
                                if (create_winlist())
                                    Win_open = open_window();
                            }

                            if (Win_open)
                            {
                                display_next();
                            }
                            break;

                        case UPSTROKE:
                            /* If alright, raise the selected item */
                            /* then close the window */
                            if (Scr_open)
                            {
                                raise_scr();
                                close_window();
                                dispose_list();
                            }
                            if (Win_open)
                            {
                                raise_win();
                                close_window();
                                dispose_list();
                            }
                            break;
                    }

                    break;

                case CXM_COMMAND: /* Standard CX stuff... */

                    switch (id)
                    {
                        case CXCMD_DISABLE:
                            ActivateCxObj(Broker, 0);
                            break;

                        case CXCMD_ENABLE:
                            ActivateCxObj(Broker, 1);
                            break;

                        case CXCMD_KILL:
                        case CXCMD_UNIQUE:
                            /* Quit... */
                            error = 1;
                            break;
                    }
                    break;
            }
        }

        /* If we received a CTRL-C, quit */
        if (signal & SIGBREAKF_CTRL_C)
            error = 1;
    }

    /* Before quitting, close open windows and free allocated resources */
    if (Win_open || Scr_open)
    {
        close_window();
        dispose_list();
    }
}
///

/// create_winlist()
/*  ŻŻŻŻŻŻŻŻŻŻŻŻŻŻŻŻ
** Examine the list of open windows and build a chain of "items"
** or "records". The first win in the chain must be the Special One
** according to the OMM (Olivier's Memory Method). The last win
** should be the currently active one.
** Windows whithout printable title are skipped.
*/

int create_winlist(void)
{
    Active_item = First_item = Special_item = Displayed_item = NULL;

    /* Examine all the wins, starting with the currently active one */
    struct Window *aw = CURRENTLY_ACTIVE_WIN;
    struct Window *w = aw;

    do {
        /* Has printable title? */
        STRPTR title = w->Title;
        if ( title && (*title != '\0') )
        {
            /* Yes: allocate space for a new record and fill it */
            struct record *item = xalloc(sizeof(struct record));
            if (item == NULL)
                break;

            item->len = copy_title(item->title, title);
            item->id = w;

            /* Is this win the Special One according to OMM? */
            if (w == Last_active_win && w != aw)
            {
                /* Yes: keep it apart */
                Special_item = item;
            }
            else
            {
                /* No: add it to the head of the chain */
                item->next = First_item;
                First_item = item;
            }

            /* Is this win the currently active one? */
            if (w == aw)
                Active_item = item;
        }

        /* Examine the next win. If we reach the end of the list */
        /* restart from the first win of this screen */

        w = w->NextWindow;
        if (w == NULL)
            w = FIRST_AVAILABLE_WIN;

    /* Stop when we cycled thru all available wins and we came back to */
    /* the firstly examined one, which happens to be the currently active one */
    } while (w != aw);

    /* If a Special Win was found, add it to the head of the chain, so that */
    /* it will appear first (making Olivier happy) */
    if (Special_item)
    {
        Special_item->next = First_item;
        First_item = Special_item;
    }

    /* Now, if we found only one win and that win is already selected */
    /* (the currently active one) then Altabber is obviously of no use, */
    /* so don't even open the window: discard the chain and return an error */
    if (First_item == Active_item)
    {
        dispose_list();
        return 0; /* means: couldn't correctly initiate the wins chain */
    }

    return 1;   /* Ok */
}
///

/// create_scrlist()
/*  ŻŻŻŻŻŻŻŻŻŻŻŻŻŻŻŻ
** Examine the list of open screens and build a chain of "items"
** or "records". The first scr in the chain must be the Special One
** according to the OMM (Olivier's Memory Method). The last scr
** should be the currently active one.
**
** Please see the comments in the create_winlist() function.
*/

int create_scrlist(void)
{
    Active_item = First_item = Special_item = Displayed_item = NULL;

    struct Screen *as = CURRENTLY_ACTIVE_SCR;
    struct Screen *s = as;

    do {
        STRPTR title = s->Title;    /* Or DefaultTitle ?!? */

        /* Screen are never skipped, regardless of their title */
        //if ( title && (*title != '\0') )
        {
            struct record *item = xalloc(sizeof(struct record));
            if (item == NULL)
                break;

            /* If the scr has no printable title, a default one */
            /* is provided */
            if ( title && (*title != '\0') )
                item->len = copy_title(item->title, title);
            else
                item->len = copy_title(item->title, "<no title>");

            item->id = s;

            if (s == Last_active_scr && s != as)
            {
                Special_item = item;
            }
            else
            {
                item->next = First_item;
                First_item = item;
            }

            if (s == as)
                Active_item = item;
        }

        s = s->NextScreen;
        if (s == NULL)
            s = FIRST_AVAILABLE_SCR;

    } while (s != as);

    if (Special_item)
    {
        Special_item->next = First_item;
        First_item = Special_item;
    }

    if (First_item == Active_item)
    {
        dispose_list();
        return 0;
    }

    return 1;   /* Ok */

}
///

/// open_window()
/*  ŻŻŻŻŻŻŻŻŻŻŻŻŻ
*/

int open_window(void)
{
    struct Screen *as = IntuitionBase->ActiveScreen;
    struct RastPort *tmp_rp = &as->RastPort;

    /* Get the screen font's height */
    GG.font_height = as->Font->ta_YSize;

    /* Try to determine the max window width required */
    /* by the titles in the chain */
    GG.max_width = 0;
    struct record *item = First_item;
    while (item)
    {
        int width = TextLength(tmp_rp, item->title, item->len);
        item->width = width;
        if (width > GG.max_width)
            GG.max_width = width;

        item = item->next;
    }

    /* Leave a margin of twice the font's height on both sides */
    /* and above and below the print-area */
    int winw = GG.max_width + 4 * GG.font_height;
    int winh = 5 * GG.font_height;

    /* Open the window on the currently active screen */
    /* with the calculated size and without any title */
    struct Window  *win = OpenWindowTags(NULL,
            WA_Left,        (as->Width - winw)/2,
            WA_Top,         (as->Height - winh)/2,
            WA_Width,       winw,
            WA_Height,      winh,
            WA_CustomScreen,(ULONG) as,
            TAG_END,        0
        );

    if (win == NULL)
        return 0;

    /* Fill in the Global Gfx variables */
    GG.win = win;
    GG.rp = win->RPort;
    /* These are the coordinates of the print-area */
    /* inside the window */
    GG.left_corner = 2 * GG.font_height;
    GG.top_corner = 2 * GG.font_height;

    /* Set the same font as the screen's one */
    GG.font = OpenFont(as->Font);
    if (GG.font)
    {
        SetFont(GG.rp, GG.font);
    }

    GG.font_baseline = GG.font->tf_Baseline;

    /* Be sure we are writing in JAM1 mode */
    SetABPenDrMd(GG.rp, 1, 0, JAM1);

    return 1;   /* Ok */
}
///

/// display_next()
/*  ŻŻŻŻŻŻŻŻŻŻŻŻŻŻ
*/

void display_next(void)
{
    /* Select the next item */
    if (Displayed_item)
        Displayed_item = Displayed_item->next;

    /* If at the end of the chain (or if called for the first time) */
    /* restart from the head of the chain */
    if (Displayed_item == NULL)
        Displayed_item = First_item;

    /* Clear the print-area */
    EraseRect(GG.rp,
        GG.left_corner,
        GG.top_corner,
        GG.left_corner + GG.max_width,
        GG.top_corner + GG.font_height
    );

    /* Center the title in the print-area */
    Move(GG.rp,
        GG.left_corner + (GG.max_width - Displayed_item->width)/2,
        GG.top_corner + GG.font_baseline
    );

    /* Use color2 (white) if we are showing the currently active win/scr, */
    /* color1 (black) otherwise */
    if (Displayed_item == Active_item)
        SetAPen(GG.rp, *Opt.color2);
    else
        SetAPen(GG.rp, *Opt.color1);

    Text(GG.rp, Displayed_item->title, Displayed_item->len);
}
///

/// close_window()
/*  ŻŻŻŻŻŻŻŻŻŻŻŻŻŻ
** Close the window and the font (if opened),
** then free all the items of the chain.
*/

void close_window(void)
{
    if (GG.font)
    {
        CloseFont(GG.font);
        GG.font = NULL;
    }

    if (GG.win)
    {
        CloseWindow(GG.win);
        GG.win = NULL;
    }

    Scr_open = Win_open = 0;
}
///

/// dispose_list()
/*  ŻŻŻŻŻŻŻŻŻŻŻŻŻŻ
** This routine frees all items of the chain.
*/

void dispose_list(void)
{
    struct record *item = First_item;
    while(item)
    {
        struct record *next = item->next;
        xfree(item);
        item = next;
    }
}
///

/// raise_win()
/*  ŻŻŻŻŻŻŻŻŻŻŻ
** Bring the selected win to the foreground, but only if it's not
** the currently active one.
*/

void raise_win(void)
{
    if (Displayed_item && Displayed_item != Active_item)
    {
        Last_active_win = CURRENTLY_ACTIVE_WIN; /* OMM! */
        WindowToFront(Displayed_item->id);
        ActivateWindow(Displayed_item->id);       
    }
}
///

/// raise_scr()
/*  ŻŻŻŻŻŻŻŻŻŻŻ
** Bring the selected scr to the foreground, but only if it's not
** the currently active one. If the AUTOACTIVE flag is on, then
** activate the first window of the newly raised screen.
*/

void raise_scr(void)
{
    if (Displayed_item && Displayed_item != Active_item)
    {
        Last_active_scr = CURRENTLY_ACTIVE_SCR; /* OMM! */
        ScreenToFront(Displayed_item->id);
        if (Opt.aa)
            /* Not yet fully functional! */
            ActivateWindow(FIRST_AVAILABLE_WIN);
    }
}
///

/// copy_title()
/*  ŻŻŻŻŻŻŻŻŻŻŻŻ
** Same as strcpy(), but ignores multiple spaces and copies
** only up to MAX_TITLE_LEN-1 chars (including trailing '\0').
**
** Returns the number of chars copied, same as strlen(dst).
*/

int copy_title(STRPTR dst, STRPTR src)
{
    int i = 0;

    while ( *src && i < MAX_TITLE_LEN-1 )
    {
        if (*src == ' ')
        {
            while (*src == ' ')
                ++src;
            if (i)
                dst[i++] = ' ';
        }
        else
        {
            dst[i++] = *(src++);
        }
    }

    dst[i] = '\0';

    return i;
}
///

/// ShowError()
/*  ŻŻŻŻŻŻŻŻŻŻŻ
** Prints an error message in a requester.
*/

int ShowError(STRPTR err)
{
    struct EasyStruct errmsg =
    {
        sizeof(struct EasyStruct),
        0,
        "Altabber",
        "An error has occurred:\n%s\nQuitting.",
        "Ok"
    };

    EasyRequest(NULL, &errmsg, NULL, err);

    return 10;
}
///

/// qualcode()
/*  ŻŻŻŻŻŻŻŻŻŻ
** Every "qualifier" key has a raw code associated, as any other
** "normal" key. Unfortunately there's no way to access to these keys
** as normal keys using the standard "input expression" method.
** So to get the correct raw code we must work it out manually
*/

int qualcode(STRPTR key)
{
    if (!stricmp(key, "ctrl"))      return 0x63;
    if (!stricmp(key, "control"))   return 0x63;
    if (!stricmp(key, "lalt"))      return 0x64;
    if (!stricmp(key, "ralt"))      return 0x65;
    if (!stricmp(key, "lamiga"))    return 0x66;
    if (!stricmp(key, "ramiga"))    return 0x67;
    if (!stricmp(key, "lcommand"))  return 0x66;
    if (!stricmp(key, "rcommand"))  return 0x67;
    if (!stricmp(key, "lshift"))    return 0x60;
    if (!stricmp(key, "rshift"))    return 0x61;
    if (!stricmp(key, "caps"))      return 0x62;
    if (!stricmp(key, "capslock"))  return 0x62;

    return 0;
}
///


