%% Copyright (c) 2026, Benoit Chesneau.
%% Licensed under the Apache License, Version 2.0.
%%
%% @doc WebTransport stream state management.
%%
%% This module provides functional state management for individual WebTransport
%% streams. Streams are managed by the session process and don't run as separate
%% processes.
%%
%% Stream IDs follow these conventions:
%% - Client-initiated bidirectional: 0, 4, 8, ... (4n)
%% - Server-initiated bidirectional: 1, 5, 9, ... (4n+1)
%% - Client-initiated unidirectional: 2, 6, 10, ... (4n+2)
%% - Server-initiated unidirectional: 3, 7, 11, ... (4n+3)
%%
-module(webtransport_stream).

-include("webtransport.hrl").

%% Stream creation
-export([new/3, new/4]).

%% State queries
-export([id/1, type/1, state/1]).
-export([is_open/1, is_writable/1, is_readable/1]).
-export([send_window/1, recv_window/1]).
-export([bytes_sent/1, bytes_received/1]).
-export([send_buffer/1, recv_buffer/1]).

%% State transitions
-export([send/2, receive_data/2, receive_fin/1]).
-export([close_local/1, close_remote/1, close/1]).
-export([reset/2, stop_sending/2, peer_stop_sending/2]).
-export([update_send_window/2, update_recv_window/2]).

%% Buffer management
-export([flush_send_buffer/1, flush_recv_buffer/1]).
-export([buffer_send/2]).

%% Stream ID helpers
-export([stream_type/1, initiator/1]).
-export([is_bidi/1, is_uni/1]).
-export([is_client_initiated/1, is_server_initiated/1]).

-record(stream, {
    id :: non_neg_integer(),
    type :: bidi | uni,
    state = open :: stream_state(),
    %% Flow control
    send_window :: non_neg_integer(),
    recv_window :: non_neg_integer(),
    bytes_sent = 0 :: non_neg_integer(),
    bytes_received = 0 :: non_neg_integer(),
    %% Buffers
    send_buffer = <<>> :: binary(),
    recv_buffer = <<>> :: binary(),
    %% Flags
    local_fin = false :: boolean(),
    remote_fin = false :: boolean(),
    reset_code :: undefined | non_neg_integer(),
    %% We sent STOP_SENDING to the peer: our read side is finished.
    local_stop_sending_code :: undefined | non_neg_integer(),
    %% Peer sent us STOP_SENDING: our write side must stop.
    peer_stop_sending_code :: undefined | non_neg_integer()
}).

-opaque stream() :: #stream{}.
-type stream_state() :: open | half_closed_local | half_closed_remote | closed.

-export_type([stream/0, stream_state/0]).

%% ============================================================================
%% Stream Creation
%% ============================================================================

%% @doc Create a new stream with symmetric send/recv windows.
-spec new(non_neg_integer(), bidi | uni, non_neg_integer()) -> stream().
new(Id, Type, Window) ->
    new(Id, Type, Window, Window).

%% @doc Create a new stream with separate send and recv window sizes.
-spec new(non_neg_integer(), bidi | uni, non_neg_integer(), non_neg_integer()) -> stream().
new(Id, Type, SendWindow, RecvWindow) ->
    #stream{
        id = Id,
        type = Type,
        state = open,
        send_window = SendWindow,
        recv_window = RecvWindow
    }.

%% ============================================================================
%% State Queries
%% ============================================================================

