/* Copyright 2010-2011 Fredrik Wikstrom. All rights reserved.
**
** Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions
** are met:
**
** 1. Redistributions of source code must retain the above copyright
**    notice, this list of conditions and the following disclaimer.
**
** 2. Redistributions in binary form must reproduce the above copyright
**    notice, this list of conditions and the following disclaimer in the
**    documentation and/or other materials provided with the distribution.
**
** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS `AS IS'
** AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
** ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
** LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
** CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
** SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
** INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
** CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
** ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
** POSSIBILITY OF SUCH DAMAGE.
*/

#include <exec/exec.h>
#include <dos/dos.h>
#include <intuition/intuition.h>
#include <reaction/reaction.h>
#include <devices/ahi.h>
#include <proto/exec.h>
#include <proto/dos.h>
#include <proto/utility.h>
#include <proto/intuition.h>
#include <proto/icon.h>
#include <proto/graphics.h>
#include "scsicmd.h"
#include "diskchange.h"
#include "speedbar.h"
#include "debug.h"
#include "amiupdate.h"
#include "locale.h"
#include "endian.h"
#include "swab2.h"
#include "PlayCDDA_rev.h"

#define FRAME_SIZE 2352
#define CDDA_FRAMES 150
#define CDDA_BUFFER_SIZE (FRAME_SIZE*CDDA_FRAMES)
#define AHI_FRAMES 15
#define AHI_BUFFER_SIZE (FRAME_SIZE*AHI_FRAMES)

#ifndef MIN
#define MIN(a,b) ((a)<=(b)?(a):(b))
#endif
#define SWAP_PTR(a,b) { APTR tmp_ptr = a; a = b; b = tmp_ptr; }

#define GA(o) ((struct Gadget *)o)

#define PROGNAME "PlayCDDA"
const TEXT USED verstag[] = VERSTAG;

struct LocaleInfo LocaleInfo;

enum {
	MID_DUMMY = 0,
	MID_ABOUT,
	MID_ICONIFY,
	MID_QUIT
};

enum {
	GID_POSITION = 0,
	GID_SPEEDBAR,
	GID_VOLUME,
	GID_TRACK01,
	GID_TRACK32 = GID_TRACK01 + 31,
	GID_STATUSDISP,
	NUM_GADGETS
};

enum {
	SBID_EJECT = 1,
	SBID_STOP,
	SBID_PAUSE,
	SBID_PREV,
	SBID_PLAY,
	SBID_NEXT
};

struct MainGUI {
	struct Screen *screen;
	Object *window;
	struct MsgPort *app_mp;
	struct DiskObject *icon;
	struct List *speedbuttons;
	struct Hook *idcmphook;
	Object *gadgets[NUM_GADGETS];
	volatile int8 seek_mode;
	uint32 varargs[3];
};

struct MainGUI *CreateGUI (void);
void DeleteGUI (struct MainGUI *gui);
void UpdateGUI (struct MainGUI *gui, struct CDTOC *cd_toc);
Object *CreateTable (int32 rows, int32 cols, int32 base_id, Object **buttons);
Object *CreateColumn (int32 rows, int32 cols, int32 i, int32 base_id, Object **buttons);
void AboutRequester (struct MainGUI *gui);

int32 cd_in_drive = FALSE;

