// -------------------------------------------------------------------------------------
// ScrollText.m
// Martin D. Flynn, NeXT Computer, Inc.
// (see ScrollText.h for usage information)
// -------------------------------------------------------------------------------------

#import <appkit/appkit.h>
#import <libc.h>
#import <mach/cthreads.h>
#import <stdlib.h>
#import <stdarg.h>
#import <string.h>
#import <pwd.h>
#import <sys/types.h>
#import <sys/wait.h>
#import "ScrollText.h"

// -------------------------------------------------------------------------------------
// null text attribute structure
static NXColor          nullColor = { 0 };
static textAttr_t       nullAttr = { (id)nil, 0 };
#define isNullAttr(X)   (!X.fontId && !X.colorMode)

// -------------------------------------------------------------------------------------
// main thread handle
#define isMainTHREAD    (mainThread == cthread_self())
static cthread_t        mainThread = (cthread_t)nil;

// -------------------------------------------------------------------------------------
// Object thread support
// Support is included to take advantage of the features found in objectThreadPerform.m.
// While including objectThreadPerfrom.m in your application is not required to use
// ScrollText, it is recommended if your app utilizes multiple threads.
@interface Object(ThreadPerform)
- mainThreadPerform:(SEL)aSelector with:anArg wait:(BOOL)wait;
@end

// -------------------------------------------------------------------------------------
// private methods
@interface ScrollText(Private)
- _setTextAttrFont:fontId color:(int)mode:(NXColor)color;
- (BOOL)_appendTextToView:(const char*)buffer len:(int)len attr:(textAttr_t)tAttr;
- _appendTextAndMakeVisible:(const char*)buffer attr:(textAttr_t)tAttr;
- _appendTextToQueue:(const char*)buffer attr:(textAttr_t)tAttr;
- _appendQueue:sender;
- _delayedAppendQueue;
- (int)_queueLength:(textQueue_t*)queue;
- (int)_maxRunLength:(textQueue_t*)queue;
- _stopCommand;
- (char**)_resetToUser:(const char*)userName;
- (int)_popen:(const char*)cmd cmdPath:(const char*)cmdPath user:(char*)userName;
- (int)_pclose;
- (void)_gotData;
@end

// -------------------------------------------------------------------------------------
/* convert color to gray */
static float cvtColor2Gray(NXColor color)
{
    float   gray;
    NXConvertColorToGray(color, &gray);
    return gray;
}

// -------------------------------------------------------------------------------------
@implementation ScrollText

// -------------------------------------------------------------------------------------
/* initializes the outlet (to be executed by main thread only!) */
+ newScrollText:anObject
{
    if (!mainThread) mainThread = cthread_self();
    self             = [super new];
    scrollView       = anObject;
    textView         = [scrollView docView];
    autoLf           = NO;
    queueMutex       = mutex_alloc();
    queueData        = (textQueue_t*)nil;
    queueBack        = (textQueue_t*)nil;
    runAttr          = nullAttr;
    [textView setEditable:NO];
    [textView setMonoFont:NO];
    return self;
}

/* return textView id (docView) */
- docView
{
    return textView;
}

/* return scroll view */
- scrollView
{
    return scrollView;
}

/* set auto linefeed mode */
- setAutoLineFeed:(BOOL)mode
{
    autoLf = mode;
    return self;
}
       
// --------------------------------------------------------------------------------

/* set font */
- _setTextAttrFont:fontId color:(int)mode:(NXColor)color
{
    textAttr_t  tAttr = nullAttr;
    
    /* init text attributes */
    tAttr.fontId    = fontId;
    tAttr.colorMode = mode;
    tAttr.color     = color;
    [self _appendTextAndMakeVisible:(char*)nil attr:tAttr];
    
    return self;
}

/* set font */
- setTextAttributeFont:fontId
{
    return [self _setTextAttrFont:fontId color:0:nullColor];
}

/* set gray */
- setTextAttributeGray:(float)aGray
{
    return [self _setTextAttrFont:(id)nil color:1:NXConvertGrayToColor(aGray)];
}

