; _______________________________________________________________________________________
;|                                                                                       |
;| ..::FreshLib::..  Free, open source. Licensed under "BSD 2-clause" license."          |
;|_______________________________________________________________________________________|
;
;  Description: TText structure and handling library.
;
;  Target OS: Any
;
;  Dependencies:
;
;  Notes: TText is so called "buffer gap" method of editing text information.
;         it allows fast insert and delete operations.
;_________________________________________________________________________________________
module "Buffer gap engine"


gapDefaultSize = 256

; There are 3 coordinate systems in the TText structure:
;
;   1. "Offset"   - this is simple offset inside the memory, relative to the begining of the structure.
;
;   2. "Position" - this is the coordinate of the text characters in the buffer, excluding the gap (which can be placed anywhere in the text).
;
;   3. "Index"    - character position - the number of the character in the text buffer. In the case the buffer contains only ANSI characters index=pos;
;                   In the case when the buffer contains UTF-8 characters, the relation is not so direct and has to be computed for every character.
;                   This coordinate is not easy to be calculated, but it is rarely needed.
;
; The structure TText is backward structure. All members has negative offsets and the
; pointer to the structure points to its end, not to the start.
;
; The structure has fixed header and variable, dynamically changed length.
; All the text in the structure has positive offsets and all the header fields, negative.

rstruct TText
  .TextLen    dd  ?       ; In bytes
  .GapStart   dd  ?       ; In offset units.
  .GapLen     dd  ?       ; In bytes
  .Caret      dd  ?       ; In position units.
  .struc_size dd  ?       ; the size of the structure (it can be extended up).
  label .     byte        ; the text in UTF-8 encoding.
ends



; Converts from text position (in bytes) to Buffer position (in bytes).
; returns CF=1 if the position is outside the text.
; the returned position is relative to TText.text
;
; esi == .buffer; eax = .pos
;
; If eax is above the size of the text, the position of the buffer end is returned.
; All positions are unsigned, so passing eax = $ffffffff will return the end of the
; data in the buffer.

proc TextPosToOffset
begin
        cmp     eax, [esi+TText.TextLen]
        cmova   eax, [esi+TText.TextLen]

        cmp     eax, [esi+TText.GapStart]
        jbe     @f
        add     eax, [esi+TText.GapLen]
@@:
        return
endp


macro pos2ofs  pos, text {
local ..skip

  if pos eqtype eax & text eqtype eax
    cmp    pos, [text+TText.TextLen]
    cmova  pos, [text+TText.TextLen]
    cmp    pos, [text+TText.GapStart]
    jb     ..skip
    add    pos, [text+TText.GapLen]
..skip:
  else
    error "Error! Arguments must be registers!"
    err
  end if
}


; Converts from buffer offset to the text position.
; esi points to the TText strucure, eax is the offset
; if the buffer offset is placed inside the buffer, the start of the buffer is returned.

proc TextOffsetToPos
begin
        cmp     eax, [esi+TText.GapStart]
        jb      .finish

        sub     eax, [esi+TText.GapLen]
        cmp     eax, [esi+TText.GapStart]
        cmovb   eax, [esi+TText.GapStart]

        cmp     eax, [esi+TText.TextLen]
        cmova   eax, [esi+TText.TextLen]

.finish:
        return
endp


macro ofs2pos ofs, text {
local ..skip

  if ofs eqtype eax & text eqtype eax
        cmp     ofs, [text+TText.GapStart]
        jb      .skip

        sub     ofs, [text+TText.GapLen]
        cmp     ofs, [text+TText.GapStart]
        cmovb   ofs, [text+TText.GapStart]

        cmp     ofs, [text+TText.TextLen]
        cmova   ofs, [text+TText.TextLen]
..skip:
  else
    error "Error! Arguments must be registers!"
    err
  end if
}


; .pText        - pointer to the TText
; .position     - position of the gap in position units.

proc TextMoveGap, .pText, .position
begin
        pushad

        mov     edx, [.pText]
        mov     eax, [.position]

        cmp     [edx+TText.GapLen], 0
        je      .end_move

        pos2ofs eax, edx

        mov     esi, [edx+TText.GapStart]
        mov     edi, [edx+TText.GapStart]

        mov     ecx, eax
        sub     ecx, [edx+TText.GapStart]
        je      .finish                                 ; source and destination are the same - no move.
        jb      .move_back

        add     esi, [edx+TText.GapLen]
        sub     ecx, [edx+TText.GapLen]

        add     edi, edx
        add     esi, edx

        push    ecx
        shr     ecx, 2
        rep movsd
        pop     ecx
        and     ecx, 3
        rep movsb

        sub     edi, edx
        mov     [edx+TText.GapStart], edi

