/******************************************************************************/
/*                                                                            */
/* HTTP.OutputStream.cls - A buffered output stream for HTTP responses        */
/* ===================================================================        */
/*                                                                            */
/* This file is part of the RexxHTTP package                                  */
/* [See https://rexx.epbcn.com/rexxhttp/]                                     */
/*                                                                            */
/* Copyright (c) 2006-2026 Josep Maria Blasco <josep.maria.blasco@epbcn.com>  */
/*                                                                            */
/* License: Apache License 2.0 (https://www.apache.org/licenses/LICENSE-2.0)  */
/*                                                                            */
/* Version history:                                                           */
/*                                                                            */
/* Date     Version Details                                                   */
/* -------- ------- --------------------------------------------------------- */
/* 20061102    0.1  First version                                             */
/* 20170301    0.2  Drop support for mod_rexx, OS/2, IIS, rsp compilers       */
/*                  Change the way request~message works -- see 'unknown'.    */
/*                  By default, it looks in the environment variable pool.    */
/* 20260611    1.0  Global refactor, switch to Apache license                 */
/*                                                                            */
/******************************************************************************/

.environment~HTTP.OutputStream = .HTTP.OutputStream

::Class "HTTP.OutputStream" SubClass Stream Public

--------------------------------------------------------------------------------
-- INIT                                                                       --
--------------------------------------------------------------------------------

::Method init

  Expose underlyingstream stream_name buffer state description opened datetime timestamp eol response

  Parse Value Date("S") Time() With y +4 m +2 d time
  -- QUERY DATETIME answers in US format, mm-dd-yy, like a real Stream does;
  -- QUERY TIMESTAMP in yyyy-mm-dd. (The day and the two-digit year were
  -- transposed here until v123, so DATETIME read mm-yy-dd.)
  datetime  = m"-"d"-"Right(y,2) time
  timestamp = y"-"m"-"d time

  Use Strict Arg underlyingstream, response
  If Arg(1)~class \= .Stream Then
    Raise Syntax 93.948 array (1,"Stream")

  stream_name = self~class~id

  -- Initialize the buffer and other variables
  buffer = .MutableBuffer~new(,32768)
  state  =  underlyingstream~state
  opened =  state == "READY"
  If opened Then description = ""
  Else description = "Underlying stream not ready:" underlyingstream~description

  -- End-of-line for the HTTP body: LF, the web convention for text bodies.
  eol = '0a'x

  Return

/*******************************************************************************
* The following are new methods                                                *
*******************************************************************************/
--------------------------------------------------------------------------------
-- UNDERLYINGSTREAM                                                           --
--------------------------------------------------------------------------------

::Attribute underlyingStream Get

/*******************************************************************************
* The following are STREAM input methods                                       *
*                                                                              *
* INVARIANT: on the input side we answer exactly what .stdout answers. It is   *
* write-only for the same reason we are, so it has already settled every one   *
* of these questions, and copying it means a rexxlet meets no surprise our     *
* own taste invented. Measured against ooRexx 5.3.0, that is three behaviours, *
* not one:                                                                     *
*                                                                              *
*   chars, lines          -> 0. A count is a question with a true answer;      *
*                            raising would break Do While stream~lines > 0.    *
*   charin, linein        -> Raise notready. An actual read attempt fails.     *
*   arrayin, makearray,   -> an empty collection. Nothing to read is not an    *
*   supplier                 error, it is emptiness. (.stdout~supplier hands   *
*                            back a StreamSupplier; we hand back a plain       *
*                            Supplier, equally empty -- a StreamSupplier can   *
*                            only be built over a genuinely readable stream.)  *
*                                                                              *
* Before v123 arrayin/makearray/supplier raised notready here. See             *
* ref-decisiones.md.                                                           *
*******************************************************************************/

::Method arrayin
  Return .array~new

::Method charin
  Expose stream_name
  Raise notready Additional (self) Description (stream_name) Return ""

::Method chars
  Return 0

::Method linein
  Expose stream_name
  Raise notready Additional (self) Description (stream_name) Return ""

::Method lines
  Return 0

::Method makearray
  Return .array~new

