{$MODE OBJFPC}
{$MODESWITCH OUT}
{$MODESWITCH RESULT}
{$MODESWITCH DEFAULTPARAMETERS}
unit djson_node;

interface

uses
  derror,
  dansiview,
  dstrnum,
  dstream,
  dstreammem,
  dtokenizer,
  //djson_sax,
  dnode;

const
  JSON_NODE_FLAG_ALLOW_ENDING_COMMA = 1;
  JSON_NODE_FLAG_ALLOW_IDENTIFIER_NAME = 2;
  JSON_NODE_FLAG_ALLOW_PLUS = 4;
  JSON_NODE_FLAG_NOT_STRICT = JSON_NODE_FLAG_ALLOW_ENDING_COMMA or
                              JSON_NODE_FLAG_ALLOW_IDENTIFIER_NAME or
                              JSON_NODE_FLAG_ALLOW_PLUS;

  JSON_NODE_ERROR_UNKNOWN_IDENTIFIER = 1;
  JSON_NODE_ERROR_ARRAY_NOT_CLOSED   = 2;
  JSON_NODE_ERROR_OBJECT_NOT_CLOSED   = 3;
  JSON_NODE_ERROR_UNEXPECTED_END_OF_NUMBER   = 4;
  JSON_NODE_ERROR_NO_COLON   = 5;
  JSON_NODE_ERROR_UNCLOSED_STRING = 6;
  JSON_NODE_ERROR_UNKNOWN_CHARACTER = 7;
  JSON_NODE_ERROR_MEMORY_EXCEEDED = 8;
  JSON_NODE_ERROR_NO_COMMA = 9;
  JSON_NODE_ERROR_UNCLOSED_ARRAY = 10;
  JSON_NODE_ERROR_TAILING_COMMA = 11;
  JSON_NODE_ERROR_EXTRA_CHARS = 12;
  JSON_NODE_ERROR_DIGIT_EXPECTED = 13;
  JSON_NODE_ERROR_LEADING_PLUS = 14;
  JSON_NODE_ERROR_LEADING_ZERO = 15;
  JSON_NODE_ERROR_SINGLE_QUOTE = 16;

type
TError = derror.TError;

TJsonNodeBuilder = object
private
  FError: TError;
  FLookupReader: TLookupReader;
  FNodeManager: PNodeManagerStatic;
  FStrict: Boolean;
  FStreamMem: TStreamMem;
  function GetPos: LongInt; inline;
  function GetLine: LongInt; inline;
  function ReadIdentifier(var S: AnsiString): Boolean;
  function ReadIdentifier(var S: PAnsiChar; var E: PAnsiChar): Boolean;
  function ReadString(var S: PAnsiChar; var E: PAnsiChar): Boolean;
  function ParseDecimalInt(var I: Int64): Boolean;
  function ParseDecimalInt(var I: Int64; var Fraction: LongInt): Boolean;
  function ParseNumber(out Value: TNodeValue): Boolean;
  function ParseJsonValueToNode(out Value: TNodeValue): Boolean;
public
  procedure Init(Strict: Boolean; NodeManager: PNodeManagerStatic);
  procedure Done;
  function ParseJsonValueToNode(Data: TData;
                                out NodeValue: TNodeValue): Boolean;
  function ParseJsonValueToNode(var Source: PAnsiChar;
                                SourceEnd: PAnsiChar;
                                out NodeValue: TNodeValue): Boolean;
  property Pos: LongInt read GetPos;
  property Line: LongInt read GetLine;
  property Error: TError read FError;
end;

function ParseJsonValueToNode(Data: TData;
                              NodeManager: PNodeManagerStatic;
                              out NodeValue: TNodeValue;
                              Strict: Boolean = False;
                              Error: PError = nil): Boolean;
function ParseJsonValueToNode(var Source: PAnsiChar;
                              SourceEnd: PAnsiChar;
                              NodeManager: PNodeManagerStatic;
                              out NodeValue: TNodeValue;
                              Strict: Boolean = False;
                              Error: PError = nil): Boolean;
// TODO do not use AnsiString in this module
function NodeToString(const Value: TNodeValue;
                      const Ident: AnsiString = '';
                      const Shift: AnsiString = '';
                      const NewLine: AnsiString = LineEnding): AnsiString;

