MODULE editor1;

(* based on the kilo editor building course*)

(* Step 49 *)

(* step already implemented earlier ... *)
(******************** INCLUDES ******************)

IMPORT IO, Strings, ASCII, NumberIO, termios, FIO, Delay, SplitV1, STextIO;
IMPORT libc, SYSTEM, Storage, MemUtils;

(******************** DATA ******************)


CONST 
    KILO_VERSION =  "0.0.1";
    ARROW_LEFT   = "s";
    ARROW_RIGHT  = "d";
    ARROW_UP     = "e";
    ARROW_DOWN   = "x";

TYPE
    abufType            = RECORD
                            b   : SYSTEM.ADDRESS;
                            len : CARDINAL;
                          END;
    abufTypePtr         = POINTER TO abufType;
    Mode                = (edit,command);

VAR
    editorConfig        : RECORD
                            screenrows : CARDINAL;
                            screencols : CARDINAL;
                            theTermios : termios.TERMIOS;
                            posX, posY : CARDINAL;
                         END;

    TCSAFLUSH           : INTEGER;

    theStructure        : SplitV1.Structure;

    cursorposStr        : ARRAY [0..15] OF CHAR;
    clearScreenStr      : ARRAY[0..6] OF CHAR;
    cursorHomeStr       : ARRAY[0..2] OF CHAR;
    requestCursorPosStr : ARRAY[0..15] OF CHAR;
    cursorOnStr         : ARRAY[0..6] OF CHAR;
    cursorOffStr        : ARRAY[0..6] OF CHAR;
    tildeStr            : ARRAY[0..3] OF CHAR;
    eraseLineStr        : ARRAY[0..2] OF CHAR;

    resultStr           : ARRAY [0..15] OF CHAR;
    tmpString           : ARRAY[0..15] OF CHAR;
    tmpString1          : ARRAY[0..15] OF CHAR;
    tmpString2          : Strings.String1;
    cc                  : Strings.String1;
    i                   : CARDINAL;
    str                 : ARRAY [0..15] OF CHAR;

    abuf                : abufType;
    mode                : Mode;
 