::Method supplier
  Return .array~new~supplier

/*******************************************************************************
* The following are STREAM methods                                             *
*******************************************************************************/

--------------------------------------------------------------------------------
-- ARRAYOUT                                                                   --
--------------------------------------------------------------------------------

::Method arrayout
  Expose buffer eol state

  -- Max two args
  If Arg() > 2 Then Raise syntax 93.902 array (2)

  -- First argument must not be omitted...
  If Arg(1,'O') Then Raise Syntax 93.903 array (1)

  -- ... and must be an array...
  array = Arg(1)~request("ARRAY")
  If array == .nil Then Raise syntax 93.939 array (1)
  -- ... of dimension 1.
  If array~dimension \== 1 Then Raise syntax 93.939 array (1)

  -- Second argument must be one of "LINES", "CHARS". If omitted, assume "LINES"
  If Arg(2,'O') Then option = "LINES"
  Else Do
    option = .Validate~requestClassType(2, Arg(2), .String)
    option = option~strip~translate~left(1)
    If option = "" Then option = "L"
    If "LC"~pos(option) == 0 Then
      Raise syntax 93.914 array (2,"LC",option)
    If option == "L" Then option = "LINES"
    If option == "C" Then option = "CHARS"
  End

  -- Guard on the underlying stream's state, exactly as charout and lineout do.
  -- Before this, arrayout was the only output method that would append to the
  -- buffer even when the underlying stream was not READY. The class convention
  -- is 0 on success, 1 on a state error (a uniform flag, deliberately simpler
  -- than .Stream's residual-line count). See audit ยง3.8.
  If state \== "READY" Then Return 1

  -- Line separator is this class's own eol (LF, the HTTP body convention set
  -- in init), NOT makeString's default .endOfLine, which is platform-dependent
  -- (CRLF on Windows). A protocol stream must be deterministic across platforms,
  -- so we supply the separator by hand and terminate the last line too, matching
  -- lineout. This deliberately drops byte-parity with .Stream~arrayout. In CHARS
  -- mode makeString adds no terminators and already agrees with .Stream.
  If option == "LINES" Then Do
    buffer~append(array~makestring("Line", eol))
    buffer~append(eol)
  End
  Else
    buffer~append(array~makestring("Char"))

  Return 0

--------------------------------------------------------------------------------
-- CHAROUT                                                                    --
--------------------------------------------------------------------------------

::Method charout
  Expose buffer state

  -- Max two args
  If Arg() > 2 Then Raise syntax 93.902 array (2)

  -- First argument must be a string. If not specified, close the stream
  If Arg(1,'O') Then Do
    self~close
    Return 0
    End
  Else Do
    string = Arg(1)
    string = .Validate~requestClassType(1, string, .String)
  End

  -- Cannot position transient streams
  If Arg(2,'E') Then Raise syntax 93.958

  If state \== "READY" Then Return 1  -- There was an error

  buffer~append(string)

  Return 0

--------------------------------------------------------------------------------
-- CLOSE                                                                      --
--------------------------------------------------------------------------------

::Method close
  Expose opened
  Use Strict Arg

  If \opened Then Return ""

  Return self~flush

--------------------------------------------------------------------------------
-- COMMAND                                                                    --
--------------------------------------------------------------------------------

::Method command
  Use Strict Arg one

  command = .Validate~requestClassType(1, one, .String)

  command = command~space~translate

  Parse var command verb options

  Select
    When verb == "OPEN"     Then Return self~open(options)
    When verb == "CLOSE"    Then Do
      If options \== "" Then Raise Syntax 93
      Return self~close
      End
    When verb == "FLUSH"    Then Do
      If options \== "" Then Raise Syntax 93
      Return self~flush()
      End
    When verb == "SEEK"     Then Return self~seek(options)
    When verb == "POSITION" Then Return self~seek(options)
    When verb == "QUERY"    Then Return self~query(options)
    Otherwise               Raise Syntax 93
  End

--------------------------------------------------------------------------------
-- DESCRIPTION                                                                --
--------------------------------------------------------------------------------

::Method description
  Expose description state
  Use Strict Arg
  Return (state":"description)~strip

