(* Визуальный эмулятор ГЭВМ - Гипотетической электронно-вычислительной машины.
ГЭВМ состоит из оперативной памяти на 256 байтов и регистра "программный счётчик".
Перемещайтесь по ячейкам памяти с помощью клавиш со стрелками, вводите значения
ячеек памяти при помощи цифр от 0 до 9 и букв от A до F.
Зелёным цветом изображаются шестнадцатеричные числа, красным - десятичные.
Клавиши:
Табуляция - переставить курсор на следующую ячейку памяти и обнулить её.
P - переключить курсор на программный счётчик (и обратно).
R - выполнить инструкцию (процессор прорабатывает один такт).
X - сменить основание системы счисления значения в данной ячейке с 10 на 16 (и обратно).
Y - сменить основание системы счисления адресов ячеек.
[ - привести основание системы счисления значений всех ячеек к основанию адресов.
] - перезагрузить ГЭВМ.
Escape - закрыть эмулятор.

Инструкции ГЭВМ:
E4 - сложить два числа, адреса которых даны в первых двух параметрах,
     и записать результат в ячейку по адресу, данному в третьем параметре.
A1 - безусловный переход на адрес, данный в [единственном] параметре.
B4 - уменьшить значение ячейки, адрес которой дан в первом параметре, на единицу.
A2 - если значение по адресу, данному в первом параметре, равно нулю, перейти на
     адрес, данный во втором параметре.
00 - останов.

6 апреля 2019 г. Рига. *)

MODULE GEVM;
IMPORT Files, G := Graph, Out;
CONST
  w = 8; (* Ячеек ОЗУ в строке *)
  h = 5; (* Ячеек ОЗУ в столбце *)
  cw = 60; (* Ширина ячейки в пикселях *)
  ch = 24; (* Высота ячейки в пикселях *)
  intX = 13; (* Интервал между ячейками по горизонтали в пикселях *)
  intY = 32; (* Интервал между ячейками по вертикали в пикселях *)

  (* Клавиши *)
  tab    = CHR(9);
  backsp = CHR(8);
  enter  = CHR(13);
  esc    = CHR(27);
  right  = CHR(83);
  left   = CHR(82);
  down   = CHR(85);
  up     = CHR(84);

VAR F: Files.File;
  r: Files.Rider;
  font: G.Font;
  charW, charH: INTEGER;

  mem: ARRAY 256 OF CHAR; (* ОЗУ *)
  base10: ARRAY 256 OF BOOLEAN; (* Основание системы счисления = 10 *)
  mark: ARRAY 256 OF BOOLEAN; (* Пометка изменений *)
  pc: CHAR; (* Программный счётчик *)
  off: BOOLEAN; (* Выключена ли ГЭВМ *)

  first: INTEGER; (* Номер первой видимой ячейки ОЗУ *)
  cur: INTEGER; (* Номер выделенной ячейки ОЗУ *)
  curPC: BOOLEAN; (* Находится ли курсор на программном счётчике *)
  addr10: BOOLEAN; (* Показываются ли адреса в десятичном виде *)

  sw, sh: INTEGER; (* Размеры экрана в пикселях *)
  cx: INTEGER; (* Отступ слева в пикселях *)
  cy: INTEGER; (* Отступ сверху в пикселях *)

  (* Цвета *)
  black, white, grey, green, dkgreen: G.Color;
  ltgreen, red, dkred, ltred, yellow: G.Color;

PROCEDURE ResetMem;
VAR i: INTEGER;
BEGIN
  FOR i := 0 TO LEN(mem) - 1 DO
    mem[i] := 0X;
    base10[i] := TRUE
  END;
  pc := 0X; first := 0; cur := 0;
  curPC := FALSE; addr10 := TRUE; off := TRUE
END ResetMem;

PROCEDURE ResetBase;
VAR i: INTEGER;
BEGIN
  FOR i := 0 TO LEN(mem) - 1 DO base10[i] := addr10 END
END ResetBase;

PROCEDURE Init(): BOOLEAN;
VAR i: INTEGER;
  ok: BOOLEAN;
BEGIN
  ok := TRUE;
  G.GetScreenSize(sw, sh);
  font := G.LoadFont("../Data/Fonts/Main");
  IF font = NIL THEN
    Out.String("Could not load font."); Out.Ln;
    ok := FALSE
  END;

  IF ok THEN
    G.GetMonoFontSize(font, charW, charH);
    cx := (sw - (cw + intX) * w - intX) DIV 2;
    cy := (sh - (ch + intY) * h) DIV 2;

    G.MakeCol(black,  0,   0,   0);
    G.MakeCol(white  , 255, 255, 255);
    G.MakeCol(grey   , 120, 120, 120);
    G.MakeCol(red    , 230,   0,   0);
    G.MakeCol(dkred  , 100,   0,   0);
    G.MakeCol(ltred  , 255,  90,  90);
    G.MakeCol(green  ,   0, 230,   0);
    G.MakeCol(dkgreen,   0, 100,   0);
    G.MakeCol(ltgreen,  90, 255,  90);
    G.MakeCol(yellow , 255, 255,  90);

    ResetMem;
    ResetBase
  END;
  RETURN ok