.finish:
        popad
        return


.move_back:
        neg     ecx

        std

        add     edi, [edx+TText.GapLen]

        add     edi, edx
        add     esi, edx

        push    ecx
        shr     ecx, 2
        jz      .dword_ok

        sub     esi, 4
        sub     edi, 4
        rep movsd
        add     esi, 4
        add     edi, 4

.dword_ok:
        pop     ecx
        and     ecx, 3
        dec     esi
        dec     edi
        rep movsb

        cld

.end_move:
        mov     [edx+TText.GapStart], eax
        popad
        return

endp




proc TextCreate, .struc_size
begin
        push    ecx
        mov     ecx, [.struc_size]
        mov     eax, ecx
        add     eax, gapDefaultSize

        stdcall GetMem, eax
        jc      .finish

        lea     eax, [eax+ecx]
        mov     [eax+TText.struc_size], ecx
        and     [eax+TText.TextLen], 0
        and     [eax+TText.GapStart], 0
        and     [eax+TText.Caret], 0
        mov     [eax+TText.GapLen], gapDefaultSize

.finish:
        pop     ecx
        return
endp


;---------------------------------------------------------------------------------
; Duplicates TText structure of any size. Returns pointer to the structure in EAX
; The returned pointer have to be freed with TextFree when not needed anymore.
;
; Arguments:
;   .pText - pointer to the source structure
;
; Returns:
;   EAX - pointer to the new TText structure.
;---------------------------------------------------------------------------------

proc TextDup, .pText
begin
        pushad

        mov     esi, [.pText]
        test    esi, esi
        jz      .finish

        mov     ecx, [esi+TText.TextLen]
        add     ecx, [esi+TText.GapLen]
        add     ecx, [esi+TText.struc_size]

        stdcall GetMem, ecx

        mov     edi, eax

        mov     edx, [esi+TText.struc_size]
        add     eax, edx

        mov     [esp+4*regEAX], eax
        sub     esi, edx

        mov     ebx, ecx
        shr     ecx, 2
        and     ebx, 3

        rep movsd

        mov     ecx, ebx
        rep movsb

.finish:
        popad
        return
endp



proc TextFree, .pBuffer
begin
        push    eax
        mov     eax, [.pBuffer]
        test    eax, eax
        jz      .exit

        sub     eax, [eax+TText.struc_size]
        stdcall FreeMem, eax

.exit:
        pop     eax
        return
endp



; Ensures that the gap size is at least [.desired_size] bytes long.
; If needed, reallocates the TText
;
; In order to provide less frequent reallocations, the procedure
; always allocates bigger gap than requested.
; max([.desired_size]*2, gapDefaultSize) memory is allocated,
; if the buffer is to be reallocated.
;
; Arguments:
;   .pSize        - pointer to TText
;   .desired_size - desired size of the gap.
;
; Returns
;   edx - pointer to the same TText. Can be changed because of memory reallocation.
;   CF = 0 if OK
;   CF = 1 if the memory reallocation failed.
;

proc TextSetGapSize, .pText, .desired_size
begin
        pushad

        mov     edx, [.pText]
        mov     ecx, [.desired_size]

        cmp     [edx+TText.GapLen], ecx
        jae     .gap_ok

        lea     ecx, [2*ecx]
        mov     eax, gapDefaultSize
        cmp     ecx, eax
        cmovl   ecx, eax

        mov     eax, [edx+TText.TextLen]
        lea     ebx, [ecx+eax]

        add     eax, [edx+TText.GapLen]           ; current data size.
        add     ebx, [edx+TText.struc_size]       ; desired size. it will always be higher than the current.

        push    [edx+TText.struc_size]
        sub     edx, [edx+TText.struc_size]
        stdcall ResizeMem, edx, ebx
        pop     edx
        lea     edx, [edx+eax]
        jc      .gap_ok                           ; well, it is just an error, but how to proceed?

        sub     ebx, [edx+TText.struc_size]       ; total size of the buffer

