#import "Finder.h"
#import "Eval.h"
#import "CodeBrowser.h"
#import "support.h"
#import <regex.h>
#import <strings.h>
#import <libc.h>

/**
 ** Finder.m
 ** A find panel with regular expression searching and 
 ** the ability to store "bookmarks"
 **/

#define MAXFINDERCELLLEN 80

// there is only one instance of finder, and it's 
// id gets stuffed into this var:
id finderObject ;

@implementation Finder: Panel
{  id findLineNum,
      findText,
      replaceText,
      caseSwitch,
      regExpSwitch, 
      selScopeSwitch,
      lineNumBrowser,
           currentCodeBrowser ;
}


- (int)browser:sender fillMatrix:matrix inColumn:(int)column ;
{ // Delegate method for browser object. 
 if(currentCodeBrowser)
    return [[currentCodeBrowser lineList] count] ;
 return 0 ;
}

- browser:sender loadCell:cell atRow:(int)row inColumn:(int)column ;
{ int i,lineNum, charNum ; 

  char buf1[MAXFINDERCELLLEN+10], buf2[MAXFINDERCELLLEN + 17] ;
  id theText = [currentCodeBrowser codeBrowserText] ;   

  lineNum = * (int *) [[currentCodeBrowser lineList] elementAt: row] ;
  charNum = [theText positionFromLine: lineNum] ;
  [theText getSubstring: buf1 start: charNum length: MAXFINDERCELLLEN] ;
  for(i = 0 ; i < MAXFINDERCELLLEN ; i++)
  { if(buf1[i] == '\n') // don't grab more than 1 line...
    { buf1[i] = '\0' ;
      break ;
    }
  }
  buf1[i] = '\0' ; // make sure null terminated!
  sprintf(buf2,"%-5d %s",lineNum,buf1) ;
  [cell setStringValue: buf2] ;
  [cell setLeaf:YES] ;
  return self ;
}

- doFind: sender
{ // called by the Find menu option
  [self makeKeyAndOrderFront: self] ;
  [[findText controlView] selectText: self] ;
  return self ;
}

- doFindLine: sender
{ // called by the fine line menu option
  [self makeKeyAndOrderFront: self] ;
  [self finderCurrent: self] ;
  [findLineNum  selectText: self] ;
  return self ;
}

- find: (char *) text regEx: (BOOL) regEx caseSensitive: (BOOL) caseSensitive inSelection: (BOOL) inSelection ;
{ // find the next occurence of "text" in the window's text. It may contain
  // a regular expression.  The search may be case sensitive.  The search may be
  // confined to the current selection.  Returns nil if text is not found, else
  // makes the text the current selection. If text is NULL or the empty string,
  // does nothing, but returns self. 
  int origLen, newLen ;
  long firstStart, lastStart ; 
  BOOL wrapped = NO ;
  char textBuf[1024] ;
  if(![[NXApp mainWindow] isKindOf: [CodeBrowser class]])
    return self ;
  if(!text || (text[0] == '\0'))
    return self ;
  origLen = newLen = strlen(text) ;
  if(!regEx) 
    newLen *= 2 ; // not a rexp:leave room to quote everything
  { int i,j  ;
    char specialChars[] = "[]^*$\\." ; 
    struct regex *ex ;
    NXStream *aStream ;
    NXSelPt start, end ;
    char regexText[newLen+1] ;
    id textView ;
    if(!regEx) 
    { for(i = 0,j = 0 ; text[i] ; i++,j++)
      { if(index(specialChars,text[i])) // if a special char,
          regexText[j++] = '\\' ; // then turn off its special meaning by 
        regexText[j] = text[i] ; // quoting it.
      }
      regexText[j] = '\0' ;
    }
    else
      strcpy(regexText,text) ;
    ex = re_compile(regexText,caseSensitive) ;
    textView = [[NXApp mainWindow] codeBrowserText] ;
    [textView getSel: &start :&end] ;
    if(!inSelection) // scope is entire file
    {  aStream = [textView stream] ;
      // search begins after current selection
      firstStart = lastStart = (long) end.cp ;
    }
    else // scope is selection only
    {  int len = end.cp - start.cp ;
      { char aBuf[len + 1] ;
        aStream = NXOpenMemory(NULL, 0, NX_READWRITE) ;
        [textView getSubstring: aBuf start: start.cp length: len] ;
        NXWrite(aStream, aBuf, len) ;
        // search begins at beginning of stream
        firstStart = lastStart = 0 ;
      }
    }
    NXSeek(aStream, lastStart, NX_FROMSTART) ;
    EvalGets(aStream,textBuf) ;
    do
    { if(re_match(textBuf,ex))
      { if(inSelection)
        {  [textView setSel:(int) (lastStart + ex->start -
             (int) textBuf + start.cp)
             :(int) (lastStart + ex->end)-
             (int) textBuf + start.cp] ;       
           NXCloseMemory(aStream, NX_FREEBUFFER) ;
        }
        else
          [textView setSel:(int) (lastStart + ex->start - (int) textBuf)
                     :(int) (lastStart + ex->end)- (int) textBuf] ;
        [textView scrollSelToVisible] ;
        free(ex) ;
        [[textView window] makeKeyAndOrderFront: self] ;
        return self ;
      }
      if(NXAtEOS(aStream)) // wrap around to beginning
      { NXSeek(aStream, 0L,NX_FROMSTART) ;
        wrapped = YES ;
      }
      lastStart = NXTell(aStream) ;
      EvalGets(aStream,textBuf) ;
    } while(!(wrapped && (lastStart >= firstStart))) ;
    free(ex) ;
    if(inSelection)
       NXCloseMemory(aStream, NX_FREEBUFFER) ;
    [self setTitle: "Not Found"] ;
    [self perform:@selector(resetTitle:) 
	  with:self afterDelay:3500 cancelPrevious:YES];
    return nil ;
  }
  return self ;
}