int main () {
	struct IOStdReq *cd_io = NULL;
	APTR cd_change = NULL;
	struct MainGUI *gui = NULL;
	int32 cd_signal = -1;
	uint32 sigs, cd_sig;
	uint32 res, gad_id;
	uint16 code;
	int32 load_eject = 1;
	struct CDTOC cd_toc;
	int32 quit = FALSE;
	int rc = RETURN_FAIL;
	uint32 volume = 0x10000;
	const char *dosdev;
	struct Menu *menu;
	struct MenuItem *item;
	uint32 menu_id;
	struct MsgPort *ahi_mp = NULL;
	struct AHIRequest *ahi_io = NULL, *ahi_io2 = NULL, *join = NULL;
	APTR ahi_buf = NULL, ahi_buf2 = NULL;
	struct IOStdReq *cdda_io = NULL;
	BOOL cdda_io_busy = FALSE;
	APTR cdda_buf = NULL, cdda_buf2 = NULL;
	uint32 cdda_addr = -1, cdda_pos = 0, cdda_frames = 0;
	struct SCSICmd scsicmd = { 0 };
	UBYTE scsisense[20];
	UBYTE readcmd[12] = { SCSI_CMD_READ_CD, 0x04, 0, 0, 0, 0, 0, 0, 0, 0x10, 0, 0 };
	
	SetAmiUpdateENVVariable(PROGNAME);

	InitLocaleInfo(&LocaleInfo, PROGNAME".catalog");

	if (!(ahi_buf = AllocVecTags(AHI_BUFFER_SIZE,
			AVT_Type,	MEMF_SHARED,
			TAG_END)) ||
		!(ahi_buf2 = AllocVecTags(AHI_BUFFER_SIZE,
			AVT_Type,	MEMF_SHARED,
			TAG_END)) ||
		!(cdda_buf = AllocVecTags(CDDA_BUFFER_SIZE,
			AVT_Type,	MEMF_SHARED,
			TAG_END)) ||
		!(cdda_buf2 = AllocVecTags(CDDA_BUFFER_SIZE,
			AVT_Type,	MEMF_SHARED,
			TAG_END)))
	{
		goto error;
	}

	ahi_mp = AllocSysObject(ASOT_PORT, NULL);
	ahi_io = AllocSysObjectTags(ASOT_IOREQUEST,
		ASOIOR_Size,		sizeof(*ahi_io),
		ASOIOR_ReplyPort,	ahi_mp,
		TAG_END);
	if (!ahi_io) {
		goto error;
	}
	if (OpenDevice(AHINAME, AHI_DEFAULT_UNIT, (struct IORequest *)ahi_io, 0) != IOERR_SUCCESS) {
		ahi_io->ahir_Std.io_Device = NULL;
		goto error;
	}
	ahi_io2 = AllocSysObjectTags(ASOT_IOREQUEST,
		ASOIOR_Duplicate,	ahi_io,
		TAG_END);
	if (!ahi_io2) {
		goto error;
	}

	gui = CreateGUI();
	if (!gui) {
		goto error;
	}

	dosdev = FindToolType(gui->icon->do_ToolTypes, "DOSDEV");
	if (!dosdev) {
		dosdev = "CD0";
	}
	cd_io = GetCDDevice(dosdev);
	if (!cd_io) {
		goto error;
	}

	cdda_io = AllocSysObjectTags(ASOT_IOREQUEST,
		ASOIOR_Duplicate,	cd_io,
		TAG_END);
	if (!cdda_io) {
		goto error;
	}

	scsicmd.scsi_SenseData = scsisense;
	scsicmd.scsi_SenseLength = sizeof(scsisense);
	scsicmd.scsi_Command = readcmd;
	scsicmd.scsi_CmdLength = sizeof(readcmd);
	scsicmd.scsi_Flags = SCSIF_READ|SCSIF_AUTOSENSE;
	cdda_io->io_Command = HD_SCSICMD;
	cdda_io->io_Data = &scsicmd;
	cdda_io->io_Length = sizeof(scsicmd);

	cd_signal = AllocSignal(-1);
	cd_sig = 1UL << cd_signal;
	cd_change = AddDiskChangeHandler(cd_io, cd_sig);
	if (!cd_change) {
		goto error;
	}
	
	cd_io->io_Command = TD_CHANGESTATE;
	DoIO((struct IORequest *)cd_io);
	cd_in_drive = (cd_io->io_Error || cd_io->io_Actual) ? FALSE : TRUE;
	ClearMem(&cd_toc, sizeof(cd_toc));
	if (cd_in_drive) {
		load_eject = 1;
		ReadCDTOC(cd_io, &cd_toc);
		cdda_addr = cd_toc.CurrentAddr;
	}
	UpdateGUI(gui, &cd_toc);

	rc = RETURN_OK;
	while (!quit) {
		GetAttr(WINDOW_SigMask, gui->window, &sigs);
		sigs |= (cd_sig|SIGBREAKF_CTRL_C|SIGBREAKF_CTRL_F);
		if (!cd_toc.IsPlaying || cd_toc.IsPaused) {
			sigs = Wait(sigs);
		} else {
			sigs = CheckSignal(sigs);
		}
		
		if (sigs & SIGBREAKF_CTRL_C) {
			break;
		}

		if (sigs & SIGBREAKF_CTRL_F) {
			RA_OpenWindow(gui->window);
		}

		if (sigs & cd_sig) {
			if (cdda_io_busy) {
				AbortIO((struct IORequest *)cdda_io);
				WaitIO((struct IORequest *)cdda_io);
				cdda_io_busy = FALSE;
			}
			cdda_frames = 0;
			cd_io->io_Command = TD_CHANGESTATE;
			DoIO((struct IORequest *)cd_io);
			cd_in_drive = (cd_io->io_Error || cd_io->io_Actual) ? FALSE : TRUE;
			ClearMem(&cd_toc, sizeof(cd_toc));
			if (cd_in_drive) {
				load_eject = 1;
				ReadCDTOC(cd_io, &cd_toc);
			}
			UpdateGUI(gui, &cd_toc);
			cdda_addr = cd_toc.CurrentAddr;
		}

		while ((res = RA_HandleInput(gui->window, &code)) != WMHI_LASTMSG) {
			switch (res & WMHI_CLASSMASK) {
				case WMHI_CLOSEWINDOW:
					quit = TRUE;
					break;
						
				case WMHI_ICONIFY:
					RA_Iconify(gui->window);
					break;
						
				case WMHI_UNICONIFY:
					RA_OpenWindow(gui->window);
					break;

				case WMHI_MENUPICK:
					GetAttr(WINDOW_MenuStrip, gui->window, (uint32 *)&menu);
					while (menu && (item = ItemAddress(menu, code))) {
						menu_id = (uint32)GTMENUITEM_USERDATA(item);
						code = item->NextSelect;
						switch (menu_id) {
							case MID_ABOUT:
								AboutRequester(gui);
								break;
							case MID_ICONIFY:
								RA_Iconify(gui->window);
								menu = NULL;
								break;
							case MID_QUIT:
								quit = TRUE;
								break;
						}
					}
					break;

				case WMHI_GADGETUP:
					gad_id = res & WMHI_GADGETMASK;
					switch (gad_id) {
						case GID_POSITION:
							gui->seek_mode = FALSE;
							if (cdda_io_busy) {
								AbortIO((struct IORequest *)cdda_io);
								WaitIO((struct IORequest *)cdda_io);
								cdda_io_busy = FALSE;
							}
							cdda_frames = 0;
							GetAttr(SLIDER_Level, gui->gadgets[GID_POSITION], &cd_toc.CurrentAddr);
							cdda_addr = cd_toc.CurrentAddr;
							break;
					
						case GID_SPEEDBAR:
							switch (code) {
								case SBID_EJECT:
									cd_io->io_Command = TD_EJECT;
									cd_io->io_Length = load_eject;
									DoIO((struct IORequest *)cd_io);
									load_eject ^= 1;
									break;
								
								case SBID_STOP:
									if (cd_toc.IsPlaying) {
										if (cdda_io_busy) {
											AbortIO((struct IORequest *)cdda_io);
											WaitIO((struct IORequest *)cdda_io);
											cdda_io_busy = FALSE;
										}
										cdda_frames = 0;
										cd_toc.IsPlaying = FALSE;
										cd_toc.IsPaused = FALSE;
										cdda_addr = cd_toc.CurrentAddr =
											cd_toc.Tracks[cd_toc.CurrentTrack-1].StartAddr;
										UpdateGUI(gui, &cd_toc);
									}
									break;
								
								case SBID_PAUSE:
									if (cd_toc.IsPlaying && !cd_toc.IsPaused) {
										cd_toc.IsPaused = TRUE;
										UpdateGUI(gui, &cd_toc);
									}
									break;
								
								case SBID_PREV:
									if (cd_toc.CurrentTrack > cd_toc.FirstTrack) {
										if (cdda_io_busy) {
											AbortIO((struct IORequest *)cdda_io);
											WaitIO((struct IORequest *)cdda_io);
											cdda_io_busy = FALSE;
										}
										cdda_frames = 0;
										cd_toc.CurrentTrack--;
										cdda_addr = cd_toc.CurrentAddr =
											cd_toc.Tracks[cd_toc.CurrentTrack-1].StartAddr;
										cd_toc.IsPaused = FALSE;
										UpdateGUI(gui, &cd_toc);
									}
									break;
								
								case SBID_PLAY:
									if (!cd_toc.IsPlaying || cd_toc.IsPaused) {
										cd_toc.IsPlaying = TRUE;
										cd_toc.IsPaused = FALSE;
										UpdateGUI(gui, &cd_toc);
									}
									break;
								
								case SBID_NEXT:
									if (cd_toc.CurrentTrack < cd_toc.LastTrack) {
										if (cdda_io_busy) {
											AbortIO((struct IORequest *)cdda_io);
											WaitIO((struct IORequest *)cdda_io);
											cdda_io_busy = FALSE;
										}
										cdda_frames = 0;
										cd_toc.CurrentTrack++;
										cdda_addr = cd_toc.CurrentAddr =
											cd_toc.Tracks[cd_toc.CurrentTrack-1].StartAddr;
										cd_toc.IsPaused = FALSE;
										UpdateGUI(gui, &cd_toc);
									}
									break;
							}
							break;
							
						case GID_VOLUME:
							volume = code << 10;
							break;

						default:
							if (gad_id >= GID_TRACK01 && gad_id <= GID_TRACK32) {
								if (cdda_io_busy) {
									AbortIO((struct IORequest *)cdda_io);
									WaitIO((struct IORequest *)cdda_io);
									cdda_io_busy = FALSE;
								}
								cdda_frames = 0;
								cd_toc.CurrentTrack = gad_id - GID_TRACK01 + 1;
								cdda_addr = cd_toc.CurrentAddr =
									cd_toc.Tracks[cd_toc.CurrentTrack-1].StartAddr;
								cd_toc.IsPlaying = TRUE;
								cd_toc.IsPaused = FALSE;
								UpdateGUI(gui, &cd_toc);
							}
							break;
					}
					break;
			}
		}
		if (quit) {
			break;
		}

		if (cd_toc.IsPlaying && !cd_toc.IsPaused) {
			uint32 track = cd_toc.CurrentTrack;
			uint32 addr = cd_toc.CurrentAddr;
			uint32 endaddr = cd_toc.Tracks[cd_toc.LastTrack-1].EndAddr;
			uint32 trackaddr = addr - cd_toc.Tracks[track-1].StartAddr;
			if (!cdda_frames) {
				uint32 frames, bytes;
				uint32 cdda_endaddr = 
				cdda_pos = 0;
				if (cdda_io_busy) {
					WaitIO((struct IORequest *)cdda_io);
					cdda_io_busy = FALSE;
					SWAP_PTR(cdda_buf, cdda_buf2);
				} else {
					frames = MIN(CDDA_FRAMES, endaddr - cdda_addr);
					bytes = frames * FRAME_SIZE;
					scsicmd.scsi_Data = cdda_buf;
					scsicmd.scsi_Length = bytes;
					wbe32(&readcmd[2], cdda_addr);
					readcmd[8] = frames;
					DoIO((struct IORequest *)cdda_io);
					cdda_addr += frames;
				}
				if (cdda_io->io_Error == IOERR_SUCCESS) {
					cdda_frames = CDDA_FRAMES;
					frames = MIN(CDDA_FRAMES, endaddr - cdda_addr);
					bytes = frames * FRAME_SIZE;
					scsicmd.scsi_Data = cdda_buf2;
					scsicmd.scsi_Length = bytes;
					wbe32(&readcmd[2], cdda_addr);
					readcmd[8] = frames;
					SendIO((struct IORequest *)cdda_io);
					cdda_addr += frames;
					cdda_io_busy = TRUE;
				}
			}
			gui->varargs[0] = track;
			gui->varargs[1] = (trackaddr / 75) / 60;
			gui->varargs[2] = (trackaddr / 75) % 60;
			if (!gui->seek_mode) {
				struct Window *window = NULL;
				GetAttr(WINDOW_Window, gui->window, (APTR)&window);
				if (window && AttemptLockLayerRom(gui->screen->BarLayer)) {
					UnlockLayerRom(gui->screen->BarLayer);
					SetGadgetAttrs(GA(gui->gadgets[GID_STATUSDISP]), window, NULL,
						GA_Text,		GetString(&LocaleInfo, MSG_PLAYING),
						BUTTON_VarArgs,	gui->varargs,
						TAG_END);
					SetGadgetAttrs(GA(gui->gadgets[GID_POSITION]), window, NULL,
						SLIDER_Level,	addr,
						TAG_END);
				}
			}
			if (cdda_frames) {
				uint32 frames = MIN(AHI_FRAMES, endaddr - addr);
				uint32 bytes = frames * FRAME_SIZE;
				swab2((UBYTE *)cdda_buf + (cdda_pos * FRAME_SIZE), ahi_buf, bytes);
				ahi_io->ahir_Std.io_Command = CMD_WRITE;
				ahi_io->ahir_Std.io_Offset = 0;
				ahi_io->ahir_Std.io_Data = ahi_buf;
				ahi_io->ahir_Std.io_Length = bytes;
				ahi_io->ahir_Type = AHIST_S16S;
				ahi_io->ahir_Frequency = 44100;
				ahi_io->ahir_Volume = volume;
				ahi_io->ahir_Position = 0x8000;
				ahi_io->ahir_Link = join;
				SendIO((struct IORequest *)ahi_io);
				if (join) {
					WaitIO((struct IORequest *)join);
				}
				join = ahi_io;
				SWAP_PTR(ahi_io, ahi_io2);
				SWAP_PTR(ahi_buf, ahi_buf2);
				addr += frames;
				cdda_pos += frames;
				cdda_frames -= frames;
				cd_toc.CurrentAddr = addr;
				if (addr == endaddr) {
					cd_toc.IsPlaying = FALSE;
				} else if (addr >= cd_toc.Tracks[track-1].EndAddr) {
					while (addr >= cd_toc.Tracks[track-1].EndAddr) {
						cd_toc.CurrentTrack = ++track;
					}
					UpdateGUI(gui, &cd_toc);
				}
			} else {
				cd_toc.IsPlaying = FALSE;
			}
			if (!cd_toc.IsPlaying) {
				if (cdda_io_busy) {
					AbortIO((struct IORequest *)cdda_io);
					WaitIO((struct IORequest *)cdda_io);
					cdda_io_busy = FALSE;
				}
				cd_toc.IsPaused = FALSE;
				cd_toc.CurrentTrack = track = cd_toc.FirstTrack;
				cdda_addr = cd_toc.CurrentAddr = cd_toc.Tracks[track-1].StartAddr;
				cdda_frames = 0;
				UpdateGUI(gui, &cd_toc);
			}
		}
	}
	
error:
	if (ahi_io && ahi_io->ahir_Std.io_Device) {
		CloseDevice((struct IORequest *)ahi_io);
	}
	if (cdda_io && cdda_io->io_Device && cdda_io_busy) {
		AbortIO((struct IORequest *)cdda_io);
		WaitIO((struct IORequest *)cdda_io);
	}
	FreeSysObject(ASOT_IOREQUEST, cdda_io);
	FreeVec(ahi_buf);
	FreeVec(ahi_buf2);
	FreeVec(cdda_buf);
	FreeVec(cdda_buf2);
	RemDiskChangeHandler(cd_change);
	FreeSignal(cd_signal);
	FreeCDDevice(cd_io);
	DeleteGUI(gui);
	FreeSysObject(ASOT_IOREQUEST, ahi_io);
	FreeSysObject(ASOT_IOREQUEST, ahi_io2);
	FreeSysObject(ASOT_PORT, ahi_mp);
	FreeLocaleInfo(&LocaleInfo);

	return rc;
}

