; _______________________________________________________________________________________
;|                                                                                       |
;| ..::FreshLib::..  Free, open source. Licensed under "BSD 2-clause" license."          |
;|_______________________________________________________________________________________|
;
;  Description: Asynchronous timer library.
;
;  Target OS: Any
;
;  Dependencies: memory.asm
;
;  Notes: This is OS independent part. Includes OS dependent part files as well.
;_________________________________________________________________________________________
module "Timers library"

tmfDoNothing  = 0       ; when the timer expires no action should be performed. .Expired field of the timer will be incremented.
tmfCallProc   = 1       ; [.callback] contains pointer to procedure that to be executed once per timer expiration.

tmfSyncDestroy = $10    ; If this flag is set, the timer will be destroyed on the next timer expiration. In this case, the configured event is fired and then the timer is destroyed.
                        ; The flag is checked after the event returns, so the event handler can reset this flag and thus to prevent destruction.

tmfRunning     = $80    ; If this flag is set, the timer runs. If the event handler resets this flag, the timer will fire only once and will be suspended.



struct TTimer
; internal pointers of the linked list. The field .next MUST have offset = 0
  .next dd ?            ; don't touch this

; timers parameters.
  .interval dd ?        ; in ms
  .value    dd ?        ; current value in ms. When this value becomes higher than [.interval] an event is fired and the value becomes 0.
                        ; the value is incremented by the system dependent time - probably something like 1..100ms

  .flags    dd ?        ; set of tmfXXXX values.
  .Callback dd ?        ; callback - proc Timer, .ptrTimer; TWindow or 0
  .Expired  dd ?        ; count of the timer expirations.
  .tag      dd ?        ; user value associated with the timer.
ends


struct TDateTime
  .date    dd ?         ; [1..31] depending on month
  .month   dd ?         ; [1..12]
  .year    dd ?         ;
  .hour    dd ?         ; [0..23]
  .minute  dd ?         ; [0..59]
  .second  dd ?         ; [0..59]
  .day     dd ?         ; [0..6] == [monday..sanday]
ends



uglobal
  if used __ptrFirstTimer
    __ptrFirstTimer dd ?
  end if
endg











; Creates a timer and returns pointer to TTimer structure.
; the timer is created suspended. The user can set or reset tmfRunning in [.flags] in order to start or stop the timer.
; also, the user have to enter proper values in the remaining fields.
proc TimerCreate
begin
        push    ecx

        stdcall GetMem, sizeof.TTimer
        jc      .finish

        mov     ecx, [__ptrFirstTimer]
        mov     [eax+TTimer.next], ecx
        mov     [__ptrFirstTimer], eax
        clc

.finish:
        pop     ecx
        return
endp





proc TimerDestroy, .ptrTimer
begin
        push    eax ecx

        mov     eax, [__ptrFirstTimer]
        mov     ecx, __ptrFirstTimer

        test    eax, eax
        jz      .finish                 ; if the list is empty - returns no error.

.loop:
        cmp     eax, [.ptrTimer]
        je      .found

        mov     ecx, eax
        mov     eax, [eax+TTimer.next]
        test    eax, eax
        jnz     .loop

;not found
        pop     ecx eax
        stc
        return

.found:
        push    [eax+TTimer.next]
        pop     [ecx+TTimer.next]       ; if this is the first timer, ecx == __ptrFirstTimer

        stdcall FreeMem, eax

.finish:
        pop     ecx eax
        clc
        return
endp



SECONDS_PER_DAY = 24*60*60
YEAR_EPOCH_START     = 1970


proc TimeToDateTime, .pTime, .pDateTime
.days   dd ?
begin
        pushad

        mov     edi, [.pDateTime]
        mov     esi, [.pTime]

        mov     eax, [esi+4]
        cdq

        mov     ecx, SECONDS_PER_DAY

        idiv    ecx

        mov     eax, [esi]

        idiv    ecx

        test    edx, edx
        jns     @f
        add     edx, ecx        ;SECONDS_PER_DAY
        sub     eax, 1
@@:
        mov     [.days], eax  ; dword [esi] := time div SECONDS_PER_DAY

        mov     eax, edx
        mov     ecx, 60
        cdq                    ; the time should be positive here...

        idiv    ecx
        mov     [edi+TDateTime.second], edx

        cdq
        idiv    ecx
        mov     [edi+TDateTime.minute], edx
        mov     [edi+TDateTime.hour], eax

; date processing. I will assume it is 32bit
; some time in the future, it will overflow (around the 5000000 year), so it will needs a fix. ;)

        mov     edx, [.days]    ; days after YEAR_EPOCH_START
        xor     ebx, ebx        ; years after YEAR_EPOCH_START

.year_loop:
        test    edx, edx
        jns     .positive
