/*******************************************************************************
*									       *
* shift.c -- Nirvana Editor built-in filter commands			       *
*									       *
* Copyright (c) 1991 Universities Research Association, Inc.		       *
* All rights reserved.							       *
* 									       *
* This material resulted from work developed under a Government Contract and   *
* is subject to the following license:  The Government retains a paid-up,      *
* nonexclusive, irrevocable worldwide license to reproduce, prepare derivative *
* works, perform publicly and display publicly by or for the Government,       *
* including the right to distribute to other Government contractors.  Neither  *
* the United States nor the United States Department of Energy, nor any of     *
* their employees, makes any warrenty, express or implied, or assumes any      *
* legal liability or responsibility for the accuracy, completeness, or         *
* usefulness of any information, apparatus, product, or process disclosed, or  *
* represents that its use would not infringe privately owned rights.           *
*                                        				       *
* Fermilab Nirvana GUI Library						       *
* June 18, 1991								       *
*									       *
* Written by Mark Edel							       *
*									       *
*******************************************************************************/
static char SCCSID[] = "@(#)shift.c	1.10     8/3/94";
#ifdef VMS
#include "../util/VMSparam.h"
#else
#include <sys/param.h>
#endif /*VMS*/
#include <limits.h>
#include <ctype.h>
#include <Xm/Xm.h>
#include "../util/DialogF.h"
#include "textBuf.h"
#include "text.h"
#include "nedit.h"
#include "shift.h"
#include "window.h"

static char *shiftText(char *text, int direction, int tabsAllowed, int tabDist,
	int nChars, int *newLen);
static void shiftRect(WindowInfo *window, int direction, int byTab,
	int selStart, int selEnd, int rectStart, int rectEnd);
static void changeCase(WindowInfo *window, int makeUpper);
static void shiftLineRight(char *line, int tabsAllowed, int tabDist,
	int nChars);
static void shiftLineLeft(char *line, int tabDist, int nChars);
static int findLeftMargin(char *text, int tabDist);
static char *fillParagraph(char *text, int addIndent, int width,
	int startCol, int tabDist, int allowTabs, int *filledLen);
static int atTabStop(int pos, int tabDist);
static int nextTab(int pos, int tabDist);
static int countLines(char *text);
static int findParagraphStart(textBuffer *buf, int startPos);
static int findParagraphEnd(textBuffer *buf, int startPos);

/*
** Shift the selection left or right by a single character, or by one tab stop
** if "byTab" is true.  (The length of a tab stop is the size of an emulated
** tab if emulated tabs are turned on, or a hardware tab if not).
*/
void ShiftSelection(WindowInfo *window, int direction, int byTab)
{
    int selStart, selEnd, isRect, rectStart, rectEnd;
    int shiftedLen, newEndPos, cursorPos, origLength, emTabDist, shiftDist;
    char *text, *shiftedText;
    textBuffer *buf = window->buffer;

    /* get selection, if no text selected, use current insert position */
    if (!BufGetSelectionPos(buf, &selStart, &selEnd, &isRect,
    	    &rectStart, &rectEnd)) {
    	cursorPos = TextGetCursorPos(window->lastFocus);
    	selStart = BufStartOfLine(buf, cursorPos);
    	selEnd = BufEndOfLine(buf, cursorPos);
	if (selEnd < buf->length)
	    selEnd++;
	BufSelect(buf, selStart, selEnd);
    	isRect = False;
    	text = BufGetRange(buf, selStart, selEnd);
    } else if (isRect) {
	cursorPos = TextGetCursorPos(window->lastFocus);
	origLength = buf->length;
	shiftRect(window, direction, byTab, selStart, selEnd, rectStart,
		rectEnd);
	TextSetCursorPos(window->lastFocus, (cursorPos < (selEnd+selStart)/2) ?
		selStart : cursorPos + (buf->length - origLength));
	return;
    } else {
	selStart = BufStartOfLine(buf, selStart);
	if (selEnd != 0 && BufGetCharacter(buf, selEnd-1) != '\n') {
	    selEnd = BufEndOfLine(buf, selEnd);
	    if (selEnd < buf->length)
		selEnd++;
	}
	BufSelect(buf, selStart, selEnd);
	text = BufGetRange(buf, selStart, selEnd);
    }
    
    /* shift the text by the appropriate distance */
    if (byTab) {
    	XtVaGetValues(window->textArea, textNemulateTabs, &emTabDist, 0);
    	shiftDist = emTabDist == 0 ? buf->tabDist : emTabDist;
    } else
    	shiftDist = 1;
    shiftedText = shiftText(text, direction, buf->useTabs, buf->tabDist,
    	    shiftDist, &shiftedLen);
    XtFree(text);
    BufReplaceSelected(buf, shiftedText);
    XtFree(shiftedText);
    
    newEndPos = selStart + shiftedLen;
    BufSelect(buf, selStart, newEndPos);
}