#define STR_ID(x) ((STRPTR)(x))
#define MENU_ID(x) ((APTR)(x))

static BOOL translate_gui = TRUE;

static struct NewMenu menu_specs[] = {
	{ NM_TITLE, STR_ID(-1),                  NULL, 0, 0, MENU_ID(0)           },
	{ NM_ITEM,  STR_ID(MSG_PROJECT_ABOUT),   "?",  0, 0, MENU_ID(MID_ABOUT)   },
	{ NM_ITEM,  STR_ID(MSG_PROJECT_ICONIFY), "I",  0, 0, MENU_ID(MID_ICONIFY) },
	{ NM_ITEM,  NM_BARLABEL,                 NULL, 0, 0, MENU_ID(0)           },
	{ NM_ITEM,  STR_ID(MSG_PROJECT_QUIT),    "Q",  0, 0, MENU_ID(MID_QUIT)    },
	{ NM_END,   STR_ID(-1),                  NULL, 0, 0, MENU_ID(0)           }
};

static const struct SpeedButtonSpec sb_specs[] = {
	{ SBID_EJECT,              0, "tapeeject" },
	{ SBID_STOP,  SBFLG_DISABLED, "tapestop"  },
	{ SBID_PAUSE, SBFLG_DISABLED, "tapepause" },
	{ SBID_PREV,  SBFLG_DISABLED, "tapelast"  },
	{ SBID_PLAY,  SBFLG_DISABLED, "tapeplay"  },
	{ SBID_NEXT,  SBFLG_DISABLED, "tapenext"  },
	{ ~0,                      0, NULL,       }
};

