/**
 * \file main-cocoa.m
 * \brief OS X front end
 *
 * Copyright (c) 2011 Peter Ammon
 *
 * This work is free software; you can redistribute it and/or modify it
 * under the terms of either:
 *
 * a) the GNU General Public License as published by the Free Software
 *    Foundation, version 2, or
 *
 * b) the "Angband licence":
 *    This software may be copied and distributed for educational, research,
 *    and not for profit purposes provided that this copyright and statement
 *    are included in all such copies.  Other copyrights may also apply.
 */

#include "angband.h"
/* This is not included in angband.h in Hengband. */
#include "grafmode.h"

#include "cmd-dump.h"
#include "init.h"
#include "files.h"
#include "core.h"
#include "world.h"
#include "term.h"

#if defined(MACH_O_COCOA)

/* Mac headers */
#include <cocoa/AppDelegate.h>
//#include <Carbon/Carbon.h> /* For keycodes */
/* Hack - keycodes to enable compiling in macOS 10.14 */
#define kVK_Return 0x24
#define kVK_Tab    0x30
#define kVK_Delete 0x33
#define kVK_Escape 0x35
#define kVK_ANSI_KeypadEnter 0x4C

static NSString * const AngbandDirectoryNameLib = @"lib";
static NSString * const AngbandDirectoryNameBase = @"Hengband";

static NSString * const AngbandMessageCatalog = @"Localizable";
static NSString * const AngbandTerminalsDefaultsKey = @"Terminals";
static NSString * const AngbandTerminalRowsDefaultsKey = @"Rows";
static NSString * const AngbandTerminalColumnsDefaultsKey = @"Columns";
static NSString * const AngbandTerminalVisibleDefaultsKey = @"Visible";
static NSString * const AngbandGraphicsDefaultsKey = @"GraphicsID";
static NSString * const AngbandFrameRateDefaultsKey = @"FramesPerSecond";
static NSString * const AngbandSoundDefaultsKey = @"AllowSound";
static NSInteger const AngbandWindowMenuItemTagBase = 1000;
static NSInteger const AngbandCommandMenuItemTagBase = 2000;

/* We can blit to a large layer or image and then scale it down during live
 * resize, which makes resizing much faster, at the cost of some image quality
 * during resizing */
#ifndef USE_LIVE_RESIZE_CACHE
# define USE_LIVE_RESIZE_CACHE 1
#endif

/* Global defines etc from Angband 3.5-dev - NRM */
#define ANGBAND_TERM_MAX 8

static bool new_game = TRUE;

#define MAX_COLORS 256
#define MSG_MAX SOUND_MAX

/* End Angband stuff - NRM */

/* Application defined event numbers */
enum
{
    AngbandEventWakeup = 1
};

/* Redeclare some 10.7 constants and methods so we can build on 10.6 */
enum
{
    Angband_NSWindowCollectionBehaviorFullScreenPrimary = 1 << 7,
    Angband_NSWindowCollectionBehaviorFullScreenAuxiliary = 1 << 8
};

@interface NSWindow (AngbandLionRedeclares)
- (void)setRestorable:(BOOL)flag;
@end

/* Delay handling of pre-emptive "quit" event */
static BOOL quit_when_ready = FALSE;

/* Set to indicate the game is over and we can quit without delay */
static Boolean game_is_finished = FALSE;

/* Our frames per second (e.g. 60). A value of 0 means unthrottled. */
static int frames_per_second;

/* Function to get the default font */
static NSFont *default_font;

@class AngbandView;

/*
 * To handle fonts where an individual glyph's bounding box can extend into
 * neighboring columns, Term_curs_cocoa(), Term_pict_cocoa(),
 * Term_text_cocoa(), and Term_wipe_cocoa() merely record what needs to be
 * done with the actual drawing happening in response to the notification to
 * flush all rows, the TERM_XTRA_FRESH case in Term_xtra_cocoa().  Can not use
 * the TERM_XTRA_FROSH notification (the per-row flush), since with a software
 * cursor, there are calls to Term_pict_cocoa(), Term_text_cocoa(), or
 * Term_wipe_cocoa() to take care of the old cursor position which are not
 * followed by a row flush.
 */
enum PendingCellChangeType {
    CELL_CHANGE_NONE = 0,
    CELL_CHANGE_WIPE,
    CELL_CHANGE_TEXT,
    CELL_CHANGE_PICT
};
struct PendingCellChange {
    /*
     * For text rendering, stores the character as a wchar_t; for tile
     * rendering, stores the column in the tile set for the source tile.
     */
    union { wchar_t w; char c; } c;
    /*
     * For text rendering, stores the color; for tile rendering, stores the
     * row in the tile set for the source tile.
     */
    TERM_COLOR a;
    /*
     * For text rendering, is one if wc is a character that takes up two
     * columns (i.e. Japanese kanji); otherwise it is zero.  For tile
     * rendering, Stores the column in the tile set for the terrain tile. */
    char tcol;
    /*
     * For tile rendering, stores the row in the tile set for the
     * terrain tile.
     */
    TERM_COLOR trow;
    enum PendingCellChangeType change_type;
};

struct PendingRowChange
{
    /*
     * These are the first and last columns, inclusive, that have been
     * modified.  xmin is greater than xmax if no changes have been made.
     */
    int xmin, xmax;
    /*
     * This points to storage for a number of elements equal to the number
     * of columns (implicitly gotten from the enclosing AngbandContext).
     */
    struct PendingCellChange* cell_changes;
};

static struct PendingRowChange* create_row_change(int ncol)
{
    struct PendingRowChange* prc =
	(struct PendingRowChange*) malloc(sizeof(struct PendingRowChange));
    struct PendingCellChange* pcc = (struct PendingCellChange*)
	malloc(ncol * sizeof(struct PendingCellChange));
    int i;

    if (prc == 0 || pcc == 0) {
	if (pcc != 0) {
	    free(pcc);
	}
	if (prc != 0) {
	    free(prc);
	}
	return 0;
    }

    prc->xmin = ncol;
    prc->xmax = -1;
    prc->cell_changes = pcc;
    for (i = 0; i < ncol; ++i) {
	pcc[i].change_type = CELL_CHANGE_NONE;
    }
    return prc;
}


static void destroy_row_change(struct PendingRowChange* prc)
{
    if (prc != 0) {
	if (prc->cell_changes != 0) {
	    free(prc->cell_changes);
	}
	free(prc);
    }
}


struct PendingChanges
{
    /* Hold the number of rows specified at creation. */
    int nrow;
    /*
     * Hold the position set for the software cursor.  Use negative indices
     * to indicate that the cursor is not displayed.
     */
    int xcurs, ycurs;
    /* Is nonzero if the cursor should be drawn at double the tile width. */
    int bigcurs;
    /* Record whether the changes include any text, picts, or wipes. */
    int has_text, has_pict, has_wipe;
    /*
     * These are the first and last rows, inclusive, that have been
     * modified.  ymin is greater than ymax if no changes have been made.
     */
    int ymin, ymax;
    /*
     * This is an array of pointers to the changes.  The number of elements
     * is the number of rows.  An element will be a NULL pointer if no
     * modifications have been made to the row.
     */
    struct PendingRowChange** rows;
};


static struct PendingChanges* create_pending_changes(int ncol, int nrow)
{
    struct PendingChanges* pc =
	(struct PendingChanges*) malloc(sizeof(struct PendingChanges));
    struct PendingRowChange** pprc = (struct PendingRowChange**)
	malloc(nrow * sizeof(struct PendingRowChange*));
    int i;

    if (pc == 0 || pprc == 0) {
	if (pprc != 0) {
	    free(pprc);
	}
	if (pc != 0) {
	    free(pc);
	}
	return 0;
    }

    pc->nrow = nrow;
    pc->xcurs = -1;
    pc->ycurs = -1;
    pc->bigcurs = 0;
    pc->has_text = 0;
    pc->has_pict = 0;
    pc->has_wipe = 0;
    pc->ymin = nrow;
    pc->ymax = -1;
    pc->rows = pprc;
    for (i = 0; i < nrow; ++i) {
	pprc[i] = 0;
    }
    return pc;
}


static void destroy_pending_changes(struct PendingChanges* pc)
{
    if (pc != 0) {
	if (pc->rows != 0) {
	    int i;

	    for (i = 0; i < pc->nrow; ++i) {
		if (pc->rows[i] != 0) {
		    destroy_row_change(pc->rows[i]);
		}
	    }
	    free(pc->rows);
	}
	free(pc);
    }
}


static void clear_pending_changes(struct PendingChanges* pc)
{
    pc->xcurs = -1;
    pc->ycurs = -1;
    pc->bigcurs = 0;
    pc->has_text = 0;
    pc->has_pict = 0;
    pc->has_wipe = 0;
    pc->ymin = pc->nrow;
    pc->ymax = -1;
    if (pc->rows != 0) {
	int i;

	for (i = 0; i < pc->nrow; ++i) {
	    if (pc->rows[i] != 0) {
		destroy_row_change(pc->rows[i]);
		pc->rows[i] = 0;
	    }
	}
    }
}


/* Return zero if successful; otherwise return a nonzero value. */
static int resize_pending_changes(struct PendingChanges* pc, int nrow)
{
    struct PendingRowChange** pprc;
    int i;

    if (pc == 0) {
	return 1;
    }

    pprc = (struct PendingRowChange**)
	malloc(nrow * sizeof(struct PendingRowChange*));
    if (pprc == 0) {
	return 1;
    }
    for (i = 0; i < nrow; ++i) {
	pprc[i] = 0;
    }

    if (pc->rows != 0) {
	for (i = 0; i < pc->nrow; ++i) {
	    if (pc->rows[i] != 0) {
		destroy_row_change(pc->rows[i]);
	    }
	}
	free(pc->rows);
    }
    pc->nrow = nrow;
    pc->xcurs = -1;
    pc->ycurs = -1;
    pc->bigcurs = 0;
    pc->has_text = 0;
    pc->has_pict = 0;
    pc->has_wipe = 0;
    pc->ymin = nrow;
    pc->ymax = -1;
    pc->rows = pprc;
    return 0;
}


/* The max number of glyphs we support.  Currently this only affects
 * updateGlyphInfo() for the calculation of the tile size, fontAscender,
 * fontDescender, ncol_pre, and ncol_post (the glyphArray and glyphWidths
 * members of AngbandContext are only used in updateGlyphInfo()).  The
 * rendering in drawWChar will work for glyphs not in updateGlyphInfo()'s
 * set, and that is used for rendering Japanese characters.
 */
#define GLYPH_COUNT 256

/* An AngbandContext represents a logical Term (i.e. what Angband thinks is
 * a window). This typically maps to one NSView, but may map to more than one
 * NSView (e.g. the Test and real screen saver view). */
@interface AngbandContext : NSObject <NSWindowDelegate>
{
@public
    
    /* The Angband term */
    term *terminal;
    
    /* Column and row cont, by default 80 x 24 */
    size_t cols;
    size_t rows;
    
    /* The size of the border between the window edge and the contents */
    NSSize borderSize;
    
    /* Our array of views */
    NSMutableArray *angbandViews;
    
    /* The buffered image */
    CGLayerRef angbandLayer;

    /* The font of this context */
    NSFont *angbandViewFont;
    
    /* If this context owns a window, here it is */
    NSWindow *primaryWindow;
    
    /* "Glyph info": an array of the CGGlyphs and their widths corresponding to
	 * the above font. */
    CGGlyph glyphArray[GLYPH_COUNT];
    CGFloat glyphWidths[GLYPH_COUNT];
    
    /* The size of one tile */
    NSSize tileSize;
    
    /* Font's ascender and descender */
    CGFloat fontAscender, fontDescender;
    
    /* Whether we are currently in live resize, which affects how big we render
	 * our image */
    int inLiveResize;
    
    /* Last time we drew, so we can throttle drawing */
    CFAbsoluteTime lastRefreshTime;

    struct PendingChanges* changes;
    /*
     * These are the number of columns before or after, respectively, a text
     * change that may need to be redrawn.
     */
    int ncol_pre, ncol_post;

    /* Flags whether or not a fullscreen transition is in progress. */
    BOOL in_fullscreen_transition;

@private

    BOOL _hasSubwindowFlags;
    BOOL _windowVisibilityChecked;
}

@property (nonatomic, assign) BOOL hasSubwindowFlags;
@property (nonatomic, assign) BOOL windowVisibilityChecked;

- (void)drawRect:(NSRect)rect inView:(NSView *)view;

/* Called at initialization to set the term */
- (void)setTerm:(term *)t;

/* Called when the context is going down. */
- (void)dispose;

/* Returns the size of the image. */
- (NSSize)imageSize;

/* Return the rect for a tile at given coordinates. */
- (NSRect)rectInImageForTileAtX:(int)x Y:(int)y;

/* Draw the given wide character into the given tile rect. */
- (void)drawWChar:(wchar_t)wchar inRect:(NSRect)tile context:(CGContextRef)ctx;

/* Locks focus on the Angband image, and scales the CTM appropriately. */
- (CGContextRef)lockFocus;

/* Locks focus on the Angband image but does NOT scale the CTM. Appropriate
 * for drawing hairlines. */
- (CGContextRef)lockFocusUnscaled;

/* Unlocks focus. */
- (void)unlockFocus;

/* Returns the primary window for this angband context, creating it if
 * necessary */
- (NSWindow *)makePrimaryWindow;

/* Called to add a new Angband view */
- (void)addAngbandView:(AngbandView *)view;

/* Make the context aware that one of its views changed size */
- (void)angbandViewDidScale:(AngbandView *)view;

/* Handle becoming the main window */
- (void)windowDidBecomeMain:(NSNotification *)notification;

/* Return whether the context's primary window is ordered in or not */
- (BOOL)isOrderedIn;

/* Return whether the context's primary window is key */
- (BOOL)isMainWindow;

/* Invalidate the whole image */
- (void)setNeedsDisplay:(BOOL)val;

/* Invalidate part of the image, with the rect expressed in base coordinates */
- (void)setNeedsDisplayInBaseRect:(NSRect)rect;

/* Display (flush) our Angband views */
- (void)displayIfNeeded;

/* Resize context to size of contentRect, and optionally save size to
 * defaults */
- (void)resizeTerminalWithContentRect: (NSRect)contentRect saveToDefaults: (BOOL)saveToDefaults;

/*
 * Change the minimum size for the window associated with the context.
 * termIdx is the index for the terminal:  pass it so this function can be
 * used when self->terminal has not yet been set.
 */
- (void)setMinimumWindowSize:(int)termIdx;

/* Called from the view to indicate that it is starting or ending live resize */
- (void)viewWillStartLiveResize:(AngbandView *)view;
- (void)viewDidEndLiveResize:(AngbandView *)view;
- (void)saveWindowVisibleToDefaults: (BOOL)windowVisible;
- (BOOL)windowVisibleUsingDefaults;

/* Class methods */

/* Begins an Angband game. This is the entry point for starting off. */
+ (void)beginGame;

/* Internal method */
- (AngbandView *)activeView;

@end

/**
 * Generate a mask for the subwindow flags. The mask is just a safety check to
 * make sure that our windows show and hide as expected.  This function allows
 * for future changes to the set of flags without needed to update it here
 * (unless the underlying types change).
 */
u32b AngbandMaskForValidSubwindowFlags(void)
{
    int windowFlagBits = sizeof(*(window_flag)) * CHAR_BIT;
    int maxBits = MIN( 16, windowFlagBits );
    u32b mask = 0;

    for( int i = 0; i < maxBits; i++ )
    {
        if( window_flag_desc[i] != NULL )
        {
            mask |= (1 << i);
        }
    }

    return mask;
}

/**
 * Check for changes in the subwindow flags and update window visibility.
 * This seems to be called for every user event, so we don't
 * want to do any unnecessary hiding or showing of windows.
 */
static void AngbandUpdateWindowVisibility(void)
{
    /* Because this function is called frequently, we'll make the mask static.
	 * It doesn't change between calls, as the flags themselves are hardcoded */
    static u32b validWindowFlagsMask = 0;

    if( validWindowFlagsMask == 0 )
    {
        validWindowFlagsMask = AngbandMaskForValidSubwindowFlags();
    }

    /* Loop through all of the subwindows and see if there is a change in the
	 * flags. If so, show or hide the corresponding window. We don't care about
	 * the flags themselves; we just want to know if any are set. */
    for( int i = 1; i < ANGBAND_TERM_MAX; i++ )
    {
        AngbandContext *angbandContext = angband_term[i]->data;

        if( angbandContext == nil )
        {
            continue;
        }

        /* This horrible mess of flags is so that we can try to maintain some
		 * user visibility preference. This should allow the user a window and
		 * have it stay closed between application launches. However, this
		 * means that when a subwindow is turned on, it will no longer appear
		 * automatically. Angband has no concept of user control over window
		 * visibility, other than the subwindow flags. */
        if( !angbandContext.windowVisibilityChecked )
        {
            if( [angbandContext windowVisibleUsingDefaults] )
            {
                [angbandContext->primaryWindow orderFront: nil];
                angbandContext.windowVisibilityChecked = YES;
            }
            else
            {
                [angbandContext->primaryWindow close];
                angbandContext.windowVisibilityChecked = NO;
            }
        }
        else
        {
            BOOL termHasSubwindowFlags = ((window_flag[i] & validWindowFlagsMask) > 0);

            if( angbandContext.hasSubwindowFlags && !termHasSubwindowFlags )
            {
                [angbandContext->primaryWindow close];
                angbandContext.hasSubwindowFlags = NO;
                [angbandContext saveWindowVisibleToDefaults: NO];
            }
            else if( !angbandContext.hasSubwindowFlags && termHasSubwindowFlags )
            {
                [angbandContext->primaryWindow orderFront: nil];
                angbandContext.hasSubwindowFlags = YES;
                [angbandContext saveWindowVisibleToDefaults: YES];
            }
        }
    }

    /* Make the main window key so that user events go to the right spot */
    AngbandContext *mainWindow = angband_term[0]->data;
    [mainWindow->primaryWindow makeKeyAndOrderFront: nil];
}