static void shiftRect(WindowInfo *window, int direction, int byTab,
	int selStart, int selEnd, int rectStart, int rectEnd)
{
    int offset, emTabDist;
    textBuffer *tempBuf, *buf = window->buffer;
    char *text;
    
    /* Make sure selStart and SelEnd refer to whole lines */
    selStart = BufStartOfLine(buf, selStart);
    selEnd = BufEndOfLine(buf, selEnd);
    
    /* Calculate the the left/right offset for the new rectangle */
    if (byTab) {
    	XtVaGetValues(window->textArea, textNemulateTabs, &emTabDist, 0);
    	offset = emTabDist == 0 ? buf->tabDist : emTabDist;
    } else
    	offset = 1;
    offset *= direction == SHIFT_LEFT ? -1 : 1;
    if (rectStart + offset < 0)
	offset = -rectStart;
    
    /* Create a temporary buffer for the lines containing the selection, to
       hide the intermediate steps from the display update routines */
    tempBuf = BufCreate();
    tempBuf->tabDist = buf->tabDist;
    tempBuf->useTabs = buf->useTabs;
    text = BufGetRange(buf, selStart, selEnd);
    BufSetAll(tempBuf, text);
    XtFree(text);
    
    /* Do the shift in the temporary buffer */
    text = BufGetTextInRect(buf, selStart, selEnd, rectStart, rectEnd);
    BufRemoveRect(tempBuf, 0, selEnd-selStart, rectStart, rectEnd);
    BufInsertCol(tempBuf, rectStart+offset, 0, text, NULL, NULL);
    XtFree(text);
    
    /* Make the change in the real buffer */
    text = BufGetAll(tempBuf);
    BufReplace(buf, selStart, selEnd, text);
    XtFree(text);
    BufRectSelect(buf, selStart, selStart + tempBuf->length,
	    rectStart+offset, rectEnd+offset);
    BufFree(tempBuf);
}

void UpcaseSelection(WindowInfo *window)
{
    changeCase(window, True);
}

void DowncaseSelection(WindowInfo *window)
{
    changeCase(window, False);
}

/*
** Capitalize or lowercase the contents of the selection (or of the character
** before the cursor if there is no selection).  If "makeUpper" is true,
** change to upper case, otherwise, change to lower case.
*/
static void changeCase(WindowInfo *window, int makeUpper)
{
    textBuffer *buf = window->buffer;
    char *text, *c;
    int cursorPos, start, end, isRect, rectStart, rectEnd;
    
    /* Get the selection.  Use character before cursor if no selection */
    if (!BufGetSelectionPos(buf, &start, &end, &isRect, &rectStart, &rectEnd)) {
    	char bufChar[2] = " ";
    	cursorPos = TextGetCursorPos(window->lastFocus);
    	if (cursorPos == 0) {
    	    XBell(TheDisplay, 0);
    	    return;
	}
    	*bufChar = BufGetCharacter(buf, cursorPos-1);
    	*bufChar = makeUpper ? toupper(*bufChar) : tolower(*bufChar);
    	BufReplace(buf, cursorPos-1, cursorPos, bufChar);
    } else {
	text = BufGetSelectionText(buf);
	for (c=text; *c!='\0'; c++)
    	    *c = makeUpper ? toupper(*c) : tolower(*c);
	BufReplaceSelected(buf, text);
	XtFree(text);
	if (isRect)
	    BufRectSelect(buf, start, end, rectStart, rectEnd);
	else
	    BufSelect(buf, start, end);
    }
}