(******************** Utilities *****************)

    PROCEDURE Card2Str ( x : CARDINAL; VAR str : ARRAY OF CHAR);

    BEGIN
    IF x < 10 THEN
            NumberIO.CardToStr(x,1,str);
        ELSIF x < 100 THEN
            NumberIO.CardToStr(x,2,str);
        ELSE
            NumberIO.CardToStr(x,3,str);
        END;
    END Card2Str;

    (******************** TERMINAL ******************)

    PROCEDURE CtrlKey(c: CHAR) : CARDINAL;
        (* return a value < ORD(" ") for the control keys *)
        VAR
            n : BITSET;

    BEGIN
        n := BITSET(ORD(c));
        n := n * BITSET(1FH);
        RETURN CARDINAL(n)
    END CtrlKey;

    PROCEDURE die (s : ARRAY OF CHAR);
    BEGIN
        libc.perror(s);
        FOR i := 0 TO Strings.Length(s) DO
            IO.Write(s[i])
        END;
        HALT
    END die;

    PROCEDURE disablerawMode() : INTEGER;

        VAR
            result : INTEGER;
    BEGIN
        IO.BufferedMode(0,TRUE);
        IO.BufferedMode(1,TRUE);
        result := termios.tcsetattr(FIO.StdIn, TCSAFLUSH, editorConfig.theTermios);
        IF result = -1 THEN
            die("tcsetattr")
        ELSE 
            RETURN result
        END;
    END disablerawMode;

    PROCEDURE enablerawMode;
    BEGIN
        libc.atexit(disablerawMode);

        editorConfig.theTermios := termios.InitTermios();
        IF termios.tcgetattr(FIO.StdIn, editorConfig.theTermios) = -1 THEN
            die("tcgetattr")
        END;

        IO.UnBufferedMode(0,TRUE);
        IO.UnBufferedMode(1,TRUE);
    END enablerawMode;

    PROCEDURE  editorReadKey() : CHAR;
        VAR
            c   : CHAR;
            str : ARRAY [0..2] OF CHAR;
    BEGIN
        IO.Read(c);
        IF c = ASCII.esc THEN
            IO.Read(c);
            mode := command;
            IF c  = "[" THEN
                IO.Read(c);
                 (* 
                Arrow Keys:
                Reset    Set
                -----    ---
                up          ^[A ^[OA
                down        ^[B ^[OB
                right       ^[C ^[OC
                left        ^[D ^[OD
                 *)
                CASE c OF
                    "A" : c := ARROW_UP;    |
                    "B" : c := ARROW_DOWN ; |
                    "C" : c := ARROW_RIGHT; |
                    "D" : c := ARROW_LEFT; 
                END;
            END;
        ELSE
            mode := edit;
        END;
        RETURN c
    END editorReadKey; 

    (******************** Buffer ******************)

    PROCEDURE abufInit( VAR ptr : abufTypePtr);
    BEGIN
        Storage.ALLOCATE(ptr,SYSTEM.TSIZE(abufType) );
        ptr^.b := NIL;
        ptr^.len := 0;
    END abufInit;

    PROCEDURE abAppend (VAR ptr : abufTypePtr; str: ARRAY OF CHAR );

        VAR
            llen : CARDINAL;
    BEGIN
        llen := Strings.Length(str);
        IF ptr <> NIL THEN
            IF ptr^.b = NIL THEN
                Storage.ALLOCATE(ptr^.b,llen);
            ELSE
                Storage.REALLOCATE( ptr^.b,ptr^.len + llen);
            END;
            MemUtils.MemCopy(SYSTEM.ADR(str),llen, SYSTEM.ADDADR(ptr^.b,ptr^.len));
            ptr^.len := ptr^.len + llen;
        END;
    END abAppend;

    PROCEDURE abFree (VAR ptr : abufTypePtr);

    BEGIN
        Storage.DEALLOCATE(ptr^.b, ptr^.len);
        Storage.DEALLOCATE(ptr,SYSTEM.TSIZE(abufType));
    END abFree;

    (******************** output ******************)

    PROCEDURE editorDrawRows(VAR ptr : abufTypePtr);

        VAR
            y : CARDINAL;
            str : ARRAY [0..3] OF CHAR;
            welcome : ARRAY [0..79] OF CHAR;
            welcomeLen : CARDINAL;
            blanks      : CARDINAL;
            blanksStr   : ARRAY[0..79] OF CHAR;
            template    : ARRAY[0..79] OF CHAR;
            i           : CARDINAL;
            string1     : Strings.String1;

    BEGIN
        (* template := "                                                                                               "; *)
        welcome := "Kilo editor -- version ";
        Strings.Concat(welcome,KILO_VERSION,welcome);
        welcomeLen := Strings.Length(welcome);
        cc[0] := CHR(13);
        Strings.Concat(welcome,cc,welcome);
        cc[0] := CHR(10);
        Strings.Concat(welcome,cc,welcome);
        blanks := (editorConfig.screencols - welcomeLen) /2;
        blanksStr := "";
        (*
        Copy - copy at most, length, characters in, substr, to, str,
        starting at position, index.
        *)
        (* PROCEDURE Copy (str: ARRAY OF CHAR; index, length: CARDINAL; VAR result: ARRAY OF CHAR) ; *)
        (* Strings.Copy(template,0,blanks,blanksStr); doesn't work !*)
        (* a workaround *)
        string1[0] := " ";
        FOR i := 0 TO blanks DO
            Strings.Concat(blanksStr,string1,blanksStr);
        END;

        FOR y := 1 TO editorConfig.screenrows -1 DO
            IF ((y = (editorConfig.screenrows / 3)) AND (editorConfig.screencols > welcomeLen)) THEN
                abAppend(ptr,blanksStr);
                abAppend(ptr,welcome);
            ELSE
                abAppend(ptr,tildeStr);
            END;
            abAppend(ptr,eraseLineStr)
        END;
        abAppend(ptr,"~");
    END editorDrawRows;

   PROCEDURE editorRefreshScreen;

        VAR
            ptr : abufTypePtr;
    BEGIN
        abufInit(ptr);
        abAppend(ptr,cursorOffStr);
        abAppend(ptr,cursorHomeStr);
        editorDrawRows(ptr);
        cursorpos(editorConfig.posX,editorConfig.posY,FALSE);
        abAppend(ptr,cursorposStr);
        abAppend(ptr,cursorOnStr);
        libc.write(FIO.StdOut,ptr^.b,ptr^.len);
        abFree(ptr);
    END editorRefreshScreen;

    PROCEDURE requestCursorPosition(VAR x,y : CARDINAL);

        VAR
            c   : CHAR;

    BEGIN
        str := "";
        SplitV1.InitStructure(theStructure);
        Strings.Concat(str,ASCII.esc,str);
        Strings.Concat(str,"[6n", str);
        libc.write(FIO.StdOut,SYSTEM.ADR(str),4);
        (* getting answer *)
        str := "";
        REPEAT
            IO.Read(c);
            cc[0] := c;
            Strings.Concat(str,cc,str);
        UNTIL c = "R";
        (* extracting the coordinates *)
        Strings.Delete(str,0,2);
        Strings.Delete(str,Strings.Length(str) -1 ,1);
        SplitV1.SplitStr(str, ";", theStructure);
        NumberIO.StrToCard(theStructure[0].element,y);
        NumberIO.StrToCard(theStructure[1].element,x);
    END requestCursorPosition;

    PROCEDURE cursorpos(x,y : CARDINAL; emit : BOOLEAN);
    BEGIN
        cursorposStr := "";
        Strings.Concat(cursorposStr, ASCII.esc,cursorposStr);
        Strings.Concat(cursorposStr, "[",cursorposStr);
        tmpString := "";
        Strings.Concat(tmpString,cursorposStr, tmpString);
        tmpString1 := "";
        Card2Str(y,tmpString1);
        Strings.Concat(tmpString,tmpString1, tmpString);
        tmpString2[0] := ";";
        Strings.Concat(tmpString, tmpString2,tmpString);
        tmpString1 := "";
        Card2Str(x,tmpString1);
        Strings.Concat(tmpString,tmpString1, tmpString);
        tmpString2[0] := "H";
        Strings.Concat(tmpString, tmpString2,tmpString);
        cursorposStr := tmpString;
        editorConfig.posX := x;
        editorConfig.posY := y;
        libc.write(FIO.StdOut,SYSTEM.ADR(tmpString),Strings.Length(tmpString));
    END cursorpos;

    PROCEDURE getWindowSize(VAR y : CARDINAL; VAR x: CARDINAL);
    
    BEGIN
        cursorpos(999,999,TRUE);
        requestCursorPosition(x,y);
    END getWindowSize;

    (******************** input  ******************)

    PROCEDURE editorMoveCursor(c : CHAR);

    BEGIN
        WITH editorConfig DO
            CASE c OF
                    ARROW_UP    : IF posY > 1 THEN DEC(posY) END; |
                    ARROW_DOWN  : IF posY < (screenrows - 1) THEN INC(posY) END; |
                    ARROW_LEFT  : IF posX > 1 THEN DEC(posX) END; |
                    ARROW_RIGHT : IF posX < (screencols - 1) THEN INC(posX) END; 
            END;
        END
    END editorMoveCursor;

    PROCEDURE editorProcessKeypress;
        VAR
            c       : CHAR;
            b       : BOOLEAN;
    BEGIN
        c := editorReadKey();
        CASE ORD(c) OF
            17        : libc.write(FIO.StdOut,SYSTEM.ADR(clearScreenStr),4); HALT;                                     |
            ORD("e"),
            ORD("x"),
            ORD("s"),
            ORD("d")  : editorMoveCursor(c);
        END
    END editorProcessKeypress;

    (********************  INIT  ******************)

    PROCEDURE InitScreenEscapes;

    VAR temp : ARRAY[0..10] OF CHAR;
    BEGIN
        cursorHomeStr := "";
        Strings.Concat(cursorHomeStr,ASCII.esc,cursorHomeStr);
        Strings.Concat(cursorHomeStr,"[H",cursorHomeStr);

        clearScreenStr := "";
        Strings.Concat(clearScreenStr,ASCII.esc,clearScreenStr);
        Strings.Concat(clearScreenStr,"[2J",clearScreenStr);

        cursorposStr := "";
        Strings.Concat(cursorposStr,ASCII.esc,cursorposStr);
        Strings.Concat(cursorposStr,"[",cursorposStr);
    
        requestCursorPosStr := "";
        Strings.Concat(requestCursorPosStr,ASCII.esc,requestCursorPosStr);
        Strings.Concat(requestCursorPosStr,"[6n",requestCursorPosStr);
    
        cursorOnStr := "";
        Strings.Concat(cursorOnStr,ASCII.esc,cursorOnStr);
        Strings.Concat(cursorOnStr,"[?25h",cursorOnStr);
        
        cursorOffStr := "";
        Strings.Concat(cursorOffStr,ASCII.esc,cursorOffStr);
        Strings.Concat(cursorOffStr,"[?25l",cursorOffStr);
        
        tildeStr := "~";
        cc[0] := CHR(13);
        Strings.Concat(tildeStr,cc,tildeStr);
        cc[0] := CHR(10);
        Strings.Concat(tildeStr,cc,tildeStr);

        eraseLineStr := "";
        Strings.Concat(eraseLineStr,ASCII.esc,eraseLineStr);
        Strings.Concat(eraseLineStr,"[K",eraseLineStr);

    END InitScreenEscapes;

    PROCEDURE InitEditor;

    BEGIN
        TCSAFLUSH := termios.tcsflush (); 
        InitScreenEscapes;
        editorConfig.posX := 1;
        editorConfig.posY := 1;
        getWindowSize(editorConfig.screenrows, editorConfig.screencols);
        editorConfig.posX := 1;
        editorConfig.posY := 1;
        libc.write(FIO.StdOut,SYSTEM.ADR(clearScreenStr),4);
        mode := command;
        (* NumberIO.WriteCard(editorConfig.screencols,5);
        NumberIO.WriteCard(editorConfig.screenrows,5);
        Delay.Delay(2000) *)
    END InitEditor;


BEGIN
    enablerawMode;
    InitEditor;

    LOOP
        editorRefreshScreen;
        editorProcessKeypress;
        (* editorRefreshScreen; *)
    END;
    
    IF disablerawMode() = -1 THEN
        HALT
    END;    
    (* NumberIO.WriteCard(editorConfig.screencols, 5);
    NumberIO.WriteCard(editorConfig.screenrows, 5); *)
END editor1.