- finderAddLine: sender ;
{ // add the current line to the list of lines
  if([[NXApp mainWindow] isKindOf: [CodeBrowser class]])
  { NXSelPt start, end ;
    int i ;
    id textView = [[NXApp mainWindow] codeBrowserText] ;
    [textView getSel: &start :&end] ;
    i = [textView lineFromPosition: start.cp] ;
    // the next line looks dangerous (storing a pointer to an automatic?)
    // ...but the Storage object dereferences the pointer before it stores it.
    [[[textView window] lineList] addElement: &i] ;
    [lineNumBrowser loadColumnZero] ;
    [[textView window] makeKeyAndOrderFront: self] ;
  }
  return self ;
}

- finderBrowserHit: sender ;
{  // go to the line indicated in the browser
   /* if you want to fire on double clicks, use this: 
    NXEvent * anEvent ;
    anEvent = [NXApp currentEvent] ;
    if(anEvent->data.mouse.click == 2)
    { ...}  */
  if([[NXApp mainWindow] isKindOf: [CodeBrowser class]])
  { char aBuf[MAXFINDERCELLLEN+17] ;
    int lineNum ;
    [lineNumBrowser setPathSeparator: ' '] ;
    [lineNumBrowser getPath: aBuf toColumn: 1] ;
    if(sscanf(aBuf,"%d",&lineNum))
    { id textView = [[NXApp mainWindow] codeBrowserText] ;
      [textView setSel: [textView positionFromLine: lineNum]
                  : [textView positionFromLine: lineNum + 1]] ;
      [textView scrollSelToVisible] ;
      [[textView window] makeKeyAndOrderFront: self] ;
    }
  }
  return self ;
}


- finderCurrent: sender ;
{ // put line number of beginning of selection into
  // the findLineNum 
  if([[NXApp mainWindow] isKindOf: [CodeBrowser class]])
  { NXSelPt start,end ;
    id theText = [[NXApp mainWindow] codeBrowserText] ;
    [theText getSel: &start :&end] ;
    [findLineNum setIntValue: [theText lineFromPosition: start.cp]] ;
  }
  return self ;
}

- finderGoTo: sender ;
{ // set selection at beginning of line indicated by
  // findLineNum...depending on the sate of the scopeSwitch,
  // this will be a lineNum counted from the begining of the selection
  if([[NXApp mainWindow] isKindOf: [CodeBrowser class]])
  { id textView = [[NXApp mainWindow] codeBrowserText] ;
    int pos1, pos2, lineNum ;
    lineNum = [findLineNum intValue] ;
    if([[selScopeSwitch selectedCell] tag])
    { // if scope is selection...
      NXSelPt start,end ;
      int offset ;
      [textView getSel: &start :&end] ;
      offset = [textView lineFromPosition: start.cp] ;
      lineNum += offset - 1 ;
    }
    pos1 = [textView positionFromLine:lineNum] ;
    pos2 = [textView positionFromLine: lineNum + 1] ;
    [textView setSel:pos1 :pos2] ;
    [textView scrollSelToVisible] ;
    [findLineNum selectText: self] ;
    [[textView window] makeKeyAndOrderFront: self] ;
  }
  return self ;
}


- finderNext: sender ;
{ // find the next occurence of the "find" text.
  // returns nil if unable.
  return [self find: (char *) [findText stringValue]
    regEx: [[regExpSwitch selectedCell] tag]
    caseSensitive: ![[caseSwitch selectedCell] tag]
    inSelection: [[selScopeSwitch selectedCell] tag]] ;
}