void FillSelection(WindowInfo *window)
{
    textBuffer *buf = window->buffer;
    char *c, *text, *filledText;
    int left, right, nCols, leftMargin, len, isRect, rectStart, rectEnd;
    int rightMargin, wrapMargin, nLines;
    int insertPos = TextGetCursorPos(window->lastFocus);
    int hasSelection = window->buffer->primary.selected;
    
    /* Find the range of characters and get the text to fill.  If there is a
       selection, use it but extend non-rectangular selections to encompass
       whole lines.  If there is no selection, find the paragraph containing
       the insertion cursor */
    if (!BufGetSelectionPos(buf, &left, &right, &isRect, &rectStart, &rectEnd)) {
	left = findParagraphStart(buf, insertPos);
	right = findParagraphEnd(buf, insertPos);
	if (left == right) {
    	    XBell(TheDisplay, 0);
    	    return;
	}
	text = BufGetRange(buf, left, right);
    } else if (isRect) {
    	left = BufStartOfLine(buf, left);
    	right = BufEndOfLine(buf, right);
    	nLines = BufCountLines(buf, left, right) + 1;
    	text = BufGetTextInRect(buf, left, right, rectStart, INT_MAX);
    } else {
	left = BufStartOfLine(buf, left);
	if (right != 0 && BufGetCharacter(buf, right-1) != '\n') {
	    right = BufEndOfLine(buf, right);
	    if (right < buf->length)
		right++;
	}
    	BufSelect(buf, left, right);
    	text = BufGetRange(buf, left, right);
    }

    /* Find the margins either as specified in the rectangular selection, or
       by measuring the text and querying the window's wrap margin (or width) */
    if (hasSelection && isRect) {
    	leftMargin = 0;
    	rightMargin = rectEnd - rectStart;
    } else {
    	leftMargin = findLeftMargin(text, buf->tabDist);
	XtVaGetValues(window->textArea, textNcolumns, &nCols,
    		textNwrapMargin, &wrapMargin, 0);
	rightMargin = (wrapMargin == 0 ? nCols : wrapMargin) - 1;
    }
    
    /* Fill the text */
    filledText = fillParagraph(text, leftMargin, rightMargin-leftMargin,
    	    (hasSelection && isRect) ? rectStart : 0, buf->tabDist, 
    	    buf->useTabs, &len);
    XtFree(text);
    
    /* If this is a rectangular fill, trim the trailing newlines */
    if (hasSelection && isRect) {
    	for (c=&filledText[len-1]; c>=filledText && *c=='\n'; c--) {
    	    *c = '\0';
    	    len--;
    	}
    }
    
    /* Replace the text in the window */
    if (hasSelection && isRect) {
        BufReplaceRect(buf, left, right, rectStart, INT_MAX, filledText);
        BufRectSelect(buf, left,
        	BufEndOfLine(buf, BufCountForwardNLines(buf, left,
        	countLines(filledText)-1)), rectStart, rectEnd);
    } else {
	BufReplace(buf, left, right, filledText);
	if (hasSelection)
    	    BufSelect(buf, left, left + len);
    }
    XtFree(filledText);
    
    /* Find a reasonable cursor position.  Usually insertPos is best, but
       if the text was indented, positions can shift */
    if (hasSelection && isRect)
    	TextSetCursorPos(window->lastFocus, buf->cursorPosHint);
    else
	TextSetCursorPos(window->lastFocus, insertPos < left ? left :
    		(insertPos > left + len ? left + len : insertPos));
}

