{
    Timers – запуск событий по истечении заданного времени.

    Copyright © 2016, 2019, 2022 Малик Разработчик

    Это свободная программа: вы можете перераспространять её и/или
    изменять её на условиях Меньшей Стандартной общественной лицензии GNU в том виде,
    в каком она была опубликована Фондом свободного программного обеспечения;
    либо версии 3 лицензии, либо (по вашему выбору) любой более поздней версии.

    Эта программа распространяется в надежде, что она может быть полезна,
    но БЕЗО ВСЯКИХ ГАРАНТИЙ; даже без неявной гарантии ТОВАРНОГО ВИДА
    или ПРИГОДНОСТИ ДЛЯ ОПРЕДЕЛЁННЫХ ЦЕЛЕЙ. Подробнее см. в Меньшей Стандартной
    общественной лицензии GNU.

    Вы должны были получить копию Меньшей Стандартной общественной лицензии GNU
    вместе с этой программой. Если это не так, см.
    <http://www.gnu.org/licenses/>.
}

unit Timers;

{$MODE DELPHI}
{$ASMMODE INTEL}

interface

uses
    Windows,
    Lang;

{%region public }
const
    TIMER_HANDLER_GUID = '{74C86B60-AC5B-4E8D-8ACD-29A50B5C1511}';

type
    TimerHandler = interface;
    Timer = class;

    TimerHandler = interface(_Interface) [TIMER_HANDLER_GUID]
        procedure timerHandle(timer: Timer; millisecondsElapsed: long);
    end;

    Timer = class(_Object, Runnable)
    private
        handler: TimerHandler;
        handlerLock: ThreadLock;
        nextHandleLock: ThreadLock;
        nextHandle: long;
        prevHandle: long;
        terminated: boolean;
        procedure setNextHandle(nextHandle: long);
        function getNextHandle(): long;
    public
        constructor create(handler: TimerHandler);
        destructor destroy; override;
        procedure run();
        procedure terminate();
        procedure setInterval(milliseconds: long);
        function getRemainingMilliseconds(): long;
    end;
{%endregion}

{%region routine }
    function getMilliseconds(): long;
{%endregion}

implementation

{%region routine }
    function getMilliseconds(): long; assembler; nostackframe;
    asm
                push    ebx
                push    esi
                push    edi
                xor     eax, eax
                xor     edx, edx
                call    Windows.getTickCount64
                pop     edi
                pop     esi
                pop     ebx
    end;
{%endregion}

{%region Timer }
    constructor Timer.create(handler: TimerHandler);
    begin
        inherited create();
        self.handler := handler;
        self.handlerLock := ThreadLock.create();
        self.nextHandleLock := ThreadLock.create();
        self.prevHandle := -1;
        self.nextHandle := -1;
        self.terminated := false;
        startThread(self);
    end;

    destructor Timer.destroy;
    begin
        terminate();
        while not terminated do begin
            sleep(2);
        end;
        nextHandleLock.free();
        handlerLock.free();
        inherited destroy;
    end;

    procedure Timer.setNextHandle(nextHandle: long);
    begin
        nextHandleLock.enter();
        try
            self.nextHandle := nextHandle;
        finally
            nextHandleLock.leave();
        end;
    end;

    function Timer.getNextHandle(): long;
    begin
        nextHandleLock.enter();
        try
            result := nextHandle;
        finally
            nextHandleLock.leave();
        end;
    end;

    procedure Timer.run();
    var
        m: long;
        p: long;
        n: long;
        h: TimerHandler;
    begin
        try
            repeat
                repeat
                    n := getNextHandle();
                    if n >= 0 then begin
                        break;
                    end;
                    if handler = nil then begin
                        exit;
                    end;
                    sleep(2);
                until false;
                m := getMilliseconds();
                if m >= n then begin
                    handlerLock.enter();
                    try
                        h := handler;
                        if h = nil then begin
                            exit;
                        end;
                        p := prevHandle;
                        prevHandle := m;
                        setNextHandle(-1);
                        try
                            if p < 0 then begin
                                h.timerHandle(self, 0);
                            end else begin
                                h.timerHandle(self, m - p);
                            end;
                        except
                        end;
                        h := nil;
                    finally
                        handlerLock.leave();
                    end;
                end else begin
                    sleep(2);
                end;
            until false;
        finally
            terminated := true;
        end;
    end;

    procedure Timer.terminate();
    begin
        handlerLock.enter();
        try
            handler := nil;
        finally
            handlerLock.leave();
        end;
    end;

    procedure Timer.setInterval(milliseconds: long);
    var
        next: long;
    begin
        handlerLock.enter();
        try
            next := getMilliseconds() + milliseconds;
            if (milliseconds > 0) and (next >= 0) then begin
                setNextHandle(next);
            end else begin
                setNextHandle(-1);
            end;
        finally
            handlerLock.leave();
        end;
    end;

    function Timer.getRemainingMilliseconds(): long;
    var
        milliseconds: long;
    begin
        milliseconds := getNextHandle();
        if milliseconds < 0 then begin
            result := -1;
            exit;
        end;
        result := milliseconds - getMilliseconds();
    end;
{%endregion}

end.

