-- File: PupTimeServerCold.mesa,  Last Edit: BLyon  January 16, 1981  5:08 PM
-- Please don't forget to update the herald...

DIRECTORY
  Ascii USING [TAB, SP, CR],
  InlineDefs USING [BcplToMesaLongNumber],
  Process USING [Detach, Yield],
  Runtime USING [IsBound],
  Storage USING [FreeString],
  String USING [
    AppendChar, AppendString, AppendDecimal, AppendLongDecimal, EquivalentString,
    StringToDecimal, InvalidNumber],
  System USING [GetGreenwichMeanTime],
  Time USING [AppendCurrent],

  CmFile USING [OpenSection, NextItem, Close],
  Event USING [Item, Reason, AddNotifier],
  FormSW USING [
    ClientItemsProcType, ProcType, AllocateItemDescriptor, newLine, Display,
    FindItem, CommandItem, BooleanItem, StringItem, NumberItem],
  Put USING [Line],
  Tool USING [Create, MakeSWsProc, MakeMsgSW, MakeFormSW],
  ToolWindow USING [TransitionProcType],
  Window USING [Handle],

  Clock USING [
    SetCorrection, TimeParameters, GetTimeParms, SetTimeParms, SetTime,
    SetTimeResetter],
  Indirect USING [GetParmFileName],
  MiscServerDefs USING [
    PupMiscServerOn, PupMiscServerOff, IgnoreThisPacket, SetTimeServer],
  TimeServerDefs USING [
    PupTimeServer, PupTimeFormat, parmsOk, resetAddress, correction],

  StatsDefs USING [
    StatCounterIndex, StatUpdate, StatsGetCounters, StatsStringToIndex],
  DriverDefs USING [Network],
  PupDefs USING [
    PupBuffer, PupSocketID, UniqueLocalPupSocketID, PupSocket, PupSocketMake,
    PupSocketDestroy, SecondsToTocks, GetFreePupBuffer, ReturnFreePupBuffer,
    AppendHostName, PupRouterBroadcastThis, SetPupContentsWords],
  PupTypes USING [PupAddress, fillInPupAddress, miscSrvSoc],
  BufferDefs;

