-- Stats.Mesa  Edited by Sandman on October 14, 1980  11:10 AM
-- Copyright  Xerox Corporation 1979, 1980

DIRECTORY
  AltoDefs USING [BytesPerWord, PageCount],
  AltoFileDefs USING [CFA, NullFP],
  BcdDefs USING [Base, MTIndex, VersionID],
  BcdOps USING [BcdBase, MTHandle],
  DisplayDefs USING [GetTypeScript],
  FastDirDefs USING [
    FileType, ItemHandle, LookupItem, ScanDir, LocalDir, StripExtension],
  ImageDefs USING [StopMesa],
  IODefs USING [
    CR, NUL, NumberFormat, ReadID, SP, WriteChar, WriteDecimal, WriteLine,
    WriteNumber, WriteString],
  MiscDefs USING [CallDebugger, CommandLineCFA],
  SegmentDefs USING [
    DeleteFileSegment, FileHandle, FileNameError, FileSegmentAddress,
    FileSegmentHandle, InsertFile, InvalidFP, MoveFileSegment, NewFileSegment,
    Read, SwapIn, Unlock],
  StreamDefs USING [
    CreateByteStream, JumpToFA, Read, ReadBlock, StreamError, StreamHandle,
    DiskHandle],
  String USING [AppendChar, AppendLongNumber, AppendString],
  Storage USING [Words, FreeWords, PagesForWords],
  TimeDefs USING [AppendDayTime, CurrentDayTime, UnpackDT];