; the space is allocated and there are 2 gaps. So, merge them now. The new gap is always at the end.
; the size of the new gap is

        mov     eax, [edx+TText.TextLen]
        mov     ecx, [edx+TText.TextLen]

        add     eax, [edx+TText.GapLen]
        sub     ecx, [edx+TText.GapStart]         ; the length of the text after the gap.
        jz      .gap_moved                              ; gap does not need to be moved

        std

        lea     edi, [edx + ebx]
        lea     esi, [edx + eax]

        push    ecx

        shr     ecx, 2
        jz      .dword_ok

        sub     esi, 4
        sub     edi, 4

        rep movsd

        add     esi, 4
        add     edi, 4

.dword_ok:
        pop     ecx
        and     ecx, 3
        jz      .byte_ok

        dec     esi
        dec     edi
        rep movsb

.byte_ok:
        cld


.gap_moved:
        sub     ebx, [edx+TText.TextLen]
        mov     [edx+TText.GapLen], ebx
        clc

.gap_ok:
        mov     [esp+4*regEDX], edx
        popad
        return
endp





; Inserts on the caret position up to 4 bytes of character data.
; They can be 4 ANSI characters, or one 4 byte encoded UTF-8 or
; two 2-byte encoded UTF-8, one ASCII and one 2 byte UTF-8 and so on.
;
; .pText - pointer to the TText,
; .char -  from 0 to 4 bytes character data.

proc TextAddChar, .pText, .char
begin
        push    eax ecx

        mov     edx, [.pText]

        stdcall TextSetGapSize, edx, 4
        jc      .finish

        stdcall TextMoveGap, edx, [edx+TText.Caret]

        mov     eax, [edx+TText.GapStart]

        pushd   [.char]
        popd    [edx+eax]

        mov     ecx, 4

.len_loop:
        cmp     byte [edx+eax], 0
        je      .len_ok
        inc     eax
        loop    .len_loop

.len_ok:
        sub     eax, [edx+TText.GapStart]

        add     [edx+TText.GapStart], eax
        add     [edx+TText.TextLen], eax
        add     [edx+TText.Caret], eax
                sub     [edx+TText.GapLen], eax
        clc

.finish:
        pop     ecx eax
        return
endp




; Deletes one UTF-8 character at the caret.
; esi - pointer to TText

proc TextDelChar
begin
        push    eax

        mov     eax, [esi+TText.Caret]
        stdcall TextMoveGap, esi, [esi+TText.Caret]

        mov     eax, [esi+TText.GapStart]
        add     eax, [esi+TText.GapLen]

        stdcall DecodeUtf8, [esi+eax]
        add     [esi+TText.GapLen], edx

.len_ok:
        pop     eax
        return
endp




; Inserts on the caret position the string .hString
; which can be pointer to NULL terminated string or
; string handle.
;
; esi - .buffer, edx - .hString

proc TextAddString
begin
        push    eax ecx edx edi

        stdcall StrLen, edx
        mov     ecx, eax
        mov     ebx, eax
        stdcall StrPtr, edx
        mov     edx, eax

        call    TextSetGapSize
        jc      .finish

        mov     eax, [esi+TText.Caret]
        call    TextMoveGap

        mov     eax, [esi+TText.GapStart]
        lea     edi, [esi+eax]

        xchg    esi, edx        ; the pointer of the source

        shr     ecx, 2
        rep movsd
        mov     ecx, ebx
        and     ecx, 3
        rep movsb

        mov     esi, edx

        add     [esi+TText.GapStart], ebx
        add     [esi+TText.TextLen], ebx
        add     [esi+TText.Caret], ebx
        sub     [esi+TText.GapLen], ebx   ; here the CF should be always 0

.finish:
        pop     edi edx ecx eax
        return
endp






; Moves the gap to the end of the text and inserts dword 0 after it, so
; the buffer can be used as an ASCIIZ string.

proc TextCompact
begin
        push    eax
        mov     eax, [esi+TText.TextLen]

        call    TextMoveGap

        cmp     [esi+TText.GapLen], 4
        jae     @f

        push    ecx
        mov     ecx, gapDefaultSize
        call    TextSetGapSize
        pop     ecx

@@:
        mov     eax, [esi+TText.GapStart]
        and     dword [esi+eax], 0
        pop     eax
        return
endp



endmodule