/**
 * ------------------------------------------------------------------------
 * Graphics support
 * ------------------------------------------------------------------------ */

/**
 * The tile image
 */
static CGImageRef pict_image;

/**
 * Numbers of rows and columns in a tileset,
 * calculated by the PICT/PNG loading code
 */
static int pict_cols = 0;
static int pict_rows = 0;

/**
 * Requested graphics mode (as a grafID).
 * The current mode is stored in current_graphics_mode.
 */
static int graf_mode_req = 0;

/**
 * Helper function to check the various ways that graphics can be enabled,
 * guarding against NULL
 */
static BOOL graphics_are_enabled(void)
{
    return current_graphics_mode
	&& current_graphics_mode->grafID != GRAPHICS_NONE;
}

/**
 * Hack -- game in progress
 */
static Boolean game_in_progress = FALSE;


#pragma mark Prototypes
static void wakeup_event_loop(void);
static void hook_plog(const char *str);
static void hook_quit(const char * str);
static void load_prefs(void);
static void load_sounds(void);
static void init_windows(void);
static void handle_open_when_ready(void);
static void play_sound(int event);
static BOOL check_events(int wait);
static BOOL send_event(NSEvent *event);
static void record_current_savefile(void);
#ifdef JP
static wchar_t convert_two_byte_eucjp_to_utf16_native(const char *cp);
#endif

/**
 * Available values for 'wait'
 */
#define CHECK_EVENTS_DRAIN -1
#define CHECK_EVENTS_NO_WAIT	0
#define CHECK_EVENTS_WAIT 1


/**
 * Note when "open"/"new" become valid
 */
static bool initialized = FALSE;

/* Methods for getting the appropriate NSUserDefaults */
@interface NSUserDefaults (AngbandDefaults)
+ (NSUserDefaults *)angbandDefaults;
@end

@implementation NSUserDefaults (AngbandDefaults)
+ (NSUserDefaults *)angbandDefaults
{
    return [NSUserDefaults standardUserDefaults];
}
@end

/* Methods for pulling images out of the Angband bundle (which may be separate
 * from the current bundle in the case of a screensaver */
@interface NSImage (AngbandImages)
+ (NSImage *)angbandImage:(NSString *)name;
@end

/* The NSView subclass that draws our Angband image */
@interface AngbandView : NSView
{
    AngbandContext *angbandContext;
}

- (void)setAngbandContext:(AngbandContext *)context;
- (AngbandContext *)angbandContext;

@end

@implementation NSImage (AngbandImages)

/* Returns an image in the resource directoy of the bundle containing the
 * Angband view class. */
+ (NSImage *)angbandImage:(NSString *)name
{
    NSBundle *bundle = [NSBundle bundleForClass:[AngbandView class]];
    NSString *path = [bundle pathForImageResource:name];
    NSImage *result;
    if (path) result = [[[NSImage alloc] initByReferencingFile:path] autorelease];
    else result = nil;
    return result;
}

@end


@implementation AngbandContext

@synthesize hasSubwindowFlags=_hasSubwindowFlags;
@synthesize windowVisibilityChecked=_windowVisibilityChecked;

- (NSFont *)selectionFont
{
    return angbandViewFont;
}

- (BOOL)useLiveResizeOptimization
{
    /* If we have graphics turned off, text rendering is fast enough that we
	 * don't need to use a live resize optimization. */
    return inLiveResize && graphics_are_enabled();
}

- (NSSize)baseSize
{
    /* We round the base size down. If we round it up, I believe we may end up
	 * with pixels that nobody "owns" that may accumulate garbage. In general
	 * rounding down is harmless, because any lost pixels may be sopped up by
	 * the border. */
    return NSMakeSize(floor(cols * tileSize.width + 2 * borderSize.width), floor(rows * tileSize.height + 2 * borderSize.height));
}

/* qsort-compatible compare function for CGSizes */
static int compare_advances(const void *ap, const void *bp)
{
    const CGSize *a = ap, *b = bp;
    return (a->width > b->width) - (a->width < b->width);
}

- (void)updateGlyphInfo
{
    /* Update glyphArray and glyphWidths */
    NSFont *screenFont = [angbandViewFont screenFont];

    /* Generate a string containing each MacRoman character */
    unsigned char latinString[GLYPH_COUNT];
    size_t i;
    for (i=0; i < GLYPH_COUNT; i++) latinString[i] = (unsigned char)i;
    
    /* Turn that into unichar. Angband uses ISO Latin 1. */
    unichar unicharString[GLYPH_COUNT] = {0};
    NSString *allCharsString = [[NSString alloc] initWithBytes:latinString length:sizeof latinString encoding:NSISOLatin1StringEncoding];
    [allCharsString getCharacters:unicharString range:NSMakeRange(0, MIN(GLYPH_COUNT, [allCharsString length]))];
    [allCharsString autorelease];
    
    /* Get glyphs */
    memset(glyphArray, 0, sizeof glyphArray);
    CTFontGetGlyphsForCharacters((CTFontRef)screenFont, unicharString, glyphArray, GLYPH_COUNT);
    
    /* Get advances. Record the max advance. */
    CGSize advances[GLYPH_COUNT] = {};
    CTFontGetAdvancesForGlyphs((CTFontRef)screenFont, kCTFontHorizontalOrientation, glyphArray, advances, GLYPH_COUNT);
    for (i=0; i < GLYPH_COUNT; i++) {
        glyphWidths[i] = advances[i].width;
    }
    
    /* For good non-mono-font support, use the median advance. Start by sorting
	 * all advances. */
    qsort(advances, GLYPH_COUNT, sizeof *advances, compare_advances);
    
    /* Skip over any initially empty run */
    size_t startIdx;
    for (startIdx = 0; startIdx < GLYPH_COUNT; startIdx++)
    {
        if (advances[startIdx].width > 0) break;
    }
    
    /* Pick the center to find the median */
    CGFloat medianAdvance = 0;
    if (startIdx < GLYPH_COUNT)
    {
		/* In case we have all zero advances for some reason */
        medianAdvance = advances[(startIdx + GLYPH_COUNT)/2].width;
    }
    
    /*
     * Record the ascender and descender.  Some fonts, for instance DIN
     * Condensed and Rockwell in 10.14, the ascent on '@' exceeds that
     * reported by [screenFont ascender].  Get the overall bounding box
     * for the glyphs and use that instead of the ascender and descender
     * values if the bounding box result extends farther from the baseline.
     */
    CGRect bounds = CTFontGetBoundingRectsForGlyphs((CTFontRef) screenFont, kCTFontHorizontalOrientation, glyphArray, NULL, GLYPH_COUNT);
    fontAscender = [screenFont ascender];
    if (fontAscender < bounds.origin.y + bounds.size.height) {
	fontAscender = bounds.origin.y + bounds.size.height;
    }
    fontDescender = [screenFont descender];
    if (fontDescender > bounds.origin.y) {
	fontDescender = bounds.origin.y;
    }

    /*
     * Record the tile size.  Round both values up to have tile boundaries
     * match pixel boundaries.
     */
    tileSize.width = ceil(medianAdvance);
    tileSize.height = ceil(fontAscender - fontDescender);

    /*
     * Determine whether neighboring columns need to redrawn when a character
     * changes.
     */
    CGRect boxes[GLYPH_COUNT] = {};
    CGFloat beyond_right = 0.;
    CGFloat beyond_left = 0.;
    CTFontGetBoundingRectsForGlyphs(
	(CTFontRef)screenFont,
	kCTFontHorizontalOrientation,
	glyphArray,
	boxes,
	GLYPH_COUNT);
    for (i = 0; i < GLYPH_COUNT; i++) {
	/* Account for the compression and offset used by drawWChar(). */
	CGFloat compression, offset;
	CGFloat v;

	if (glyphWidths[i] <= tileSize.width) {
	    compression = 1.;
	    offset = 0.5 * (tileSize.width - glyphWidths[i]);
	} else {
	    compression = tileSize.width / glyphWidths[i];
	    offset = 0.;
	}
	v = (offset + boxes[i].origin.x) * compression;
	if (beyond_left > v) {
	    beyond_left = v;
	}
	v = (offset + boxes[i].origin.x + boxes[i].size.width) * compression;
	if (beyond_right < v) {
	    beyond_right = v;
	}
    }
    ncol_pre = ceil(-beyond_left / tileSize.width);
    if (beyond_right > tileSize.width) {
	ncol_post = ceil((beyond_right - tileSize.width) / tileSize.width);
    } else {
	ncol_post = 0;
    }
}

- (void)updateImage
{
    NSSize size = NSMakeSize(1, 1);
    
    AngbandView *activeView = [self activeView];
    if (activeView)
    {
        /* If we are in live resize, draw as big as the screen, so we can scale
		 * nicely to any size. If we are not in live resize, then use the
		 * bounds of the active view. */
        NSScreen *screen;
        if ([self useLiveResizeOptimization] && (screen = [[activeView window] screen]) != NULL)
        {
            size = [screen frame].size;
        }
        else
        {
            size = [activeView bounds].size;
        }
    }

    CGLayerRelease(angbandLayer);
    
    /* Use the highest monitor scale factor on the system to work out what
     * scale to draw at - not the recommended method, but works where we
     * can't easily get the monitor the current draw is occurring on. */
    float angbandLayerScale = 1.0;
    if ([[NSScreen mainScreen] respondsToSelector:@selector(backingScaleFactor)]) {
        for (NSScreen *screen in [NSScreen screens]) {
            angbandLayerScale = fmax(angbandLayerScale, [screen backingScaleFactor]);
        }
    }

    /* Make a bitmap context as an example for our layer */
    CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB();
    CGContextRef exampleCtx = CGBitmapContextCreate(NULL, 1, 1, 8 /* bits per component */, 48 /* bytesPerRow */, cs, kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Host);
    CGColorSpaceRelease(cs);

    /* Create the layer at the appropriate size */
    size.width = fmax(1, ceil(size.width * angbandLayerScale));
    size.height = fmax(1, ceil(size.height * angbandLayerScale));
    angbandLayer = CGLayerCreateWithContext(exampleCtx, *(CGSize *)&size, NULL);

    CFRelease(exampleCtx);

    /* Set the new context of the layer to draw at the correct scale */
    CGContextRef ctx = CGLayerGetContext(angbandLayer);
    CGContextScaleCTM(ctx, angbandLayerScale, angbandLayerScale);

    [self lockFocus];
    [[NSColor blackColor] set];
    NSRectFill((NSRect){NSZeroPoint, [self baseSize]});
    [self unlockFocus];
}

- (void)requestRedraw
{
    if (! self->terminal) return;
    
    term *old = Term;
    
    /* Activate the term */
    Term_activate(self->terminal);
    
    /* Redraw the contents */
    Term_redraw();
    
    /* Flush the output */
    Term_fresh();
    
    /* Restore the old term */
    Term_activate(old);
}

- (void)setTerm:(term *)t
{
    terminal = t;
}

- (void)viewWillStartLiveResize:(AngbandView *)view
{
#if USE_LIVE_RESIZE_CACHE
    if (inLiveResize < INT_MAX) inLiveResize++;
    else [NSException raise:NSInternalInconsistencyException format:@"inLiveResize overflow"];
    
    if (inLiveResize == 1 && graphics_are_enabled())
    {
        [self updateImage];
        
        [self setNeedsDisplay:YES]; /* We'll need to redisplay everything anyways, so avoid creating all those little redisplay rects */
        [self requestRedraw];
    }
#endif
}

- (void)viewDidEndLiveResize:(AngbandView *)view
{
#if USE_LIVE_RESIZE_CACHE
    if (inLiveResize > 0) inLiveResize--;
    else [NSException raise:NSInternalInconsistencyException format:@"inLiveResize underflow"];
    
    if (inLiveResize == 0 && graphics_are_enabled())
    {
        [self updateImage];
        
        [self setNeedsDisplay:YES]; /* We'll need to redisplay everything anyways, so avoid creating all those little redisplay rects */
        [self requestRedraw];
    }
#endif
}

/**
 * If we're trying to limit ourselves to a certain number of frames per second,
 * then compute how long it's been since we last drew, and then wait until the
 * next frame has passed. */
- (void)throttle
{
    if (frames_per_second > 0)
    {
        CFAbsoluteTime now = CFAbsoluteTimeGetCurrent();
        CFTimeInterval timeSinceLastRefresh = now - lastRefreshTime;
        CFTimeInterval timeUntilNextRefresh = (1. / (double)frames_per_second) - timeSinceLastRefresh;
        
        if (timeUntilNextRefresh > 0)
        {
            usleep((unsigned long)(timeUntilNextRefresh * 1000000.));
        }
    }
    lastRefreshTime = CFAbsoluteTimeGetCurrent();
}

- (void)drawWChar:(wchar_t)wchar inRect:(NSRect)tile context:(CGContextRef)ctx
{
    CGFloat tileOffsetY = fontAscender;
    CGFloat tileOffsetX = 0.0;
    NSFont *screenFont = [angbandViewFont screenFont];
    UniChar unicharString[2] = {(UniChar)wchar, 0};

    /* Get glyph and advance */
    CGGlyph thisGlyphArray[1] = { 0 };
    CGSize advances[1] = { { 0, 0 } };
    CTFontGetGlyphsForCharacters((CTFontRef)screenFont, unicharString, thisGlyphArray, 1);
    CGGlyph glyph = thisGlyphArray[0];
    CTFontGetAdvancesForGlyphs((CTFontRef)screenFont, kCTFontHorizontalOrientation, thisGlyphArray, advances, 1);
    CGSize advance = advances[0];
    
    /* If our font is not monospaced, our tile width is deliberately not big
	 * enough for every character. In that event, if our glyph is too wide, we
	 * need to compress it horizontally. Compute the compression ratio.
	 * 1.0 means no compression. */
    double compressionRatio;
    if (advance.width <= NSWidth(tile))
    {
        /* Our glyph fits, so we can just draw it, possibly with an offset */
        compressionRatio = 1.0;
        tileOffsetX = (NSWidth(tile) - advance.width)/2;
    }
    else
    {
        /* Our glyph doesn't fit, so we'll have to compress it */
        compressionRatio = NSWidth(tile) / advance.width;
        tileOffsetX = 0;
    }

    
    /* Now draw it */
    CGAffineTransform textMatrix = CGContextGetTextMatrix(ctx);
    CGFloat savedA = textMatrix.a;

    /* Set the position */
    textMatrix.tx = tile.origin.x + tileOffsetX;
    textMatrix.ty = tile.origin.y + tileOffsetY;

    /* Maybe squish it horizontally. */
    if (compressionRatio != 1.)
    {
        textMatrix.a *= compressionRatio;
    }

    textMatrix = CGAffineTransformScale( textMatrix, 1.0, -1.0 );
    CGContextSetTextMatrix(ctx, textMatrix);
    CGContextShowGlyphsAtPositions(ctx, &glyph, &CGPointZero, 1);
    
    /* Restore the text matrix if we messed with the compression ratio */
    if (compressionRatio != 1.)
    {
        textMatrix.a = savedA;
        CGContextSetTextMatrix(ctx, textMatrix);
    }

    textMatrix = CGAffineTransformScale( textMatrix, 1.0, -1.0 );
    CGContextSetTextMatrix(ctx, textMatrix);
}

/* Lock and unlock focus on our image or layer, setting up the CTM
 * appropriately. */
- (CGContextRef)lockFocusUnscaled
{
    /* Create an NSGraphicsContext representing this CGLayer */
    CGContextRef ctx = CGLayerGetContext(angbandLayer);
    NSGraphicsContext *context = [NSGraphicsContext graphicsContextWithGraphicsPort:ctx flipped:NO];
    [NSGraphicsContext saveGraphicsState];
    [NSGraphicsContext setCurrentContext:context];
    CGContextSaveGState(ctx);
    return ctx;
}

- (void)unlockFocus
{
    /* Restore the graphics state */
    CGContextRef ctx = [[NSGraphicsContext currentContext] graphicsPort];
    CGContextRestoreGState(ctx);
    [NSGraphicsContext restoreGraphicsState];
}

- (NSSize)imageSize
{
    /* Return the size of our layer */
    CGSize result = CGLayerGetSize(angbandLayer);
    return NSMakeSize(result.width, result.height);
}

- (CGContextRef)lockFocus
{
    return [self lockFocusUnscaled];
}