PupTimeServerCold: PROGRAM
  IMPORTS
    InlineDefs, Process, Runtime, Storage, String, System, Time, CmFile, Event,
    FormSW, Put, Tool, Clock, Indirect, MiscServerDefs, TimeServerDefs, StatsDefs,
    PupDefs
  EXPORTS BufferDefs, TimeServerDefs
  SHARES BufferDefs =
  BEGIN OPEN StatsDefs, PupDefs, TimeServerDefs;

  -- EXPORTed TYPEs
  Network: PUBLIC TYPE = DriverDefs.Network;

  msg, form: PUBLIC Window.Handle ← NIL;
  eventItem: Event.Item ← [eventMask: 177777B, eventProc: Broom];

  useCount: CARDINAL ← 0;
  resetting, pleaseStop, running: BOOLEAN ← FALSE;

  resetText: STRING ← [30];

  stats: POINTER TO ARRAY StatCounterIndex OF LONG CARDINAL ← StatsGetCounters[];
  statText, statTenex, statAlto: PUBLIC StatCounterIndex;


  Init: PROCEDURE =
    BEGIN
    [] ← Tool.Create[
      name: "Time Server of November 16, 1980"L, makeSWsProc: MakeSWs,
      clientTransition: ClientTransition, initialState: inactive];
    Event.AddNotifier[@eventItem];
    END;

  PupTimeServerOn: PUBLIC PROCEDURE =
    BEGIN
    IF (useCount ← useCount + 1) = 1 THEN BEGIN running ← TRUE; Starter[]; END;
    UpdatePicture[];
    END;

  Starter: PROCEDURE =
    BEGIN
    MiscServerDefs.PupMiscServerOn[];
    parmsOk ← FindParameters[];
    MiscServerDefs.SetTimeServer[TimeServerDefs.PupTimeServer];
    pleaseStop ← FALSE;
    Process.Detach[FORK ResetFromAnywhere[]];
    Clock.SetTimeResetter[ResetFromAnywhere];
    END;

  PupTimeServerOff: PUBLIC PROCEDURE =
    BEGIN
    IF useCount # 0 AND (useCount ← useCount - 1) = 0 THEN
      BEGIN running ← FALSE; Stopper[]; END;
    UpdatePicture[];
    END;

  Stopper: PROCEDURE =
    BEGIN
    MiscServerDefs.PupMiscServerOff[];
    MiscServerDefs.SetTimeServer[MiscServerDefs.IgnoreThisPacket];
    pleaseStop ← TRUE;
    WHILE resetting DO Process.Yield[]; ENDLOOP;
    END;

  UpdatePicture: PROCEDURE =
    BEGIN
    IF form = NIL THEN RETURN;
    FormSW.FindItem[form, startIX].flags.invisible ← running;
    FormSW.FindItem[form, stopIX].flags.invisible ← ~running;
    FormSW.Display[form];
    END;

  ResetFromAnywhere: PROCEDURE = BEGIN ResetTime[PupTypes.fillInPupAddress]; END;

  ResetTime: PUBLIC PROCEDURE [where: PupTypes.PupAddress] =
    BEGIN
    broadcast: BOOLEAN ← where = PupTypes.fillInPupAddress;
    b: PupBuffer;
    me: PupSocketID ← UniqueLocalPupSocketID[];
    socket: PupSocket ← PupSocketMake[me, where, SecondsToTocks[2]];
    i: CARDINAL ← 0;
    delta: LONG INTEGER;
    time: LONG CARDINAL ← System.GetGreenwichMeanTime[] - 99;
    since: LONG CARDINAL;
    -- smash resetAddress even if we are already resetting to avoid getting stuck trying to reset from nowhere
    resetAddress ← where;
    IF broadcast THEN where.socket ← PupTypes.miscSrvSoc;
    resetText.length ← 0;
    IF broadcast THEN String.AppendString[resetText, "Anywhere"L]
    ELSE AppendHostName[resetText, resetAddress];
    UpdatePicture[];
    IF resetting THEN RETURN;
    resetting ← TRUE;
    UpdatePicture[];
    DO
      IF pleaseStop THEN BEGIN resetting ← FALSE; RETURN; END;
      since ← System.GetGreenwichMeanTime[] - time;
      IF since > 60*10 OR (i < 10 AND since > 5) THEN
	BEGIN
	-- Poke every few seconds for a while, then back way off.
	i ← i + 1;
	b ← GetFreePupBuffer[];
	b.pupID ← [0, i];
	b.source.socket ← me;
	b.dest ← where;
	b.pupType ← dateAltoRequest;
	SetPupContentsWords[b, 0];
	socket.setRemoteAddress[resetAddress];
	IF broadcast THEN PupRouterBroadcastThis[b] ELSE socket.put[b];
	time ← System.GetGreenwichMeanTime[];
	END;
      b ← socket.get[];
      IF b # NIL THEN
	BEGIN
	IF b.pupType = dateAltoIs THEN
	  BEGIN
	  network: Network = b.network;
	  info: LONG POINTER TO TimeServerDefs.PupTimeFormat ←
	    LOOPHOLE[@b.pupBody];
	  before, after: LONG CARDINAL;
	  oldSeconds, oldHours: LONG INTEGER;
	  IF broadcast AND network.netNumber.b = b.source.net AND
	    network.hostNumber = b.source.host THEN
	    BEGIN -- From ME
	    ReturnFreePupBuffer[b];
	    LOOP;
	    END;
	  before ← System.GetGreenwichMeanTime[];
	  StatUpdate[]; -- hackery to keep stats from getting confused
	  oldSeconds ← stats[statSeconds];
	  oldHours ← stats[statHours];
	  Clock.SetTime[LOOPHOLE[InlineDefs.BcplToMesaLongNumber[info.time]]];
	  StatUpdate[]; -- we could loose a tick or so, but that's tough
	  stats[statSeconds] ← oldSeconds;
	  stats[statHours] ← oldHours;
	  after ← System.GetGreenwichMeanTime[];
	  delta ← LOOPHOLE[after, LONG INTEGER] - LOOPHOLE[before, LONG INTEGER];
	  EXIT;
	  END;
	ReturnFreePupBuffer[b];
	END;
      ENDLOOP;
    resetAddress ← b.source;
    ReturnFreePupBuffer[b];
    IF TRUE THEN
      BEGIN
      text: STRING = [100];
      Time.AppendCurrent[text];
      String.AppendString[text, "  Time Reset from "L];
      AppendHostName[text, resetAddress];
      String.AppendString[text, ".  Correction was "L];
      String.AppendLongDecimal[text, delta];
      String.AppendChar[text, '.];
      LogString[text];
      END;
    PupSocketDestroy[socket];
    resetting ← FALSE;
    resetText.length ← 0;
    AppendHostName[resetText, resetAddress];
    UpdatePicture[];
    END;

  SetupTimeServerThings: PUBLIC PROCEDURE =
    BEGIN
    statText ← StatsStringToIndex["Text Date Requests"];
    statTenex ← StatsStringToIndex["Tenex Date Requests"];
    statAlto ← StatsStringToIndex["Alto Date Requests"];
    END;

  FindParameters: PROCEDURE RETURNS [ok: BOOLEAN] =
    BEGIN
    parmFileName: STRING ← NIL;
    dstSpecified, zoneSpecified, correctionSpecified: BOOLEAN ← FALSE;
    sectionName: STRING = "TimeServer"L;
    token, arg: STRING ← NIL;
    finger: CARDINAL;
    parms: Clock.TimeParameters ← Clock.GetTimeParms[];
    text: STRING = [100];
    GetNextToken: PROCEDURE =
      BEGIN
      c: CHARACTER;
      -- Borrow token
      token.length ← 0;
      FOR finger IN [finger..arg.length) DO
	c ← arg[finger];
	SELECT c FROM
	  Ascii.TAB, Ascii.SP => IF (token.length # 0) THEN RETURN;
	  -- flush leading blanks

	  Ascii.CR, ',, ': => BEGIN finger ← finger + 1; RETURN; END;
	  ENDCASE => String.AppendChar[token, c];
	ENDLOOP;
      END;
    GetDecimal: PROCEDURE RETURNS [n: INTEGER] =
      BEGIN
      GetNextToken[];
      -- StringToNumber dies on leading +, so flush it here
      IF token.length > 1 AND token[0] = '+ THEN
	BEGIN
	FOR i: CARDINAL IN [0..token.length - 1) DO
	  token[i] ← token[i + 1]; ENDLOOP;
	token.length ← token.length - 1;
	END;
      RETURN[
	String.StringToDecimal[
	  token !
	  String.InvalidNumber =>
	    BEGIN
	    Problem["Decimal number expected: "L, token];
	    ok ← FALSE;
	    n ← 0;
	    CONTINUE;
	    END]];
      END;
    IF Runtime.IsBound[Indirect.GetParmFileName] THEN
      parmFileName ← Indirect.GetParmFileName[];
    IF parmFileName = NIL THEN parmFileName ← "TimeServer.txt";
    ok ← TRUE;
    IF ~CmFile.OpenSection[parmFileName, sectionName] THEN
      BEGIN
      Problem["Can't find [TimeServer] section in "L, parmFileName];
      RETURN[FALSE];
      END;
    DO
      finger ← 0;
      text.length ← 0;
      [token, arg] ← CmFile.NextItem[];
      SELECT TRUE FROM
	token = NIL => EXIT;
	(token.length > 0 AND token[0] = ';) => NULL;
	String.EquivalentString[token, "CORRECTION"L] => -- <seconds per day>
	  BEGIN
	  correction ← GetDecimal[];
	  String.AppendDecimal[text, correction];
	  Problem["The clock correction is "L, text, " seconds per day"L];
	  Clock.SetCorrection[correction];
	  correctionSpecified ← TRUE;
	  END;
	String.EquivalentString[token, "DST"L] => -- <begin day> <end day>
	  BEGIN
	  first: CARDINAL ← GetDecimal[];
	  last: CARDINAL ← GetDecimal[];
	  IF first # parms.beginDst OR last # parms.endDst THEN
	    BEGIN
	    Problem["The DST info on this disk is wrong"L];
	    parms.beginDst ← first;
	    parms.endDst ← last;
	    END;
	  String.AppendString[text, "DST: begin="L];
	  String.AppendDecimal[text, first];
	  String.AppendString[text, ", last="L];
	  String.AppendDecimal[text, last];
	  Problem[text];
	  dstSpecified ← TRUE;
	  END;
	String.EquivalentString[token, "ZONE"L] => -- <sign> <hours>:<minutes>
	  BEGIN
	  hours: INTEGER ← GetDecimal[];
	  minutes: CARDINAL ← GetDecimal[];
	  IF hours # parms.zone OR minutes # parms.minutes THEN
	    BEGIN
	    Problem["The time zone info on this disk is wrong"L];
	    parms.zone ← hours;
	    parms.minutes ← minutes;
	    END;
	  String.AppendString[text, "ZONE: hours="L];
	  String.AppendDecimal[text, hours];
	  String.AppendString[text, ", minutes="L];
	  String.AppendDecimal[text, minutes];
	  Problem[text];
	  zoneSpecified ← TRUE;
	  END;
	ENDCASE => Problem["Unknown parameter: "L, token];
      Storage.FreeString[token];
      Storage.FreeString[arg];
      ENDLOOP;
    Clock.SetTimeParms[parms];
    CmFile.Close[parmFileName];
    RETURN[ok AND dstSpecified AND zoneSpecified AND correctionSpecified];
    END;

  Problem: PROCEDURE [one, two, three: STRING ← NIL] =
    BEGIN
    text: STRING = [100];
    Time.AppendCurrent[text];
    String.AppendString[text, "  TimeServer: "L];
    String.AppendString[text, one];
    IF two # NIL THEN String.AppendString[text, two];
    IF three # NIL THEN String.AppendString[text, three];
    String.AppendChar[text, '.];
    LogString[text];
    END;

  Start: FormSW.ProcType = BEGIN PupTimeServerOn[]; END;

  Stop: FormSW.ProcType = BEGIN PupTimeServerOff[]; END;

  ResetCommand: FormSW.ProcType =
    BEGIN Process.Detach[FORK ResetTime[PupTypes.fillInPupAddress]]; END;

  UseCurrentTime: FormSW.ProcType =
    BEGIN
    Clock.SetTime[System.GetGreenwichMeanTime[]];
    LogString["Using current time."L];
    END;

  LogString: PROCEDURE [text: STRING] =
    BEGIN IF msg # NIL THEN Put.Line[msg, text]; Put.Line[NIL, text]; END;

  MakeSWs: Tool.MakeSWsProc =
    BEGIN
    msg ← Tool.MakeMsgSW[window: window, lines: 5];
    form ← Tool.MakeFormSW[window: window, formProc: MakeForm];
    END;

  startIX: CARDINAL = 0;
  stopIX: CARDINAL = 1;
  MakeForm: FormSW.ClientItemsProcType =
    BEGIN
    nParams: CARDINAL = 7;
    items ← FormSW.AllocateItemDescriptor[nParams];
    items[0] ← FormSW.CommandItem[
      tag: "Start"L, proc: Start, place: FormSW.newLine];
    items[1] ← FormSW.CommandItem[
      tag: "Stop"L, proc: Stop, invisible: TRUE, place: FormSW.newLine];
    items[2] ← FormSW.CommandItem[
      tag: "UseCurrent"L, proc: UseCurrentTime, place: FormSW.newLine];
    items[3] ← FormSW.NumberItem[
      tag: "Correction"L, value: @correction, boxWidth: 50, readOnly: TRUE];
    items[4] ← FormSW.CommandItem[tag: "Reset"L, proc: ResetCommand];
    items[5] ← FormSW.BooleanItem[
      tag: "Resetting"L, switch: @resetting, readOnly: TRUE];
    items[6] ← FormSW.StringItem[
      tag: "ResetFrom"L, string: @resetText, readOnly: TRUE];
    RETURN[items, TRUE];
    END;

  ClientTransition: ToolWindow.TransitionProcType =
    BEGIN IF new = inactive THEN msg ← form ← NIL; END;


  Broom: PROCEDURE [why: Event.Reason] =
    BEGIN
    IF useCount = 0 THEN RETURN;
    SELECT why FROM
      makeImage, makeCheck => IF running THEN Stopper[];
      startImage, restartCheck, continueCheck => IF running THEN Starter[];
      ENDCASE => NULL;
    END;



  -- Initialization

  Init[];
  SetupTimeServerThings[];
  PupTimeServerOn[]; -- This may be undesirable

  END.