Stats: PROGRAM
  IMPORTS
    DisplayDefs, FastDirDefs, ImageDefs, IODefs, MiscDefs, SegmentDefs,
    StreamDefs, String, Storage, TimeDefs =PUBLIC
  
  BEGIN OPEN AltoDefs, AltoFileDefs, IODefs, SegmentDefs, String, StreamDefs;
  
  -- types and globals
  
  StatType: TYPE = {
    char, line, codebytes, framesize, ngfi, nlinks, codepages, sympages};
  
  charField, codebyteField: CARDINAL = 7;
  nlinksField, lineField, framesizeField: CARDINAL = 6;
  codepageField: CARDINAL = 5;
  sympageField: CARDINAL = 4;
  fsiField: CARDINAL = 4;
  ngfiField: CARDINAL = 3;
  filenameField: CARDINAL = 26;
  
  file: FileHandle ← NIL;
  bcd: FileSegmentHandle ← NIL;
  cmdstream: StreamHandle;
  lc: INTEGER;
  full: INTEGER = 18;
  buffer: POINTER;
  BufSize: CARDINAL = 40*256;
  
  stats: ARRAY StatType OF CARDINAL ← [0, 0, 0, 0, 0, 0, 0, 0];
  
  total, subtotal: ARRAY StatType OF LONG CARDINAL ← [0, 0, 0, 0, 0, 0, 0, 0];
  
  format: ARRAY StatType OF IODefs.NumberFormat =
    [[base: 10, zerofill: FALSE, unsigned: TRUE, columns: charField],
      [base: 10, zerofill: FALSE, unsigned: TRUE, columns: lineField],
      [base: 10, zerofill: FALSE, unsigned: TRUE, columns: codebyteField],
      [base: 10, zerofill: FALSE, unsigned: TRUE, columns: framesizeField],
      [base: 10, zerofill: FALSE, unsigned: TRUE, columns: ngfiField],
      [base: 10, zerofill: FALSE, unsigned: TRUE, columns: nlinksField],
      [base: 10, zerofill: FALSE, unsigned: TRUE, columns: codepageField],
      [base: 10, zerofill: FALSE, unsigned: TRUE, columns: sympageField]];
  
  -- the following should be sets
  
  StatsWanted, localStatsWanted: PACKED ARRAY StatType OF BOOLEAN ←
    [TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE];
  
  StatsDesired: TYPE = POINTER TO PACKED ARRAY StatType OF BOOLEAN;
  
  -- procedures
  
  
  BcdSwitches: PROCEDURE [wanted: BOOLEAN, st: StatsDesired] =
    BEGIN
    st[codebytes] ← wanted;
    st[framesize] ← wanted;
    st[ngfi] ← wanted;
    st[nlinks] ← wanted;
    st[codepages] ← wanted;
    st[sympages] ← wanted;
    END;
    
  DefsStatsAndSwitches: PROCEDURE =
    BEGIN
    localStatsWanted[codebytes] ← FALSE;
    localStatsWanted[framesize] ← FALSE;
    localStatsWanted[ngfi] ← FALSE;
    localStatsWanted[nlinks] ← FALSE;
    localStatsWanted[codepages] ← FALSE;
    stats[nlinks] ← 0;
    stats[ngfi] ← 0;
    stats[framesize] ← 0;
    stats[codepages] ← 0;
    stats[codebytes] ← 0;
    END;
    
  GetBcdStats: PROCEDURE [bcdseg: FileSegmentHandle] =
    BEGIN OPEN BcdDefs, SegmentDefs;
    bcd: BcdOps.BcdBase;
    mth: BcdOps.MTHandle;
    sgb: Base;
    SwapIn[bcdseg];
    bcd ← FileSegmentAddress[bcdseg];
    mth ← @LOOPHOLE[bcd + bcd.mtOffset, Base][FIRST[MTIndex]];
    sgb ← LOOPHOLE[bcd + bcd.sgOffset];
    IF ~bcd.definitions THEN
      BEGIN
      stats[nlinks] ← mth.frame.length;
      stats[ngfi] ← mth.ngfi;
      stats[framesize] ← mth.framesize;
      stats[codepages] ← sgb[mth.code.sgi].pages;
      stats[codebytes] ← mth.code.length;
      END
    ELSE DefsStatsAndSwitches[];
    stats[sympages] ← sgb[mth.sseg].pages;
    Unlock[bcdseg];
    RETURN
    END;
    
  GetModule: PROCEDURE [file: FileHandle] RETURNS [bcdseg: FileSegmentHandle] =
    BEGIN
    bcd: BcdOps.BcdBase;
    pages: AltoDefs.PageCount;
    bcdseg ← NewFileSegment[file, 1, 1, Read];
    SwapIn[bcdseg];
    bcd ← FileSegmentAddress[bcdseg];
    IF (pages ← bcd.nPages) # 1 THEN
      BEGIN
      Unlock[bcdseg];
      MoveFileSegment[bcdseg, 1, pages];
      SwapIn[bcdseg];
      bcd ← FileSegmentAddress[bcdseg];
      END;
    IF bcd.versionIdent # BcdDefs.VersionID THEN
      BEGIN
      WriteString["  bad version ID "L];
      WriteDecimal[bcd.versionIdent];
      Unlock[bcdseg];
      DeleteFileSegment[bcdseg];
      RETURN[NIL]
      END;
    IF bcd.nModules # 1 THEN
      BEGIN
      WriteString["  too many modules: "L];
      WriteDecimal[bcd.nModules];
      Unlock[bcdseg];
      DeleteFileSegment[bcdseg];
      RETURN[NIL]
      END;
    Unlock[bcdseg];
    RETURN
    END;
    
  CheckForExtension: PROCEDURE [name, ext: STRING] =
    BEGIN
    i: CARDINAL;
    FOR i IN [0..name.length) DO IF name[i] = '. THEN RETURN; ENDLOOP;
    String.AppendString[name, ext];
    RETURN
    END;
    
  GetSrcStats: PROCEDURE [stream: StreamHandle] =
    BEGIN
    count, i: CARDINAL;
    nc, nl: CARDINAL ← 0;
    crock: POINTER TO PACKED ARRAY OF CHARACTER = buffer;
    UNTIL (count ← ReadBlock[
      stream: stream, address: buffer, words: BufSize ! StreamError => CONTINUE])
      = 0 DO
      FOR i IN [0..count*2) DO
	IF crock[i] = IODefs.CR THEN nl ← nl + 1; nc ← nc + 1; ENDLOOP;
      ENDLOOP;
    stats[char] ← nc;
    stats[line] ← nl;
    RETURN
    END;
    
  GetStats: PROCEDURE =
    BEGIN
    name: STRING ← [40];
    switches: STRING ← [10];
    command: BOOLEAN;
    i: CARDINAL ← 0;
    headingWanted: BOOLEAN ← TRUE;
    buffer ← Storage.Words[BufSize];
    SetCommandInput[];
    WHILE NextFile[name, switches] DO
      IF name[0] = '? THEN HelpMessage[]
      ELSE
	BEGIN
	localStatsWanted ← StatsWanted;
	command ← FALSE;
	i ← 0;
	WHILE i < switches.length DO
	  SELECT switches[
	    i] FROM
	    'b, 'B => BcdSwitches[TRUE, @localStatsWanted];
	    'm, 'M => SourceSwitches[TRUE, @localStatsWanted];
	    'x, 'X =>
	      BEGIN
	      BcdSwitches[FALSE, @localStatsWanted];
	      ManagerSwitches[TRUE, @localStatsWanted];
	      END;
	    '- =>
	      BEGIN
	      i ← i + 1;
	      SELECT switches[
		i] FROM
		'b, 'B => BcdSwitches[FALSE, @localStatsWanted];
		'm, 'M => SourceSwitches[FALSE, @localStatsWanted];
		'x, 'X => ManagerSwitches[FALSE, @localStatsWanted];
		ENDCASE => HelpMessage[];
	      END;
	    'c, 'C =>
	      BEGIN
	      SELECT name[
		0] FROM
		'b, 'B => BcdSwitches[TRUE, @StatsWanted];
		'd, 'D => MiscDefs.CallDebugger[NIL];
		'h, 'H => Heading[];
		'm, 'M => SourceSwitches[TRUE, @StatsWanted];
		'x, 'X =>
		  BEGIN
		  BcdSwitches[FALSE, @StatsWanted];
		  ManagerSwitches[TRUE, @StatsWanted];
		  END;
		't, 'T => Total["TOTAL:"L, FALSE];
		's, 'S => Total["SUBTOTAL:"L, TRUE];
		'- =>
		  SELECT name[
		    1] FROM
		    'b, 'B => BcdSwitches[FALSE, @StatsWanted];
		    'm, 'M => SourceSwitches[FALSE, @StatsWanted];
		    'x, 'X => ManagerSwitches[FALSE, @StatsWanted];
		    ENDCASE => HelpMessage[];
		ENDCASE => HelpMessage[];
	      command ← TRUE;
	      END;
	    ENDCASE => HelpMessage[];
	  i ← i + 1;
	  ENDLOOP;
	IF NOT command THEN
	  BEGIN
	  IF headingWanted THEN BEGIN headingWanted ← FALSE; Heading[] END;
	  FastDirDefs.StripExtension[name, NIL];
	  WriteString[name];
	  THROUGH [name.length..filenameField) DO WriteChar[' ] ENDLOOP;
	  RecordSrcStats[
	    name !
	    FileNameError =>
	      BEGIN SourceSwitches[FALSE, @localStatsWanted]; CONTINUE END];
	  RecordBcdStats[
	    name !
	    FileNameError =>
	      BEGIN BcdSwitches[FALSE, @localStatsWanted]; CONTINUE END];
	  TallyStats[];
	  PrintStats[];
	  WriteChar[CR];
	  END;
	END;
      ENDLOOP;
    Storage.FreeWords[buffer];
    END;
    
  Heading: PROCEDURE =
    BEGIN
    type: StatType;
    header: ARRAY StatType OF STRING =
      ["  chars "L, "  lines "L, "  code  "L, "   frame "L, " ngfi "L, "nlinks "L,
	" code    "L, "symbol"L];
    header2: ARRAY StatType OF STRING =
      ["        "L, "        "L, "  bytes "L, "   size  "L, "      "L, "       "L,
	" pages   "L, "pages"L];
    WriteChar[CR];
    THROUGH [0..filenameField) DO WriteChar[' ] ENDLOOP;
    FOR type IN StatType DO
      IF localStatsWanted[type] THEN WriteString[header[type]] ENDLOOP;
    WriteChar[CR];
    THROUGH [0..filenameField) DO WriteChar[' ] ENDLOOP;
    FOR type IN StatType DO
      IF localStatsWanted[type] THEN WriteString[header2[type]] ENDLOOP;
    WriteChar[CR];
    THROUGH [0..filenameField) DO WriteChar[' ] ENDLOOP;
    WriteDashes[];
    END;
    
  WriteDashes: PROCEDURE =
    BEGIN
    type: StatType;
    FOR type IN StatType DO
      IF localStatsWanted[type] THEN
	BEGIN
	IF type = LAST[StatType] THEN WriteChar[' ];
	THROUGH [0..format[type].columns) DO WriteChar['-]; ENDLOOP;
	WriteString["  "L];
	END;
      ENDLOOP;
    WriteChar[CR];
    WriteChar[CR];
    END;
    
  HelpMessage: PROCEDURE =
    BEGIN
    WriteLine["The following commands are available: "L];
    WriteLine["    b,B - bcd stats"L];
    WriteLine["    d,D - enter debugger"L];
    WriteLine["    h,H  - print column headings"L];
    WriteLine["    m,M  - source stats"L];
    WriteLine["    s,S - subtotal"L];
    WriteLine["    t,T - total"L];
    WriteLine[
      "    x,X - source characters, source lines, code bytes, and frame size only"L];
    WriteLine[
      "b, m, and x are also valid file switches which override the global settings for the file. c and C are switches which indicate a command, i.e. d/c is the command to call the debugger. To suppress the printing of a set of stats, either locally or globally, precede the switch or command by a minus ('-'). Hence -b/c suppresses the printing of all bcd stats, and foo/-b suppresses the printing of bcd stats for file foo. The default settings are b and m."L];
    END;
    
  ManagerSwitches: PROCEDURE [wanted: BOOLEAN, st: StatsDesired] =
    BEGIN
    st[char] ← wanted;
    st[line] ← wanted;
    st[codebytes] ← wanted;
    st[framesize] ← wanted;
    END;
    
  NextFile: PROCEDURE [name, switches: STRING] RETURNS [BOOLEAN] =
    BEGIN
    temp: STRING ← [80];
    i: CARDINAL ← 0;
    
    get1: PROCEDURE RETURNS [CHARACTER] =
      BEGIN
      RETURN[
	IF cmdstream.endof[cmdstream] THEN IODefs.NUL
	ELSE cmdstream.get[cmdstream]]
      END;
      
    get2: PROCEDURE RETURNS [c: CHARACTER] =
      BEGIN
      IF i < temp.length THEN BEGIN c ← temp[i]; i ← i + 1 END
      ELSE c ← IODefs.NUL;
      RETURN[c]
      END;
      
    IF cmdstream # NIL THEN GetToken[get1, name, switches]
    ELSE
      BEGIN
      WriteString["File: "L];
      ReadID[temp];
      WriteChar[CR];
      GetToken[get2, name, switches];
      END;
    RETURN[name.length # 0];
    END;
    
  OpenFile: PROCEDURE [file: STRING, ft: FastDirDefs.FileType]
    RETURNS [fh: FileHandle] =
    BEGIN
    ih: FastDirDefs.ItemHandle ← FastDirDefs.LookupItem[file, ft];
    IF ih = NIL THEN
      BEGIN SIGNAL SegmentDefs.FileNameError[file]; RETURN[NIL] END;
    fh ← SegmentDefs.InsertFile[@ih.fp, SegmentDefs.Read];
    RETURN;
    END;
    
  GetToken: PROCEDURE [
    get: PROCEDURE RETURNS [CHARACTER], token, switches: STRING] =
    BEGIN OPEN IODefs;
    s: STRING;
    c: CHARACTER;
    token.length ← switches.length ← 0;
    s ← token;
    WHILE (c ← get[]) # NUL DO
      SELECT c FROM
	SP, CR => IF token.length # 0 OR switches.length # 0 THEN RETURN;
	'/ => s ← switches;
	ENDCASE => String.AppendChar[s, c];
      ENDLOOP;
    RETURN
    END;
    
  PrintStats: PROCEDURE =
    BEGIN OPEN IODefs;
    type: StatType;
    i: CARDINAL;
    FOR type IN StatType DO
      IF localStatsWanted[type] THEN
	BEGIN
	WriteNumber[stats[type], format[type]];
	IF type = codepages THEN
	  BEGIN
	  bytes: CARDINAL ← stats[codebytes] + stats[nlinks]*2;
	  bytes ← (IF stats[nlinks] MOD 2 = 0 THEN 4 ELSE 2) + bytes;
	  IF Storage.PagesForWords[bytes/AltoDefs.BytesPerWord] # stats[codepages]
	    THEN WriteChar['*]
	  ELSE WriteChar[SP];
	  END;
	IF type # LAST[StatType] THEN WriteString["  "L];
	END
      ELSE
	BEGIN
	IF type = codepages THEN WriteChar[' ];
	FOR i IN [0..format[type].columns) DO WriteChar[' ] ENDLOOP;
	IF type # LAST[StatType] THEN WriteString["  "L];
	END
      ENDLOOP;
    END;
    
  RecordBcdStats: PROCEDURE [name: STRING] =
    BEGIN OPEN String;
    type: StatType;
    any: BOOLEAN ← FALSE;
    FOR type IN [codebytes..sympages] DO
      any ← any OR localStatsWanted[type] ENDLOOP;
    IF any THEN
      BEGIN
      file ← OpenFile[name, bcd];
      bcd ← GetModule[file];
      IF bcd # NIL THEN BEGIN GetBcdStats[bcd]; DeleteFileSegment[bcd]; END
      ELSE BcdSwitches[FALSE, @localStatsWanted];
      END;
    RETURN
    END;
    
  RecordSrcStats: PROCEDURE [name: STRING] =
    BEGIN OPEN String, SegmentDefs;
    stream: StreamHandle;
    IF localStatsWanted[char] OR localStatsWanted[line] THEN
      BEGIN
      file ← OpenFile[name, source];
      stream ← CreateByteStream[file, Read];
      GetSrcStats[stream];
      stream.destroy[stream];
      END;
    RETURN;
    END;
    
  SetCommandInput: PROCEDURE =
    BEGIN OPEN SegmentDefs;
    cfa: POINTER TO AltoFileDefs.CFA ← MiscDefs.CommandLineCFA[];
    IF cfa.fp = AltoFileDefs.NullFP THEN BEGIN cmdstream ← NIL; RETURN END;
    cmdstream ← StreamDefs.CreateByteStream[
      InsertFile[@cfa.fp, Read], Read ! InvalidFP => GOTO noCommandLine];
    StreamDefs.JumpToFA[cmdstream, @cfa.fa ! ANY => GOTO noCommandLine];
    IF cmdstream.endof[cmdstream] THEN
      BEGIN cmdstream.destroy[cmdstream]; cmdstream ← NIL; END;
    EXITS noCommandLine => cmdstream ← NIL;
    END;
    
  SourceSwitches: PROCEDURE [wanted: BOOLEAN, st: StatsDesired] =
    BEGIN st[char] ← wanted; st[line] ← wanted; END;
    
  TallyStats: PROCEDURE =
    BEGIN
    type: StatType;
    FOR type IN StatType DO
      IF localStatsWanted[type] THEN
	subtotal[type] ← subtotal[type] + stats[type];
      ENDLOOP;
    END;
    
  Total: PROCEDURE [name: STRING, subt: BOOLEAN] =
    BEGIN
    type: StatType;
    THROUGH [0..filenameField) DO WriteChar[' ] ENDLOOP;
    WriteDashes[];
    WriteString[name];
    THROUGH [name.length..filenameField) DO WriteChar[' ] ENDLOOP;
    FOR type IN StatType DO
      IF localStatsWanted[type] THEN
	BEGIN
	WriteDouble[
	  IF subt THEN subtotal[type] ELSE subtotal[type] + total[type], format[
	  type]];
	IF type = codepages THEN WriteChar[' ];
	IF type # LAST[StatType] THEN WriteString["  "L];
	END;
      ENDLOOP;
    WriteChar[CR];
    WriteChar[CR];
    IF subt THEN
      BEGIN
      FOR type IN StatType DO total[type] ← total[type] + subtotal[type]; ENDLOOP;
      END
    ELSE total ← [0, 0, 0, 0, 0, 0, 0, 0];
    subtotal ← [0, 0, 0, 0, 0, 0, 0, 0];
    END;
    
  WriteDouble: PROCEDURE [num: LONG INTEGER, format: IODefs.NumberFormat] =
    BEGIN
    i: CARDINAL;
    temp: STRING ← [20];
    String.AppendLongNumber[temp, num, 10];
    IF temp.length > format.columns THEN
      FOR i IN [0..format.columns) DO WriteChar['*] ENDLOOP
    ELSE
      BEGIN
      FOR i IN [temp.length..format.columns) DO WriteChar[' ]; ENDLOOP;
      WriteString[temp];
      END;
    END;
    
  WriteStatsHerald: PROCEDURE =
    BEGIN
    dh: StreamDefs.DiskHandle = DisplayDefs.GetTypeScript[];
    time: STRING ← [18];
    dh.reset[dh];
    TimeDefs.AppendDayTime[time, TimeDefs.UnpackDT[TimeDefs.CurrentDayTime[]]];
    IODefs.WriteString["

Alto/Mesa Statistics Package 6.0

Statistics as of "L];
    IODefs.WriteLine[time];
    END;
    
  -- main body
  
  WriteStatsHerald[];
  FastDirDefs.ScanDir[
    [TRUE, TRUE, FALSE, FALSE, FALSE, FALSE], FastDirDefs.LocalDir];
  GetStats[];
  ImageDefs.StopMesa[];
  
  END.