- (NSRect)rectInImageForTileAtX:(int)x Y:(int)y
{
    int flippedY = y;
    return NSMakeRect(x * tileSize.width + borderSize.width, flippedY * tileSize.height + borderSize.height, tileSize.width, tileSize.height);
}

- (void)setSelectionFont:(NSFont*)font adjustTerminal: (BOOL)adjustTerminal
{
    /* Record the new font */
    [font retain];
    [angbandViewFont release];
    angbandViewFont = font;
    
    /* Update our glyph info */
    [self updateGlyphInfo];

    if( adjustTerminal )
    {
        /* Adjust terminal to fit window with new font; save the new columns
		 * and rows since they could be changed */
        NSRect contentRect = [self->primaryWindow contentRectForFrameRect: [self->primaryWindow frame]];

	[self setMinimumWindowSize:[self terminalIndex]];
	NSSize size = self->primaryWindow.contentMinSize;
	BOOL windowNeedsResizing = NO;
	if (contentRect.size.width < size.width) {
	    contentRect.size.width = size.width;
	    windowNeedsResizing = YES;
	}
	if (contentRect.size.height < size.height) {
	    contentRect.size.height = size.height;
	    windowNeedsResizing = YES;
	}
	if (windowNeedsResizing) {
	    size.width = contentRect.size.width;
	    size.height = contentRect.size.height;
	    [self->primaryWindow setContentSize:size];
	}
        [self resizeTerminalWithContentRect: contentRect saveToDefaults: YES];
    }

    /* Update our image */
    [self updateImage];
}

- (id)init
{
    if ((self = [super init]))
    {
        /* Default rows and cols */
        self->cols = 80;
        self->rows = 24;

        /* Default border size */
        self->borderSize = NSMakeSize(2, 2);

        /* Allocate our array of views */
        angbandViews = [[NSMutableArray alloc] init];
        
	self->changes = create_pending_changes(self->cols, self->rows);
	if (self->changes == 0) {
	    NSLog(@"AngbandContext init:  out of memory for pending changes");
	}
	self->ncol_pre = 0;
	self->ncol_post = 0;

	self->in_fullscreen_transition = NO;

        /* Make the image. Since we have no views, it'll just be a puny 1x1 image. */
        [self updateImage];

        _windowVisibilityChecked = NO;
    }
    return self;
}

/**
 * Destroy all the receiver's stuff. This is intended to be callable more than
 * once.
 */
- (void)dispose
{
    terminal = NULL;
    
    /* Disassociate ourselves from our angbandViews */
    [angbandViews makeObjectsPerformSelector:@selector(setAngbandContext:) withObject:nil];
    [angbandViews release];
    angbandViews = nil;
    
    /* Destroy the layer/image */
    CGLayerRelease(angbandLayer);
    angbandLayer = NULL;

    /* Font */
    [angbandViewFont release];
    angbandViewFont = nil;
    
    /* Window */
    [primaryWindow setDelegate:nil];
    [primaryWindow close];
    [primaryWindow release];
    primaryWindow = nil;

    /* Pending changes */
    destroy_pending_changes(self->changes);
    self->changes = 0;
}

/* Usual Cocoa fare */
- (void)dealloc
{
    [self dispose];
    [super dealloc];
}



#pragma mark -
#pragma mark Directories and Paths Setup

/**
 * Return the path for Angband's lib directory and bail if it isn't found. The
 * lib directory should be in the bundle's resources directory, since it's
 * copied when built.
 */
+ (NSString *)libDirectoryPath
{
    NSString *bundleLibPath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent: AngbandDirectoryNameLib];
    BOOL isDirectory = NO;
    BOOL libExists = [[NSFileManager defaultManager] fileExistsAtPath: bundleLibPath isDirectory: &isDirectory];

    if( !libExists || !isDirectory )
    {
        NSLog( @"[%@ %@]: can't find %@/ in bundle: isDirectory: %d libExists: %d", NSStringFromClass( [self class] ), NSStringFromSelector( _cmd ), AngbandDirectoryNameLib, isDirectory, libExists );

	NSString *msg = NSLocalizedStringWithDefaultValue(
	    @"Error.MissingResources",
	    AngbandMessageCatalog,
	    [NSBundle mainBundle],
	    @"Missing Resources",
	    @"Alert text for missing resources");
	NSString *info = NSLocalizedStringWithDefaultValue(
	    @"Error.MissingAngbandLib",
	    AngbandMessageCatalog,
	    [NSBundle mainBundle],
	    @"Hengband was unable to find required resources and must quit. Please report a bug on the Angband forums.",
	    @"Alert informative message for missing Angband lib/ folder");
	NSString *quit_label = NSLocalizedStringWithDefaultValue(
	    @"Label.Quit", AngbandMessageCatalog, [NSBundle mainBundle],
	    @"Quit", @"Quit");
	NSAlert *alert = [[NSAlert alloc] init];

	/*
	 * Note that NSCriticalAlertStyle was deprecated in 10.10.  The
	 * replacement is NSAlertStyleCritical.
	 */
	alert.alertStyle = NSCriticalAlertStyle;
	alert.messageText = msg;
	alert.informativeText = info;
	[alert addButtonWithTitle:quit_label];
	NSModalResponse result = [alert runModal];
	[alert release];
        exit( 0 );
    }

	return bundleLibPath;
}

/**
 * Return the path for the directory where Angband should look for its standard
 * user file tree.
 */
+ (NSString *)angbandDocumentsPath
{
	NSString *documents = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];

#if defined(SAFE_DIRECTORY)
	NSString *versionedDirectory = [NSString stringWithFormat: @"%@-%s", AngbandDirectoryNameBase, VERSION_STRING];
	return [documents stringByAppendingPathComponent: versionedDirectory];
#else
	return [documents stringByAppendingPathComponent: AngbandDirectoryNameBase];
#endif
}

/**
 * Adjust directory paths as needed to correct for any differences needed by
 * Angband. \c init_file_paths() currently requires that all paths provided have
 * a trailing slash and all other platforms honor this.
 *
 * \param originalPath The directory path to adjust.
 * \return A path suitable for Angband or nil if an error occurred.
 */
static NSString *AngbandCorrectedDirectoryPath(NSString *originalPath)
{
	if ([originalPath length] == 0) {
		return nil;
	}

	if (![originalPath hasSuffix: @"/"]) {
		return [originalPath stringByAppendingString: @"/"];
	}

	return originalPath;
}

/**
 * Give Angband the base paths that should be used for the various directories
 * it needs. It will create any needed directories.
 */
+ (void)prepareFilePathsAndDirectories
{
	char libpath[PATH_MAX + 1] = "\0";
	NSString *libDirectoryPath = AngbandCorrectedDirectoryPath([self libDirectoryPath]);
	[libDirectoryPath getFileSystemRepresentation: libpath maxLength: sizeof(libpath)];

	char basepath[PATH_MAX + 1] = "\0";
	NSString *angbandDocumentsPath = AngbandCorrectedDirectoryPath([self angbandDocumentsPath]);
	[angbandDocumentsPath getFileSystemRepresentation: basepath maxLength: sizeof(basepath)];

	init_file_paths(libpath, basepath);
	create_needed_dirs();
}

#pragma mark -

#if 0
/* From the Linux mbstowcs(3) man page:
 *   If dest is NULL, n is ignored, and the conversion  proceeds  as  above,
 *   except  that  the converted wide characters are not written out to mem‐
 *   ory, and that no length limit exists.
 */
static size_t Term_mbcs_cocoa(wchar_t *dest, const char *src, int n)
{
    int i;
    int count = 0;

    /* Unicode code point to UTF-8
     *  0x0000-0x007f:   0xxxxxxx
     *  0x0080-0x07ff:   110xxxxx 10xxxxxx
     *  0x0800-0xffff:   1110xxxx 10xxxxxx 10xxxxxx
     * 0x10000-0x1fffff: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
     * Note that UTF-16 limits Unicode to 0x10ffff. This code is not
     * endian-agnostic.
     */
    for (i = 0; i < n || dest == NULL; i++) {
        if ((src[i] & 0x80) == 0) {
            if (dest != NULL) dest[count] = src[i];
            if (src[i] == 0) break;
        } else if ((src[i] & 0xe0) == 0xc0) {
            if (dest != NULL) dest[count] = 
                            (((unsigned char)src[i] & 0x1f) << 6)| 
                            ((unsigned char)src[i+1] & 0x3f);
            i++;
        } else if ((src[i] & 0xf0) == 0xe0) {
            if (dest != NULL) dest[count] = 
                            (((unsigned char)src[i] & 0x0f) << 12) | 
                            (((unsigned char)src[i+1] & 0x3f) << 6) |
                            ((unsigned char)src[i+2] & 0x3f);
            i += 2;
        } else if ((src[i] & 0xf8) == 0xf0) {
            if (dest != NULL) dest[count] = 
                            (((unsigned char)src[i] & 0x0f) << 18) | 
                            (((unsigned char)src[i+1] & 0x3f) << 12) |
                            (((unsigned char)src[i+2] & 0x3f) << 6) |
                            ((unsigned char)src[i+3] & 0x3f);
            i += 3;
        } else {
            /* Found an invalid multibyte sequence */
            return (size_t)-1;
        }
        count++;
    }
    return count;
}
#endif

/**
 * Entry point for initializing Angband
 */
+ (void)beginGame
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    
    /* Hooks in some "z-util.c" hooks */
    plog_aux = hook_plog;
    quit_aux = hook_quit;
    
    /* Initialize file paths */
    [self prepareFilePathsAndDirectories];

    /* Note the "system" */
    ANGBAND_SYS = "coc";

    /* Load preferences */
    load_prefs();
    
    /* Load possible graphics modes */
    init_graphics_modes();

    /* Prepare the windows */
    init_windows();
    
    /* Set up game event handlers */
    /* init_display(); */
    
    /* Register the sound hook */
    /* sound_hook = play_sound; */
    
    /* Initialise game */
    init_angband(p_ptr);

    /* Initialize some save file stuff */
    p_ptr->player_egid = getegid();
    
    /* We are now initialized */
    initialized = TRUE;
    
    /* Handle "open_when_ready" */
    handle_open_when_ready();
    
    /* Handle pending events (most notably update) and flush input */
    Term_flush();

    /* Prompt the user. */
    int message_row = (Term->hgt - 23) / 5 + 23;
    Term_erase(0, message_row, 255);
    put_str(
#ifdef JP
	"['ファイル' メニューから '新' または '開く' を選択します]",
	message_row, (Term->wid - 57) / 2
#else
	"[Choose 'New' or 'Open' from the 'File' menu]",
	message_row, (Term->wid - 45) / 2
#endif
    );
    Term_fresh();

    /*
     * Play a game -- "new_game" is set by "new", "open" or the open document
     * even handler as appropriate
     */
        
    [pool drain];
    
    while (!game_in_progress) {
        NSAutoreleasePool *splashScreenPool = [[NSAutoreleasePool alloc] init];
        NSEvent *event = [NSApp nextEventMatchingMask:NSAnyEventMask untilDate:[NSDate distantFuture] inMode:NSDefaultRunLoopMode dequeue:YES];
        if (event) [NSApp sendEvent:event];
        [splashScreenPool drain];
    }

    Term_fresh();
    play_game(p_ptr, new_game);

    quit(NULL);
}

- (void)addAngbandView:(AngbandView *)view
{
    if (! [angbandViews containsObject:view])
    {
        [angbandViews addObject:view];
        [self updateImage];
        [self setNeedsDisplay:YES]; /* We'll need to redisplay everything anyways, so avoid creating all those little redisplay rects */
        [self requestRedraw];
    }
}

/**
 * We have this notion of an "active" AngbandView, which is the largest - the
 * idea being that in the screen saver, when the user hits Test in System
 * Preferences, we don't want to keep driving the AngbandView in the
 * background.  Our active AngbandView is the widest - that's a hack all right.
 * Mercifully when we're just playing the game there's only one view.
 */
- (AngbandView *)activeView
{
    if ([angbandViews count] == 1)
        return [angbandViews objectAtIndex:0];
    
    AngbandView *result = nil;
    float maxWidth = 0;
    for (AngbandView *angbandView in angbandViews)
    {
        float width = [angbandView frame].size.width;
        if (width > maxWidth)
        {
            maxWidth = width;
            result = angbandView;
        }
    }
    return result;
}

- (void)angbandViewDidScale:(AngbandView *)view
{
    /* If we're live-resizing with graphics, we're using the live resize
	 * optimization, so don't update the image. Otherwise do it. */
    if (! (inLiveResize && graphics_are_enabled()) && view == [self activeView])
    {
        [self updateImage];
        
        [self setNeedsDisplay:YES]; /*we'll need to redisplay everything anyways, so avoid creating all those little redisplay rects */
        [self requestRedraw];
    }
}


- (void)removeAngbandView:(AngbandView *)view
{
    if ([angbandViews containsObject:view])
    {
        [angbandViews removeObject:view];
        [self updateImage];
        [self setNeedsDisplay:YES]; /* We'll need to redisplay everything anyways, so avoid creating all those little redisplay rects */
        if ([angbandViews count]) [self requestRedraw];
    }
}


- (NSWindow *)makePrimaryWindow
{
    if (! primaryWindow)
    {
        /* This has to be done after the font is set, which it already is in
		 * term_init_cocoa() */
        CGFloat width = self->cols * tileSize.width + borderSize.width * 2.0;
        CGFloat height = self->rows * tileSize.height + borderSize.height * 2.0;
        NSRect contentRect = NSMakeRect( 0.0, 0.0, width, height );

        NSUInteger styleMask = NSTitledWindowMask | NSResizableWindowMask | NSMiniaturizableWindowMask;

        /* Make every window other than the main window closable */
        if( angband_term[0]->data != self )
        {
            styleMask |= NSClosableWindowMask;
        }

        primaryWindow = [[NSWindow alloc] initWithContentRect:contentRect styleMask: styleMask backing:NSBackingStoreBuffered defer:YES];

        /* Not to be released when closed */
        [primaryWindow setReleasedWhenClosed:NO];
        [primaryWindow setExcludedFromWindowsMenu: YES]; /* we're using custom window menu handling */

        /* Make the view */
        AngbandView *angbandView = [[AngbandView alloc] initWithFrame:contentRect];
        [angbandView setAngbandContext:self];
        [angbandViews addObject:angbandView];
        [primaryWindow setContentView:angbandView];
        [angbandView release];

        /* We are its delegate */
        [primaryWindow setDelegate:self];

        /* Update our image, since this is probably the first angband view
		 * we've gotten. */
        [self updateImage];
    }
    return primaryWindow;
}



#pragma mark View/Window Passthrough

/**
 * This is what our views call to get us to draw to the window
 */
- (void)drawRect:(NSRect)rect inView:(NSView *)view
{
    /* Take this opportunity to throttle so we don't flush faster than desired.
	 */
    BOOL viewInLiveResize = [view inLiveResize];
    if (! viewInLiveResize) [self throttle];

    /* With a GLayer, use CGContextDrawLayerInRect */
    CGContextRef context = [[NSGraphicsContext currentContext] graphicsPort];
    NSRect bounds = [view bounds];
    if (viewInLiveResize) CGContextSetInterpolationQuality(context, kCGInterpolationLow);
    CGContextSetBlendMode(context, kCGBlendModeCopy);
    CGContextDrawLayerInRect(context, *(CGRect *)&bounds, angbandLayer);
    if (viewInLiveResize) CGContextSetInterpolationQuality(context, kCGInterpolationDefault);
}

- (BOOL)isOrderedIn
{
    return [[[angbandViews lastObject] window] isVisible];
}

- (BOOL)isMainWindow
{
    return [[[angbandViews lastObject] window] isMainWindow];
}

- (void)setNeedsDisplay:(BOOL)val
{
    for (NSView *angbandView in angbandViews)
    {
        [angbandView setNeedsDisplay:val];
    }
}

- (void)setNeedsDisplayInBaseRect:(NSRect)rect
{
    for (NSView *angbandView in angbandViews)
    {
        [angbandView setNeedsDisplayInRect: rect];
    }
}

- (void)displayIfNeeded
{
    [[self activeView] displayIfNeeded];
}

- (int)terminalIndex
{
	int termIndex = 0;

	for( termIndex = 0; termIndex < ANGBAND_TERM_MAX; termIndex++ )
	{
		if( angband_term[termIndex] == self->terminal )
		{
			break;
		}
	}

	return termIndex;
}