; negative days

        dec     ebx
        lea     eax, [ebx+YEAR_EPOCH_START]
        stdcall DaysPerYear, eax
        add     edx, eax
        jmp     .year_loop

.positive:
        lea     eax, [ebx+YEAR_EPOCH_START]
        stdcall DaysPerYear, eax

        cmp     edx, eax
        jb      .finish_year

        sub     edx, eax
        inc     ebx
        jmp     .year_loop

.finish_year:
        add     ebx, YEAR_EPOCH_START
        mov     [edi+TDateTime.year], ebx

; edx contains the days since the current year. It is positive here.
        xor     ebx, ebx

.months_loop:
        stdcall DaysPerMonth, ebx, [edi+TDateTime.year]
        cmp     edx, eax
        jb      .month_ok

        sub     edx, eax
        inc     ebx
        jmp     .months_loop

.month_ok:
        inc     edx
        inc     ebx
        mov     [edi+TDateTime.date], edx
        mov     [edi+TDateTime.month], ebx

; day of the week.

        mov     eax, [.days]      ; days after YEAR_EPOCH_START
        add     eax, 3            ; it was Thursday on 1.1.1970
        cdq
        mov     ecx, 7
        idiv    ecx
        mov     [edi+TDateTime.day], edx

        popad
        return
endp


; returns the days in the given month or CF=1 on invalid month.
; month should be in [0..11] range.

proc DaysPerMonth, .month, .year
begin
        push    ecx

        mov     ecx, [.month]
        cmp     ecx, 11
        ja      .error

        movzx   ecx, byte [.month_list+ecx]
        cmp     ecx, 28
        jne     .finish

        stdcall DaysPerYear, [.year]
        cmp     eax, 365
        je      .finish

        inc     ecx

.finish:
        mov     eax, ecx
        clc
        pop     ecx
        return

.error:
        xor     eax, eax
        stc
        pop     ecx
        return

.month_list db 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
endp


proc DaysPerYear, .year
begin
        push    ebx ecx edx

        mov     eax, [.year]
        mov     ecx, 400
        cdq
        idiv    ecx

        test    edx, edx
        jz      .leap

        mov     eax, edx
        mov     ecx, 100
        cdq
        idiv    ecx
        test    edx, edx
        jz      .not_leap

        test    edx, $03
        jz      .leap

.not_leap:
        mov     eax, 365
        pop     edx ecx ebx
        return

.leap:
        mov     eax, 366
        pop     edx ecx ebx
        return
endp





proc DateTimeToTime, .pDateTime

.timeLo dd ?
.timeHi dd ?

begin
        push    ecx esi

        mov     esi, [.pDateTime]
        xor     eax, eax
        mov     [.timeLo], eax
        mov     [.timeHi], eax

; year loop:

        mov     ecx, 1970

        cmp     [esi+TDateTime.year], ecx
        jl      .year_loop_negative
        je      .process_months

; positive

.year_loop_plus:

        stdcall DaysPerYear, ecx

        add     [.timeLo], eax
        adc     [.timeHi], 0

        inc     ecx
        cmp     ecx, [esi+TDateTime.year]
        jne     .year_loop_plus
        jmp     .process_months


.year_loop_negative:
        dec     ecx

        stdcall DaysPerYear, ecx

        sub     [.timeLo], eax
        sbb     [.timeHi], 0

        cmp     ecx, [esi+TDateTime.year]
        jne     .year_loop_negative

.process_months:

        xor     ecx, ecx
        mov     edx, [esi+TDateTime.month]
        dec     edx

.month_loop:
        cmp     ecx, edx
        je      .process_date

        stdcall DaysPerMonth, ecx, [esi+TDateTime.year]

        add     [.timeLo], eax
        adc     [.timeHi], 0

        inc     ecx
        jmp     .month_loop

.process_date:

        mov     eax, [esi+TDateTime.date]
        dec     eax

        add     [.timeLo], eax
        adc     [.timeHi], 0

; process hour

        mov     ecx, SECONDS_PER_DAY
        mov     eax, [.timeLo]
        imul    ecx

        mov     [.timeLo], eax

        mov     eax, [.timeHi]
        imul    eax, ecx
        add     eax, edx

        mov     [.timeHi], eax          ; now the time is in seconds

        mov     ecx, 60
        mov     eax, [esi+TDateTime.hour]
        imul    eax, ecx
        add     eax, [esi+TDateTime.minute]
        imul    eax, ecx
        add     eax, [esi+TDateTime.second]

        add     [.timeLo], eax
        adc     [.timeHi], 0

        mov     eax, [.timeLo]
        mov     edx, [.timeHi]

        pop     esi ecx
        return
endp





include '%TargetOS%/timers.asm'


endmodule