static struct HintInfo hintinfos[] = {
	{ GID_SPEEDBAR, SBID_EJECT, STR_ID(MSG_EJECT_GAD), 0 },
	{ GID_SPEEDBAR, SBID_STOP,  STR_ID(MSG_STOP_GAD),  0 },
	{ GID_SPEEDBAR, SBID_PAUSE, STR_ID(MSG_PAUSE_GAD), 0 },
	{ GID_SPEEDBAR, SBID_PREV,  STR_ID(MSG_PREV_GAD),  0 },
	{ GID_SPEEDBAR, SBID_PLAY,  STR_ID(MSG_PLAY_GAD),  0 },
	{ GID_SPEEDBAR, SBID_NEXT,  STR_ID(MSG_NEXT_GAD),  0 },
	{ -1,           -1,         NULL,                  0 }
};

uint32 idcmp_hook_func (struct Hook *hook, Object *window, struct IntuiMessage *msg) {
	struct MainGUI *gui = hook->h_Data;
	struct Gadget *gadget;
	switch (msg->Class) {
		case IDCMP_GADGETDOWN:
			gadget = msg->IAddress;
			if (gadget->GadgetID == GID_POSITION) {
				gui->seek_mode = TRUE;
			}
			break;
		case IDCMP_GADGETUP:
			gadget = msg->IAddress;
			if (gadget->GadgetID == GID_POSITION) {
				gui->seek_mode = FALSE;
			}
			break;
	}
	return 0;
}