/*
** shift lines left and right in a multi-line text string.  Returns the
** shifted text in memory that must be freed by the caller with XtFree.
*/
static char *shiftText(char *text, int direction, int tabsAllowed, int tabDist,
	int nChars, int *newLen)
{
    char line[MAX_LINE_LENGTH];
    char *shiftedText;
    char *textPtr, *linePtr, *shiftedPtr;
    int bufLen;
    
    /*
    ** Allocate memory for shifted string.  Shift left adds a maximum of
    ** tabDist-2 characters per line (remove one tab, add tabDist-1 spaces).
    ** Shift right adds a maximum of nChars character per line.
    */
    if (direction == SHIFT_RIGHT)
        bufLen = strlen(text) + countLines(text) * nChars;
    else
        bufLen = strlen(text) + countLines(text) * tabDist;
    shiftedText = (char *)XtMalloc(bufLen + 1);
    
    /*
    ** break into lines and call shiftLine on each
    */
    linePtr = line;
    textPtr = text;
    shiftedPtr = shiftedText;
    while (TRUE) {
	if (*textPtr=='\n' || *textPtr=='\0') {
	    *linePtr = '\0';
	    if (direction == SHIFT_RIGHT)
		shiftLineRight(line, tabsAllowed, tabDist, nChars);
	    else
		shiftLineLeft(line, tabDist, nChars);
	    strcpy(shiftedPtr, line);
	    shiftedPtr += strlen(line);
	    if (*textPtr == '\0') {
	        /* terminate string & exit loop at end of text */
	    	*shiftedPtr = '\0';
		break;
	    } else {
	    	/* move the newline from text to shifted text */
		*shiftedPtr++ = *textPtr++;
	    }
	    /* start line over */
	    linePtr = line;
	} else {
	    /* build up line */
	    *linePtr++ = *textPtr++;
	}
    }
    *newLen = shiftedPtr - shiftedText;
    return shiftedText;
}

static void shiftLineRight(char *line, int tabsAllowed, int tabDist, int nChars)
{
    char lineIn[MAX_LINE_LENGTH];
    char *lineInPtr, *lineOutPtr;
    int whiteWidth, i;
    
    strcpy(lineIn, line);
    lineInPtr = lineIn;
    lineOutPtr = line;
    whiteWidth = 0;
    while (TRUE) {
        if (*lineInPtr == '\0') {
	    /* nothing on line, wipe it out */
	    *line = '\0';
	    return;
        } else if (*lineInPtr == ' ') {
	    /* white space continues with tab, advance to next tab stop */
	    whiteWidth++;
	    *lineOutPtr++ = *lineInPtr++;
	} else if (*lineInPtr == '\t') {
	    /* white space continues with tab, advance to next tab stop */
	    whiteWidth = nextTab(whiteWidth, tabDist);
	    *lineOutPtr++ = *lineInPtr++;
	} else {
	    /* end of white space, add nChars of space */
	    for (i=0; i<nChars; i++) {
		*lineOutPtr++ = ' ';
		whiteWidth++;
		/* if we're now at a tab stop, change last 8 spaces to a tab */
		if (tabsAllowed && atTabStop(whiteWidth, tabDist)) {
		    lineOutPtr -= tabDist;
		    *lineOutPtr++ = '\t';
		}
	    }
	    /* move remainder of line */
    	    while (*lineInPtr!='\0')
		*lineOutPtr++ = *lineInPtr++;
	    *lineOutPtr = '\0';
	    return;
	}
    }
}

