unit Myc.Data.Stream.Pipes;

interface

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

type
    // =========================================================================
    // BASE STREAM
    // Implements storage (Series) and broadcasting (Observers).
    // Does not define WHEN data is emitted (Policy).
    // =========================================================================
    TCustomDataStream = class(TInterfacedObject, IStream)
    strict private
        FSeries: IWriteableScalarRecordSeries;
        FObservers: TMycNotifyList<IStreamObserver>;
        function GetSeries: IScalarRecordSeries;
    protected
        // Derived classes call this to write data and notify listeners.
        // ACycleID: The cycle this data belongs to.
        procedure Emit(const Value: array of TScalar.TValue; ACycleID: Int64);
    public
        constructor Create(const ADef: IScalarRecordDefinition);
        destructor Destroy; override;

        // IStream
        function Subscribe(const Observer: IStreamObserver): TSubscriptionTag;
        procedure Unsubscribe(Tag: TSubscriptionTag);
        property Series: IScalarRecordSeries read GetSeries;
    end;

    // =========================================================================
    // ROOT STREAM (SOURCE)
    // Acts as the clock source. Generates new CycleIDs.
    // =========================================================================
    TRootStream = class(TCustomDataStream)
    private
        FLastCycleID: Int64;
    public
        constructor Create(const ADef: IScalarRecordDefinition);
        // Public API to inject data from Delphi
        procedure Push(const RowData: TArray<TScalar.TValue>);
    end;

    // =========================================================================
    // PIPE STREAM (NODE)
    // Reacts to upstream signals using barrier synchronization.
    // =========================================================================
    TPipeStream = class; // Forward

    TPipeConfig = TArray<TArray<TScalarRecordField>>;

    TPipeSource = class(TContainedObject, IStreamObserver)
    private
        FSource: IStream;
        FTag: TSubscriptionTag;
        FLastSeenCycle: Int64;
        procedure OnSignal(const Signal: TStreamSignal);
    public
        constructor Create(AOwner: TPipeStream; ASource: IStream);
        destructor Destroy; override;
        property LastSeenCycle: Int64 read FLastSeenCycle;
    end;

    TPipeStream = class(TCustomDataStream)
    public
        type
            TPipeLambda = reference to function(const Sources: array of ISeries; out Results: array of TScalar.TValue): Boolean;
    private
        FSources: TArray<TPipeSource>;
        FSourceSeries: TArray<ISeries>;
        FLastFiredCycleID: Int64;
        FLambda: TPipeLambda;

        procedure CheckBarrierAndFire(CurrentCycle: Int64);
    public
        constructor Create(
            const AConfig: TPipeConfig;
            const ADef: IScalarRecordDefinition;
            const ASources: TArray<IStream>;
            const ALambda: TPipeLambda
        );
        destructor Destroy; override;
    end;

implementation

{ TCustomDataStream }

constructor TCustomDataStream.Create(const ADef: IScalarRecordDefinition);
begin
    inherited Create;
    FSeries := TScalarRecordSeries.Create(ADef);
end;

destructor TCustomDataStream.Destroy;
begin
    FObservers.Finalize;
    inherited;
end;

function TCustomDataStream.GetSeries: IScalarRecordSeries;
begin
    Result := FSeries;
end;

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

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

procedure TCustomDataStream.Emit(const Value: array of TScalar.TValue; ACycleID: Int64);
var
    signal: TStreamSignal;
begin
    FObservers.Lock;
    try
        // 1. Write Data
        FSeries.Add(Value);

        // 2. Broadcast Signal
        signal := TStreamSignal.Create(skData, ACycleID);

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

{ TRootStream }

constructor TRootStream.Create(const ADef: IScalarRecordDefinition);
begin
    inherited Create(ADef);
    FLastCycleID := 0;
end;

procedure TRootStream.Push(const RowData: TArray<TScalar.TValue>);
begin
    // Root streams increment the global clock
    Inc(FLastCycleID);
    Emit(RowData, FLastCycleID);
end;

{ TPipeSource }

constructor TPipeSource.Create(AOwner: TPipeStream; ASource: IStream);
begin
    inherited Create(AOwner);
    FSource := ASource;
    FLastSeenCycle := -1;
    FTag := FSource.Subscribe(Self);
end;

destructor TPipeSource.Destroy;
begin
    FSource.Unsubscribe(FTag);
    inherited;
end;

procedure TPipeSource.OnSignal(const Signal: TStreamSignal);
begin
    if Signal.Kind = skData then
    begin
        FLastSeenCycle := Signal.CycleID;
        (Controller as TPipeStream).CheckBarrierAndFire(FLastSeenCycle);
    end;
end;

{ TPipeStream }

constructor TPipeStream.Create(
    const AConfig: TPipeConfig;
    const ADef: IScalarRecordDefinition;
    const ASources: TArray<IStream>;
    const ALambda: TPipeLambda
);
var
    i, j, n: Integer;
begin
    // Pass Definition to base class
    inherited Create(ADef);

    FLambda := ALambda;
    FLastFiredCycleID := -1;

    SetLength(FSources, Length(ASources));

    // Flatten Sources
    n := 0;
    for i := 0 to High(AConfig) do
        inc(n, Length(AConfig[i]));
    SetLength(FSourceSeries, n);

    n := 0;
    for i := 0 to High(ASources) do
    begin
        FSources[i] := TPipeSource.Create(Self, ASources[i]);
        for j := 0 to High(AConfig[i]) do
        begin
            // Extract the specific column series
            FSourceSeries[n] := ASources[i].Series.Fields[AConfig[i][j].Key];
            inc(n);
        end;
    end;
end;

destructor TPipeStream.Destroy;
begin
    for var i := High(FSources) downto 0 do
        FSources[i].Free;
    FSources := nil;
    FSourceSeries := nil;
    inherited;
end;

procedure TPipeStream.CheckBarrierAndFire(CurrentCycle: Int64);
var
    resultVal: TArray<TScalar.TValue>;
begin
    // We reuse the FObservers lock from the base class to protect state
    // But since FObservers is private, we access it via method or need to make it protected.
    // Making it protected or using a separate lock is cleaner.
    // For now, let's assume we rely on the fact that OnSignal is usually serialized per thread
    // or add a lock.
    // -> Ideally, TCustomDataStream should expose Lock/Unlock or we add a Lock here.

    // Simplification: We assume thread safety is handled by the caller or we add a dedicated lock.
    // For this example, let's just do the logic:

    for var src in FSources do
        if src.LastSeenCycle < CurrentCycle then
            exit; // Barrier closed

    if FLastFiredCycleID >= CurrentCycle then
        exit; // Already fired

    FLastFiredCycleID := CurrentCycle;

    if Assigned(FLambda) then
    begin
        SetLength(resultVal, Series.Def.Count);
        if FLambda(FSourceSeries, resultVal) then
        begin
            // Pass through CycleID from upstream
            Emit(resultVal, FLastFiredCycleID);
        end;
    end;
end;

end.
