 {**************************************
	***   SORTED (INDEXED) DERIVITIVE  ***
	**************************************}
{$I compdirs}
unit Indexes;

INTERFACE

uses 	files, objects, dattime, global, tuilist, views, drivers;

{The size of indexitem is defined in the indexstream, if not then in
the RecordSize field.  Allow an extra 6 bytes for the pointers, lock and
hole, and remember the keystring has a length byte}
const
	TIndexItemSize =10; {base index size - incl ver, lock, ptr, hole, string length byte; just add string length}

type
	PIndexItem = ^TIndexItem;
	TIndexItem = object(TDataItem)
				Idx2Dat : longint;
				Hole : boolean;
				Tag  : boolean;
				ixType : byte;  {used to define which data back pointer required for
											multiple index items in one file.  eg in fiFullDirIdx,
											for each data item there might be one indexitem marked
											ixtype=1 (ordinary index), and one marked ixtype=2
											(alias).  Needs to know from this end for shuffling, etc}
				KeyString : string;

				{-- Methods --}
				constructor Init;
				constructor Load(var S : TDataStream);
				procedure   Store(var S : TDataSTream); virtual;
				function    GetKey : string; virtual;
{$IFDEF fixit}	 function		 Edit(Caller : PView) : word; virtual; {for debug} {$ENDIF}
	end;

const
	{--- Required for Stream ----}
 RIndexItem : TStreamRec = (
				ObjType : srIndexItem;                 {Set to arbitrary number defined in a common bit of code such as MSDSKTOP}
				VmtLink : Ofs(TypeOf(TIndexItem)^);
				Load : @TIndexItem.Load;
				Store : @TIndexItem.Store);

	HoleMark = '{Hole}';{}