- (void)resizeTerminalWithContentRect: (NSRect)contentRect saveToDefaults: (BOOL)saveToDefaults
{
    CGFloat newRows = floor( (contentRect.size.height - (borderSize.height * 2.0)) / tileSize.height );
    CGFloat newColumns = ceil( (contentRect.size.width - (borderSize.width * 2.0)) / tileSize.width );

    if (newRows < 1 || newColumns < 1) return;
    self->cols = newColumns;
    self->rows = newRows;

    if (resize_pending_changes(self->changes, self->rows) != 0) {
	destroy_pending_changes(self->changes);
	self->changes = 0;
	NSLog(@"out of memory for pending changes with resize of terminal %d",
	      [self terminalIndex]);
    }

    if( saveToDefaults )
    {
        int termIndex = [self terminalIndex];
        NSArray *terminals = [[NSUserDefaults standardUserDefaults] valueForKey: AngbandTerminalsDefaultsKey];

        if( termIndex < (int)[terminals count] )
        {
            NSMutableDictionary *mutableTerm = [[NSMutableDictionary alloc] initWithDictionary: [terminals objectAtIndex: termIndex]];
            [mutableTerm setValue: [NSNumber numberWithUnsignedInt: self->cols] forKey: AngbandTerminalColumnsDefaultsKey];
            [mutableTerm setValue: [NSNumber numberWithUnsignedInt: self->rows] forKey: AngbandTerminalRowsDefaultsKey];

            NSMutableArray *mutableTerminals = [[NSMutableArray alloc] initWithArray: terminals];
            [mutableTerminals replaceObjectAtIndex: termIndex withObject: mutableTerm];

            [[NSUserDefaults standardUserDefaults] setValue: mutableTerminals forKey: AngbandTerminalsDefaultsKey];
            [mutableTerminals release];
            [mutableTerm release];
        }
        [[NSUserDefaults standardUserDefaults] synchronize];
    }

    term *old = Term;
    Term_activate( self->terminal );
    Term_resize( (int)newColumns, (int)newRows);
    Term_redraw();
    Term_activate( old );
}

- (void)setMinimumWindowSize:(int)termIdx
{
    NSSize minsize;

    if (termIdx == 0) {
	minsize.width = 80;
	minsize.height = 24;
    } else {
	minsize.width = 1;
	minsize.height = 1;
    }
    minsize.width =
	minsize.width * self->tileSize.width + self->borderSize.width * 2.0;
    minsize.height =
        minsize.height * self->tileSize.height + self->borderSize.height * 2.0;
    [[self makePrimaryWindow] setContentMinSize:minsize];
}

- (void)saveWindowVisibleToDefaults: (BOOL)windowVisible
{
	int termIndex = [self terminalIndex];
	BOOL safeVisibility = (termIndex == 0) ? YES : windowVisible; /* Ensure main term doesn't go away because of these defaults */
	NSArray *terminals = [[NSUserDefaults standardUserDefaults] valueForKey: AngbandTerminalsDefaultsKey];

	if( termIndex < (int)[terminals count] )
	{
		NSMutableDictionary *mutableTerm = [[NSMutableDictionary alloc] initWithDictionary: [terminals objectAtIndex: termIndex]];
		[mutableTerm setValue: [NSNumber numberWithBool: safeVisibility] forKey: AngbandTerminalVisibleDefaultsKey];

		NSMutableArray *mutableTerminals = [[NSMutableArray alloc] initWithArray: terminals];
		[mutableTerminals replaceObjectAtIndex: termIndex withObject: mutableTerm];

		[[NSUserDefaults standardUserDefaults] setValue: mutableTerminals forKey: AngbandTerminalsDefaultsKey];
		[mutableTerminals release];
		[mutableTerm release];
	}
}

- (BOOL)windowVisibleUsingDefaults
{
	int termIndex = [self terminalIndex];

	if( termIndex == 0 )
	{
		return YES;
	}

	NSArray *terminals = [[NSUserDefaults standardUserDefaults] valueForKey: AngbandTerminalsDefaultsKey];
	BOOL visible = NO;

	if( termIndex < (int)[terminals count] )
	{
		NSDictionary *term = [terminals objectAtIndex: termIndex];
		NSNumber *visibleValue = [term valueForKey: AngbandTerminalVisibleDefaultsKey];

		if( visibleValue != nil )
		{
			visible = [visibleValue boolValue];
		}
	}

	return visible;
}

#pragma mark -
#pragma mark NSWindowDelegate Methods

/*- (void)windowWillStartLiveResize: (NSNotification *)notification
{ 
}*/ 

- (void)windowDidEndLiveResize: (NSNotification *)notification
{
    NSWindow *window = [notification object];
    NSRect contentRect = [window contentRectForFrameRect: [window frame]];
    [self resizeTerminalWithContentRect: contentRect saveToDefaults: !(self->in_fullscreen_transition)];
}

/*- (NSSize)windowWillResize: (NSWindow *)sender toSize: (NSSize)frameSize
{
} */

- (void)windowWillEnterFullScreen: (NSNotification *)notification
{
    self->in_fullscreen_transition = YES;
}

- (void)windowDidEnterFullScreen: (NSNotification *)notification
{
    NSWindow *window = [notification object];
    NSRect contentRect = [window contentRectForFrameRect: [window frame]];
    self->in_fullscreen_transition = NO;
    [self resizeTerminalWithContentRect: contentRect saveToDefaults: NO];
}

- (void)windowWillExitFullScreen: (NSNotification *)notification
{
    self->in_fullscreen_transition = YES;
}

- (void)windowDidExitFullScreen: (NSNotification *)notification
{
    NSWindow *window = [notification object];
    NSRect contentRect = [window contentRectForFrameRect: [window frame]];
    self->in_fullscreen_transition = NO;
    [self resizeTerminalWithContentRect: contentRect saveToDefaults: NO];
}

- (void)windowDidBecomeMain:(NSNotification *)notification
{
    NSWindow *window = [notification object];

    if( window != self->primaryWindow )
    {
        return;
    }

    int termIndex = [self terminalIndex];
    NSMenuItem *item = [[[NSApplication sharedApplication] windowsMenu] itemWithTag: AngbandWindowMenuItemTagBase + termIndex];
    [item setState: NSOnState];

    if( [[NSFontPanel sharedFontPanel] isVisible] )
    {
        [[NSFontPanel sharedFontPanel] setPanelFont: [self selectionFont] isMultiple: NO];
    }
}

- (void)windowDidResignMain: (NSNotification *)notification
{
    NSWindow *window = [notification object];

    if( window != self->primaryWindow )
    {
        return;
    }

    int termIndex = [self terminalIndex];
    NSMenuItem *item = [[[NSApplication sharedApplication] windowsMenu] itemWithTag: AngbandWindowMenuItemTagBase + termIndex];
    [item setState: NSOffState];
}

- (void)windowWillClose: (NSNotification *)notification
{
	[self saveWindowVisibleToDefaults: NO];
}

@end


@implementation AngbandView

- (BOOL)isOpaque
{
    return YES;
}

- (BOOL)isFlipped
{
    return YES;
}

- (void)drawRect:(NSRect)rect
{
    if (! angbandContext)
    {
        /* Draw bright orange, 'cause this ain't right */
        [[NSColor orangeColor] set];
        NSRectFill([self bounds]);
    }
    else
    {
        /* Tell the Angband context to draw into us */
        [angbandContext drawRect:rect inView:self];
    }
}

- (void)setAngbandContext:(AngbandContext *)context
{
    angbandContext = context;
}

- (AngbandContext *)angbandContext
{
    return angbandContext;
}

- (void)setFrameSize:(NSSize)size
{
    BOOL changed = ! NSEqualSizes(size, [self frame].size);
    [super setFrameSize:size];
    if (changed) [angbandContext angbandViewDidScale:self];
}

- (void)viewWillStartLiveResize
{
    [angbandContext viewWillStartLiveResize:self];
}

- (void)viewDidEndLiveResize
{
    [angbandContext viewDidEndLiveResize:self];
}

@end

/**
 * Delay handling of double-clicked savefiles
 */
Boolean open_when_ready = FALSE;



/**
 * ------------------------------------------------------------------------
 * Some generic functions
 * ------------------------------------------------------------------------ */

/**
 * Sets an Angband color at a given index
 */
static void set_color_for_index(int idx)
{
    u16b rv, gv, bv;
    
    /* Extract the R,G,B data */
    rv = angband_color_table[idx][1];
    gv = angband_color_table[idx][2];
    bv = angband_color_table[idx][3];
    
    CGContextSetRGBFillColor([[NSGraphicsContext currentContext] graphicsPort], rv/255., gv/255., bv/255., 1.);
}

/**
 * Remember the current character in UserDefaults so we can select it by
 * default next time.
 */
static void record_current_savefile(void)
{
    NSString *savefileString = [[NSString stringWithCString:savefile encoding:NSMacOSRomanStringEncoding] lastPathComponent];
    if (savefileString)
    {
        NSUserDefaults *angbandDefs = [NSUserDefaults angbandDefaults];
        [angbandDefs setObject:savefileString forKey:@"SaveFile"];
        [angbandDefs synchronize];
    }
}


#ifdef JP
/**
 * Convert a two-byte EUC-JP encoded character (both *cp and (*cp + 1) are in
 * the range, 0xA1-0xFE, or *cp is 0x8E) to a utf16 value in the native byte
 * ordering.
 */
static wchar_t convert_two_byte_eucjp_to_utf16_native(const char *cp)
{
    NSString* str = [[NSString alloc] initWithBytes:cp length:2
				      encoding:NSJapaneseEUCStringEncoding];
    wchar_t result = [str characterAtIndex:0];

    [str release];
    return result;
}
#endif /* JP */


/**
 * ------------------------------------------------------------------------
 * Support for the "z-term.c" package
 * ------------------------------------------------------------------------ */


/**
 * Initialize a new Term
 */
static void Term_init_cocoa(term *t)
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    AngbandContext *context = [[AngbandContext alloc] init];
    
    /* Give the term a hard retain on context (for GC) */
    t->data = (void *)CFRetain(context);
    [context release];
    
    /* Handle graphics */
    t->higher_pict = !! use_graphics;
    t->always_pict = FALSE;
    
    NSDisableScreenUpdates();
    
    /* Figure out the frame autosave name based on the index of this term */
    NSString *autosaveName = nil;
    int termIdx;
    for (termIdx = 0; termIdx < ANGBAND_TERM_MAX; termIdx++)
    {
        if (angband_term[termIdx] == t)
        {
            autosaveName = [NSString stringWithFormat:@"AngbandTerm-%d", termIdx];
            break;
        }
    }

    /* Set its font. */
    NSString *fontName = [[NSUserDefaults angbandDefaults] stringForKey:[NSString stringWithFormat:@"FontName-%d", termIdx]];
    if (! fontName) fontName = [default_font fontName];

    /* Use a smaller default font for the other windows, but only if the font
	 * hasn't been explicitly set */
    float fontSize = (termIdx > 0) ? 10.0 : [default_font pointSize];
    NSNumber *fontSizeNumber = [[NSUserDefaults angbandDefaults] valueForKey: [NSString stringWithFormat: @"FontSize-%d", termIdx]];

    if( fontSizeNumber != nil )
    {
        fontSize = [fontSizeNumber floatValue];
    }

    [context setSelectionFont:[NSFont fontWithName:fontName size:fontSize] adjustTerminal: NO];

    NSArray *terminalDefaults = [[NSUserDefaults standardUserDefaults] valueForKey: AngbandTerminalsDefaultsKey];
    NSInteger rows = 24;
    NSInteger columns = 80;

    if( termIdx < (int)[terminalDefaults count] )
    {
        NSDictionary *term = [terminalDefaults objectAtIndex: termIdx];
        NSInteger defaultRows = [[term valueForKey: AngbandTerminalRowsDefaultsKey] integerValue];
        NSInteger defaultColumns = [[term valueForKey: AngbandTerminalColumnsDefaultsKey] integerValue];

        if (defaultRows > 0) rows = defaultRows;
        if (defaultColumns > 0) columns = defaultColumns;
    }

    context->cols = columns;
    context->rows = rows;

    if (resize_pending_changes(context->changes, context->rows) != 0) {
	destroy_pending_changes(context->changes);
	context->changes = 0;
	NSLog(@"initializing terminal %d:  out of memory for pending changes",
	      termIdx);
    }

    /* Get the window */
    NSWindow *window = [context makePrimaryWindow];

    /* Set its title and, for auxiliary terms, tentative size */
    NSString *title = [NSString stringWithCString:angband_term_name[termIdx]
#ifdef JP
				encoding:NSJapaneseEUCStringEncoding
#else
				encoding:NSMacOSRomanStringEncoding
#endif
    ];
    [window setTitle:title];
    [context setMinimumWindowSize:termIdx];

    /* If this is the first term, and we support full screen (Mac OS X Lion or
	 * later), then allow it to go full screen (sweet). Allow other terms to be
	 * FullScreenAuxilliary, so they can at least show up. Unfortunately in
	 * Lion they don't get brought to the full screen space; but they would
	 * only make sense on multiple displays anyways so it's not a big loss. */
    if ([window respondsToSelector:@selector(toggleFullScreen:)])
    {
        NSWindowCollectionBehavior behavior = [window collectionBehavior];
        behavior |= (termIdx == 0 ? Angband_NSWindowCollectionBehaviorFullScreenPrimary : Angband_NSWindowCollectionBehaviorFullScreenAuxiliary);
        [window setCollectionBehavior:behavior];
    }
    
    /* No Resume support yet, though it would not be hard to add */
    if ([window respondsToSelector:@selector(setRestorable:)])
    {
        [window setRestorable:NO];
    }

	/* default window placement */ {
		static NSRect overallBoundingRect;

		if( termIdx == 0 )
		{
			/* This is a bit of a trick to allow us to display multiple windows
			 * in the "standard default" window position in OS X: the upper
			 * center of the screen.
			 * The term sizes set in load_prefs() are based on a 5-wide by
			 * 3-high grid, with the main term being 4/5 wide by 2/3 high
			 * (hence the scaling to find */

			/* What the containing rect would be). */
			NSRect originalMainTermFrame = [window frame];
			NSRect scaledFrame = originalMainTermFrame;
			scaledFrame.size.width *= 5.0 / 4.0;
			scaledFrame.size.height *= 3.0 / 2.0;
			scaledFrame.size.width += 1.0; /* spacing between window columns */
			scaledFrame.size.height += 1.0; /* spacing between window rows */
			[window setFrame: scaledFrame  display: NO];
			[window center];
			overallBoundingRect = [window frame];
			[window setFrame: originalMainTermFrame display: NO];
		}

		static NSRect mainTermBaseRect;
		NSRect windowFrame = [window frame];

		if( termIdx == 0 )
		{
			/* The height and width adjustments were determined experimentally,
			 * so that the rest of the windows line up nicely without
			 * overlapping */
            windowFrame.size.width += 7.0;
			windowFrame.size.height += 9.0;
			windowFrame.origin.x = NSMinX( overallBoundingRect );
			windowFrame.origin.y = NSMaxY( overallBoundingRect ) - NSHeight( windowFrame );
			mainTermBaseRect = windowFrame;
		}
		else if( termIdx == 1 )
		{
			windowFrame.origin.x = NSMinX( mainTermBaseRect );
			windowFrame.origin.y = NSMinY( mainTermBaseRect ) - NSHeight( windowFrame ) - 1.0;
		}
		else if( termIdx == 2 )
		{
			windowFrame.origin.x = NSMaxX( mainTermBaseRect ) + 1.0;
			windowFrame.origin.y = NSMaxY( mainTermBaseRect ) - NSHeight( windowFrame );
		}
		else if( termIdx == 3 )
		{
			windowFrame.origin.x = NSMaxX( mainTermBaseRect ) + 1.0;
			windowFrame.origin.y = NSMinY( mainTermBaseRect ) - NSHeight( windowFrame ) - 1.0;
		}
		else if( termIdx == 4 )
		{
			windowFrame.origin.x = NSMaxX( mainTermBaseRect ) + 1.0;
			windowFrame.origin.y = NSMinY( mainTermBaseRect );
		}
		else if( termIdx == 5 )
		{
			windowFrame.origin.x = NSMinX( mainTermBaseRect ) + NSWidth( windowFrame ) + 1.0;
			windowFrame.origin.y = NSMinY( mainTermBaseRect ) - NSHeight( windowFrame ) - 1.0;
		}

		[window setFrame: windowFrame display: NO];
	}

	/* Override the default frame above if the user has adjusted windows in
	 * the past */
	if (autosaveName) [window setFrameAutosaveName:autosaveName];

    /* Tell it about its term. Do this after we've sized it so that the sizing
	 * doesn't trigger redrawing and such. */
    [context setTerm:t];
    
    /* Only order front if it's the first term. Other terms will be ordered
	 * front from AngbandUpdateWindowVisibility(). This is to work around a
	 * problem where Angband aggressively tells us to initialize terms that
	 * don't do anything! */
    if (t == angband_term[0]) [context->primaryWindow makeKeyAndOrderFront: nil];
    
    NSEnableScreenUpdates();
    
    /* Set "mapped" flag */
    t->mapped_flag = true;
    [pool drain];
}



/**
 * Nuke an old Term
 */
static void Term_nuke_cocoa(term *t)
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    
    AngbandContext *context = t->data;
    if (context)
    {
        /* Tell the context to get rid of its windows, etc. */
        [context dispose];
        
        /* Balance our CFRetain from when we created it */
        CFRelease(context);
        
        /* Done with it */
        t->data = NULL;
    }
    
    [pool drain];
}

/**
 * Returns the CGImageRef corresponding to an image with the given name in the
 * resource directory, transferring ownership to the caller
 */