struct MainGUI *CreateGUI (void) {
	struct MainGUI *gui;
	
	if (translate_gui) {
		translate_gui = FALSE;
		TranslateMenus(&LocaleInfo, menu_specs);
		menu_specs[0].nm_Label = PROGNAME;
		TranslateHints(&LocaleInfo, hintinfos);
	}

	gui = AllocVec(sizeof(struct MainGUI), MEMF_PRIVATE|MEMF_CLEAR);
	if (!gui) {
		goto error;
	}
	
	gui->screen = LockPubScreen(NULL);
	if (!gui->screen) {
		goto error;
	}
	
	gui->app_mp = AllocSysObject(ASOT_PORT, NULL);
	if (!gui->app_mp) {
		goto error;
	}
	
	gui->icon = GetDiskObjectNew("PROGDIR:"PROGNAME);
	if (!gui->icon) {
		goto error;
	}
	
	gui->speedbuttons = MakeSpeedButtonList(gui->screen, sb_specs);
	if (!gui->speedbuttons) {
		goto error;
	}
	
	gui->idcmphook = AllocSysObjectTags(ASOT_HOOK,
		ASOHOOK_Entry,	idcmp_hook_func,
		ASOHOOK_Data,	gui,
		TAG_END);
	if (!gui->idcmphook) {
		goto error;
	}

