unit Myc.Data.Stream;

interface

uses
    System.SysUtils,
    System.Classes,
    System.Generics.Collections,
    System.SyncObjs,
    Myc.Data.Scalar,
    Myc.Data.Keyword,
    Myc.Core.Notifier;

type
    TSignalKind = (skData, skHeartbeat);

    TStreamSignal = record
    private
        FCycleID: Int64;
        FKind: TSignalKind;
    public
        constructor Create(AKind: TSignalKind; ACycleID: Int64);
        function ToString: string;
        property CycleID: Int64 read FCycleID;
        property Kind: TSignalKind read FKind;
    end;

    IStreamObserver = interface
        procedure OnSignal(const Signal: TStreamSignal);
    end;

    TSubscriptionTag = Pointer;

    IStream = interface
        {$region 'private'}
        function GetSeries: IScalarRecordSeries;
        {$endregion}

        function Subscribe(const Observer: IStreamObserver): TSubscriptionTag;
        procedure Unsubscribe(Tag: TSubscriptionTag);

        // Returns the Series of the records this stream produces. Streams are scalar record series. Always!
        property Series: IScalarRecordSeries read GetSeries;
    end;

    IInputChannel = interface
        {$region 'private'}
        function GetStream: IStream;
        {$endregion}
        procedure Push(const Value: IKeywordMapping<TScalar>);
        property Stream: IStream read GetStream;
    end;

    IGraphExecutor = interface
        {$region 'private'}
        function GetCycleID: Int64;
        {$endregion}

        // Now requires a Definition when creating a channel
        function GetInputChannel(const Name: string; const Def: IScalarRecordDefinition): IInputChannel;
        procedure Step;
        property CycleID: Int64 read GetCycleID;
    end;

    TGraphExecutor = class(TInterfacedObject, IGraphExecutor)
    private
        type
            TChannel = class(TInterfacedObject, IInputChannel, IStream)
            private
                type
                    TStreamNotifier = TMycNotifyList<IStreamObserver>;
            private
                FName: string;
                FSeries: IWriteableScalarRecordSeries;
                FQueue: TQueue<IKeywordMapping<TScalar>>;
                FQueueLock: TSpinLock;
                FObservers: TStreamNotifier;

                procedure Push(const Value: IKeywordMapping<TScalar>);
                function GetStream: IStream;
                function Subscribe(const Observer: IStreamObserver): TSubscriptionTag;
                procedure Unsubscribe(Tag: TSubscriptionTag);
                procedure Emit(ACycle: Int64);

                function GetSeries: IScalarRecordSeries;

            public
                constructor Create(const AName: string; const ASeries: IWriteableScalarRecordSeries);
                destructor Destroy; override;
            end;

    private
        FCycleID: Int64;
        FChannels: TDictionary<string, IInputChannel>;
        FLock: TSpinLock;
        function GetInputChannel(const Name: string; const Def: IScalarRecordDefinition): IInputChannel;
        function GetCycleID: Int64;
    public
        constructor Create;
        destructor Destroy; override;
        procedure Step;
    end;

implementation

uses
    Winapi.Windows;

constructor TStreamSignal.Create(AKind: TSignalKind; ACycleID: Int64);
begin
    FKind := AKind;
    FCycleID := ACycleID;
end;

function TStreamSignal.ToString: string;
begin
    if Kind = skData then
        Result := Format('Signal(Data, #%d)', [CycleID])
    else
        Result := Format('Signal(Heartbeat, #%d)', [CycleID]);
end;

{ TGraphExecutor.TChannel }

constructor TGraphExecutor.TChannel.Create(const AName: string; const ASeries: IWriteableScalarRecordSeries);
begin
    inherited Create;
    FName := AName;
    FSeries := ASeries;
    FQueue := TQueue<IKeywordMapping<TScalar>>.Create;
end;

destructor TGraphExecutor.TChannel.Destroy;
begin
    FObservers.Finalize;
    FQueue.Free;
    inherited;
end;

procedure TGraphExecutor.TChannel.Push(const Value: IKeywordMapping<TScalar>);
begin
    FQueueLock.Enter;
    try
        FQueue.Enqueue(Value);
    finally
        FQueueLock.Exit;
    end;
end;

function TGraphExecutor.TChannel.GetStream: IStream;
begin
    Result := Self;
end;

function TGraphExecutor.TChannel.Subscribe(const Observer: IStreamObserver): TSubscriptionTag;
begin
    FObservers.Lock;
    try
        Result := FObservers.Advise(Observer);
    finally
        FObservers.Release;
    end;
end;

procedure TGraphExecutor.TChannel.Unsubscribe(Tag: TSubscriptionTag);
begin
    FObservers.Lock;
    try
        FObservers.Unadvise(Tag);
    finally
        FObservers.Release;
    end;
end;

procedure TGraphExecutor.TChannel.Emit(ACycle: Int64);
var
    signal: TStreamSignal;
begin
    FQueueLock.Enter;
    try
        FSeries.Add(FQueue.Dequeue);
        signal :=
            TStreamSignal.Create(
                if FQueue.Count > 0 then skData
                else skHeartbeat,
                ACycle
            )
    finally
        FQueueLock.Exit;
    end;

    FObservers.Lock;
    try
        FObservers.Notify(
            function(const Obs: IStreamObserver): Boolean
            begin
                Obs.OnSignal(signal);
                Result := True;
            end
        );
    finally
        FObservers.Release;
    end;
end;

function TGraphExecutor.TChannel.GetSeries: IScalarRecordSeries;
begin
    Result := FSeries;
end;

{ TGraphExecutor }

constructor TGraphExecutor.Create;
begin
    inherited Create;
    FChannels := TDictionary<string, IInputChannel>.Create;
    FCycleID := 0;
end;

destructor TGraphExecutor.Destroy;
begin
    FChannels.Free;
    inherited;
end;

function TGraphExecutor.GetInputChannel(const Name: string; const Def: IScalarRecordDefinition): IInputChannel;
var
    impl: TChannel;
begin
    FLock.Enter;
    try
        if not FChannels.TryGetValue(Name, Result) then
        begin
            impl := TChannel.Create(Name, TScalarRecordSeries.Create(Def));
            Result := impl;
            FChannels.Add(Name, Result);
        end;
    finally
        FLock.Exit;
    end;
end;

function TGraphExecutor.GetCycleID: Int64;
begin
    Result := FCycleID;
end;

procedure TGraphExecutor.Step;
var
    channelsArr: TArray<IInputChannel>;
    channel: IInputChannel;
begin
    FLock.Enter;
    try
        Inc(FCycleID);
        channelsArr := FChannels.Values.ToArray;
    finally
        FLock.Exit;
    end;

    for channel in channelsArr do
        (channel as TChannel).Emit(FCycleID);
end;

initialization
    TMycNotifyList<IStreamObserver>.ReverseOnNotify := False;

end.