static CGImageRef create_angband_image(NSString *path)
{
    CGImageRef decodedImage = NULL, result = NULL;
    
    /* Try using ImageIO to load the image */
    if (path)
    {
        NSURL *url = [[NSURL alloc] initFileURLWithPath:path isDirectory:NO];
        if (url)
        {
            NSDictionary *options = [[NSDictionary alloc] initWithObjectsAndKeys:(id)kCFBooleanTrue, kCGImageSourceShouldCache, nil];
            CGImageSourceRef source = CGImageSourceCreateWithURL((CFURLRef)url, (CFDictionaryRef)options);
            if (source)
            {
                /* We really want the largest image, but in practice there's
				 * only going to be one */
                decodedImage = CGImageSourceCreateImageAtIndex(source, 0, (CFDictionaryRef)options);
                CFRelease(source);
            }
            [options release];
            [url release];
        }
    }
    
    /* Draw the sucker to defeat ImageIO's weird desire to cache and decode on
	 * demand. Our images aren't that big! */
    if (decodedImage)
    {
        size_t width = CGImageGetWidth(decodedImage), height = CGImageGetHeight(decodedImage);
        
        /* Compute our own bitmap info */
        CGBitmapInfo imageBitmapInfo = CGImageGetBitmapInfo(decodedImage);
        CGBitmapInfo contextBitmapInfo = kCGBitmapByteOrderDefault;
        
        switch (imageBitmapInfo & kCGBitmapAlphaInfoMask) {
            case kCGImageAlphaNone:
            case kCGImageAlphaNoneSkipLast:
            case kCGImageAlphaNoneSkipFirst:
                /* No alpha */
                contextBitmapInfo |= kCGImageAlphaNone;
                break;
            default:
                /* Some alpha, use premultiplied last which is most efficient. */
                contextBitmapInfo |= kCGImageAlphaPremultipliedLast;
                break;
        }

        /* Draw the source image flipped, since the view is flipped */
        CGContextRef ctx = CGBitmapContextCreate(NULL, width, height, CGImageGetBitsPerComponent(decodedImage), CGImageGetBytesPerRow(decodedImage), CGImageGetColorSpace(decodedImage), contextBitmapInfo);
        CGContextSetBlendMode(ctx, kCGBlendModeCopy);
        CGContextTranslateCTM(ctx, 0.0, height);
        CGContextScaleCTM(ctx, 1.0, -1.0);
        CGContextDrawImage(ctx, CGRectMake(0, 0, width, height), decodedImage);
        result = CGBitmapContextCreateImage(ctx);

        /* Done with these things */
        CFRelease(ctx);
        CGImageRelease(decodedImage);
    }
    return result;
}

/**
 * React to changes
 */
static errr Term_xtra_cocoa_react(void)
{
    /* Don't actually switch graphics until the game is running */
    if (!initialized || !game_in_progress) return (-1);

    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    AngbandContext *angbandContext = Term->data;

    /* Handle graphics */
    int expected_graf_mode = (current_graphics_mode) ?
	current_graphics_mode->grafID : GRAPHICS_NONE;
    if (graf_mode_req != expected_graf_mode)
    {
        graphics_mode *new_mode;
	if (graf_mode_req != GRAPHICS_NONE) {
	    new_mode = get_graphics_mode(graf_mode_req);
	} else {
	    new_mode = NULL;
        }
        
        /* Get rid of the old image. CGImageRelease is NULL-safe. */
        CGImageRelease(pict_image);
        pict_image = NULL;
        
        /* Try creating the image if we want one */
        if (new_mode != NULL)
        {
            NSString *img_path = [NSString stringWithFormat:@"%s/%s", new_mode->path, new_mode->file];
            pict_image = create_angband_image(img_path);

            /* If we failed to create the image, set the new desired mode to
			 * NULL */
            if (! pict_image)
                new_mode = NULL;
        }
        
        /* Record what we did */
        use_graphics = new_mode ? new_mode->grafID : 0;
        ANGBAND_GRAF = (new_mode ? new_mode->graf : "ascii");
        current_graphics_mode = new_mode;
        
        /* Enable or disable higher picts. Note: this should be done for all
		 * terms. */
        angbandContext->terminal->higher_pict = !! use_graphics;
        
        if (pict_image && current_graphics_mode)
        {
            /* Compute the row and column count via the image height and width.
			 */
            pict_rows = (int)(CGImageGetHeight(pict_image) / current_graphics_mode->cell_height);
            pict_cols = (int)(CGImageGetWidth(pict_image) / current_graphics_mode->cell_width);
        }
        else
        {
            pict_rows = 0;
            pict_cols = 0;
        }
        
        /* Reset visuals */
        if (initialized && game_in_progress)
        {
            reset_visuals(p_ptr);
        }
    }

    [pool drain];
    
    /* Success */
    return (0);
}


/**
 * Draws one tile as a helper function for Term_xtra_cocoa_fresh().
 */
static void draw_image_tile(
    NSGraphicsContext* nsContext,
    CGContextRef cgContext,
    CGImageRef image,
    NSRect srcRect,
    NSRect dstRect,
    NSCompositingOperation op)
{
    /* Flip the source rect since the source image is flipped */
    CGAffineTransform flip = CGAffineTransformIdentity;
    flip = CGAffineTransformTranslate(flip, 0.0, CGImageGetHeight(image));
    flip = CGAffineTransformScale(flip, 1.0, -1.0);
    CGRect flippedSourceRect =
	CGRectApplyAffineTransform(NSRectToCGRect(srcRect), flip);

    /*
     * When we use high-quality resampling to draw a tile, pixels from outside
     * the tile may bleed in, causing graphics artifacts. Work around that.
     */
    CGImageRef subimage =
	CGImageCreateWithImageInRect(image, flippedSourceRect);
    [nsContext setCompositingOperation:op];
    CGContextDrawImage(cgContext, NSRectToCGRect(dstRect), subimage);
    CGImageRelease(subimage);
}


/**
 * This is a helper function for Term_xtra_cocoa_fresh():  look before a block
 * of text on a row to see if the bounds for rendering and clipping need to be
 * extended.
 */
static void query_before_text(
    struct PendingRowChange* prc, int iy, int npre, int* pclip, int* prend)
{
    int start = *prend;
    int i = start - 1;

    while (1) {
	if (i < 0 || i < start - npre) {
	    break;
	}

	if (prc->cell_changes[i].change_type == CELL_CHANGE_PICT) {
	    /*
	     * The cell has been rendered with a tile.  Do not want to modify
	     * its contents so the clipping and rendering region can not be
	     * extended.
	     */
	    break;
	} else if (prc->cell_changes[i].change_type == CELL_CHANGE_NONE) {
	    /* It has not changed so inquire what it is. */
	    TERM_COLOR a[2];
	    char c[2];

	    Term_what(i, iy, a + 1, c + 1);
	    if (use_graphics && (a[1] & 0x80) && (c[1] & 0x80)) {
		/*
		 * It is an unchanged location rendered with a tile.  Do not
		 * want to modify its contents so the clipping and rendering
		 * region can not be extended.
		 */
		break;
	    }
	    /*
	     * It is unchanged text.  A character from the changed region
	     * may have extended into it so render it to clear that.
	     */
#ifdef JP
	    /* Check to see if it is the second part of a kanji character. */
	    if (i > 0) {
		Term_what(i - 1, iy, a, c);
		if (iskanji(c)) {
		    prc->cell_changes[i - 1].c.w =
			convert_two_byte_eucjp_to_utf16_native(c);
		    prc->cell_changes[i - 1].a = a[0];
		    prc->cell_changes[i - 1].tcol = 1;
		    prc->cell_changes[i].c.w = 0;
		    prc->cell_changes[i].a = a[0];
		    prc->cell_changes[i].tcol = 0;
		    *pclip = i - 1;
		    *prend = i - 1;
		    --i;
		} else {
		    prc->cell_changes[i].c.w = c[1];
		    prc->cell_changes[i].a = a[1];
		    prc->cell_changes[i].tcol = 0;
		    *pclip = i;
		    *prend = i;
		}
	    } else {
		prc->cell_changes[i].c.w = c[1];
		prc->cell_changes[i].a = a[1];
		prc->cell_changes[i].tcol = 0;
		*pclip = i;
		*prend = i;
	    }
#else
	    prc->cell_changes[i].c.w = c[1];
	    prc->cell_changes[i].a = a[1];
	    prc->cell_changes[i].tcol = 0;
	    *pclip = i;
	    *prend = i;
#endif
	    --i;
	} else {
	    /*
	     * The cell has been wiped or had changed text rendered.  Do
	     * not need to render.  Can extend the clipping rectangle into it.
	     */
	    *pclip = i;
	    --i;
	}
    }
}


/**
 * This is a helper function for Term_xtra_cocoa_fresh():  look after a block
 * of text on a row to see if the bounds for rendering and clipping need to be
 * extended.
 */
static void query_after_text(
    struct PendingRowChange* prc,
    int iy,
    int ncol,
    int npost,
    int* pclip,
    int* prend)
{
    int end = *prend;
    int i = end + 1;

    while (1) {
	/*
	 * Be willing to consolidate this block with the one after it.  This
	 * logic should be sufficient to avoid redraws of the region between
	 * changed blocks of text if angbandContext->ncol_pre is zero or one.
	 * For larger values of ncol_pre, would need to do something more to
	 * avoid extra redraws.
	 */
	if (i >= ncol ||
	    (i > end + npost &&
	     prc->cell_changes[i].change_type != CELL_CHANGE_TEXT &&
	     prc->cell_changes[i].change_type != CELL_CHANGE_WIPE)) {
	    break;
	}

	if (prc->cell_changes[i].change_type == CELL_CHANGE_PICT) {
	    /*
	     * The cell has been rendered with a tile.  Do not want to modify
	     * its contents so the clipping and rendering region can not be
	     * extended.
	     */
	    break;
	} else if (prc->cell_changes[i].change_type == CELL_CHANGE_NONE) {
	    /* It has not changed so inquire what it is. */
	    TERM_COLOR a[2];
	    char c[2];

	    Term_what(i, iy, a, c);
	    if (use_graphics && (a[0] & 0x80) && (c[0] & 0x80)) {
		/*
		 * It is an unchanged location rendered with a tile.  Do not
		 * want to modify its contents so the clipping and rendering
		 * region can not be extended.
		 */
		break;
	    }
	    /*
	     * It is unchanged text.  A character from the changed region
	     * may have extended into it so render it to clear that.
	     */
#ifdef JP
	    /* Check to see if it is the first part of a kanji character. */
	    if (i < ncol - 1) {
		Term_what(i + 1, iy, a + 1, c + 1);
		if (iskanji(c)) {
		    prc->cell_changes[i].c.w =
			convert_two_byte_eucjp_to_utf16_native(c);
		    prc->cell_changes[i].a = a[0];
		    prc->cell_changes[i].tcol = 1;
		    prc->cell_changes[i + 1].c.w = 0;
		    prc->cell_changes[i + 1].a = a[0];
		    prc->cell_changes[i + 1].tcol = 0;
		    *pclip = i + 1;
		    *prend = i + 1;
		    ++i;
		} else {
		    prc->cell_changes[i].c.w = c[0];
		    prc->cell_changes[i].a = a[0];
		    prc->cell_changes[i].tcol = 0;
		    *pclip = i;
		    *prend = i;
		}
	    } else {
		prc->cell_changes[i].c.w = c[0];
		prc->cell_changes[i].a = a[0];
		prc->cell_changes[i].tcol = 0;
		*pclip = i;
		*prend = i;
	    }
#else
	    prc->cell_changes[i].c.w = c[0];
	    prc->cell_changes[i].a = a[0];
	    prc->cell_changes[i].tcol = 0;
	    *pclip = i;
	    *prend = i;
#endif
	    ++i;
	} else {
	    /*
	     * Have come to another region of changed text or another region
	     * to wipe.  Combine the regions to minimize redraws.
	     */
	    *pclip = i;
	    *prend = i;
	    end = i;
	    ++i;
	}
    }
}


/**
 * Draw the pending changes saved in angbandContext->changes.
 */
static void Term_xtra_cocoa_fresh(AngbandContext* angbandContext)
{
    int graf_width, graf_height, alphablend;

    if (angbandContext->changes->has_pict) {
	CGImageAlphaInfo ainfo = CGImageGetAlphaInfo(pict_image);

	graf_width = current_graphics_mode->cell_width;
	graf_height = current_graphics_mode->cell_height;
	/*
	 * As of this writing, a value of zero for
	 * current_graphics_mode->alphablend can mean either that the tile set
	 * doesn't have an alpha channel or it does but it only takes on values
	 * of 0 or 255.  For main-cocoa.m's purposes, the latter is rendered
	 * using the same procedure as if alphablend was nonzero.  The former
	 * is handled differently, but alphablend doesn't distinguish it from
	 * the latter.  So ignore alphablend and directly test whether an
	 * alpha channel is present.
	 */
	alphablend = (ainfo & (kCGImageAlphaPremultipliedFirst |
			       kCGImageAlphaPremultipliedLast)) ? 1 : 0;
    } else {
	graf_width = 0;
	graf_height = 0;
	alphablend = 0;
    }

    CGContextRef ctx = [angbandContext lockFocus];

    if (angbandContext->changes->has_text ||
	angbandContext->changes->has_wipe) {
	NSFont *selectionFont = [[angbandContext selectionFont] screenFont];
	[selectionFont set];
    }

    int iy;
    for (iy = angbandContext->changes->ymin;
	 iy <= angbandContext->changes->ymax;
	 ++iy) {
	struct PendingRowChange* prc = angbandContext->changes->rows[iy];
	int ix;

	/* Skip untouched rows. */
	if (prc == 0) {
	    continue;
	}

	ix = prc->xmin;
	while (1) {
	    int jx;

	    if (ix > prc->xmax) {
		break;
	    }

	    switch (prc->cell_changes[ix].change_type) {
	    case CELL_CHANGE_NONE:
		++ix;
		break;

	    case CELL_CHANGE_PICT:
		{
		    /*
		     * Because changes are made to the compositing mode, save
		     * the incoming value.
		     */
		    NSGraphicsContext *nsContext =
			[NSGraphicsContext currentContext];
		    NSCompositingOperation op = nsContext.compositingOperation;

		    jx = ix;
		    while (jx <= prc->xmax &&
			   prc->cell_changes[jx].change_type
			   == CELL_CHANGE_PICT) {
			NSRect destinationRect =
			    [angbandContext rectInImageForTileAtX:jx Y:iy];
			NSRect sourceRect, terrainRect;

			sourceRect.origin.x = graf_width *
			    prc->cell_changes[jx].c.c;
			sourceRect.origin.y = graf_height *
			    prc->cell_changes[jx].a;
			sourceRect.size.width = graf_width;
			sourceRect.size.height = graf_height;
			terrainRect.origin.x = graf_width *
			    prc->cell_changes[jx].tcol;
			terrainRect.origin.y = graf_height *
			    prc->cell_changes[jx].trow;
			terrainRect.size.width = graf_width;
			terrainRect.size.height = graf_height;
			if (alphablend) {
			    draw_image_tile(
				nsContext,
				ctx,
				pict_image,
				terrainRect,
				destinationRect,
				NSCompositeCopy);
			    /*
			     * Skip drawing the foreground if it is the same
			     * as the background.
			     */
			    if (sourceRect.origin.x != terrainRect.origin.x ||
				sourceRect.origin.y != terrainRect.origin.y) {
				draw_image_tile(
				    nsContext,
				    ctx,
				    pict_image,
				    sourceRect,
				    destinationRect,
				    NSCompositeSourceOver);
			    }
			} else {
			    draw_image_tile(
				nsContext,
				ctx,
				pict_image,
				sourceRect,
				destinationRect,
				NSCompositeCopy);
			}
			++jx;
		    }

		    [nsContext setCompositingOperation:op];

		    NSRect rect =
			[angbandContext rectInImageForTileAtX:ix Y:iy];
		    rect.size.width =
			angbandContext->tileSize.width * (jx - ix);
		    [angbandContext setNeedsDisplayInBaseRect:rect];
		}
		ix = jx;
		break;

	    case CELL_CHANGE_WIPE:
	    case CELL_CHANGE_TEXT:
		/*
		 * For a wiped region, treat it as if it had text (the only
		 * loss if it was not is some extra work rendering
		 * neighboring unchanged text).
		 */
		jx = ix + 1;
		while (jx < angbandContext->cols &&
		       (prc->cell_changes[jx].change_type
			== CELL_CHANGE_TEXT
			|| prc->cell_changes[jx].change_type
			== CELL_CHANGE_WIPE)) {
		    ++jx;
		}
		{
		    int isclip = ix;
		    int ieclip = jx - 1;
		    int isrend = ix;
		    int ierend = jx - 1;
		    int set_color = 1;
		    TERM_COLOR alast = 0;
		    NSRect r;
		    int k;

		    query_before_text(
			prc, iy, angbandContext->ncol_pre, &isclip, &isrend);
		    query_after_text(
			prc,
			iy,
			angbandContext->cols,
			angbandContext->ncol_post,
			&ieclip,
			&ierend
		    );
		    ix = ierend + 1;

		    /* Save the state since the clipping will be modified. */
		    CGContextSaveGState(ctx);

		    /* Clear the area where rendering will be done. */
		    r = [angbandContext rectInImageForTileAtX:isrend Y:iy];
		    r.size.width = angbandContext->tileSize.width *
			(ierend - isrend + 1);
		    [[NSColor blackColor] set];
		    NSRectFill(r);

		    /*
		     * Clear the current path so it does not affect clipping.
		     * Then set the clipping rectangle.  Using
		     * CGContextSetTextDrawingMode() to include clipping does
		     * not appear to be necessary on 10.14 and is actually
		     * detrimental:  when displaying more than one character,
		     * only the first is visible.
		     */
		    CGContextBeginPath(ctx);
		    r = [angbandContext rectInImageForTileAtX:isclip Y:iy];
		    r.size.width = angbandContext->tileSize.width *
			(ieclip - isclip + 1);
		    CGContextClipToRect(ctx, r);

		    /* Render. */
		    k = isrend;
		    while (k <= ierend) {
			NSRect rectToDraw;

			if (prc->cell_changes[k].change_type
			    == CELL_CHANGE_WIPE) {
			    /* Skip over since no rendering is necessary. */
			    ++k;
			    continue;
			}

			if (set_color || alast != prc->cell_changes[k].a) {
			    set_color = 0;
			    alast = prc->cell_changes[k].a;
			    set_color_for_index(alast % MAX_COLORS);
			}

			rectToDraw =
			    [angbandContext rectInImageForTileAtX:k Y:iy];
			if (prc->cell_changes[k].tcol) {
			    rectToDraw.size.width *= 2.0;
			    [angbandContext drawWChar:prc->cell_changes[k].c.w
					    inRect:rectToDraw context:ctx];
			    k += 2;
			} else {
			    [angbandContext drawWChar:prc->cell_changes[k].c.w
					    inRect:rectToDraw context:ctx];
			    ++k;
			}
		    }

		    /*
		     * Inform the context that the area in the clipping
		     * rectangle needs to be redisplayed.
		     */
		    [angbandContext setNeedsDisplayInBaseRect:r];

		    CGContextRestoreGState(ctx);
		}
		break;
	    }
	}
    }

    if (angbandContext->changes->xcurs >= 0 &&
	angbandContext->changes->ycurs >= 0) {
	NSRect rect = [angbandContext
			  rectInImageForTileAtX:angbandContext->changes->xcurs
			  Y:angbandContext->changes->ycurs];

	if (angbandContext->changes->bigcurs) {
	    rect.size.width += angbandContext->tileSize.width;
	}
	[[NSColor yellowColor] set];
	NSFrameRectWithWidth(rect, 1);
	/* Invalidate that rect */
	[angbandContext setNeedsDisplayInBaseRect:rect];
    }

    [angbandContext unlockFocus];
}