--------------------------------------------------------------------------------
-- RESET                                                                      --
--------------------------------------------------------------------------------
--
-- Discards any buffered output that has not yet been flushed. Used by
-- HTTP.Response~error and ~redirect to clear a half-built body before
-- emitting an error or redirect response, mirroring the rexxlet model's
-- implicit buffer reset. Has no effect on already-flushed (committed)
-- output: bytes that have left the buffer cannot be recalled.
--

::Method reset
  Expose buffer
  Use Strict Arg
  buffer~setbuffersize(0)
  Return

--------------------------------------------------------------------------------
-- FLUSH                                                                      --
--------------------------------------------------------------------------------

::Method flush
  Expose state buffer underlyingstream response

  Use Strict Arg

  If state \== "READY" Then Return state":"

  If \response~committed Then response~commit

  underlyingstream~charout(buffer~string)
  underlyingstream~flush

  buffer~setbuffersize(0)

  Return "READY:"

--------------------------------------------------------------------------------
-- LINEOUT                                                                    --
--------------------------------------------------------------------------------

::Method lineout
  Expose buffer state eol

  -- Max two args
  If Arg() > 2 Then Raise syntax 93.902 array (2)

  -- First argument must be a string.
  -- If not specified, close the stream (i.e., flush and continue)
  If Arg(1,'O') Then Do
    self~close
    Return 0
    End
  Else Do
    string = .Validate~requestClassType(1, Arg(1), .String)
  End

  -- Cannot position transient streams
  If Arg(2,'E') Then Raise syntax 93.958

  If state \== "READY" Then Return 1 -- There was a previous error

  buffer~append(string)
  buffer~append(eol)

  Return 0

--------------------------------------------------------------------------------
-- OPEN                                                                       --
--   HTTP OutputStreams are always open, unless their underlying stream       --
--   reports a problem, in which case they cannot be opened                   --
--------------------------------------------------------------------------------

::Method open
  Expose state
  Return state":"

--------------------------------------------------------------------------------
-- POSITION                                                                   --
--------------------------------------------------------------------------------

::Method position
  Raise syntax 93.958

--------------------------------------------------------------------------------
-- QUALIFY                                                                    --
--------------------------------------------------------------------------------

::Method qualify
  Expose stream_name
  Use Strict Arg
  Return stream_name

--------------------------------------------------------------------------------
-- QUERY                                                                      --
--------------------------------------------------------------------------------

::Method query
  Expose stream_name buffer opened datetime timestamp

  -- We want exactly one argument...
  If Arg() < 1 Then Raise syntax 93.903 array (1)
  If Arg() > 1 Then Raise syntax 93.902 array (1)

  -- ...which can be converted into a string...
  query = .Validate~requestClassType(1, Arg(1), .String)

  query = query~space~translate

  Parse var query option more

  Select
    When query == "DATETIME"   Then Return datetime
    When query == "EXISTS"     Then Return stream_name
    When query == "HANDLE"     Then Raise syntax 93
    When query == "SIZE"       Then Return ""
    When query == "STREAMTYPE" Then Return "UNKNOWN"
    When query == "TIMESTAMP"  Then Return timestamp
    When option  == "POSITION" |,
         option  == "SEEK"     Then Return 1
    Otherwise Raise syntax 93
  End

--------------------------------------------------------------------------------
-- SAY                                                                        --
--------------------------------------------------------------------------------

::Method say
  Use Strict arg line = ""

  -- Let LINEOUT do the error checking
  Return self~lineout(line)

--------------------------------------------------------------------------------
-- SEEK                                                                       --
--------------------------------------------------------------------------------

::Method seek
  Raise syntax 93.958

--------------------------------------------------------------------------------
-- STATE                                                                      --
--------------------------------------------------------------------------------

::Method state
  Expose state
  Use Strict Arg
  Return state

--------------------------------------------------------------------------------
-- STRING                                                                     --
--------------------------------------------------------------------------------

::Method string
  Expose stream_name
  Return stream_name

--------------------------------------------------------------------------------
-- UNINIT                                                                     --
--------------------------------------------------------------------------------

-- This method is needed so that we don't get an error 40
::Method uninit
  Return