/* set gray */
- setTextAttributeColor:(NXColor)aColor
{
    return [self _setTextAttrFont:(id)nil color:2:aColor];
}

// --------------------------------------------------------------------------------

/* clear text scroll view area */
- clearScrollText
{
    [textView setEditable:YES];
    [textView setText:""];
    [textView setEditable:NO];
    [scrollView display];
    return self;
}

/* delete lines */
- deleteLinesFrom:(int)fLine to:(int)tLine
{
    [textView setEditable:YES];
    [textView setSel:[textView positionFromLine:fLine]:[textView positionFromLine:tLine+1]];
    [textView replaceSel:""];
    [textView setEditable:NO];
    [scrollView display];
    return self;
}

/* return current number of lines */
- (int)textLines
{
    return [textView lineFromPosition:[textView textLength]];
}
    
// --------------------------------------------------------------------------------
// append text to scroll view

/* append buffer to view: return YES if text was visible */
- (BOOL)_appendTextToView:(const char*)buffer len:(int)len attr:(textAttr_t)tAttr
{
    int     txtLen;
    NXSelPt startPt, endPt;
    NXRect  rect;
    
    /* check for font/gray change (save state) */
    if (tAttr.fontId) {
        runAttr.fontId = tAttr.fontId;
    }
    if (tAttr.colorMode) {
        runAttr.colorMode = tAttr.colorMode;
        runAttr.color = tAttr.color;
    }
    if (!buffer || !*buffer || (len == 0)) return NO;
    
    /* get ready to print text */
    [textView getVisibleRect:&rect];                      // visible rectangle
    [textView setEditable:YES];
    txtLen = [textView textLength];
    [textView setSel:txtLen :txtLen];
    [textView getSel:&startPt :&endPt];                   // selected coordinates

    /* set text run attributes if specified */
    if (!isNullAttr(runAttr)) {
        if (runAttr.fontId) [textView setSelFont:runAttr.fontId];
        if (runAttr.colorMode == 1) [textView setSelGray:cvtColor2Gray(runAttr.color)]; else
        if (runAttr.colorMode == 2) [textView setSelColor:runAttr.color];
        runAttr = nullAttr;
    }
    
    /* print text */
    if (len > 0) [textView replaceSel:buffer length:len];
    else [textView replaceSel:buffer];
    [textView setEditable:NO];
    
    return (rect.origin.y + rect.size.height > endPt.y);  // was visible?
}

/* append text to view and scroll to visible */
- _appendTextAndMakeVisible:(const char*)buffer attr:(textAttr_t)tAttr
{
    BOOL    wasVisible;
  
    /* if not main thread, append buffer to queue */
    if (!isMainTHREAD) return [self _appendTextToQueue:buffer attr:tAttr];

    /* print queue contents */
    [self _appendQueue:self];
  
    /* print buffer */
    wasVisible = [self _appendTextToView:buffer len:-1 attr:tAttr];
    if (autoLf && buffer) [self _appendTextToView:"\n" len:-1 attr:nullAttr];
    if (wasVisible) [textView scrollSelToVisible];
  
    return self;
}
    
// --------------------------------------------------------------------------------
// queue up text to print / print text queue

/* return queue length */
- (int)_queueLength:(textQueue_t*)queue
{
    int         len;
    textQueue_t *qPtr;
    qPtr = (queue)? queue: queueData;
    for (len = 0; qPtr; qPtr = qPtr->next) {
        if (qPtr->record) len += strlen(qPtr->record) + 1;
    }
    return len;
}

/* return queue length */
- (int)_maxRunLength:(textQueue_t*)queue
{
    int         len, maxLen;
    textQueue_t *qPtr;
    qPtr = (queue)? queue: queueData;
    for (maxLen = len = 0; qPtr; qPtr = qPtr->next) {
        if (!isNullAttr(qPtr->attr)) {
            if (len > maxLen) maxLen = len;
            len = 0;
        }
        if (qPtr->record) len += strlen(qPtr->record) + 1;
    }
    return MAX(maxLen, len);
}