END Init;

PROCEDURE NumToStr(n: INTEGER; b10: BOOLEAN; VAR str: ARRAY OF CHAR);
VAR i, b, x: INTEGER;
  L, R: INTEGER;
  c: CHAR;
BEGIN
  IF b10 THEN b := 10 ELSE b := 16 END;
  i := 0;
  REPEAT
    x := n MOD b;
    IF x < 10 THEN str[i] := CHR(ORD('0') + x)
    ELSE str[i] := CHR(ORD('A') + x - 10)
    END;
    n := n DIV b;
    INC(i)
  UNTIL n = 0;
  (* Возможно дописать 0 *)
  IF i < 2 THEN str[i] := '0'; INC(i) END;
  str[i] := 0X;
  (* Перевенуть задом наперёд *)
  L := 0; R := i - 1;
  WHILE L < R DO c := str[L]; str[L] := str[R]; str[R] := c; INC(L); DEC(R) END
END NumToStr;

(* v - значение в ячейке;
   type = 0 или 1, это тип (цвет) кнопки *)
PROCEDURE DrawReg(v: INTEGER; b10: BOOLEAN; x0, y0, x1, y1, type: INTEGER;
    shine: BOOLEAN);
VAR str: ARRAY 16 OF CHAR;
  c1, c2, c3: G.Color;
BEGIN
  IF type = 0 THEN c1 := green; c2 := dkgreen; c3 := ltgreen
  ELSE c1 := red; c2 := dkred; c3 := ltred
  END;
  G.FillRect(x0, y0, x1, y1, c3);
  G.HLine(x0, y0, x1, c1);
  G.VLine(x0, y0, y1, c1);
  G.HLine(x0, y1, x1, c2);
  G.VLine(x1, y0, y1, c2);
  (* Внутри *)
  NumToStr(v, b10, str);
  G.DrawString(str, x0 + (cw - charW * 2) DIV 2,
    y0 + (ch - charH) DIV 2, font, black);
  (* Курсор *)
  IF shine THEN
    G.Rect(x0 - 1, y0 - 1, x1 + 1, y1 + 1, yellow);
    G.Rect(x0 - 3, y0 - 3, x1 + 3, y1 + 3, yellow)
  END
END DrawReg;

(* n - индекс ячейки;
   type = 0 или 1, это тип (цвет) кнопки *)
PROCEDURE DrawCell(n, x0, y0, x1, y1, type: INTEGER);
VAR str: ARRAY 16 OF CHAR;
  x, y: INTEGER;
  c: G.Color;
BEGIN
  (* Подсветка изменений *)
  IF n = ORD(pc) THEN
    G.FillRect(x0 - 3, y0 - 3, x1 + 3, y1 + charH + 3, grey);
    G.Rect(x0 - 3, y0 - 3, x1 + 3, y1 + charH + 3, white);
    G.Rect(x0 - 4, y0 - 4, x1 + 4, y1 + charH + 4, ltred);
    G.Rect(x0 - 5, y0 - 5, x1 + 5, y1 + charH + 5, red)
  END;

  DrawReg(ORD(mem[n]), base10[n], x0, y0, x1, y1, type, (cur = n) & ~curPC);
  (* Подпись внизу *)
  NumToStr(n, addr10, str);
  IF n = ORD(pc) THEN c := yellow
  ELSIF addr10 THEN c := ltred
  ELSE c := ltgreen
  END;
  x := x0 + (cw - charW * 2) DIV 2; y := y1 + 3;
  G.DrawString(str, x, y, font, c);
  IF mark[n] THEN
    G.HLine(x - 1, y + charH - 2, x + charW * 2 - 1, yellow)
  END
END DrawCell;

PROCEDURE DrawMem;
VAR i, type: INTEGER;
  last: INTEGER;
  x, y: INTEGER;
BEGIN
  last := first + w * h - 1;
  IF last >= LEN(mem) THEN last := LEN(mem) - 1 END;
  x := cx; y := cy;
  FOR i := first TO last DO
    IF base10[i] THEN type := 1 ELSE type := 0 END;
    DrawCell(i, x, y, x + cw - 1, y + ch - 1, type);
    IF (i + 1) MOD w # 0 THEN INC(x, cw + intX)
    ELSE INC(y, ch + intY); x := cx
    END
  END
END DrawMem;

(* Рисует регистр "программный счётчик" *)
PROCEDURE DrawPC;
VAR x, y, type: INTEGER;
  str: ARRAY 8 OF CHAR;
  c: G.Color;
BEGIN
  IF addr10 THEN type := 1; c := ltred ELSE type := 0; c := ltgreen END;
  x := cx + (w - 1) * (cw + intX);
  y := cy - ch - 20;
  str := "Пр. сч.";
  G.DrawString(str, x - 8 * charW, y + (ch - charH) DIV 2, font, c);
  DrawReg(ORD(pc), addr10, x, y, x + cw - 1, y + ch - 1, type, curPC)