/**
 * Do a "special thing"
 */
static errr Term_xtra_cocoa(int n, int v)
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    AngbandContext* angbandContext = Term->data;
    
    errr result = 0;
    
    /* Analyze */
    switch (n)
    {
		/* Make a noise */
        case TERM_XTRA_NOISE:
        {
            NSBeep();
            
            /* Success */
            break;
        }

	/*  Make a sound */
        case TERM_XTRA_SOUND:
	    play_sound(v);
	    break;

            /* Process random events */
        case TERM_XTRA_BORED:
        {
            /* Show or hide cocoa windows based on the subwindow flags set by
			 * the user */
            AngbandUpdateWindowVisibility();

            /* Process an event */
            (void)check_events(CHECK_EVENTS_NO_WAIT);
            
            /* Success */
            break;
        }
            
		/* Process pending events */
        case TERM_XTRA_EVENT:
        {
            /* Process an event */
            (void)check_events(v);
            
            /* Success */
            break;
        }
            
		/* Flush all pending events (if any) */
        case TERM_XTRA_FLUSH:
        {
            /* Hack -- flush all events */
            while (check_events(CHECK_EVENTS_DRAIN)) /* loop */;
            
            /* Success */
            break;
        }
            
		/* Hack -- Change the "soft level" */
        case TERM_XTRA_LEVEL:
        {
            /* Here we could activate (if requested), but I don't think Angband
			 * should be telling us our window order (the user should decide
			 * that), so do nothing. */            
            break;
        }
            
		/* Clear the screen */
        case TERM_XTRA_CLEAR:
        {        
            [angbandContext lockFocus];
            [[NSColor blackColor] set];
            NSRect imageRect = {NSZeroPoint, [angbandContext imageSize]};
            NSRectFillUsingOperation(imageRect, NSCompositeCopy);
            [angbandContext unlockFocus];
            [angbandContext setNeedsDisplay:YES];
            /* Success */
            break;
        }
            
		/* React to changes */
        case TERM_XTRA_REACT:
        {
            /* React to changes */
            return (Term_xtra_cocoa_react());
        }
            
		/* Delay (milliseconds) */
        case TERM_XTRA_DELAY:
        {
            /* If needed */
            if (v > 0)
            {
                
                double seconds = v / 1000.;
                NSDate* date = [NSDate dateWithTimeIntervalSinceNow:seconds];
                do
                {
                    NSEvent* event;
                    do
                    {
                        event = [NSApp nextEventMatchingMask:-1 untilDate:date inMode:NSDefaultRunLoopMode dequeue:YES];
                        if (event) send_event(event);
                    } while (event);
                } while ([date timeIntervalSinceNow] >= 0);
                
            }
            
            /* Success */
            break;
        }
            
        case TERM_XTRA_FRESH:
	    /* Draw the pending changes. */
	    if (angbandContext->changes != 0) {
		Term_xtra_cocoa_fresh(angbandContext);
		clear_pending_changes(angbandContext->changes);
	    }
            break;
            
        default:
            /* Oops */
            result = 1;
            break;
    }
    
    [pool drain];
    
    /* Oops */
    return result;
}

static errr Term_curs_cocoa(int x, int y)
{
    AngbandContext *angbandContext = Term->data;

    if (angbandContext->changes == 0) {
	/* Bail out; there was an earlier memory allocation failure. */
	return 1;
    }
    angbandContext->changes->xcurs = x;
    angbandContext->changes->ycurs = y;
    angbandContext->changes->bigcurs = 0;

    /* Success */
    return 0;
}

/**
 * Draw a cursor that's two tiles wide.  For Japanese, that's used when
 * the cursor points at a kanji character, irregardless of whether operating
 * in big tile mode.
 */
static errr Term_bigcurs_cocoa(int x, int y)
{
    AngbandContext *angbandContext = Term->data;

    if (angbandContext->changes == 0) {
	/* Bail out; there was an earlier memory allocation failure. */
	return 1;
    }
    angbandContext->changes->xcurs = x;
    angbandContext->changes->ycurs = y;
    angbandContext->changes->bigcurs = 1;

    /* Success */
    return 0;
}

/**
 * Low level graphics (Assumes valid input)
 *
 * Erase "n" characters starting at (x,y)
 */
static errr Term_wipe_cocoa(int x, int y, int n)
{
    AngbandContext *angbandContext = Term->data;
    struct PendingCellChange *pc;

    if (angbandContext->changes == 0) {
	/* Bail out; there was an earlier memory allocation failure. */
	return 1;
    }
    if (angbandContext->changes->rows[y] == 0) {
	angbandContext->changes->rows[y] =
	    create_row_change(angbandContext->cols);
	if (angbandContext->changes->rows[y] == 0) {
	    NSLog(@"failed to allocate changes for row %d", y);
	    return 1;
	}
	if (angbandContext->changes->ymin > y) {
	    angbandContext->changes->ymin = y;
	}
	if (angbandContext->changes->ymax < y) {
	    angbandContext->changes->ymax = y;
	}
    }

    angbandContext->changes->has_wipe = 1;
    if (angbandContext->changes->rows[y]->xmin > x) {
	angbandContext->changes->rows[y]->xmin = x;
    }
    if (angbandContext->changes->rows[y]->xmax < x + n - 1) {
	angbandContext->changes->rows[y]->xmax = x + n - 1;
    }
    for (pc = angbandContext->changes->rows[y]->cell_changes + x;
	 pc != angbandContext->changes->rows[y]->cell_changes + x + n;
	 ++pc) {
	pc->change_type = CELL_CHANGE_WIPE;
    }
    
    /* Success */
    return (0);
}

static errr Term_pict_cocoa(int x, int y, int n, TERM_COLOR *ap,
                            const char *cp, const TERM_COLOR *tap,
                            const char *tcp)
{
    
    /* Paranoia: Bail if we don't have a current graphics mode */
    if (! current_graphics_mode) return -1;
    
    AngbandContext* angbandContext = Term->data;
    int any_change = 0;
    struct PendingCellChange *pc;

    if (angbandContext->changes == 0) {
	/* Bail out; there was an earlier memory allocation failure. */
	return 1;
    }
    if (angbandContext->changes->rows[y] == 0) {
	angbandContext->changes->rows[y] =
	    create_row_change(angbandContext->cols);
	if (angbandContext->changes->rows[y] == 0) {
	    NSLog(@"failed to allocate changes for row %d", y);
	    return 1;
	}
	if (angbandContext->changes->ymin > y) {
	    angbandContext->changes->ymin = y;
	}
	if (angbandContext->changes->ymax < y) {
	    angbandContext->changes->ymax = y;
	}
    }

    if (angbandContext->changes->rows[y]->xmin > x) {
	angbandContext->changes->rows[y]->xmin = x;
    }
    if (angbandContext->changes->rows[y]->xmax < x + n - 1) {
	angbandContext->changes->rows[y]->xmax = x + n - 1;
    }
    for (pc = angbandContext->changes->rows[y]->cell_changes + x;
	 pc != angbandContext->changes->rows[y]->cell_changes + x + n;
	 ++pc) {
	TERM_COLOR a = *ap++;
	char c = *cp++;
	TERM_COLOR ta = *tap++;
	char tc = *tcp++;

	if (use_graphics && (a & 0x80) && (c & 0x80)) {
	    pc->c.c = ((byte)c & 0x7F) % pict_cols;
	    pc->a = ((byte)a & 0x7F) % pict_rows;
	    pc->tcol = ((byte)tc & 0x7F) % pict_cols;
	    pc->trow = ((byte)ta & 0x7F) % pict_rows;
	    pc->change_type = CELL_CHANGE_PICT;
	    any_change = 1;
	}
    }
    if (any_change) {
	angbandContext->changes->has_pict = 1;
    }
    
    /* Success */
    return (0);
}

/**
 * Low level graphics.  Assumes valid input.
 *
 * Draw several ("n") chars, with an attr, at a given location.
 */
static errr Term_text_cocoa(int x, int y, int n, byte_hack a, concptr cp)
{
    AngbandContext* angbandContext = Term->data;
    struct PendingCellChange *pc;

    if (angbandContext->changes == 0) {
	/* Bail out; there was an earlier memory allocation failure. */
	return 1;
    }
    if (angbandContext->changes->rows[y] == 0) {
	angbandContext->changes->rows[y] =
	    create_row_change(angbandContext->cols);
	if (angbandContext->changes->rows[y] == 0) {
	    NSLog(@"failed to allocate changes for row %d", y);
	    return 1;
	}
	if (angbandContext->changes->ymin > y) {
	    angbandContext->changes->ymin = y;
	}
	if (angbandContext->changes->ymax < y) {
	    angbandContext->changes->ymax = y;
	}
    }

    angbandContext->changes->has_text = 1;
    if (angbandContext->changes->rows[y]->xmin > x) {
	angbandContext->changes->rows[y]->xmin = x;
    }
    if (angbandContext->changes->rows[y]->xmax < x + n - 1) {
	angbandContext->changes->rows[y]->xmax = x + n - 1;
    }
    pc = angbandContext->changes->rows[y]->cell_changes + x;
    while (pc != angbandContext->changes->rows[y]->cell_changes + x + n) {
#ifdef JP
	if (iskanji(*cp)) {
	    if (pc + 1 ==
		angbandContext->changes->rows[y]->cell_changes + x + n) {
		/*
		 * The second byte of the character is past the end.  Ignore
		 * the character.
		 */
		break;
	    } else {
		pc->c.w = convert_two_byte_eucjp_to_utf16_native(cp);
		pc->a = a;
		pc->tcol = 1;
		pc->change_type = CELL_CHANGE_TEXT;
		++pc;
		/*
		 * Fill in a dummy value since the previous character will take
		 * up two columns.
		 */
		pc->c.w = 0;
		pc->a = a;
		pc->tcol = 0;
		pc->change_type = CELL_CHANGE_TEXT;
		++pc;
		cp += 2;
	    }
	} else {
	    pc->c.w = *cp;
	    pc->a = a;
	    pc->tcol = 0;
	    pc->change_type = CELL_CHANGE_TEXT;
	    ++pc;
	    ++cp;
	}
#else
	pc->c.w = *cp;
	pc->a = a;
	pc->tcol = 0;
	pc->change_type = CELL_CHANGE_TEXT;
	++pc;
	++cp;
#endif
    }
    
    /* Success */
    return (0);
}

/**
 * Post a nonsense event so that our event loop wakes up
 */
static void wakeup_event_loop(void)
{
    /* Big hack - send a nonsense event to make us update */
    NSEvent *event = [NSEvent otherEventWithType:NSApplicationDefined location:NSZeroPoint modifierFlags:0 timestamp:0 windowNumber:0 context:NULL subtype:AngbandEventWakeup data1:0 data2:0];
    [NSApp postEvent:event atStart:NO];
}


/**
 * Create and initialize window number "i"
 */
static term *term_data_link(int i)
{
    NSArray *terminalDefaults = [[NSUserDefaults standardUserDefaults] valueForKey: AngbandTerminalsDefaultsKey];
    NSInteger rows = 24;
    NSInteger columns = 80;

    if( i < (int)[terminalDefaults count] )
    {
        NSDictionary *term = [terminalDefaults objectAtIndex: i];
        rows = [[term valueForKey: AngbandTerminalRowsDefaultsKey] integerValue];
        columns = [[term valueForKey: AngbandTerminalColumnsDefaultsKey] integerValue];
    }

    /* Allocate */
    term *newterm = ZNEW(term);

    /* Initialize the term */
    term_init(newterm, columns, rows, 256 /* keypresses, for some reason? */);
    
    /* Differentiate between BS/^h, Tab/^i, etc. */
    /* newterm->complex_input = TRUE; */

    /* Use a "software" cursor */
    newterm->soft_cursor = TRUE;
    
    /* Disable the per-row flush notifications since they are not used. */
    newterm->never_frosh = TRUE;

    /* Erase with "white space" */
    newterm->attr_blank = TERM_WHITE;
    newterm->char_blank = ' ';
    
    /* Prepare the init/nuke hooks */
    newterm->init_hook = Term_init_cocoa;
    newterm->nuke_hook = Term_nuke_cocoa;
    
    /* Prepare the function hooks */
    newterm->xtra_hook = Term_xtra_cocoa;
    newterm->wipe_hook = Term_wipe_cocoa;
    newterm->curs_hook = Term_curs_cocoa;
    newterm->bigcurs_hook = Term_bigcurs_cocoa;
    newterm->text_hook = Term_text_cocoa;
    newterm->pict_hook = Term_pict_cocoa;
    /* newterm->mbcs_hook = Term_mbcs_cocoa; */
    
    /* Global pointer */
    angband_term[i] = newterm;
    
    return newterm;
}

/**
 * Load preferences from preferences file for current host+current user+
 * current application.
 */
static void load_prefs()
{
    NSUserDefaults *defs = [NSUserDefaults angbandDefaults];
    
    /* Make some default defaults */
    NSMutableArray *defaultTerms = [[NSMutableArray alloc] init];

    /* The following default rows/cols were determined experimentally by first
	 * finding the ideal window/font size combinations. But because of awful
	 * temporal coupling in Term_init_cocoa(), it's impossible to set up the
	 * defaults there, so we do it this way. */
    for( NSUInteger i = 0; i < ANGBAND_TERM_MAX; i++ )
    {
		int columns, rows;
		BOOL visible = YES;

		switch( i )
		{
			case 0:
				columns = 129;
				rows = 32;
				break;
			case 1:
				columns = 84;
				rows = 20;
				break;
			case 2:
				columns = 42;
				rows = 24;
				break;
			case 3:
				columns = 42;
				rows = 20;
				break;
			case 4:
				columns = 42;
				rows = 16;
				break;
			case 5:
				columns = 84;
				rows = 20;
				break;
			default:
				columns = 80;
				rows = 24;
				visible = NO;
				break;
		}

		NSDictionary *standardTerm = [NSDictionary dictionaryWithObjectsAndKeys:
									  [NSNumber numberWithInt: rows], AngbandTerminalRowsDefaultsKey,
									  [NSNumber numberWithInt: columns], AngbandTerminalColumnsDefaultsKey,
									  [NSNumber numberWithBool: visible], AngbandTerminalVisibleDefaultsKey,
									  nil];
        [defaultTerms addObject: standardTerm];
    }

    NSDictionary *defaults = [[NSDictionary alloc] initWithObjectsAndKeys:
#ifdef JP
                              @"Osaka", @"FontName",
#else
                              @"Menlo", @"FontName",
#endif
                              [NSNumber numberWithFloat:13.f], @"FontSize",
                              [NSNumber numberWithInt:60], AngbandFrameRateDefaultsKey,
                              [NSNumber numberWithBool:YES], AngbandSoundDefaultsKey,
                              [NSNumber numberWithInt:GRAPHICS_NONE], AngbandGraphicsDefaultsKey,
                              defaultTerms, AngbandTerminalsDefaultsKey,
                              nil];
    [defs registerDefaults:defaults];
    [defaults release];
    [defaultTerms release];
    
    /* Preferred graphics mode */
    graf_mode_req = [defs integerForKey:AngbandGraphicsDefaultsKey];
    
    /* Use sounds; set the Angband global */
    use_sound = ([defs boolForKey:AngbandSoundDefaultsKey] == YES) ? TRUE : FALSE;
    
    /* fps */
    frames_per_second = [defs integerForKey:AngbandFrameRateDefaultsKey];
    
    /* Font */
    default_font = [[NSFont fontWithName:[defs valueForKey:@"FontName-0"] size:[defs floatForKey:@"FontSize-0"]] retain];
    if (! default_font) default_font = [[NSFont fontWithName:@"Menlo" size:13.] retain];
}