/* delayed append queue */
- _delayedAppendQueue
{
    if ([self respondsTo:@selector(mainThreadPerform:with:wait:)])
        [self mainThreadPerform:@selector(_appendQueue:) with:self wait:NO];
    return self;
}

/* add text string to queue (to be executed by non-main thread only) */
- _appendTextToQueue:(const char*)buffer attr:(textAttr_t)tAttr
{
    textQueue_t *newData;

    /* load temporarily permanent buffer */
    newData         = (textQueue_t*)malloc(sizeof(textQueue_t));
    newData->record = buffer? NXCopyStringBuffer(buffer) : (char*)nil;
    newData->attr   = tAttr;
    newData->next   = (textQueue_t*)nil;
  
    /* lock the queue */
    mutex_lock(queueMutex);
  
    /* add it to the queue */
    if (!queueData) { queueData = newData; [self _delayedAppendQueue]; }
    else queueBack->next = newData;
    queueBack = newData;

    /* unlock the queue */
    mutex_unlock(queueMutex);
  
    return self;
}

/* print text queue contents (executed by main thread only!) */
// needs to be fixed to support fonts/grays
- _appendQueue:sender
{
    int         i;
    char        *text;
    textQueue_t *queue, *next;
  
    /* get pointer to queue list */
    mutex_lock(queueMutex);
    queue = queueData;
    queueData = queueBack = (textQueue_t*)nil;
    mutex_unlock(queueMutex);
    if (!queue) return self;
  
    /* concatenate text */
    text = (char*)malloc([self _maxRunLength:queue] + 1);
    for (i = 0; queue;) {
    
        /* check for attribute change */
        if (!isNullAttr(queue->attr)) { // print contents of 'text'
            if ((i > 0) && [self _appendTextToView:text len:-1 attr:nullAttr])
                [textView scrollSelToVisible];
            [self _appendTextToView:"" len:0 attr:queue->attr];
            text[i = 0] = 0;
        }
        
        /* append record to string buffer */
        if (queue->record) {
            i += sprintf(&text[i], "%s%s", queue->record, ((autoLf)? "\n": ""));
            free(queue->record);
        }
        
        /* next node */
        next = queue->next;
        free(queue);
        queue = next;
        
    }
  
    /* place remaining text into scroll view */
    if ([self _appendTextToView:text len:-1 attr:nullAttr]) [textView scrollSelToVisible];
    free(text);
    
    return self;
}

// --------------------------------------------------------------------------------
// append a formatted text string message into a text view
// input from ktaylor

/* append text with variable args into textView */
- (int)textPrintf:(const char*)fmt args:(va_list)args
{
    char        *tempString;
    int         retVal;
    tempString = (char *)malloc(textStringSIZE); *tempString = 0;
    retVal = vsprintf(tempString, fmt, args);
    [self _appendTextAndMakeVisible:tempString attr:nullAttr];
    free(tempString);     
    return retVal;
}

/* append text with variable args into textView */
- (int)textPrintf:(const char*)fmt, ...
{
    va_list     args;
    int         retVal;
    va_start(args, fmt);
    retVal = [self textPrintf:fmt args:args];     
    va_end(args);
    return retVal;
}

/* append text with fprintf(...) style format */
int textPrintf(id self, const char *fmt, ...)
{
    va_list     args;
    int         retVal;
    va_start(args, fmt);
    retVal = [self textPrintf:fmt args:args]; 
    va_end(args);
    return retVal;
}

// --------------------------------------------------------------------------------
// view printing

/* prints the textView */
- print:sender
{
    id      oldInfo, pInfo = [PrintInfo new];
    [pInfo setVertCentered:NO];
    oldInfo = [NXApp setPrintInfo:pInfo];
    [textView lockFocus];
    [textView printPSCode:self];
    [textView unlockFocus];
    [NXApp setPrintInfo:oldInfo];
    [pInfo free];
    return self;
}
  
@end