	gui->window = WindowObject,
		WINDOW_CharSet,			LocaleInfo.li_CodeSet,
		WA_Title,				VERS,
		WA_Flags,				WFLG_CLOSEGADGET|WFLG_DRAGBAR|WFLG_DEPTHGADGET
								|WFLG_NOCAREREFRESH|WFLG_ACTIVATE,
		WA_IDCMP,				IDCMP_CLOSEWINDOW|IDCMP_GADGETDOWN|IDCMP_GADGETUP
								|IDCMP_MENUPICK,
		WINDOW_Position,		WPOS_CENTERMOUSE,
		WINDOW_AppPort,			gui->app_mp,
		WINDOW_IconifyGadget,	TRUE,
		WINDOW_IconTitle,		PROGNAME,
		WINDOW_Icon,			gui->icon,
		WINDOW_IconNoDispose,	TRUE,
		WINDOW_IDCMPHook,		gui->idcmphook,
		WINDOW_IDCMPHookBits,	IDCMP_GADGETDOWN|IDCMP_GADGETUP,
		WINDOW_NewMenu,			menu_specs,
		WINDOW_HintInfo,		hintinfos,
		WINDOW_GadgetHelp,		TRUE,
		WINDOW_Layout,			HLayoutObject,
			LAYOUT_AddChild,				VLayoutObject,
				LAYOUT_AddChild,			gui->gadgets[GID_STATUSDISP] = ButtonObject,
					GA_ID,					GID_STATUSDISP,
					GA_ReadOnly,			TRUE,
					GA_Text,				"",
				End,
				LAYOUT_AddChild,			gui->gadgets[GID_POSITION] = SliderObject,
					GA_ID,					GID_POSITION,
					GA_Immediate,			TRUE,
					GA_RelVerify,			TRUE,
					GA_Disabled,			TRUE,
					SLIDER_Orientation,		SLIDER_HORIZONTAL,
				End,
				CHILD_WeightedHeight,		0,
				LAYOUT_AddChild,			gui->gadgets[GID_SPEEDBAR] = SpeedBarObject,
					GA_ID,					GID_SPEEDBAR,
					GA_RelVerify,			TRUE,
					SPEEDBAR_Buttons,		gui->speedbuttons,
					SPEEDBAR_BevelStyle,	BVS_NONE,
				End,
				CHILD_NominalSize,			TRUE,
				CHILD_WeightedHeight,		0,
			End,
			LAYOUT_AddChild,				CreateTable(4, 8, GID_TRACK01, &gui->gadgets[GID_TRACK01]),
			LAYOUT_AddChild,				VLayoutObject,
				LAYOUT_HorizAlignment,		LALIGN_CENTER,
				LAYOUT_AddChild,			gui->gadgets[GID_VOLUME] = SliderObject,
					GA_ID,					GID_VOLUME,
					GA_RelVerify,			TRUE,
					SLIDER_Orientation,		SLIDER_VERTICAL,
					SLIDER_Min,				0,
					SLIDER_Max,				64,
					SLIDER_Level,			64,
					SLIDER_Invert,			TRUE,
				End,
				CHILD_WeightedWidth,		0,
				LAYOUT_AddImage,			LabelObject,
					LABEL_Text,				GetString(&LocaleInfo, MSG_VOLUME_GAD),
				End,
			End,
		End,
	End;
	if (!gui->window) {
		goto error;
	}
	