static void shiftLineLeft(char *line, int tabDist, int nChars)
{
    char lineIn[MAX_LINE_LENGTH];
    int i, whiteWidth, lastWhiteWidth, whiteGoal;
    char *lineInPtr, *lineOutPtr;
    
    strcpy(lineIn, line);
    lineInPtr = lineIn;
    lineOutPtr = line;
    whiteWidth = 0;
    lastWhiteWidth = 0;
    while (TRUE) {
        if (*lineInPtr == '\0') {
	    /* nothing on line, wipe it out */
	    *line = '\0';
	    return;
        } else if (*lineInPtr == ' ') {
	    /* white space continues with space, advance one character */
	    whiteWidth++;
	    *lineOutPtr++ = *lineInPtr++;
	} else if (*lineInPtr == '\t') {
	    /* white space continues with tab, advance to next tab stop	    */
	    /* save the position, though, in case we need to remove the tab */
	    lastWhiteWidth = whiteWidth;
	    whiteWidth = nextTab(whiteWidth, tabDist);
	    *lineOutPtr++ = *lineInPtr++;
	} else {
	    /* end of white space, remove nChars characters */
	    for (i=1; i<=nChars; i++) {
		if (lineOutPtr != line) {
		    if (*(lineOutPtr-1) == ' ') {
			/* end of white space is a space, just remove it */
			lineOutPtr--;
		    } else {
	    		/* end of white space is a tab, remove it and add
	    		   back spaces */
			lineOutPtr--;
			whiteGoal = whiteWidth - i;
			whiteWidth = lastWhiteWidth;
			while (whiteWidth < whiteGoal) {
			    *lineOutPtr++ = ' ';
			    whiteWidth++;
			}
		    }
		}
	    }
	    /* move remainder of line */
    	    while (*lineInPtr!='\0')
		*lineOutPtr++ = *lineInPtr++;
	    /* add a null */
	    *lineOutPtr = '\0';
	    return;
	}
    }
}
       
static int atTabStop(int pos, int tabDist)
{
    return (pos%tabDist == 0);
}

static int nextTab(int pos, int tabDist)
{
    return (pos/tabDist)*tabDist + tabDist;
}

static int countLines(char *text)
{
    int count = 1;
    
    while(*text != '\0') {
    	if (*text++ == '\n') {
	    count++;
	}
    }
    return count;
}

/*
** Find the implied left margin of a text string (the number of columns to the
** first non-whitespace character on any line).
*/
static int findLeftMargin(char *text, int tabDist)
{
    char *c;
    int col = 0, leftMargin = INT_MAX;
    int inMargin = True;
    
    for (c=text; *c!='\0'; c++) {
    	if (*c == '\t') {
    	    col += tabDist - (col % tabDist);
    	} else if (*c == ' ') {
    	    col++;
    	} else if (*c == '\n') {
    	    col = 0;
    	    inMargin = True;
    	} else {
    	    /* non-whitespace */
    	    if (col < leftMargin && inMargin)
    	    	leftMargin = col;
    	    inMargin = False;
    	}
    }
    
    /* if no text is found, the leftMargin will never be set */
    if (leftMargin == INT_MAX)
    	return 0;
    
    return leftMargin;
}