- finderRemLine: sender ;
{ // remove the browser-selected line from the list of lines
  if([[NXApp mainWindow] isKindOf: [CodeBrowser class]])
  { [[[NXApp mainWindow] lineList] removeElementAt: [[lineNumBrowser matrixInColumn:0] selectedRow]] ;
    [lineNumBrowser loadColumnZero] ;
  }
  return self ;
}

- finderReplace: sender ;
{ // replace the highlighted text with the current text
  if([[NXApp mainWindow] isKindOf: [CodeBrowser class]])
  { [[[NXApp mainWindow] codeBrowserText] replaceSel: [replaceText stringValue]] ;
  }
  return self ;
}

- finderReplaceAll: sender ;
{ // replace all occurences of the find text with the replace text
  if([[NXApp mainWindow] isKindOf: [CodeBrowser class]])
  { id textView = [[NXApp mainWindow] codeBrowserText] ;
    NXSelPt start, end ;
    char buf[80] ;
    int repKnt = 0 ;
    [[textView window] disableFlushWindow] ;
    if(![[selScopeSwitch selectedCell] tag])
    { // Scope is entire file.  We need to prevent entering
      // a continuous loop in cases where a string is being
      // replaced by another which contains (or matches) the first.
      // To do this, we select the entire text, then do a find
      // with inSelection set to YES, then move the selection forward.
      NXSelPt saveStart,saveEnd ;
      int saveLineNum, saveCharNum, saveSelLen ;
      // after the replace, we want to be sure to put the selection back
      // on the same line, and have the same length, as before the replace
      [textView getSel: &saveStart :&saveEnd] ;
      saveLineNum = [textView lineFromPosition: saveStart.cp] ;
      saveCharNum = saveStart.cp - [textView positionFromLine: saveLineNum] ;
      saveSelLen = saveEnd.cp - saveStart.cp ;
      [textView setSel: 1 :[textView textLength]] ;
      while([self find: (char *) [findText stringValue]
        regEx: [[regExpSwitch selectedCell] tag]
        caseSensitive: ![[caseSwitch selectedCell] tag]
        inSelection: YES])
      { [self finderReplace: self] ;
        [textView getSel: &start :&end] ;
        [textView setSel: end.cp :[textView textLength]] ;
        repKnt++ ;
      }
      saveStart.cp = [textView positionFromLine: saveLineNum] + saveCharNum ;
      [textView setSel: saveStart.cp :saveStart.cp + saveSelLen] ;
    }
    else 
    { // scope is current selection
      int diff ;
      NXSelPt newStart, newEnd ;
      diff = strlen([replaceText stringValue]) -
             strlen([findText stringValue]) ; 
      [textView getSel:&start :&end] ;
      if([self finderNext: self])
      { do
        { end.cp += diff ;
          [self finderReplace: self] ;
          // must reset the (adjusted) selection
          [textView getSel: &newStart :&newEnd] ;
          [textView setSel: newStart.cp :end.cp] ;  
          repKnt++ ;      
        } while([self finderNext: self]) ;
        [textView setSel: start.cp :end.cp] ;
      }
    }
    [[textView window] reenableFlushWindow] ;
    [[textView window] display] ;
    sprintf(buf,"%d replacement%s",repKnt,repKnt == 1 ? "":"s") ;
    [self setTitle: buf] ;
    [self perform:@selector(resetTitle:) 
  	with:self  afterDelay:3500 cancelPrevious:YES];
  }
  return self ;  
}

- finderReplaceAndFind: sender ;
{ // replace the highlighted text with the replacement text,
  // find next occurence of find text
  [self finderReplace: self] ;
   return [self finderNext: self] ;
  return self ;
}

- findLine ;
{ return findLineNum ;
}

- findText ;
{ return findText ;
}

- jump: sender ;
{ // scroll selection to visible
  if([[NXApp mainWindow] isKindOf: [CodeBrowser class]])
  {  [[[NXApp mainWindow] codeBrowserText] scrollSelToVisible] ;
  }
  return self ;
}


- lineNumBrowser ;
{ return lineNumBrowser ;
}

- reset: sender ;
{ // called from a CodeBrowser to cause me to 
  // reload the linenum browser
  currentCodeBrowser = sender ;
  [lineNumBrowser loadColumnZero] ;
  return self ;
}

- resetTitle: sender ;
{ // Reset our title to "Find"...normally
  // sent via a delayed message send.
  [self setTitle: "Find"] ;
  return self ; 
}


@end