procedure NodeToJson(const Value: TNodeValue; OutStream: PStream);

function UnescapeJsonStringInPlace(S: PAnsiChar;
                                   var E: PAnsiChar): Boolean;

implementation

function IsIdentifierChar(B: Byte): Boolean;
begin
  Result := ((Ord('a') <= B) and (B <= Ord('z'))) or
            ((Ord('A') <= B) and (B <= Ord('Z'))) or
            (B = Ord('$')) or (B = Ord('_'));
end;

function TJsonNodeBuilder.GetPos: LongInt;
begin
  Result := FLookupReader.Pos;
end;

function TJsonNodeBuilder.GetLine: LongInt;
begin
  Result := FLookupReader.Line;
end;

function TJsonNodeBuilder.ReadIdentifier(var S: AnsiString): Boolean;
begin
  S := '';
  while IsIdentifierChar(FLookupReader.Lookup) do begin
    S := S + AnsiChar(FLookupReader.Lookup);
    if not FLookupReader.NextChar then
      Exit(False);
  end;
  Exit(True);
end;

function TJsonNodeBuilder.ReadIdentifier(var S: PAnsiChar; var E: PAnsiChar): Boolean;
begin
  S := PAnsiChar(FStreamMem.MemPos - 1);
  while IsIdentifierChar(FLookupReader.Lookup) do begin
    if not FLookupReader.NextChar then begin
      E := PAnsiChar(FStreamMem.MemPos);
      Exit(False);
    end;
  end;
  E := PAnsiChar(FStreamMem.MemPos - 1);
  Exit(True);
end;

function TJsonNodeBuilder.ReadString(var S: PAnsiChar; var E: PAnsiChar): Boolean;
var
  EndChar: Byte;