/**
 * Arbitary limit on number of possible samples per event
 */
#define MAX_SAMPLES            16

/**
 * Struct representing all data for a set of event samples
 */
typedef struct
{
	int num;        /* Number of available samples for this event */
	NSSound *sound[MAX_SAMPLES];
} sound_sample_list;

/**
 * Array of event sound structs
 */
static sound_sample_list samples[MSG_MAX];


/**
 * Load sound effects based on sound.cfg within the xtra/sound directory;
 * bridge to Cocoa to use NSSound for simple loading and playback, avoiding
 * I/O latency by cacheing all sounds at the start.  Inherits full sound
 * format support from Quicktime base/plugins.
 * pelpel favoured a plist-based parser for the future but .cfg support
 * improves cross-platform compatibility.
 */
static void load_sounds(void)
{
#if 0
	char sound_dir[1024];
	char path[1024];
	char buffer[2048];
	FILE *fff;
    
	/* Build the "sound" path */
	path_build(sound_dir, sizeof(sound_dir), ANGBAND_DIR_XTRA, "sound");
    
	/* Find and open the config file */
	path_build(path, sizeof(path), sound_dir, "sound.cfg");
	fff = my_fopen(path, "r");
    
	/* Handle errors */
	if (!fff)
	{
		NSLog(@"The sound configuration file could not be opened.");
		return;
	}
	
	/* Instantiate an autorelease pool for use by NSSound */
	NSAutoreleasePool *autorelease_pool;
	autorelease_pool = [[NSAutoreleasePool alloc] init];
    
    /* Use a dictionary to unique sounds, so we can share NSSounds across
	 * multiple events */
    NSMutableDictionary *sound_dict = [NSMutableDictionary dictionary];
    
	/*
	 * This loop may take a while depending on the count and size of samples
	 * to load.
	 */
    
	/* Parse the file */
	/* Lines are always of the form "name = sample [sample ...]" */
        while (my_fgets(fff, buffer, sizeof(buffer)) == 0)
	{
		char *msg_name;
		char *cfg_sample_list;
		char *search;
		char *cur_token;
		char *next_token;
		int event;
        
		/* Skip anything not beginning with an alphabetic character */
		if (!buffer[0] || !isalpha((unsigned char)buffer[0])) continue;
        
		/* Split the line into two: message name, and the rest */
		search = strchr(buffer, ' ');
		cfg_sample_list = strchr(search + 1, ' ');
		if (!search) continue;
		if (!cfg_sample_list) continue;
        
		/* Set the message name, and terminate at first space */
		msg_name = buffer;
		search[0] = '\0';
        
		/* Make sure this is a valid event name */
		for (event = MSG_MAX - 1; event >= 0; event--)
		{
			if (strcmp(msg_name, angband_sound_name[event]) == 0)
				break;
		}
		if (event < 0) continue;
        
		/* Advance the sample list pointer so it's at the beginning of text */
		cfg_sample_list++;
		if (!cfg_sample_list[0]) continue;
        
		/* Terminate the current token */
		cur_token = cfg_sample_list;
		search = strchr(cur_token, ' ');
		if (search)
		{
			search[0] = '\0';
			next_token = search + 1;
		}
		else
		{
			next_token = NULL;
		}
        
		/*
		 * Now we find all the sample names and add them one by one
		 */
		while (cur_token)
		{
			int num = samples[event].num;
            
			/* Don't allow too many samples */
			if (num >= MAX_SAMPLES) break;
            
            NSString *token_string = [NSString stringWithUTF8String:cur_token];
            NSSound *sound = [sound_dict objectForKey:token_string];
            
            if (! sound)
            {
		struct stat stb;

                /* We have to load the sound. Build the path to the sample */
                path_build(path, sizeof(path), sound_dir, cur_token);
                if (stat(path, &stb) == 0)
		{
                    
                    /* Load the sound into memory */
                    sound = [[[NSSound alloc] initWithContentsOfFile:[NSString stringWithUTF8String:path] byReference:YES] autorelease];
                    if (sound) [sound_dict setObject:sound forKey:token_string];
                }
            }
            
            /* Store it if we loaded it */
            if (sound)
            {
                samples[event].sound[num] = [sound retain];
                
                /* Imcrement the sample count */
                samples[event].num++;
            }
            
            
			/* Figure out next token */
			cur_token = next_token;
			if (next_token)
			{
				/* Try to find a space */
				search = strchr(cur_token, ' ');
                
				/* If we can find one, terminate, and set new "next" */
				if (search)
				{
					search[0] = '\0';
					next_token = search + 1;
				}
				else
				{
					/* Otherwise prevent infinite looping */
					next_token = NULL;
				}
			}
		}
	}
    
	/* Release the autorelease pool */
	[autorelease_pool release];
    
	/* Close the file */
	my_fclose(fff);
#endif
}

/**
 * Play sound effects asynchronously.  Select a sound from any available
 * for the required event, and bridge to Cocoa to play it.
 */
static void play_sound(int event)
{    
	/* Paranoia */
	if (event < 0 || event >= MSG_MAX) return;
    
    /* Load sounds just-in-time (once) */
    static BOOL loaded = NO;
    if (!loaded) {
        loaded = YES;
        load_sounds();
    }
    
    /* Check there are samples for this event */
    if (!samples[event].num) return;
    
    /* Instantiate an autorelease pool for use by NSSound */
    NSAutoreleasePool *autorelease_pool;
    autorelease_pool = [[NSAutoreleasePool alloc] init];
    
    /* Choose a random event */
    int s = randint0(samples[event].num);
    
    /* Stop the sound if it's currently playing */
    if ([samples[event].sound[s] isPlaying])
        [samples[event].sound[s] stop];
    
    /* Play the sound */
    [samples[event].sound[s] play];
    
    /* Release the autorelease pool */
    [autorelease_pool drain];
}

/*
 * 
 */
static void init_windows(void)
{
    /* Create the main window */
    term *primary = term_data_link(0);
    
    /* Prepare to create any additional windows */
    int i;
    for (i=1; i < ANGBAND_TERM_MAX; i++) {
        term_data_link(i);
    }
    
    /* Activate the primary term */
    Term_activate(primary);
}

/**
 * Handle the "open_when_ready" flag
 */
static void handle_open_when_ready(void)
{
    /* Check the flag XXX XXX XXX make a function for this */
    if (open_when_ready && initialized && !game_in_progress)
    {
        /* Forget */
        open_when_ready = FALSE;
        
        /* Game is in progress */
        game_in_progress = TRUE;
        
        /* Wait for a keypress */
        pause_line(Term->hgt - 1);
    }
}


/**
 * Handle quit_when_ready, by Peter Ammon,
 * slightly modified to check inkey_flag.
 */
static void quit_calmly(void)
{
    /* Quit immediately if game's not started */
    if (!game_in_progress || !current_world_ptr->character_generated) quit(NULL);

    /* Save the game and Quit (if it's safe) */
    if (inkey_flag)
    {
        /* Hack -- Forget messages and term */
        msg_flag = FALSE;
		Term->mapped_flag = FALSE;

        /* Save the game */
        do_cmd_save_game(p_ptr, FALSE);
        record_current_savefile();

        /* Quit */
        quit(NULL);
    }

    /* Wait until inkey_flag is set */
}



/**
 * Returns YES if we contain an AngbandView (and hence should direct our events
 * to Angband)
 */
static BOOL contains_angband_view(NSView *view)
{
    if ([view isKindOfClass:[AngbandView class]]) return YES;
    for (NSView *subview in [view subviews]) {
        if (contains_angband_view(subview)) return YES;
    }
    return NO;
}


/**
 * Queue mouse presses if they occur in the map section of the main window.
 */
static void AngbandHandleEventMouseDown( NSEvent *event )
{
#if 0
	AngbandContext *angbandContext = [[[event window] contentView] angbandContext];
	AngbandContext *mainAngbandContext = angband_term[0]->data;

	if (mainAngbandContext->primaryWindow && [[event window] windowNumber] == [mainAngbandContext->primaryWindow windowNumber])
	{
		int cols, rows, x, y;
		Term_get_size(&cols, &rows);
		NSSize tileSize = angbandContext->tileSize;
		NSSize border = angbandContext->borderSize;
		NSPoint windowPoint = [event locationInWindow];

		/* Adjust for border; add border height because window origin is at
		 * bottom */
		windowPoint = NSMakePoint( windowPoint.x - border.width, windowPoint.y + border.height );

		NSPoint p = [[[event window] contentView] convertPoint: windowPoint fromView: nil];
		x = floor( p.x / tileSize.width );
		y = floor( p.y / tileSize.height );

		/* Being safe about this, since xcode doesn't seem to like the
		 * bool_hack stuff */
		BOOL displayingMapInterface = ((int)inkey_flag != 0);

		/* Sidebar plus border == thirteen characters; top row is reserved. */
		/* Coordinates run from (0,0) to (cols-1, rows-1). */
		BOOL mouseInMapSection = (x > 13 && x <= cols - 1 && y > 0  && y <= rows - 2);

		/* If we are displaying a menu, allow clicks anywhere; if we are
		 * displaying the main game interface, only allow clicks in the map
		 * section */
		if (!displayingMapInterface || (displayingMapInterface && mouseInMapSection))
		{
			/* [event buttonNumber] will return 0 for left click,
			 * 1 for right click, but this is safer */
			int button = ([event type] == NSLeftMouseDown) ? 1 : 2;

#ifdef KC_MOD_ALT
			NSUInteger eventModifiers = [event modifierFlags];
			byte angbandModifiers = 0;
			angbandModifiers |= (eventModifiers & NSShiftKeyMask) ? KC_MOD_SHIFT : 0;
			angbandModifiers |= (eventModifiers & NSControlKeyMask) ? KC_MOD_CONTROL : 0;
			angbandModifiers |= (eventModifiers & NSAlternateKeyMask) ? KC_MOD_ALT : 0;
			button |= (angbandModifiers & 0x0F) << 4; /* encode modifiers in the button number (see Term_mousepress()) */
#endif

			Term_mousepress(x, y, button);
		}
	}
#endif
    /* Pass click through to permit focus change, resize, etc. */
    [NSApp sendEvent:event];
}



/**
 * Encodes an NSEvent Angband-style, or forwards it along.  Returns YES if the
 * event was sent to Angband, NO if Cocoa (or nothing) handled it */
static BOOL send_event(NSEvent *event)
{

    /* If the receiving window is not an Angband window, then do nothing */
    if (! contains_angband_view([[event window] contentView]))
    {
        [NSApp sendEvent:event];
        return NO;
    }

    /* Analyze the event */
    switch ([event type])
    {
        case NSKeyDown:
        {
            /* Try performing a key equivalent */
            if ([[NSApp mainMenu] performKeyEquivalent:event]) break;
            
            unsigned modifiers = [event modifierFlags];
            
            /* Send all NSCommandKeyMasks through */
            if (modifiers & NSCommandKeyMask)
            {
                [NSApp sendEvent:event];
                break;
            }
            
            if (! [[event characters] length]) break;
            
            
            /* Extract some modifiers */
            int mc = !! (modifiers & NSControlKeyMask);
            int ms = !! (modifiers & NSShiftKeyMask);
            int mo = !! (modifiers & NSAlternateKeyMask);
            int kp = !! (modifiers & NSNumericPadKeyMask);
            
            
            /* Get the Angband char corresponding to this unichar */
            unichar c = [[event characters] characterAtIndex:0];
            char ch;
	    /*
	     * Have anything from the numeric keypad generate a macro
	     * trigger so that shift or control modifiers can be passed.
	     */
	    if (c <= 0x7F && !kp)
	    {
		ch = (char) c;
	    }
	    else {
		/*
		 * The rest of Hengband uses Angband 2.7's or so key handling:
		 * so for the rest do something like the encoding that
		 * main-win.c does:  send a macro trigger with the Unicode
		 * value encoded into printable ASCII characters.
		 */
		ch = '\0';
            }
            
            /* override special keys */
            switch([event keyCode]) {
                case kVK_Return: ch = '\r'; break;
                case kVK_Escape: ch = 27; break;
                case kVK_Tab: ch = '\t'; break;
                case kVK_Delete: ch = '\b'; break;
	        case kVK_ANSI_KeypadEnter: ch = '\r'; kp = TRUE; break;
            }

            /* Hide the mouse pointer */
            [NSCursor setHiddenUntilMouseMoves:YES];
            
            /* Enqueue it */
            if (ch != '\0')
            {
                Term_keypress(ch);
            }
	    else
	    {
		/*
		 * Could use the hexsym global but some characters overlap with
		 * those used to indicate modifiers.
		 */
		const char encoded[16] = {
		    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b',
		    'c', 'd', 'e', 'f'
		};

		/* Begin the macro trigger. */
		Term_keypress(31);

		/* Send the modifiers. */
		if (mc) Term_keypress('C');
		if (ms) Term_keypress('S');
		if (mo) Term_keypress('O');
		if (kp) Term_keypress('K');

		do {
		    Term_keypress(encoded[c & 0xF]);
		    c >>= 4;
		} while (c > 0);

		/* End the macro trigger. */
		Term_keypress(13);
	    }
            
            break;
        }
            
        case NSLeftMouseDown:
		case NSRightMouseDown:
			AngbandHandleEventMouseDown(event);
            break;

        case NSApplicationDefined:
        {
            if ([event subtype] == AngbandEventWakeup)
            {
                return YES;
            }
            break;
        }
            
        default:
            [NSApp sendEvent:event];
            return YES;
    }
    return YES;
}

/**
 * Check for Events, return TRUE if we process any
 */
static BOOL check_events(int wait)
{ 
    
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    
    /* Handles the quit_when_ready flag */
    if (quit_when_ready) quit_calmly();
    
    NSDate* endDate;
    if (wait == CHECK_EVENTS_WAIT) endDate = [NSDate distantFuture];
    else endDate = [NSDate distantPast];
    
    NSEvent* event;
    for (;;) {
        if (quit_when_ready)
        {
            /* send escape events until we quit */
            Term_keypress(0x1B);
            [pool drain];
            return false;
        }
        else {
            event = [NSApp nextEventMatchingMask:-1 untilDate:endDate inMode:NSDefaultRunLoopMode dequeue:YES];
            if (! event)
            {
                [pool drain];
                return FALSE;
            }
            if (send_event(event)) break;
        }
    }
    
    [pool drain];
    
    /* Something happened */
    return YES;
    
}

/**
 * Hook to tell the user something important
 */
static void hook_plog(const char * str)
{
    if (str)
    {
	NSString *msg = NSLocalizedStringWithDefaultValue(
	    @"Warning", AngbandMessageCatalog, [NSBundle mainBundle],
	    @"Warning", @"Alert text for generic warning");
        NSString *info = [NSString stringWithCString:str
#ifdef JP
				   encoding:NSJapaneseEUCStringEncoding
#else
				   encoding:NSMacOSRomanStringEncoding
#endif
	];
	NSAlert *alert = [[NSAlert alloc] init];

	alert.messageText = msg;
	alert.informativeText = info;
	NSModalResponse result = [alert runModal];
	[alert release];
    }
}


/**
 * Hook to tell the user something, and then quit
 */
static void hook_quit(const char * str)
{
    plog(str);
    exit(0);
}

/**
 * ------------------------------------------------------------------------
 * Main program
 * ------------------------------------------------------------------------ */

@implementation AngbandAppDelegate

@synthesize graphicsMenu=_graphicsMenu;
@synthesize commandMenu=_commandMenu;
@synthesize commandMenuTagMap=_commandMenuTagMap;

- (IBAction)newGame:sender
{
    /* Game is in progress */
    game_in_progress = TRUE;
    new_game = TRUE;
}

- (IBAction)editFont:sender
{
    NSFontPanel *panel = [NSFontPanel sharedFontPanel];
    NSFont *termFont = default_font;

    int i;
    for (i=0; i < ANGBAND_TERM_MAX; i++) {
        if ([(id)angband_term[i]->data isMainWindow]) {
            termFont = [(id)angband_term[i]->data selectionFont];
            break;
        }
    }
    
    [panel setPanelFont:termFont isMultiple:NO];
    [panel orderFront:self];
}

/**
 * Implent NSObject's changeFont() method to receive a notification about the
 * changed font.  Note that, as of 10.14, changeFont() is deprecated in
 * NSObject - it will be removed at some point and the application delegate
 * will have to be declared as implementing the NSFontChanging protocol.
 */