type
	PIndexStream = ^TIndexStream;
	TIndexStream = object(TDataStream)

		NonHoleIndex : boolean;

		constructor Init(NFileName : FNameStr; NRecSize : word);
		procedure SkipHoles(var Item : PindexItem; var RecNo : longint; step : integer);
		function  FindFirst(KeyString: string; var RecNo, HoleRec, ID : longint) : integer; {Search for match}
		function  FindFrom(KeyString : string; var RecNo, HoleRec, ID : longint) : integer;  {Search for next match}
		procedure FindHole(var RecNo : longint);  virtual; {Searches for hole or end of disk}
		function FindMatch(KeyString : string; var RecNo, HoleRec, ID : longint) : integer;  {Search for next non-hole match}
		function  Compare(Item1, Item2 : PIndexItem) : integer; virtual; {compare index}

		procedure Delete(RecNo : longint; LockStatus : byte); virtual;

		procedure Insert(fiType :byte; Item : PIndexItem; var RecNo : longint);{Inserts new record}

		procedure SetDat2Idx(const IndexItem : PIndexItem; const NewIdxPtr : longint); virtual; {for data-associated descendants}

	end;

	{--- LIST INTERIOR ---}
	PIndexListView   = ^TIndexListView;          {Interior}
	TIndexListView   = object(TListView)

		fiType : word;

		{CANNOT DO THIS DUE TO WP LINK - which will close down all streams and
		re-open, therefore possibly changing the pointers}
{		IndexStream : PIndexStream; {used as shorthand to access Stream(fitype)}
		{also makes it a bit quicker to access as the current Stream(fitype) requires
		a search through a collection}

		FileVer : byte; {for comparing with data stream changedver for redraw}

		constructor Init(var Bounds: TRect; const NlsType, NfiType : word);
		destructor Done; virtual;
		function GetText(const RecNo: longint) : string; virtual;  {}
		function  GetIndexText(const ItemNo : longint) : string; virtual;

		procedure FocusText(Text : string); virtual;

		procedure HandleEvent(var Event : TEvent); virtual;

		procedure Del(RecNo : LongInt); virtual; {}
		procedure Redraw;  virtual;

		{Ranging}
		procedure SetRange; virtual;
		procedure GetNextItemNo(var ItemNo : longint); virtual;
		procedure GetPrevItemNo(var ItemNo : longint); virtual;
		procedure FindOKItemNo(var ItemNo : longint); virtual;
		procedure StepItemNo(var ItemNo : longint; const Step : integer); virtual;
	end;


function IndexStream(fiType : word) : PIndexStream; {shorthand}


IMPLEMENTATION

uses  app,
			tasks,
{$IFDEF fixit} tuiedit, {$ENDIF}
			{$IFNDEF SIngleUser} muser, {$ENDIF}
			tui, help,
			status, {for locks, etc}
			tuimsgs, minilib;


{This little function just saves having to do a PIndexStream(Stream(fiType))
every time you want to access a particular index stream.  Now you just do
IndexStream(fiType) and it'll do it for you...}
function IndexStream(fiType : word) : PIndexStream;
begin IndexStream := PIndexStream(Stream(fiType)); end;

{****************************************************************************
 ***              INDEX STREAM DEFINITION                                ***
 ****************************************************************************}
{ A Data stream modified to form an index
 for use in conjunction with a "main" data file.
 Based upon the PPMS/MicroClub "holed" index.
 To initialise, pass it a name and the length of each "record"

To store a new one, pass it a string (the sort key) and the pointer to the
main data file, and it will insert it alphabetically. The sort key string
must be in format so that it can be directly compared with a Key1>Key2 type
of statement, although case will be ignored.

To retrieve a name, pass it as much of the corresponding sort key string
as you know (eg "Hil" for "Hill") and it will return the complete string
and pointer of the FIRST match it makes. An add-on to this ought to be a flag
to say if there are any more matches...

To delete one, pass it either the sort key string or the position in the file.
}


{********************************
 ***    THE INDEX ITEM        ***
 ********************************}
{Defining the "record"}


{---- INITIALISE -------------}
constructor TIndexItem.Init;

begin
	inherited Init;
	KeyString := '';
	Hole := False;
	Idx2Dat := -1;
	ixType := 1;
end;

{======= STREAMING =============}

{---------- LOAD ---------------}
constructor TIndexItem.Load;
var Ver : byte;
begin
	S.Read(Ver, 1);
	LockTerminal := 0;
	LockCount := 0;
	case Ver of
		3 : begin
			S.Read(ixType, 1);
			Hole := ((ixType and $80)>0);  {use one byte to store both}
			Tag := ((ixType and $40)>0);
			ixType := ixType and $0F;
			S.Read(Idx2Dat,sizeof(Idx2Dat));
			S.Read(LockTerminal, 1);
			KeyString := S.ReadStr;
		end;
		2 : begin
			ixType := 1;
			S.Read(Hole,sizeof(boolean));
			S.Read(Idx2Dat,sizeof(Idx2Dat));
			S.Read(LockTerminal, 1);
			KeyString := S.ReadStr;
		end
	else
		fail;
	end;
end;

{-------- STORE ----------------}
procedure TIndexItem.Store;
var StartPos : longint;
		Ver : byte;
		B : byte;

begin
	StartPos := S.GetPos;
	Ver := 3; S.Write(Ver, 1);

	B := ixType;
	if Hole then B := B or $80;  {use one byte to store both}
	if Tag then B := B or $40; {use one byte to store both}
	S.Write(B, 1);

	S.Write(Idx2Dat,sizeof(Idx2Dat));
	S.Write(LockTerminal, 1);
	KeyString := Copy(KeyString, 1, PDataStream(@S)^.RecSize - 10); {safety}
	S.WriteStr(@KeyString);

	TopUpRecord(S, PDataStream(@S)^.RecSize, StartPos);
end;

function TIndexItem.GetKey : string;
begin
	GetKey := KeyString;
end;


{$IFDEF fixit}
{=== EDIT =======================}
{Edit Item - for debug purposes}
function TIndexItem.Edit;
var EditBox : PEditBox;
		R : TRect;

begin
	R.Assign(0,0,36,11);
	CentreOnView(R, Caller);
	New(EditBox, init(R, 'Index Fix',Caller));

	with EditBox^ do begin

		Insert(New(PSkipBytes, init(2))); {data item id}
		Insert(New(PSkipBytes, init(4))); {skip recno}

		InsTitledField(10,1,  3, 1, 'Lock Term', New(PInputByte, init(R, 3)));
		InsTitledField(10,2,  3, 1, 'Lock Count', New(PInputByte, init(R, 3)));
		InsTitledField(10,3,  6, 1, 'Idx2Dat', New(PInputLint, init(R, 7)));
		InsTitledField(10,4,  1, 1, 'Hole', New(PInputBoolean, init(R)));
		InsTitledField(10,5,  1, 1, 'Tag',  New(PInputBoolean, init(R)));
		InsTitledField(10,6,  3, 1, 'ixType', New(PinputByte, init(R, 3)));
		InsTitledField(10,7, 25, 1, 'Key', New(PinputELine, init(R, 255)));

		InsOKButton(5, 8, @Self);
		InsCancelButton(16, 8);

		EndInit;

		SetData(Self);
	end;

	Edit := Desktop^.ExecView(EditBox);

end;
{$ENDIF}



{************************************************************************
 ***                                                                  ***
 ***                       THE INDEX STREAM                           ***
 ***                                                                  ***
 ************************************************************************}
{Note on buffer size: For searching, buffer size should be reasonably
small as searching skips a lot (binary chop).  Shuffling however would
benefit from better buffering *as long as the shuffling reads from low
record number to higher*.  So for the moment, keep buffer low (record
size) and do an insertholes if necessary...}
constructor TIndexStream.Init;
begin
	inherited Init(NFileName, NRecSize, NRecSize);
	NonHoleIndex := False;
end;


{=== SEARCH ==============================}
{Looks for the first match for a particular item}
{Returns -1 if not found, 0 if one exact match, 1 if duplicate matches
	(matches only occur if exact keystring match AND not a hole
 and RecNo as the file record number of the match, or if none the one directly
 after where the match would be}

function TIndexStream.FindFirst;
var Work : PIndexItem;
		StopChop : boolean;
		Pos1,Pos2,ORecNo : longint;
		KeyItem : PIndexItem;

begin
	FindFirst := -1;   {Haven't found anything yet}
	HoleRec := -1;     {Find next will locate the proper hole}
  ID := -1;
	Work := nil;
  StopChop := False;
  New(KeyItem, init); KeyItem^.KeyString := KeyString;

  {-- Binary Chop search ---}
  {At the moment searches to nearest 5, as binary chop difficult to do with
      a holed database}
	Pos1 := 0; Pos2 := NoRecs -1;   {Start & End of file}
	RecNo := 0;
  if (Pos2-Pos1) <5 then StopChop := True;   {Very little data}

  while not StopChop do begin
		RecNo := (Pos1 + Pos2) div 2; ORecNo := RecNo;

		Work := PIndexItem(GetAt(RecNo));

		{safety check}
		if Work = nil then begin
			RecordError('DATABASE ERROR:','Doing Binary Chop Index Search','Encountered nil index record at '+N2Str(RecNo)
														+' Status'+N2Str(Status)+' ErrInfo'+N2Str(ErrorInfo));
      Reset;
			RecNo := RecNo+1; {move on to next}
			StopChop := True; {quit binary chop and do straight search}
		end else begin
			if Compare(KeyItem, Work) <0 then Pos2 := RecNo           {Get point after match}
				else if Compare(KeyItem, Work) > 0 then Pos1 := RecNo {Get point before match}
					else
						{Ok, so compare matches, but compare does a partial string match
						(see .compare and this means that this search would stop as soon
						as partial match found - eg S found amongst S's, then go on to
						findnext, very slow, so:}
					if KeyItem^.GetKey = Work^.GetKey then
          	begin StopChop := True; FindFirst := 0; end  {Get point matches}
          else
						Pos2 := RecNo; {look earlier for match}


			ID := Work^.Idx2Dat;
			dispose(Work, done);

    	{or if no change in position - perhaps lots of holes}
     	if ((Pos2 - Pos1)<10) or (((Pos2+Pos1) div 2) = ORecNo)  then begin
     		RecNo := (Pos1 + Pos2) div 2;     {One last chop}
				stopChop := True;  {Will be no change in record number}
			end;
		end;
	end;

  dispose(KeyItem, done);

	{By the time we reach a close match, might as well do ordinary search -
 	that way there's also a good chance of finding a hole...
 	especially because I'm not sure if this works OK at the end}
	FindFirst := FindFrom(KeyString, RecNo, HoleRec, ID);{}

end;

{Searches in either direction from RecNo.  See FindFirst for returned values}
function TIndexStream.FindFrom;
var OWork, Work,KeyItem : PIndexItem;
		NumMatches : byte;
		DupRecNo : longint;

begin
	FindFrom := -1;  {None found so far}
	Work := nil;  OWork := nil;
	HoleRec := -1;   {While it's looking, it might as well record the nearest prev hole}
	ID := -1;
	New(KeyItem, init); KeyItem^.KeyString := KeyString;
	NumMatches := 0;

	{Safety}
	if RecNo>=NoRecs then RecNo := NoRecs-1;
	if RecNo<0 then RecNo := 0;   {Also compensates for NoRecs = 0 .--> RecNo = -1 in above line}

	if NoRecs>0 then begin {JIC file empty}

		{Get the first one}
		Work := PIndexItem(GetAt(RecNo));
		if Work = nil then begin
			DBaseMessage(@Self,'Failed to load index item #'+N2Str(RecNo),mfError,hcInternalErrorMsg);
			Reset;
			exit;
		end;

{		if {(RecNo=0) and{} {(Compare(KeyItem, Work)=0) and (not Work^.Hole) then inc(NumMatches);{}

		{Check for direction of search}
		if Compare(KeyItem, Work)<=0 then begin
			{-- Search backwards for *first* match or overshoot -----}
			while (RecNo>0) and (Compare(KeyItem, Work)<=0) do begin
				RecNo := RecNo -1;
				Dispose(Work, Done);
				Work := PIndexItem(GetAt(RecNo));
				if Work^.Hole then HoleRec := RecNo;     {Record latest hole found}
{num matches only important for user search - do below
				if not Work^.Hole and (Compare(KeyItem, Work)=0) then inc(NumMatches); {Has found a non-holed match}
			end;
			{Has now reached the stage where Compare() would return 1 or 0}
			if Compare(KeyItem, Work)=1 then begin{Compensate for overshooting}
				RecNo := RecNo +1;
				dispose(Work, Done);
				Work := PIndexItem(GetAt(RecNo)); {reget so id set OK and compares, etc below work OK}
			end;

			ID := Work^.Idx2Dat;    {Bring ID up-to-step}

		{Other direction}
		end else {if Compare(KeyItem, Work)>0 then{} begin
				{---- Search forwards ------}
				while (RecNo<(NoRecs-1)) and (Compare(KeyItem, Work)>0) do begin {Search forward}
					RecNo := RecNo +1;
					Dispose(Work, Done);          {Delete last record picked up}
					Work := PIndexItem(GetAt(RecNo));    {Load up new one onto heap}
					if Work^.Hole then
						HoleRec := RecNo;     {Record latest hole found}
{					else
						if Compare(KeyItem, Work)=0 then inc(NumMatches); {not a hole and a proper match}
				end;
				if (RecNo = NoRecs-1) and (Compare(KeyItem, Work)=1) then RecNo := NoRecs; {reached end of file}
				{Compare() should now return 1 or 0}
				ID := Work^.Idx2Dat;    {Return Datpointer}
		end;

		{Have now come to closest match/first in list of matches - this is sufficient for database
		operations involving search/insert, but for inputdirectoryline, etc,
		we need to check and see if 1) there are any a) exact, b) non-hole
		matches, and 2) if there is more than 1}
		if Compare(KeyItem, Work)=0 then begin {currently on an exact match}
			{if done backwards, (and Key=Work) then NumMatches>=1, if it's 1
			we cannot assume there really is only one duplicate as the binary
			chop may have dropped it straight there, so do a check if NumMatches
			is 1 or less}
			if not Work^.Hole then NumMatches := 1; {this one...}
			DupRecNo := RecNo +1; {dummy record number - don't change recno which points to first match}
			while (NumMatches <=1) and (DuprecNo<=(NoRecs-1)) and (Compare(KeyItem, Work)=0) do begin
				dispose(Work, done);
				Work := PIndexItem(GetAt(DupREcnO));
				if (not Work^.Hole) and (Compare(KeyItem, Work)=0) then inc(NumMatches); {another, non-hole, match}
				inc(DupRecNo);
			end;

			if NumMatches>1 then
				FindFrom := +1  {Mark as found more than one match}
			else
				if NumMatches=1 then
					FindFrom := 0  {Mark as found one Exact Match}
				else
					FindFrom := -1; {none non-holes found}

		end;

		if Work <>nil then Dispose(Work, Done);
	end;

  dispose(KeyItem, done);
end;


{============= FIND NON-HOLE MATCH ====================}
function TINdexStream.FindMatch(KeyString : string; var RecNo, HoleRec, ID : longint) : integer;
var F : integer;
		IndexItem : PIndexItem;
begin
	F := FindFirst(KeyString, RecNo, HoleRec, ID);
	{skip holes}
	if (ID=-1) and (F>-1) then begin
		{one/more data matches, but first one returned as recno is a hole}
		inc(RecNo);
		IndexItem := PindexItem(GetAt(RecNo));

		while IndexItem^.Hole do begin
			dispose(IndexItem, done);
			inc(RecNo);
			IndexItem := PIndexItem(GetAt(recNo));
		end;

		ID := IndexItem^.Idx2Dat;
		dispose(IndexItem, done);
	end;

	FindMatch := F;
end;



{========== FIND HOLE ========================================}
{Returns RecNo as next hole position, or record at end of file}
procedure TIndexStream.FindHole(var RecNo : longint);
var Work : PIndexItem;

begin
  Work := nil;
  if RecNo<NoRecs then begin
    {--- Run through until hole ----}
    repeat
      if Work <> nil then Dispose(Work, done);
      SeekRec(RecNo); Work := PIndexItem(Get);
      RecNo := RecNo +1;
    until (RecNo>=NoRecs) or Work^.hole;

		if Work<> nil then begin
      if Work^.Hole then RecNo := RecNo -1; {ie if not end of file, compensate for last +1}
			Dispose(Work, done);
    end;
	end;
end;


{=== COMPARE =============================}
{Compare two Index items, pointed to by Key1 and Key2.
	Return -1 if Key1<Key2
           0 if Key1=Key2
	 +1 if Key1>Key2}

  {WARNING!!! It compares the string of Item1 *partially* with Item2;
  ie it chops off Item2 to match Item1.  This means if you are searching
  for a partial match (ie Hil for Hill) your searching one should be
  item1 and your working one (reading from the file) should be item2. o/w
  it might go a bit funny}

function TIndexStream.Compare(Item1, Item2 : PIndexItem) : integer;
var Key1,Key2 : string;
begin
	if (Item1 = nil) or (Item2 = nil) then begin
		ProgramError('Cannot Compare Index Items'#13#10'One is nil',hcInternalErrorMsg);
		Compare := 0;
		exit;
	end;

  {Compares strings partially - ie if either one is smaller than other, truncate}
  Key1 := Item1^.GetKey; Key2 := Item2^.GetKey;
  if length(Key2)>length(Key1) then Key2 := Copy(Key2, 1, length(Key1)){ else Key1 := Copy(Key1, 1, length(Key2)){};

  if (Key1<Key2) then Compare := -1
  	else if (Key1=Key2) then Compare := 0
			else Compare := 1;{}
{  if (Item1^.getKey<Item2^.GetKey) then Compare := -1
	else if (Item1^.GetKey=Item2^.GetKey) then Compare := 0
	else Compare := 1;{}
end;

{Returns RecNo as next non-hole, or -1 if only holes before and NoRecs if only holes after}
procedure TIndexStream.SkipHoles(var Item : PIndexItem; var RecNo : longint; step : integer);
{assumes item has just been gotten from RecNo, or is nil in which case it will get it}
begin
	if Step>0 then Step := +1;
	if Step<0 then Step := -1;  {Just to make sure}
	if RecNo>(NoRecs-1) then RecNo := (Norecs-1); {Bring back into range}
	if Item = nil then Item := PIndexItem(GetAt(RecNo));

	while ((RecNo>0) or (Step=1)) and
				(((RecNo<=(NoRecs-1))) or (Step=-1)) and
				(Item^.Hole) do begin
		Dispose(Item, done);
		RecNo := RecNo + Step;
		Item := PIndexItem(GetAt(RecNo));
	end;
end;


{=== DELETE ===============================}
procedure TIndexStream.Delete(RecNo : Longint; LockStatus : byte);
var Work, IndexItem : PIndexItem;
		ShuffleRec : longint;
		Control : word;{}

begin
	if RecNo = -1 then exit;                      {Nothing to delete}

	IndexItem := PIndexItem(GetAt(RecNo));                      {Load onto heap and set pointer}
	if IndexItem = nil then begin
		DBaseMessage(@Self, 'Failed to Load Index Item'#13#10'Delete; RecNo'+N2Str(RecNo), mfError,hcInternalErrorMsg);
		exit;
	end;

	{Check for lock}
	if (IndexItem^.GetLock >0) and ((LockStatus and lkIgnore)=0) then begin
		dispose(IndexItem, done);
		Status := stError;
		ErrorInfo := eiLocked; {DOS error 33 = lock violation}
		exit;
	end;{}

	{a non-holed index is simply an index like this one that does not generate
	holes when deleting - instead it shuffles down from the end of the file...}
	{Used for situations where the index numbering is actually important - eg
	race position in the rally program}
	if NonHoleIndex then begin
		for ShuffleRec := RecNo+1 to NoRecs-1 do begin
			{Retrieve work record to be shuffled}
			Work := PIndexItem(GetAt(ShuffleRec));        {Get Source of copy onto heap}
			PutAt(ShuffleRec-1, Work);
			SetDat2Idx(Work, ShuffleRec-1); {override by descendants requiring
																			data back pointer changes}
			Dispose(Work, Done);                      {Dispose of object off heap}
		end;
		SeekRec(NoRecs-1);
		Truncate; {chops off last record}
	end else begin
		{Mark as hole}
		IndexItem^.Hole := True;                           {Set hole marker}
		IndexItem^.Idx2Dat := -1;                          {Clear pointer}
		PutAt(RecNo, IndexItem);                           {Store on stream}
	end;

	{Now ought to set DataStream's back pointers...}
	{Although that would slow down the normal "Delete, reinsert" process after editing}

	Dispose(IndexItem, done);                          {Remove from heap}

	NewVer;   																{notify all associated views of a change}
end;

{***************************************
 ***          INSERT NEW RECORD      ***
 ***************************************}
{Ignore locking in index - always make sure that routines re-get
 index pointers afterwards. Otherwise the complete file will lock
 up if someone has a record open in the way}

procedure TIndexStream.Insert(fiType : byte; Item : PIndexItem; var RecNo : longint);
var
	HoleRec : longint;
	ShuffleRec : longint;
	Work : PIndexItem; {Work record}
	RecStat : byte;
	LS : word;
	Step : integer;
	ID : longint;
	Control : word;

begin
	if Status<>stOk then ErrorMsg('Before Insert');                 {safety}
	Work := nil;                                  {safety}

	if Item = nil then begin
		ProgramError('Trying to Insert a nil item'#13#10'fiType='+N2Str(fiType),hcInternalErrorMsg);
		Status := stError;
		exit;
	end;

	{--- Lock file/check for lock ---}
	{$IFNDEF SingleUser}
		if CheckFileLock(srIndexItem,'Index File'#13#10'Inserting new index item')<>0 then begin
			Status := stError;
			ErrorInfo := eiLocked;
			exit;
		end else
			SetFileLock(srIndexItem, True);

		Flush; {clear buffers so we are reading from latest disk info}

	{$ENDIF}


	{--- Find insert & hole position -------}
	ThinkingOn('Searching');
	FindFirst(Item^.GetKey, RecNo, HoleRec, ID);                   {Find pos to insert}
	{RecNo is now like a cursor; it is over the record immediately after
	the insert point}

	if HoleRec =-1 then begin
		HoleRec := RecNo;               {Start at rec no}
		FindHole(HoleRec);            {Find nearest hole}
	end;

	{Decide which way to shuffle}
	if HoleRec < RecNo then Step := +1 else Step := -1;

	ThinkingOff;

	ThinkingOn('Inserting');
	ShuffleRec := HoleRec;             {Record to shuffle IN to}

	{The shuffling takes the one from hole rec + step and puts it into the hole record,
	ie moving the hole up until it co-incides with the place we want to put the new
	record}

	{If shuffling from before, we do not want to shuffle the one PAST the ins pt, so move recno down one}
	if (Step = +1) then RecNo := RecNo -1;

	while (ShuffleRec<>RecNo) do begin  {Shuffle up to but not inc RecNo}

		{Retrieve work record to be shuffled}
		Work := PIndexItem(GetAt(ShuffleRec+Step));        {Get Source of copy onto heap}

		{Check retreival was OK}
		if Work = nil then begin
			DBaseMessage(@Self,'Shuffling'#13#10'Get IndexItem failed, IDX #'+N2Str(ShuffleRec-1),mfError,hcInternalErrorMsg);
			if Status=stOK then Status := stError; {make sure it's set}
			exit;
		end else begin

			{Store in hole - new position}
			PutAt(ShuffleRec, Work);

			SetDat2Idx(Work, ShuffleRec); {override by descendants requiring
																			data back pointer changes}

			Dispose(Work, Done);                      {Dispose of object off heap}
		end;

		ShuffleRec := ShuffleRec + Step;          {Move on to next one}
	end;

	{--- Write over RecNo pos ----}
	PutAt(RecNo, Item);      {store}

	{$IFNDEF SingleUser}
		SetFileLock(srIndexItem, False); {clear lock}
		Flush; {clear buffers so we are reading from latest disk info}
	{$ENDIF}

	ThinkingOff;

	NewVer;   																{notify all associated views of a change}
end;


procedure TIndexStream.SetDat2Idx(const IndexItem : PIndexItem; const NewIdxPtr : longint);
begin end;

{**************************************************************************
 ***                                                                    ***
 ***                        VIEW ON INDEX STREAM                        ***
 ***                                                                    ***
 **************************************************************************}

{=== INITIALISE ==========================================}
constructor TIndexListView.Init;

begin
	Inherited Init(Bounds, NlsType);

	fiType := NfiType;  {pass fitype as index file type}

	{Set up/check appropriate stream}
	FileAdmin(fiType)^.LogOn;
{	IndexStream := PIndexStream(Stream(fitype));	{can't do this as pointer may
																								change when calling WP, etc}
{	EventMask := EventMask or evBroadcast; {done in tuilist}
{	HelpCtx := hcIndexList;{}
end;

{=== DONE ==================================================}
destructor TIndexListView.Done;
begin
	FileAdmin(fiType)^.LogOff;
	inherited Done;                                                                          {Close list view}
end;


{===== HANDLE EVENT ===========================}
procedure TIndexListView.HandleEvent;
var IndexItem : PIndexItem;
begin
	{Tagging}
	if (Event.What = evKeyboard) and (Event.CharCode = '*') then begin
		IndexItem := PIndexItem(Stream(fiType)^.GetAt(Focused));
		IndexItem^.tag := not IndexItem^.Tag;
		Stream(fiType)^.PutAt(Focused, IndexItem);
		RedrawItem(Focused);
		ClearEvent(Event);
	end;

	{Broadcast change requiring check & redraw - eg in multi-windowed desktop}
	if (Event.Command = cmCheckFileVer) and (FileVer<>Stream(fiType)^.ChangeVer) then begin
			FileVer := Stream(fiType)^.ChangeVer;
			SetRange;
			Redraw;
{			while not Drawn do RedrawNextLine; {draw complete view}
					{now done through groups idle method}
	end;

	{Do before inherited so it doesn't attempt a creator}
	if (Event.What and evCommand)>0 then begin

		if (Event.Command=cmCopyJimmy) then begin
			EditNew(Focused, cmCopyJimmy);
			ClearEvent(Event);
		end;


		if (Event.Command = cmAccept) then
			if DrawnFocused then begin
				IndexItem := PIndexItem(Stream(fiType)^.GetAt(Focused));
				SendAcceptMessage(Event, cmAccept, IndexItem);
				Dispose(IndexItem, Done); IndexItem := nil;
				if Event.What=evNothing then ClearEvent(Event); {If accepted, clear event}
			end else begin
				{if it's not drawn yet, this ends up turning
			 into an edit (in inherited handleevent), and as the inherited method
				also does an idle, this can result in it being drawn and then edited,
				rather than accepted.  So if an accept is required, wait until it's been
				drawn...}
				if not ReDrawn then begin
					PutEvent(Event);
					Idle;
				end;
				ClearEvent(Event); {if it's redrawn then it's empty, and cannot accept}
			end;
  end;

	inherited HandleEvent(event);
end;


{--- Get Text -------------------------------------------}
{For each line}
function TIndexListView.GetText(const RecNo : longint) : string;
begin
	GetText := GetIndexText(RecNo); {default}
end;

{--- For the search ---}
function TIndexListView.GetIndexText;
var IndexItem : PIndexItem;

begin
	IndexItem := PIndexItem(IndexStream(fitype)^.GetAt(ItemNo));

	if IndexItem <> nil then begin
		if IndexItem^.Hole then
			GetIndexText := HoleMark
		else
			GetIndexText := IndexItem^.GetKey;
		dispose(IndexItem, done);
	end else
		GetIndexText := HoleMark; {nil indexitem}
end;

procedure TIndexListView.FocusText;
var	HoleRec, ID, ItemNo : longint;

begin
{	Text := ucase(Text);{}

	{See if match already on-screen}
	if AllowSearchScreen then ItemNo := SearchScreen(Text) else ItemNo:= -1;

	if ItemNo <> -1 then begin
		Focused := ItemNo;
		DrawView;
	end else begin
		IndexStream(fitype)^.FindFirst(Text, ItemNo, HoleRec, ID); {Search for match}
		FindOKItemNo(ItemNo); {skips holes etc}
		FocusItem(ItemNo);
	end;

end;


{======= DELETE ITEM ===========}
procedure TIndexListView.Del(RecNo : longint);
begin
	if InRange(RecNo) then begin

		{Delete appropriate index item}
		IndexStream(fitype)^.Delete(RecNo, LkCheck);        {Delete}

    FindOKItemNo(RecNo);				{find nearest ok record to focus on}
		FocusItem(RecNo);               {Focus on it}
		ReDraw;                         {Re draw screen}
	end;
	Message(Desktop, evBroadCast, cmCheckFileVer, nil); {get all views to check their file links}
end;

{old method was to use "if GetText(ItemNo) = HoleMark" but this involves
getting jimmys if they aren't - this should be quicker}
{don't use this either - use GetIndexText instead!}
{function TIndexListView.IsHole(const ItemNo : longint) : boolean;
var
	IndexItem : PIndexItem;
begin
	IndexItem := PIndexItem(IndexStream(fiType)^.GetAt(ItemNo));
  if (IndexItem=nil) or (IndexItem^.Hole) then IsHole := True else IsHole := False;
	if IndexItem<>nil then dispose(IndexItem, done);
end;{}


procedure TIndexListView.GetNextItemNo;
begin
	if ItemNo = -1 then
		ItemNo := FirstItem
  else
		inc(ItemNo);

  while (ItemNo<=LastItem) and (GetIndexText(ItemNo)=HoleMark) do inc(ItemNo);

  if ItemNo>LastItem then ItemNo := -1;
end;

procedure TIndexListView.GetPrevItemNo;
begin
	if ItemNo = -1 then
  	ItemNo := LastItem
  else
  	dec(ItemNo);

  while (ItemNo>=FirstItem) and (GetIndexText(ItemNo)=HoleMark) do dec(ItemNo);

  if ItemNo<FirstItem then ItemNo := -1;
end;

procedure TIndexListView.StepItemNo;
begin
	inherited StepItemNo(ItemNo, Step);
  FindOKItemNo(ItemNo);
end;

{hunts about for nearest non-hole item within range, or returns -1}
procedure TIndexListView.FindOKItemNo(var ItemNo : longint);

begin
  if ItemNo<FirstItem then ItemNo := FIrstItem;
  if ItemNo>LastItem then ItemNo := LastItem;

  if ItemNo<>-1 then
  	if GetIndexText(ItemNo)=HoleMark then begin
			GetNextItemNo(ItemNo);
			if (ItemNo=-1) then ItemNo := LastItem;

			if (GetIndexText(ItemNo)=HoleMark) then
				GetPrevItemNo(ItemNo);
		end;
end;


procedure TIndexListView.SetRange;
var ItemNo : longint; {need to use temporary vars so that findokitem works}
begin
	FirstItem := 0;
	LastItem := Stream(fitype)^.NoRecs-1;             {Set list range}

	ItemNo := FirstItem; FindOKItemNO(ItemNo); FirstItem := ItemNO;
	ItemNo := LastItem; FindOKItemNo(ItemNo); LAstItem := ItemNo;

	inherited SetRange;
end;


{******************************
 *** REDRAW                 ***
 ******************************}
procedure TIndexListView.Redraw;
begin
	{in case of deletions,etc}
	if Focused<FirstItem then Focused := FirstItem;
	if Focused>LastItem then Focused := LastItem;

	TopItem := Focused;        {Move focused to top so that user can move while displaying}

	inherited Redraw;
end;



BEGIN
	RegisterType(RIndexItem);
end.