	if (RA_OpenWindow(gui->window)) {
		return gui;
	}
	
error:
	DeleteGUI(gui);
	return NULL;
}

void DeleteGUI (struct MainGUI *gui) {
	if (gui) {
		DisposeObject(gui->window);
		FreeSysObject(ASOT_HOOK, gui->idcmphook);
		FreeSpeedButtonList(gui->speedbuttons);
		FreeDiskObject(gui->icon);
		FreeSysObject(ASOT_PORT, gui->app_mp);
		UnlockPubScreen(NULL, gui->screen);
		FreeVec(gui);
	}
}

void UpdateGUI (struct MainGUI *gui, struct CDTOC *cd_toc) {
	struct Window *window;
	uint32 track, track_addr;
	struct List *sb_list;
	struct Node *sb_node;
	int32 enable;

	GetAttr(WINDOW_Window, gui->window, (APTR)&window);

	track = cd_toc->CurrentTrack;
	if (track) {
		track_addr = cd_toc->CurrentAddr - cd_toc->Tracks[track-1].StartAddr;
	} else {
		track_addr = 0;
	}
	gui->varargs[0] = track;
	gui->varargs[1] = (track_addr / 75UL) / 60UL;
	gui->varargs[2] = (track_addr / 75UL) % 60UL;
	if (cd_toc->IsPlaying && track && cd_toc->Tracks[track-1].IsAudio) {
		SetGadgetAttrs(GA(gui->gadgets[GID_STATUSDISP]), window, NULL,
			GA_Text,		GetString(&LocaleInfo, MSG_PLAYING),
			BUTTON_VarArgs,	gui->varargs,
			TAG_END);
		SetGadgetAttrs(GA(gui->gadgets[GID_POSITION]), window, NULL,
			GA_Disabled,	FALSE,
			SLIDER_Min,		cd_toc->Tracks[track-1].StartAddr,
			SLIDER_Max,		cd_toc->Tracks[track-1].EndAddr,
			SLIDER_Level,	cd_toc->CurrentAddr,
			TAG_END);
	} else {
		SetGadgetAttrs(GA(gui->gadgets[GID_STATUSDISP]), window, NULL,
			GA_Text,		track ? GetString(&LocaleInfo, MSG_STOPPED) : 
							cd_in_drive ? GetString(&LocaleInfo, MSG_NOTRACK) :
							GetString(&LocaleInfo, MSG_NODISC),
			BUTTON_VarArgs,	gui->varargs,
			TAG_END);
		SetGadgetAttrs(GA(gui->gadgets[GID_POSITION]), window, NULL,
			GA_Disabled,	TRUE,
			SLIDER_Level,	0,
			TAG_END);
	}

	for (track = 0; track < MAX_TRACKS; track++) {
		SetGadgetAttrs(GA(gui->gadgets[GID_TRACK01 + track]), window, NULL,
			GA_Disabled,		cd_toc->Tracks[track].IsAudio ? FALSE : TRUE,
			BUTTON_SoftStyle,	cd_toc->CurrentTrack == (track + 1) ? FSF_BOLD : FS_NORMAL,
			TAG_END);
	}
	
	sb_list = gui->speedbuttons;
	SetAttrs(gui->gadgets[GID_SPEEDBAR],
		SPEEDBAR_Buttons,	~0,
		TAG_END);
	for (sb_node = GetHead(sb_list); sb_node; sb_node = GetSucc(sb_node)) {
		switch (sb_node->ln_Pri) {
			case SBID_EJECT:
				enable = TRUE;
				break;
			case SBID_STOP:
				enable = cd_toc->IsPlaying || cd_toc->IsPaused;
				break;
			case SBID_PAUSE:
				enable = cd_toc->IsPlaying && !cd_toc->IsPaused;
				break;
			case SBID_PREV:
				enable = cd_toc->CurrentTrack && (cd_toc->CurrentTrack > cd_toc->FirstTrack);
				break;
			case SBID_PLAY:
				enable = cd_toc->CurrentTrack && (!cd_toc->IsPlaying || cd_toc->IsPaused);
				break;
			case SBID_NEXT:
				enable = cd_toc->CurrentTrack && (cd_toc->CurrentTrack < cd_toc->LastTrack);
				break;
		}
		SetSpeedButtonNodeAttrs(sb_node,
			SBNA_Disabled,	!enable,
			TAG_END);
	}
	SetGadgetAttrs(GA(gui->gadgets[GID_SPEEDBAR]), window, NULL,
		SPEEDBAR_Buttons,	sb_list,
		TAG_END);
}