END DrawPC;

PROCEDURE Move(c: CHAR);
BEGIN
  IF ~curPC THEN
    IF c = up THEN DEC(cur, w)
    ELSIF c = left THEN DEC(cur)
    ELSIF c = right THEN INC(cur)
    ELSE (* c = down *) INC(cur, w)
    END;
    IF cur < 0 THEN cur := 0
    ELSIF cur >= LEN(mem) THEN cur := LEN(mem) - 1
    END;
    IF cur < first THEN DEC(first, w)
    ELSIF cur >= first + w * h THEN INC(first, w)
    END
  END
END Move;

PROCEDURE Backspace;
VAR b: INTEGER;
BEGIN
  IF curPC THEN
    IF addr10 THEN b := 10 ELSE b := 16 END;
    pc := CHR(ORD(pc) DIV b)
  ELSE
    IF base10[cur] THEN b := 10 ELSE b := 16 END;
    mem[cur] := CHR(ORD(mem[cur]) DIV b)
  END
END Backspace;

PROCEDURE Input(x: INTEGER);
VAR v, v0, b: INTEGER;
  b10: BOOLEAN;
BEGIN
  IF curPC THEN
    b10 := addr10;
    v0 := ORD(pc)
  ELSE
    b10 := base10[cur];
    v0 := ORD(mem[cur])
  END;
  IF ~b10 OR (x < 10) THEN
    IF b10 THEN b := 10 ELSE b := 16 END;
    v := v0 * b + x;
    IF v > 255 THEN v := v0 MOD b * b + x END;
    IF curPC THEN pc := CHR(v) ELSE mem[cur] := CHR(v) END
  END
END Input;

PROCEDURE Mark(n: INTEGER);
BEGIN
  mark[n] := TRUE
END Mark;

PROCEDURE ClearMarks;
VAR i: INTEGER;
BEGIN
  FOR i := 0 TO LEN(mark) - 1 DO mark[i] := FALSE END
END ClearMarks;

PROCEDURE GoTick;
VAR instr, p0, p1, p2, p3, p4, v1, v2, v3: INTEGER;
BEGIN
  ClearMarks;
  off := FALSE;
  p0 := ORD(pc); instr := ORD(mem[p0]);
  p1 := (p0 + 1) MOD LEN(mem); v1 := ORD(mem[p1]);
  p2 := (p0 + 2) MOD LEN(mem); v2 := ORD(mem[p2]);
  p3 := (p0 + 3) MOD LEN(mem); v3 := ORD(mem[p3]);
  p4 := (p0 + 4) MOD LEN(mem);
  IF instr = 0E4H THEN
    mem[v3] := CHR((ORD(mem[v1]) + ORD(mem[v2])) MOD 256);
    Mark(v3);
    pc := CHR(p4)
  ELSIF instr = 0A1H THEN pc := mem[p1]
  ELSIF instr = 0B4H THEN
    mem[v1] := CHR((ORD(mem[v1]) - 1) MOD 256);
    Mark(v1);
    pc := CHR(p2)
  ELSIF instr = 0A2H THEN
    IF mem[v1] = 0X THEN pc := mem[p2]
    ELSE pc := CHR(p3)
    END
  ELSIF instr = 0 THEN off := TRUE; pc := 0X
  END
END GoTick;

PROCEDURE DrawMsg;
VAR z: ARRAY 5 OF CHAR;
BEGIN
  IF off THEN
    z := 'ГЭВМ';
    G.DrawString(z, cx, cy - charH - intY, font, yellow)
  END
END DrawMsg;

PROCEDURE Run;
VAR done: BOOLEAN;
  c: CHAR;
BEGIN
  done := FALSE;
  REPEAT
    G.ClearScreen;
    DrawMem;
    DrawPC;
    DrawMsg;
    G.Flip;
    c := G.ReadKey();
    IF (c = up) OR (c = down) OR (c = right) OR (c = left) THEN Move(c)
    ELSIF c = tab THEN Move(right); mem[cur] := 0X
    ELSIF c = backsp THEN Backspace
    ELSIF ("0" <= c) & (c <= "9") THEN Input(ORD(c) - ORD("0"))
    ELSIF ("a" <= c) & (c <= "f") THEN Input(ORD(c) - ORD("a") + 10)
    ELSIF c = "x" THEN base10[cur] := ~base10[cur]
    ELSIF c = "y" THEN addr10 := ~addr10
    ELSIF c = "p" THEN curPC := ~curPC
    ELSIF c = "r" THEN GoTick
    ELSIF c = "[" THEN ResetBase
    ELSIF c = "]" THEN ResetMem
    ELSIF c = esc THEN done := TRUE
    END
  UNTIL done
END Run;

BEGIN
  G.Settings(640, 400, {});
  G.Init;
  IF G.Done & Init() THEN Run
  ELSE Out.String("Init failed."); Out.Ln
  END;
  G.Close
END GEVM.