- (void)changeFont:(id)sender
{
    int mainTerm;
    for (mainTerm=0; mainTerm < ANGBAND_TERM_MAX; mainTerm++) {
        if ([(id)angband_term[mainTerm]->data isMainWindow]) {
            break;
        }
    }

    /* Bug #1709: Only change font for angband windows */
    if (mainTerm == ANGBAND_TERM_MAX) return;
    
    NSFont *oldFont = default_font;
    NSFont *newFont = [sender convertFont:oldFont];
    if (! newFont) return; /*paranoia */
    
    /* Store as the default font if we changed the first term */
    if (mainTerm == 0) {
        [newFont retain];
        [default_font release];
        default_font = newFont;
    }
    
    /* Record it in the preferences */
    NSUserDefaults *defs = [NSUserDefaults angbandDefaults];
    [defs setValue:[newFont fontName] 
        forKey:[NSString stringWithFormat:@"FontName-%d", mainTerm]];
    [defs setFloat:[newFont pointSize]
        forKey:[NSString stringWithFormat:@"FontSize-%d", mainTerm]];
    [defs synchronize];
    
    NSDisableScreenUpdates();
    
    /* Update window */
    AngbandContext *angbandContext = angband_term[mainTerm]->data;
    [(id)angbandContext setSelectionFont:newFont adjustTerminal: YES];
    
    NSEnableScreenUpdates();

    if (mainTerm == 0 && game_in_progress) {
	/* Mimics the logic in setGraphicsMode(). */
	do_cmd_redraw(p_ptr);
	wakeup_event_loop();
    } else {
	[(id)angbandContext requestRedraw];
    }
}

- (IBAction)openGame:sender
{
    NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
    BOOL selectedSomething = NO;
    int panelResult;
    
    /* Get where we think the save files are */
    NSURL *startingDirectoryURL = [NSURL fileURLWithPath:[NSString stringWithCString:ANGBAND_DIR_SAVE encoding:NSASCIIStringEncoding] isDirectory:YES];
    
    /* Set up an open panel */
    NSOpenPanel* panel = [NSOpenPanel openPanel];
    [panel setCanChooseFiles:YES];
    [panel setCanChooseDirectories:NO];
    [panel setResolvesAliases:YES];
    [panel setAllowsMultipleSelection:NO];
    [panel setTreatsFilePackagesAsDirectories:YES];
    [panel setDirectoryURL:startingDirectoryURL];
    
    /* Run it */
    panelResult = [panel runModal];
    if (panelResult == NSOKButton)
    {
        NSArray* fileURLs = [panel URLs];
        if ([fileURLs count] > 0 && [[fileURLs objectAtIndex:0] isFileURL])
        {
            NSURL* savefileURL = (NSURL *)[fileURLs objectAtIndex:0];
            /* The path property doesn't do the right thing except for
             * URLs with the file scheme. We had getFileSystemRepresentation
             * here before, but that wasn't introduced until OS X 10.9. */
            selectedSomething = [[savefileURL path] getCString:savefile 
                maxLength:sizeof savefile encoding:NSMacOSRomanStringEncoding];
        }
    }
    
    if (selectedSomething)
    {
        /* Remember this so we can select it by default next time */
        record_current_savefile();
        
        /* Game is in progress */
        game_in_progress = TRUE;
        new_game = FALSE;
    }
    
    [pool drain];
}

- (IBAction)saveGame:sender
{
    /* Hack -- Forget messages */
    msg_flag = FALSE;
    
    /* Save the game */
    do_cmd_save_game(p_ptr, FALSE);
    
    /* Record the current save file so we can select it by default next time.
	 * It's a little sketchy that this only happens when we save through the
	 * menu; ideally game-triggered saves would trigger it too. */
    record_current_savefile();
}

/**
 * Implement NSObject's validateMenuItem() method to override enabling or
 * disabling a menu item.  Note that, as of 10.14, validateMenuItem() is
 * deprecated in NSObject - it will be removed at some point and  the
 * application delegate will have to be declared as implementing the
 * NSMenuItemValidation protocol.
 */
- (BOOL)validateMenuItem:(NSMenuItem *)menuItem
{
    SEL sel = [menuItem action];
    NSInteger tag = [menuItem tag];

    if( tag >= AngbandWindowMenuItemTagBase && tag < AngbandWindowMenuItemTagBase + ANGBAND_TERM_MAX )
    {
        if( tag == AngbandWindowMenuItemTagBase )
        {
            /* The main window should always be available and visible */
            return YES;
        }
        else
        {
	    /*
	     * Another window is only usable after Term_init_cocoa() has
	     * been called for it.  For Angband if window_flag[i] is nonzero
	     * then that has happened for window i.  For Hengband, that is
	     * not the case so also test angband_term[i]->data.
	     */
            NSInteger subwindowNumber = tag - AngbandWindowMenuItemTagBase;
            return (angband_term[subwindowNumber]->data != 0
		    && window_flag[subwindowNumber] > 0);
        }

        return NO;
    }

    if (sel == @selector(newGame:))
    {
        return ! game_in_progress;
    }
    else if (sel == @selector(editFont:))
    {
        return YES;
    }
    else if (sel == @selector(openGame:))
    {
        return ! game_in_progress;
    }
    else if (sel == @selector(setRefreshRate:) &&
	     [[menuItem parentItem] tag] == 150)
    {
        NSInteger fps = [[NSUserDefaults standardUserDefaults] integerForKey:AngbandFrameRateDefaultsKey];
        [menuItem setState: ([menuItem tag] == fps)];
        return YES;
    }
    else if( sel == @selector(setGraphicsMode:) )
    {
        NSInteger requestedGraphicsMode = [[NSUserDefaults standardUserDefaults] integerForKey:AngbandGraphicsDefaultsKey];
        [menuItem setState: (tag == requestedGraphicsMode)];
        return YES;
    }
    else if( sel == @selector(toggleSound:) )
    {
	BOOL is_on = [[NSUserDefaults standardUserDefaults]
			 boolForKey:AngbandSoundDefaultsKey];

	[menuItem setState: ((is_on) ? NSOnState : NSOffState)];
	return YES;
    }
    else if( sel == @selector(sendAngbandCommand:) ||
	     sel == @selector(saveGame:) )
    {
        /*
         * we only want to be able to send commands during an active game
         * after the birth screens
         */
        return !!game_in_progress && current_world_ptr->character_generated;
    }
    else return YES;
}


- (IBAction)setRefreshRate:(NSMenuItem *)menuItem
{
    frames_per_second = [menuItem tag];
    [[NSUserDefaults angbandDefaults] setInteger:frames_per_second forKey:AngbandFrameRateDefaultsKey];
}

- (void)selectWindow: (id)sender
{
    NSInteger subwindowNumber = [(NSMenuItem *)sender tag] - AngbandWindowMenuItemTagBase;
    AngbandContext *context = angband_term[subwindowNumber]->data;
    [context->primaryWindow makeKeyAndOrderFront: self];
	[context saveWindowVisibleToDefaults: YES];
}

- (void)prepareWindowsMenu
{
    /* Get the window menu with default items and add a separator and item for
	 * the main window */
    NSMenu *windowsMenu = [[NSApplication sharedApplication] windowsMenu];
    [windowsMenu addItem: [NSMenuItem separatorItem]];

    NSString *title1 = [NSString stringWithCString:angband_term_name[0]
#ifdef JP
				encoding:NSJapaneseEUCStringEncoding
#else
				encoding:NSMacOSRomanStringEncoding
#endif
    ];
    NSMenuItem *angbandItem = [[NSMenuItem alloc] initWithTitle:title1 action: @selector(selectWindow:) keyEquivalent: @"0"];
    [angbandItem setTarget: self];
    [angbandItem setTag: AngbandWindowMenuItemTagBase];
    [windowsMenu addItem: angbandItem];
    [angbandItem release];

    /* Add items for the additional term windows */
    for( NSInteger i = 1; i < ANGBAND_TERM_MAX; i++ )
    {
        NSString *title = [NSString stringWithCString:angband_term_name[i]
#ifdef JP
				    encoding:NSJapaneseEUCStringEncoding
#else
				    encoding:NSMacOSRomanStringEncoding
#endif
	];
        NSString *keyEquivalent = [NSString stringWithFormat: @"%ld", (long)i];
        NSMenuItem *windowItem = [[NSMenuItem alloc] initWithTitle: title action: @selector(selectWindow:) keyEquivalent: keyEquivalent];
        [windowItem setTarget: self];
        [windowItem setTag: AngbandWindowMenuItemTagBase + i];
        [windowsMenu addItem: windowItem];
        [windowItem release];
    }
}

- (void)setGraphicsMode:(NSMenuItem *)sender
{
    /* We stashed the graphics mode ID in the menu item's tag */
    graf_mode_req = [sender tag];

    /* Stash it in UserDefaults */
    [[NSUserDefaults angbandDefaults] setInteger:graf_mode_req forKey:AngbandGraphicsDefaultsKey];
    [[NSUserDefaults angbandDefaults] synchronize];
    
    if (game_in_progress)
    {
        /* Hack -- Force redraw */
        do_cmd_redraw(p_ptr);
        
        /* Wake up the event loop so it notices the change */
        wakeup_event_loop();
    }
}

- (IBAction) toggleSound: (NSMenuItem *) sender
{
    BOOL is_on = (sender.state == NSOnState);

    /* Toggle the state and update the Angband global and preferences. */
    sender.state = (is_on) ? NSOffState : NSOnState;
    use_sound = (is_on) ? FALSE : TRUE;
    [[NSUserDefaults angbandDefaults] setBool:(! is_on)
				      forKey:AngbandSoundDefaultsKey];
}

/**
 *  Send a command to Angband via a menu item. This places the appropriate key
 * down events into the queue so that it seems like the user pressed them
 * (instead of trying to use the term directly).
 */
- (void)sendAngbandCommand: (id)sender
{
    NSMenuItem *menuItem = (NSMenuItem *)sender;
    NSString *command = [self.commandMenuTagMap objectForKey: [NSNumber numberWithInteger: [menuItem tag]]];
    NSInteger windowNumber = [((AngbandContext *)angband_term[0]->data)->primaryWindow windowNumber];

    /* Send a \ to bypass keymaps */
    NSEvent *escape = [NSEvent keyEventWithType: NSKeyDown
                                       location: NSZeroPoint
                                  modifierFlags: 0
                                      timestamp: 0.0
                                   windowNumber: windowNumber
                                        context: nil
                                     characters: @"\\"
                    charactersIgnoringModifiers: @"\\"
                                      isARepeat: NO
                                        keyCode: 0];
    [[NSApplication sharedApplication] postEvent: escape atStart: NO];

    /* Send the actual command (from the original command set) */
    NSEvent *keyDown = [NSEvent keyEventWithType: NSKeyDown
                                        location: NSZeroPoint
                                   modifierFlags: 0
                                       timestamp: 0.0
                                    windowNumber: windowNumber
                                         context: nil
                                      characters: command
                     charactersIgnoringModifiers: command
                                       isARepeat: NO
                                         keyCode: 0];
    [[NSApplication sharedApplication] postEvent: keyDown atStart: NO];
}

/**
 *  Set up the command menu dynamically, based on CommandMenu.plist.
 */
- (void)prepareCommandMenu
{
    NSString *commandMenuPath = [[NSBundle mainBundle] pathForResource: @"CommandMenu" ofType: @"plist"];
    NSArray *commandMenuItems = [[NSArray alloc] initWithContentsOfFile: commandMenuPath];
    NSMutableDictionary *angbandCommands = [[NSMutableDictionary alloc] init];
    NSString *tblname = @"CommandMenu";
    NSInteger tagOffset = 0;

    for( NSDictionary *item in commandMenuItems )
    {
        BOOL useShiftModifier = [[item valueForKey: @"ShiftModifier"] boolValue];
        BOOL useOptionModifier = [[item valueForKey: @"OptionModifier"] boolValue];
        NSUInteger keyModifiers = NSCommandKeyMask;
        keyModifiers |= (useShiftModifier) ? NSShiftKeyMask : 0;
        keyModifiers |= (useOptionModifier) ? NSAlternateKeyMask : 0;

        NSString *lookup = [item valueForKey: @"Title"];
	NSString *title = NSLocalizedStringWithDefaultValue(
	    lookup, tblname, [NSBundle mainBundle], lookup, @"");
        NSString *key = [item valueForKey: @"KeyEquivalent"];
        NSMenuItem *menuItem = [[NSMenuItem alloc] initWithTitle: title action: @selector(sendAngbandCommand:) keyEquivalent: key];
        [menuItem setTarget: self];
        [menuItem setKeyEquivalentModifierMask: keyModifiers];
        [menuItem setTag: AngbandCommandMenuItemTagBase + tagOffset];
        [self.commandMenu addItem: menuItem];
        [menuItem release];

        NSString *angbandCommand = [item valueForKey: @"AngbandCommand"];
        [angbandCommands setObject: angbandCommand forKey: [NSNumber numberWithInteger: [menuItem tag]]];
        tagOffset++;
    }

    [commandMenuItems release];

    NSDictionary *safeCommands = [[NSDictionary alloc] initWithDictionary: angbandCommands];
    self.commandMenuTagMap = safeCommands;
    [safeCommands release];
    [angbandCommands release];
}

- (void)awakeFromNib
{
    [super awakeFromNib];

    [self prepareWindowsMenu];
    [self prepareCommandMenu];
}

- (void)applicationDidFinishLaunching:sender
{
    [AngbandContext beginGame];
    
    /* Once beginGame finished, the game is over - that's how Angband works,
	 * and we should quit */
    game_is_finished = TRUE;
    [NSApp terminate:self];
}

- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender
{
    if (p_ptr->playing == FALSE || game_is_finished == TRUE)
    {
        return NSTerminateNow;
    }
    else if (! inkey_flag)
    {
        /* For compatibility with other ports, do not quit in this case */
        return NSTerminateCancel;
    }
    else
    {
        /* Stop playing */
        /* player->upkeep->playing = FALSE; */

        /* Post an escape event so that we can return from our get-key-event
		 * function */
        wakeup_event_loop();
        quit_when_ready = true;
        /* Must return Cancel, not Later, because we need to get out of the
		 * run loop and back to Angband's loop */
        return NSTerminateCancel;
    }
}

/**
 * Dynamically build the Graphics menu
 */
- (void)menuNeedsUpdate:(NSMenu *)menu {
    
    /* Only the graphics menu is dynamic */
    if (! [menu isEqual:self.graphicsMenu])
        return;
    
    /* If it's non-empty, then we've already built it. Currently graphics modes
	 * won't change once created; if they ever can we can remove this check.
     * Note that the check mark does change, but that's handled in
	 * validateMenuItem: instead of menuNeedsUpdate: */
    if ([menu numberOfItems] > 0)
        return;
    
    /* This is the action for all these menu items */
    SEL action = @selector(setGraphicsMode:);
    
    /* Add an initial Classic ASCII menu item */
    NSString *tblname = @"GraphicsMenu";
    NSString *key = @"Classic ASCII";
    NSString *title = NSLocalizedStringWithDefaultValue(
	key, tblname, [NSBundle mainBundle], key, @"");
    NSMenuItem *classicItem = [menu addItemWithTitle:title action:action keyEquivalent:@""];
    [classicItem setTag:GRAPHICS_NONE];
    
    /* Walk through the list of graphics modes */
    if (graphics_modes) {
	NSInteger i;

	for (i=0; graphics_modes[i].pNext; i++)
	{
	    const graphics_mode *graf = &graphics_modes[i];

	    if (graf->grafID == GRAPHICS_NONE) {
		continue;
	    }
	    /* Make the title. NSMenuItem throws on a nil title, so ensure it's
	           * not nil. */
	    key = [[NSString alloc] initWithUTF8String:graf->menuname];
	    title = NSLocalizedStringWithDefaultValue(
		key, tblname, [NSBundle mainBundle], key, @"");
        
	    /* Make the item */
	    NSMenuItem *item = [menu addItemWithTitle:title action:action keyEquivalent:@""];
	    [key release];
	    [item setTag:graf->grafID];
	}
    }
}

/**
 * Delegate method that gets called if we're asked to open a file.
 */
- (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
{
    /* Can't open a file once we've started */
    if (game_in_progress) {
	[[NSApplication sharedApplication]
	    replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
	return;
    }

    /* We can only open one file. Use the last one. */
    NSString *file = [filenames lastObject];
    if (! file) {
	[[NSApplication sharedApplication]
	    replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
	return;
    }

    /* Put it in savefile */
    if (! [file getFileSystemRepresentation:savefile maxLength:sizeof savefile]) {
	[[NSApplication sharedApplication]
	    replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
	return;
    }

    game_in_progress = TRUE;
    new_game = FALSE;

    /* Wake us up in case this arrives while we're sitting at the Welcome
	 * screen! */
    wakeup_event_loop();

    [[NSApplication sharedApplication]
	replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
}

@end

int main(int argc, char* argv[])
{
    NSApplicationMain(argc, (void*)argv);
    return (0);
}

#endif /* MACINTOSH || MACH_O_COCOA */