begin
  EndChar := FLookupReader.Lookup;
  if not FLookupReader.NextChar then // skip "
    Exit(False);
  S := PAnsiChar(FStreamMem.MemPos - 1);
  while True do begin
    if FLookupReader.Lookup = Ord('\') then begin
      if (not FLookupReader.NextChar) or
         (not FLookupReader.NextChar) then begin
        Exit(SetupError(@FError, JSON_NODE_ERROR_UNCLOSED_STRING, 'unclosed string'));
      end;
      continue;
    end;
    if FLookupReader.Lookup = EndChar then
      break;
    if not FLookupReader.NextChar then begin
      E := PAnsiChar(FStreamMem.MemPos);
      Exit(SetupError(@FError, JSON_NODE_ERROR_UNCLOSED_STRING, 'unclosed string'));
    end;
  end;
  E := PAnsiChar(FStreamMem.MemPos - 1);
  UnescapeJsonStringInPlace(S, E);
  FLookupReader.NextChar;
  Exit(True);
end;

function TJsonNodeBuilder.ParseDecimalInt(var I: Int64): Boolean;
begin
  if not (FLookupReader.Lookup in [Ord('0') .. Ord('9')]) then
    Exit(False);
  Result := True;
  while FLookupReader.Lookup in [Ord('0') .. Ord('9')] do begin
    I := (I * 10) + FLookupReader.Lookup - Ord('0');
    if not FLookupReader.NextChar then
      Exit;
  end;
end;

function TJsonNodeBuilder.ParseDecimalInt(var I: Int64; var Fraction: LongInt): Boolean;
begin
  if not (FLookupReader.Lookup in [Ord('0') .. Ord('9')]) then
    Exit(False);
  Result := True;
  while FLookupReader.Lookup in [Ord('0') .. Ord('9')] do begin
    I := (I * 10) + FLookupReader.Lookup - Ord('0');
    Fraction := Fraction * 10;
    if not FLookupReader.NextChar then
      Exit;
  end;
end;

function TJsonNodeBuilder.ParseNumber(out Value: TNodeValue): Boolean;
label
  _ExitInt, _ExitFloat;
var
  I, AfterDot, Exponent: Int64;
  Fraction: LongInt;
begin
  Exponent := 0;
  I := FLookupReader.Lookup - Ord('0');
  if not FLookupReader.NextChar then
    goto _ExitInt;
  if (I = 0) and (FLookupReader.Lookup = Ord('x')) then begin
    // hexdecimal
    // TODO
  end else begin
    ParseDecimalInt(I);
  end;

  AfterDot := 0;
  Fraction := 1;
  if FLookupReader.Lookup = Ord('.') then begin
    // float
    if not FLookupReader.NextChar then
      goto _ExitFloat;
    ParseDecimalInt(AfterDot, Fraction);
    if Fraction = 0 then // TODO very small value
      Fraction := 1;
  end;

  if (FLookupReader.Lookup = Ord('e')) or
     (FLookupReader.Lookup = Ord('E')) then begin
    // float
    if not FLookupReader.NextChar then
      goto _ExitFloat;
    if FLookupReader.Lookup = Ord('+') then begin
      if not FLookupReader.NextChar then
        goto _ExitFloat;
    end else if FLookupReader.Lookup = Ord('-') then begin
      // TODO negative of Exponent
      if not FLookupReader.NextChar then
        goto _ExitFloat;
    end;
    // TODO check for FLookupReader.Lookup in 0..9
    ParseDecimalInt(Exponent);
  end;

_ExitFloat:
  if (AfterDot <> 0) or (Exponent <> 0) then begin
    Value.Type_ := NODE_FLOAT;
    Value.F := (I + AfterDot / Fraction); // TODO * 10^Exponent
    Exit(True);
  end;
_ExitInt:
  Value.Type_ := NODE_INT;
  Value.I := I;
  Exit(True);
end;

function TJsonNodeBuilder.ParseJsonValueToNode(out Value: TNodeValue): Boolean;
var
  S, E: PAnsiChar;
  N: TNodeString;
  List, Last: PNodeItem;
begin
  Result := FLookupReader.SkipSpaces;
  case FLookupReader.Lookup of
  Ord('0')..Ord('9'): begin
    ParseNumber(Value);
  end;
  Ord('-'): begin
    if not FLookupReader.NextChar then
      Exit(SetupError(@FError, JSON_NODE_ERROR_UNEXPECTED_END_OF_NUMBER, 'unexpected end of number'));
    if not (FLookupReader.Lookup in [Ord('0')..Ord('9')]) then
      Exit(SetupError(@FError, JSON_NODE_ERROR_DIGIT_EXPECTED, 'number must contain a digit'));
    // TODO check for .
    ParseNumber(Value);
    if Value.Type_ = NODE_INT then begin
      Value.I := - Value.I;
    end else
      Value.F := - Value.F;
  end;
  Ord('+'): begin
    if FStrict then
      Exit(SetupError(@FError, JSON_NODE_ERROR_LEADING_PLUS, 'number cannot start with plus'));
    if not FLookupReader.NextChar then
      Exit(SetupError(@FError, JSON_NODE_ERROR_UNEXPECTED_END_OF_NUMBER, 'unexpected end of number'));
    if not (FLookupReader.Lookup in [Ord('0')..Ord('9')]) then
      Exit(SetupError(@FError, JSON_NODE_ERROR_DIGIT_EXPECTED, 'number must contain a digit'));
    ParseNumber(Value);
  end;
  Ord('{'): begin
    // on first step we build a node list
    // then we can optimize it as a hashtable
    List := nil;
    Last := nil;
    if (not FLookupReader.NextChar) or
       (not FLookupReader.SkipSpaces) then
      Exit(SetupError(@FError, JSON_NODE_ERROR_OBJECT_NOT_CLOSED, 'object not closed'));
    while FLookupReader.Lookup <> Ord('}') do begin
      if not FNodeManager^.AllocItemInList(List, Last) then
        Exit(SetupError(@FError, JSON_NODE_ERROR_MEMORY_EXCEEDED, 'cannot alloc memory'));
      if (FLookupReader.Lookup = Ord('"')) or (FLookupReader.Lookup = Ord('''')) then begin
        if FStrict and (FLookupReader.Lookup = Ord('''')) then
          Exit(SetupError(@FError, JSON_NODE_ERROR_SINGLE_QUOTE, 'single quote not allowed'));
        if not ReadString(S, E) then begin
          // TODO free list
          Exit(False);
        end;
      end else if not ReadIdentifier(S, E) then begin
        // TODO free list
        Exit(False);
      end;
      if not FLookupReader.SkipSpaces then begin
        Exit(SetupError(@FError, JSON_NODE_ERROR_OBJECT_NOT_CLOSED, 'object not closed'));
      end;
      if FLookupReader.Lookup <> Ord(':') then begin
        Exit(SetupError(@FError, JSON_NODE_ERROR_NO_COLON, 'no colon after identifier'));
      end;
      if (not FLookupReader.NextChar) or
         (not FLookupReader.SkipSpaces) then begin
        Exit(SetupError(@FError, JSON_NODE_ERROR_OBJECT_NOT_CLOSED, 'object not closed'));
      end;
      if not ParseJsonValueToNode(Last^.Value^) then begin
        // TODO free list
        Exit(False);
      end;
      if not FLookupReader.SkipSpaces then begin
        // TODO free list
        Exit(False);
      end;
      Last^.Key := AnsiView(S, E - S);
      if FLookupReader.Lookup = Ord('}') then
        break;
      if FLookupReader.Lookup = Ord(',') then begin
        if (not FLookupReader.NextChar) or
           (not FLookupReader.SkipSpaces) then
          Exit(False); // TODO free, error
        // TODO check for unstrict ,}
        if FLookupReader.Lookup = Ord('}') then
          Exit(SetupError(@FError, JSON_NODE_ERROR_TAILING_COMMA, 'tailing comma'));
        continue;
      end;
      // TODO error
    end;
    FLookupReader.NextChar;
    Value.Type_ := NODE_OBJECT;
    Value.O := FNodeManager^.AllocObject(1);
    if Value.O = nil then
      Exit(SetupError(@FError, JSON_NODE_ERROR_MEMORY_EXCEEDED, 'cannot alloc memory'));
    Value.O^.BulksFirst[0] := List;
    Exit(True);
  end;
  Ord('['): begin
    // on first step we build a node list
    // then we can optimize it as an actual array
    List := nil;
    Last := nil;
    if (not FLookupReader.NextChar) or
       (not FLookupReader.SkipSpaces) then
      Exit(SetupError(@FError, JSON_NODE_ERROR_ARRAY_NOT_CLOSED, 'array not closed'));
    while FLookupReader.Lookup <> Ord(']') do begin
      if not FNodeManager^.AllocItemInList(List, Last) then
        Exit(SetupError(@FError, JSON_NODE_ERROR_MEMORY_EXCEEDED, 'cannot alloc memory'));
      if not ParseJsonValueToNode(Last^.Value^) then begin
        // TODO free list
        Exit(False);
      end;
      if not FLookupReader.SkipSpaces then begin
        // TODO free list
        Exit(False);
      end;
      if FLookupReader.Lookup = Ord(']') then
        break;
      if FLookupReader.Lookup = Ord(',') then begin
        if (not FLookupReader.NextChar) or (not FLookupReader.SkipSpaces) then begin
          Exit(SetupError(@FError, JSON_NODE_ERROR_UNCLOSED_ARRAY, 'unclosed array')); // TODO free
        end;
        if FStrict and (FLookupReader.Lookup = Ord(']')) then begin
          Exit(SetupError(@FError, JSON_NODE_ERROR_TAILING_COMMA, 'tailing comma')); // TODO free
        end;
        continue;
      end else begin
        Exit(SetupError(@FError, JSON_NODE_ERROR_NO_COMMA, 'comma expected'));
      end;
    end;
    FLookupReader.NextChar;
    Value.Type_ := NODE_LIST;
    Value.L := Pointer(List);
    Exit(True);
  end;
  Ord('"'), Ord(''''): begin
    if FStrict and (FLookupReader.Lookup = Ord('''')) then
      Exit(SetupError(@FError, JSON_NODE_ERROR_SINGLE_QUOTE, 'single quote not allowed'));
    ReadString(S, E);
    Value.Type_ := NODE_STRING;
    Value.S := AnsiView(S, E - S);
  end;
  Ord('a')..Ord('z'), Ord('A')..Ord('Z'), Ord('$'), Ord('_'), Ord('\'): begin
    // identifier
    ReadIdentifier(S, E);
    if AnsiView(S, E - S) = 'null' then begin
      Value.Type_ := NODE_NULL;
    end else if AnsiView(S, E - S) = 'true' then begin
      Value.Type_ := NODE_BOOLEAN;
      Value.B := True;
    end else if AnsiView(S, E - S) = 'false' then begin
      Value.Type_ := NODE_BOOLEAN;
      Value.B := False;
    end else begin
      Exit(SetupError(@FError, JSON_NODE_ERROR_UNKNOWN_IDENTIFIER, 'unknown identifier'));
    end;
  end;
  else
    System.Writeln(stderr, 'Line,Pos ', FLookupReader.Line, ' ', FLookupReader.Pos);
    Exit(SetupError(@FError, JSON_NODE_ERROR_UNKNOWN_CHARACTER, 'unknown character'));
  end;
end;

procedure TJsonNodeBuilder.Init(Strict: Boolean; NodeManager: PNodeManagerStatic);
begin
  FStrict := Strict;
  FNodeManager := NodeManager;
end;

procedure TJsonNodeBuilder.Done;
begin
end;

function TJsonNodeBuilder.ParseJsonValueToNode(Data: TData;
                                               out NodeValue: TNodeValue): Boolean;
var
  P: Pointer;
  S: SizeUInt;
begin
  // Load all data from stream to memory
  Data.Stream^.ReadToMemory(P, S);
  // We dont need Data anymore
  Data.Done;
  // Attach memory to node manager, so it can be auto deallocated
  FNodeManager^.AttachSource(P, S);
  // Now create stream
  FStreamMem.Init(P, S);
  FLookupReader.Init(@FStreamMem);
  Result := ParseJsonValueToNode(NodeValue);
  if Result and FStrict then begin
    // check for extra data after json value
    FLookupReader.SkipSpaces;
    if (FLookupReader.Lookup <> 0) or not FStreamMem.EOF then
      Result := SetupError(@FError, JSON_NODE_ERROR_EXTRA_CHARS, 'extra characters');
  end;
  FLookupReader.Done;
  FStreamMem.Done;
end;

function TJsonNodeBuilder.ParseJsonValueToNode(var Source: PAnsiChar;
                            SourceEnd: PAnsiChar;
                            out NodeValue: TNodeValue): Boolean;
begin
  FStreamMem.Init(Source, SourceEnd - Source);
  FLookupReader.Init(@FStreamMem);
  Result := ParseJsonValueToNode(NodeValue);
  if Result then
    Source := PAnsiChar(FStreamMem.MemPos);
  FLookupReader.Done;
  FStreamMem.Done;
end;

function ParseJsonValueToNode(Data: TData;
                              NodeManager: PNodeManagerStatic;
                              out NodeValue: TNodeValue;
                              Strict: Boolean = False;
                              Error: PError = nil): Boolean;
var
  JsonNodeBuilder: TJsonNodeBuilder;
begin
  if not Data.Stream^.Ready then begin
    NodeSetNull(NodeValue);
    Data.Done;
    Exit(False);
  end;
  JsonNodeBuilder.Init(Strict, NodeManager);
  Result := JsonNodeBuilder.ParseJsonValueToNode(Data, NodeValue);
  if Error <> nil then
    Error^ := JsonNodeBuilder.Error;
  JsonNodeBuilder.Done;
end;

function ParseJsonValueToNode(var Source: PAnsiChar;
                              SourceEnd: PAnsiChar;
                              NodeManager: PNodeManagerStatic;
                              out NodeValue: TNodeValue;
                              Strict: Boolean = False;
                              Error: PError = nil): Boolean;
var
  JsonNodeBuilder: TJsonNodeBuilder;
begin
  JsonNodeBuilder.Init(Strict, NodeManager);
  Result := JsonNodeBuilder.ParseJsonValueToNode(Source, SourceEnd, NodeValue);
  if Error <> nil then
    Error^ := JsonNodeBuilder.Error;
  JsonNodeBuilder.Done;
end;

function BooleanToString(B: Boolean): AnsiString;
begin
  if B then begin
    Result := 'true';
  end else
    Result := 'false';
end;

function NodeArrayToString(A: PNodeArray;
                           const Ident: AnsiString;
                           Shift: AnsiString;
                           const NewLine: AnsiString): AnsiString;
var
  I, E: PNodeValue;
begin
  Result := '[';
  if Ident <> '' then begin
    Shift := Shift + Ident;
  end;
  I := A^.First;
  E := I + A^.Count;
  if I < E then begin
    while True do begin
      if Ident <> '' then
        Result := Result + NewLine + Shift;
      Result := Result + NodeToString(I^, Ident, Shift, NewLine);
      Inc(I);
      if I >= E then
        break;
      Result := Result + ', ';
    end;
  end;
  if Ident <> '' then begin
    SetLength(Shift, Length(Shift) - Length(NewLine));
    Result := Result + NewLine + Shift;
  end;
  Result := Result + ']';
end;

function NodeObjectToString(O: PNodeObject; Ident, Shift, NewLine: AnsiString): AnsiString;
var
  Bulk, BulkE: PPNodeItem;
  I: PNodeItem;
begin
  Result := '{';
  Bulk := O^.BulksFirst;
  BulkE := Bulk + O^.BulksCount;
  if Ident <> '' then
    Shift := Shift + Ident;
  while True do begin
    I := Bulk^;
    if I <> nil then begin
      while True do begin
        // TODO json encoding
        if Ident <> '' then
          Result := Result + NewLine + Shift;
        Result := Result + '"' + I^.Key.ToString + '": ' + NodeToString(I^.Value^, Ident, Shift, NewLine);
        I := I^.Next;
        if I = nil then
          Break;
        Result := Result + ', ';
      end;
    end;
    Inc(Bulk);
    if Bulk >= BulkE then
      break;
    Result := Result + ', ';
  end;
  if Ident <> '' then begin
    SetLength(Shift, Length(Shift) - Length(Ident));
    Result := Result + NewLine + Shift;
  end;
  Result := Result + '}';
end;

function NodeListToString(L: PNodeItem; Ident, Shift, NewLine: AnsiString): AnsiString;
begin
  Result := '[';
  if L <> nil then begin
    if Ident <> '' then
      Shift := Shift + Ident;
    while True do begin
      if Ident <> '' then
        Result := Result + NewLine + Shift;
      Result := Result + NodeToString(L^.Value^, Ident, Shift, NewLine);
      L := L^.Next;
      if L = nil then
        Break;
      Result := Result + ', ';
    end;
    if Ident <> '' then
      SetLength(Shift, Length(Shift) - Length(Ident));
  end;
  if Ident <> '' then
    Result := Result + NewLine + Shift;
  Result := Result + ']';
end;

function NodeToString(const Value: TNodeValue;
                      const Ident: AnsiString = '';
                      const Shift: AnsiString = '';
                      const NewLine: AnsiString = LineEnding): AnsiString;
begin
  case Value.Type_ of
  NODE_NULL    : Result := 'null';
  NODE_INT     : Result := IntToStr(Value.I);
  NODE_FLOAT   : Result := FloatToStr(Value.F);
  NODE_STRING  : Result := '"' + Value.S.ToString + '"'; // TODO Escape
  NODE_BOOLEAN : Result := BooleanToString(Value.B);
  NODE_ARRAY   : Result := NodeArrayToString(Value.A, Ident, Shift, NewLine);
  NODE_OBJECT  : Result := NodeObjectToString(Value.O, Ident, Shift, NewLine);
  NODE_LIST    : Result := NodeListToString(Value.L, Ident, Shift, NewLine);
  else
    Result := '"unknown node type ' + IntToStr(Value.Type_) + '"';
  end;
end;

procedure NodeArrayToJson(A: PNodeArray; OutStream: PStream);
var
  I, E: PNodeValue;
begin
  OutStream^.WriteByte(Byte('['));
  I := A^.First;
  E := I + A^.Count;
  while True do begin
    NodeToJson(I^, OutStream);
    Inc(I);
    if I >= E then
      break;
    OutStream^.WriteData(', ');
  end;
  OutStream^.WriteByte(Byte(']'));
end;

procedure NodeObjectToJson(O: PNodeObject; OutStream: PStream);
var
  Bulk, BulkE: PPNodeItem;
  I: PNodeItem;
begin
  OutStream^.WriteByte(Byte('{'));
  Bulk := O^.BulksFirst;
  BulkE := Bulk + O^.BulksCount;
  while True do begin
    I := Bulk^;
    if I <> nil then begin
      while True do begin
        // TODO json encoding
        OutStream^.WriteByte(Byte('"'));
        OutStream^.Write(I^.Key.Data, I^.Key.Length);
        OutStream^.WriteData('" : ');
        NodeToJson(I^.Value^, OutStream);
        I := I^.Next;
        if I = nil then
          Break;
        OutStream^.WriteData(', ');
      end;
    end;
    Inc(Bulk);
    if Bulk >= BulkE then
      break;
    OutStream^.WriteData(', ');
  end;
  OutStream^.WriteData('}');
end;

procedure NodeListToJson(L: PNodeItem; OutStream: PStream);
begin
  OutStream^.WriteByte(Ord('['));
  if L <> nil then begin
    while True do begin
      NodeToJson(L^.Value^, OutStream);
      L := L^.Next;
      if L = nil then
        Break;
      OutStream^.WriteData(', ');
    end;
  end;
  OutStream^.WriteByte(Ord(']'));
end;

procedure NodeToJson(const Value: TNodeValue; OutStream: PStream);
begin
  // TODO implement stream versions of *ToStr functions
  case Value.Type_ of
  NODE_NULL    : OutStream^.WriteData('null');
  NODE_INT     : OutStream^.WriteData(IntToStr(Value.I));
  NODE_FLOAT   : OutStream^.WriteData(FloatToStr(Value.F));
  NODE_STRING  : OutStream^.WriteData('"' + Value.S.ToString + '"'); // TODO Escape
  NODE_BOOLEAN : OutStream^.WriteData(BooleanToString(Value.B));
  NODE_ARRAY   : NodeArrayToJson(Value.A, OutStream);
  NODE_OBJECT  : NodeObjectToJson(Value.O, OutStream);
  NODE_LIST    : NodeListToJson(PNodeItem(Value.L), OutStream);
  else
    OutStream^.WriteData('"unknown node type ');
    OutStream^.WriteData(IntToStr(Value.Type_));
    OutStream^.WriteData('"');
  end;
end;

function UnescapeJsonStringInPlace(S: PAnsiChar; var E: PAnsiChar): Boolean;
var
  OffsetDest: PAnsiChar;
  Offset: LongInt;
  U: Cardinal;
begin
  Result := True;
  Offset := 0;
  OffsetDest := S;
  while S < E do begin
    if S^ = '\' then begin
      if Offset > 0 then
        Move((OffsetDest + Offset)^, OffsetDest^, S - OffsetDest);
      Inc(S);
      if S >= E then
        break;
      OffsetDest := S;
      if S^ = 'u' then begin // special case
        Inc(Offset);
        if E - S < 5 then
          break;
        U := HexDigitToInt((S + 1)^) shl 12 +
             HexDigitToInt((S + 2)^) shl 8 +
             HexDigitToInt((S + 3)^) shl 4 +
             HexDigitToInt((S + 4)^);
        // TODO encode utf-8 char
        (S - Offset)^ := AnsiChar(U and 255);
        Inc(S, 5);
        Inc(Offset, 4);
      end else begin
        Inc(Offset);
        case S^ of
          '"': (S - Offset)^ := '"';
          '\': (S - Offset)^ := '\';
          '/': (S - Offset)^ := '/';
          'b': (S - Offset)^ := #08; // backspace
          'f': (S - Offset)^ := #12; // formfeed
          'n': (S - Offset)^ := #10; // new line / LF
          'r': (S - Offset)^ := #13; // carriage return / CR
          't': (S - Offset)^ := #09; // horizontal tab
        else
          (S - Offset)^ := S^;
        end;
        Inc(S);
      end;
    end else
      Inc(S);
  end;
  if Offset > 0 then begin
    Move((OffsetDest + Offset)^, OffsetDest^, E - OffsetDest);
    E := E - Offset;
  end;
end;

end.