/*
** Trim leading space, arrange text to fill width "width", and add back
** "addIndent"
*/
static char *fillParagraph(char *text, int addIndent, int width,
	int startCol, int tabDist, int allowTabs, int *filledLen)
{
    char *cleanedText, *outText, *indentString, *outPtr, *c, *b;
    int i, col, cleanedLen, indentLen, nLines = 1;
    int inWhitespace, inMargin;
    
    /* remove leading spaces, convert newlines to spaces */
    cleanedText = XtMalloc(strlen(text)+1);
    outPtr = cleanedText;
    inMargin = True;
    for (c=text; *c!='\0'; c++) {
    	if (*c == '\t' || *c == ' ') {
    	    if (!inMargin)
    	    	*outPtr++ = *c;
    	} else if (*c == '\n') {
    	    if (inMargin) {
    	    	/* a newline before any text separates paragraphs, so leave
    	    	   it in, back up, and convert the previous space back to \n */
    	    	if (outPtr > cleanedText && *(outPtr-1) == ' ')
    	    	    *(outPtr-1) = '\n';
    	    	*outPtr++ = '\n';
    	    	nLines +=2;
    	    } else
    	    	*outPtr++ = ' ';
    	    inMargin = True;
    	} else {
    	    *outPtr++ = *c;
    	    inMargin = False;
    	}
    }
    cleanedLen = outPtr - cleanedText;
    *outPtr = '\0';
    
    /* Put back newlines breaking text at word boundaries within the margins.
       Algorithm: scan through characters, counting columns, and when the
       margin width is exceeded, search backward for beginning of the word
       and convert the last whitespace character into a newline */
    col = startCol;
    for (c=cleanedText; *c!='\0'; c++) {
    	if (*c == '\n')
    	    col = startCol;
    	else
    	    col += BufCharWidth(*c, col, tabDist);
    	if (col-startCol-1 > width) {
    	    inWhitespace = True;
    	    for (b=c; b>=cleanedText && *b!='\n'; b--) {
    	    	if (*b == '\t' || *b == ' ') {
    	    	    if (!inWhitespace) {
    	    		*b = '\n';
    	    		c = b;
    	    		col = startCol;
     	    		nLines++;
   	    		break;
    	    	    }
    	    	} else 
    	    	    inWhitespace = False;
    	    }
    	}
    }
    nLines++;
    
    /* produce a string to prepend to lines to indent them to the left margin */
    indentString = XtMalloc(sizeof(char) * addIndent + 1);
    outPtr = indentString;
    if (allowTabs) {
	for (i=0; i<addIndent/tabDist; i++)
    	    *outPtr++ = '\t';
	for (i=0; i<addIndent%tabDist; i++)
    	    *outPtr++ = ' ';
    } else {
    	for (i=0; i<addIndent; i++)
    	    *outPtr++ = ' ';
    }
    indentLen = outPtr - indentString;
    *outPtr = '\0';
    
    /* allocate memory for the finished string */
    outText = XtMalloc(sizeof(char) * (cleanedLen + indentLen * nLines + 1));
    outPtr = outText;
    
    /* prepend the indent string to each line of the filled text */
    strncpy(outPtr, indentString, indentLen);
    outPtr += indentLen;
    for (c=cleanedText; *c!='\0'; c++) {
    	*outPtr++ = *c;
    	if (*c == '\n') {
    	    strncpy(outPtr, indentString, indentLen);
    	    outPtr += indentLen;
    	}
    }
    
    /* convert any trailing space to newline.  Add terminating null */
    if (*(outPtr-1) == ' ')
    	*(outPtr-1) = '\n';
    *outPtr = '\0';
    
    /* clean up, return result */
    XtFree(cleanedText);
    XtFree(indentString);
    *filledLen = outPtr - outText;
    return outText;
}

/*
** Find the boundaries of the paragraph containing pos
*/
static int findParagraphEnd(textBuffer *buf, int startPos)
{
    char c;
    int pos;
    static char whiteChars[] = " \t";

    pos = BufEndOfLine(buf, startPos)+1;
    while (pos < buf->length) {
    	c = BufGetCharacter(buf, pos);
    	if (c == '\n')
    	    break;
    	if (strchr(whiteChars, c) != NULL)
    	    pos++;
    	else
    	    pos = BufEndOfLine(buf, pos)+1;
    }
    return pos < buf->length ? pos : buf->length;
}
static int findParagraphStart(textBuffer *buf, int startPos)
{
    char c;
    int pos, parStart;
    static char whiteChars[] = " \t";

    if (startPos == 0)
    	return 0;
    parStart = BufStartOfLine(buf, startPos);
    pos = parStart - 2;
    while (pos > 0) {
    	c = BufGetCharacter(buf, pos);
    	if (c == '\n')
    	    break;
    	if (strchr(whiteChars, c) != NULL)
    	    pos--;
    	else {
    	    parStart = BufStartOfLine(buf, pos);
    	    pos = parStart - 2;
    	}
    }
    return parStart > 0 ? parStart : 0;
}