Object *CreateTable (int32 rows, int32 cols, int32 base_id, Object **buttons) {
	struct TagItem *tags, *tag;
	Object *table;
	Object *column;
	int32 i;

	tags = AllocSysObjectTags(ASOT_TAGLIST,
		ASOTAGS_NumEntries,	cols + 2,
		TAG_END);
	if (!tags) {
		return NULL;
	}
	
	tag = tags;
	tag->ti_Tag = LAYOUT_Orientation;
	tag->ti_Data = LAYOUT_HORIZONTAL;
	tag++;
	
	for (i = 0; i < cols; i++) {
		column = CreateColumn(rows, cols, i, base_id, buttons);
		tag->ti_Tag = LAYOUT_AddChild;
		tag->ti_Data = (Tag)column;
		tag++;
	}
	
	tag->ti_Tag = TAG_END;
	table = NewObjectA(LAYOUT_GetClass(), NULL, tags);
	FreeSysObject(ASOT_TAGLIST, tags);
	
	return table;
}

Object *CreateColumn (int32 rows, int32 cols, int32 i, int32 base_id, Object **buttons) {
	struct TagItem *tags, *tag;
	Object *column;
	int32 j;
	
	tags = AllocSysObjectTags(ASOT_TAGLIST,
		ASOTAGS_NumEntries, rows + 2,
		TAG_END);
	if (!tags) {
		return NULL;
	}
	
	tag = tags;
	tag->ti_Tag = LAYOUT_Orientation;
	tag->ti_Data = LAYOUT_VERTICAL;
	tag++;
	
	for (j = 0; j < rows; j++, i += cols) {
		buttons[i] = ButtonObject,
			GA_ID,					base_id + i,
			GA_RelVerify,			TRUE,
			GA_Disabled,			TRUE,
			BUTTON_Integer,			i + 1,
		End;
		tag->ti_Tag = LAYOUT_AddChild;
		tag->ti_Data = (Tag)buttons[i];
		tag++;
	}
	
	tag->ti_Tag = TAG_END;
	column = NewObjectA(LAYOUT_GetClass(), NULL, tags);
	FreeSysObject(ASOT_TAGLIST, tags);
	
	return column;
}

void AboutRequester (struct MainGUI *gui) {
	uint32 args[3];
	TEXT titletext[40];
	CONST_STRPTR bodytext;
	CONST_STRPTR gadgettext;
	Object *req;

	args[0] = (uint32)PROGNAME;
	args[1] = VERSION;
	args[2] = REVISION;

	SNPrintf(titletext, sizeof(titletext), GetString(&LocaleInfo, MSG_ABOUT_WND), PROGNAME);
	bodytext = GetString(&LocaleInfo, MSG_ABOUT_REQ);
	gadgettext = GetString(&LocaleInfo, MSG_OK_GAD);

	req = RequesterObject,
		REQ_CharSet,	LocaleInfo.li_CodeSet,
		REQ_TitleText,	titletext,
		REQ_BodyText,	bodytext,
		REQ_VarArgs,	args,
		REQ_GadgetText,	gadgettext,
	End;

	if (req) {
		struct Window *window = NULL;
		SetAttrs(gui->window, WA_BusyPointer, TRUE, TAG_END);
		GetAttr(WINDOW_Window, gui->window, (uint32 *)&window);
		DoMethod(req, RM_OPENREQ, NULL, window, gui->screen);
		SetAttrs(gui->window, WA_BusyPointer, FALSE, TAG_END);
		DisposeObject(req);
	}
}