%% @doc Return the stream ID.
-spec id(stream()) -> non_neg_integer().
id(#stream{id = Id}) -> Id.

%% @doc Return the stream type: bidi or uni.
-spec type(stream()) -> bidi | uni.
type(#stream{type = Type}) -> Type.

%% @doc Return the current stream state (open, half_closed_local, half_closed_remote, closed).
-spec state(stream()) -> stream_state().
state(#stream{state = State}) -> State.

%% @doc Return true if the stream is not fully closed.
-spec is_open(stream()) -> boolean().
is_open(#stream{state = State}) ->
    State =:= open orelse State =:= half_closed_local orelse State =:= half_closed_remote.

%% @doc Return true if the local side can still write to this stream.
-spec is_writable(stream()) -> boolean().
is_writable(#stream{state = State, local_fin = Fin, reset_code = Reset,
                    peer_stop_sending_code = PeerStop}) ->
    Reset =:= undefined andalso PeerStop =:= undefined andalso not Fin andalso
    (State =:= open orelse State =:= half_closed_remote).

%% @doc Return true if the remote side can still send data on this stream.
-spec is_readable(stream()) -> boolean().
is_readable(#stream{state = State, remote_fin = Fin,
                    local_stop_sending_code = LocalStop}) ->
    LocalStop =:= undefined andalso not Fin andalso
    (State =:= open orelse State =:= half_closed_local).

%% @doc Return the send flow-control window size.
-spec send_window(stream()) -> non_neg_integer().
send_window(#stream{send_window = W}) -> W.

%% @doc Return the receive flow-control window size.
-spec recv_window(stream()) -> non_neg_integer().
recv_window(#stream{recv_window = W}) -> W.

%% @doc Return the total bytes sent on this stream so far.
-spec bytes_sent(stream()) -> non_neg_integer().
bytes_sent(#stream{bytes_sent = B}) -> B.

%% @doc Return the total bytes received on this stream so far.
-spec bytes_received(stream()) -> non_neg_integer().
bytes_received(#stream{bytes_received = B}) -> B.

%% @doc Return the pending send buffer contents.
-spec send_buffer(stream()) -> binary().
send_buffer(#stream{send_buffer = B}) -> B.

%% @doc Return the pending receive buffer contents.
-spec recv_buffer(stream()) -> binary().
recv_buffer(#stream{recv_buffer = B}) -> B.

%% ============================================================================
%% State Transitions
%% ============================================================================

%% @doc Send data on the stream, respecting flow control. Returns the bytes actually sent.
-spec send(stream(), binary()) -> {ok, binary(), stream()} | {error, term()}.
send(#stream{state = closed}, _Data) ->
    {error, stream_closed};
send(#stream{state = half_closed_local}, _Data) ->
    {error, stream_half_closed};
send(#stream{local_fin = true}, _Data) ->
    {error, stream_fin_sent};
send(#stream{send_window = Window, bytes_sent = Sent} = Stream, Data) ->
    Available = Window - Sent,
    DataSize = byte_size(Data),
    case DataSize =< Available of
        true ->
            {ok, Data, Stream#stream{bytes_sent = Sent + DataSize}};
        false when Available > 0 ->
            <<ToSend:Available/binary, Rest/binary>> = Data,
            NewStream = Stream#stream{
                bytes_sent = Sent + Available,
                send_buffer = <<(Stream#stream.send_buffer)/binary, Rest/binary>>
            },
            {ok, ToSend, NewStream};
        false ->
            NewStream = Stream#stream{
                send_buffer = <<(Stream#stream.send_buffer)/binary, Data/binary>>
            },
            {ok, <<>>, NewStream}
    end.

%% @doc Record received data, updating flow control counters and the recv buffer.
-spec receive_data(stream(), binary()) -> {ok, stream()} | {error, term()}.
receive_data(#stream{state = closed}, _Data) ->
    {error, stream_closed};
receive_data(#stream{state = half_closed_remote}, _Data) ->
    {error, stream_half_closed};
receive_data(#stream{remote_fin = true}, _Data) ->
    {error, stream_fin_received};
receive_data(#stream{recv_window = Window, bytes_received = Received} = Stream, Data) ->
    DataSize = byte_size(Data),
    NewReceived = Received + DataSize,
    case NewReceived > Window of
        true ->
            {error, flow_control_error};
        false ->
            NewStream = Stream#stream{
                bytes_received = NewReceived,
                recv_buffer = <<(Stream#stream.recv_buffer)/binary, Data/binary>>
            },
            {ok, NewStream}
    end.

%% @doc Record that the remote side sent FIN, transitioning the stream state.
-spec receive_fin(stream()) -> {ok, stream()} | {error, term()}.
receive_fin(#stream{state = closed}) ->
    {error, stream_closed};
receive_fin(#stream{remote_fin = true}) ->
    {error, duplicate_fin};
receive_fin(#stream{state = open} = Stream) ->
    {ok, Stream#stream{state = half_closed_remote, remote_fin = true}};
receive_fin(#stream{state = half_closed_local} = Stream) ->
    {ok, Stream#stream{state = closed, remote_fin = true}};
receive_fin(#stream{state = half_closed_remote}) ->
    {error, stream_already_half_closed}.

%% @doc Half-close the local (write) side of the stream.
-spec close_local(stream()) -> {ok, stream()} | {error, term()}.
close_local(#stream{state = closed}) ->
    {error, stream_closed};
close_local(#stream{local_fin = true}) ->
    {error, duplicate_fin};
close_local(#stream{state = open} = Stream) ->
    {ok, Stream#stream{state = half_closed_local, local_fin = true}};
close_local(#stream{state = half_closed_remote} = Stream) ->
    {ok, Stream#stream{state = closed, local_fin = true}};
close_local(#stream{state = half_closed_local}) ->
    {error, stream_already_half_closed}.

%% @doc Half-close the remote (read) side of the stream. Equivalent to receive_fin/1.
-spec close_remote(stream()) -> {ok, stream()} | {error, term()}.
close_remote(Stream) ->
    receive_fin(Stream).

%% @doc Immediately close both sides of the stream.
-spec close(stream()) -> stream().
close(Stream) ->
    Stream#stream{state = closed, local_fin = true, remote_fin = true}.

%% @doc Reset the stream with the given error code, discarding the send buffer.
-spec reset(stream(), non_neg_integer()) -> stream().
reset(Stream, ErrorCode) ->
    Stream#stream{
        state = closed,
        reset_code = ErrorCode,
        send_buffer = <<>>
    }.

%% @doc Record that we sent STOP_SENDING to the peer, shutting our read side.
-spec stop_sending(stream(), non_neg_integer()) -> stream().
stop_sending(Stream, ErrorCode) ->
    Stream#stream{local_stop_sending_code = ErrorCode}.

%% @doc Record a STOP_SENDING from the peer, blocking our write side.
%% Returns `{error, duplicate}' if already received (draft-14 s6.3).
-spec peer_stop_sending(stream(), non_neg_integer()) ->
    {ok, stream()} | {error, duplicate}.
peer_stop_sending(#stream{peer_stop_sending_code = undefined} = Stream, ErrorCode) ->
    {ok, Stream#stream{peer_stop_sending_code = ErrorCode}};
peer_stop_sending(#stream{peer_stop_sending_code = _}, _ErrorCode) ->
    {error, duplicate}.

%% @doc Set a new send flow-control window size.
-spec update_send_window(stream(), non_neg_integer()) -> stream().
update_send_window(Stream, NewWindow) ->
    Stream#stream{send_window = NewWindow}.

%% @doc Set a new receive flow-control window size.
-spec update_recv_window(stream(), non_neg_integer()) -> stream().
update_recv_window(Stream, NewWindow) ->
    Stream#stream{recv_window = NewWindow}.

%% ============================================================================
%% Buffer Management
%% ============================================================================

%% @doc Return and clear the send buffer.
-spec flush_send_buffer(stream()) -> {binary(), stream()}.
flush_send_buffer(#stream{send_buffer = Buffer} = Stream) ->
    {Buffer, Stream#stream{send_buffer = <<>>}}.

%% @doc Return and clear the receive buffer.
-spec flush_recv_buffer(stream()) -> {binary(), stream()}.
flush_recv_buffer(#stream{recv_buffer = Buffer} = Stream) ->
    {Buffer, Stream#stream{recv_buffer = <<>>}}.

%% @doc Append data to the send buffer without actually sending.
-spec buffer_send(stream(), binary()) -> stream().
buffer_send(#stream{send_buffer = Buffer} = Stream, Data) ->
    Stream#stream{send_buffer = <<Buffer/binary, Data/binary>>}.

%% ============================================================================
%% Stream ID Helpers
%% ============================================================================

%% @doc Derive the stream type (bidi or uni) from a stream ID.
-spec stream_type(non_neg_integer()) -> bidi | uni.
stream_type(StreamId) when StreamId band 2 =:= 0 -> bidi;
stream_type(_StreamId) -> uni.

%% @doc Derive the initiator (client or server) from a stream ID.
-spec initiator(non_neg_integer()) -> client | server.
initiator(StreamId) when StreamId band 1 =:= 0 -> client;
initiator(_StreamId) -> server.

%% @doc Return true if the stream or stream ID is bidirectional.
-spec is_bidi(non_neg_integer() | stream()) -> boolean().
is_bidi(#stream{type = Type}) -> Type =:= bidi;
is_bidi(StreamId) when is_integer(StreamId) -> stream_type(StreamId) =:= bidi.

%% @doc Return true if the stream or stream ID is unidirectional.
-spec is_uni(non_neg_integer() | stream()) -> boolean().
is_uni(#stream{type = Type}) -> Type =:= uni;
is_uni(StreamId) when is_integer(StreamId) -> stream_type(StreamId) =:= uni.

%% @doc Return true if the stream ID was initiated by the client.
-spec is_client_initiated(non_neg_integer()) -> boolean().
is_client_initiated(StreamId) -> initiator(StreamId) =:= client.

%% @doc Return true if the stream ID was initiated by the server.
-spec is_server_initiated(non_neg_integer()) -> boolean().
is_server_initiated(StreamId) -> initiator(StreamId) =:= server.

