unit Myc.Data.Types.JSON;

interface

uses
    System.SysUtils,
    System.JSON,
    Myc.Data.Types;

type
    // Provides methods for serializing and deserializing data values to/from JSON using the TDataType helpers.
    TDataJsonConverter = class
    private
        // Serialization helpers for specific data types
        class function OrdinalToJson(const Value: TDataType.TOrdinal.TValue): TJSONValue; static;
        class function FloatToJson(const Value: TDataType.TFloat.TValue): TJSONValue; static;
        class function TextToJson(const Value: TDataType.TText.TValue): TJSONValue; static;
        class function TimestampToJson(const Value: TDataType.TTimestamp.TValue): TJSONValue; static;
        class function DecimalToJson(const Value: TDataType.TDecimal.TValue): TJSONValue; static;
        class function EnumToJson(const Value: TDataType.TEnum.TValue): TJSONValue; static;
        class function RecordToJson(const Value: TDataType.TRecord.TValue): TJSONObject; static;
        class function TupleToJson(const Value: TDataType.TTuple.TValue): TJSONArray; static;
        class function ArrayToJson(const Value: TDataType.TArray.TValue): TJSONArray; static;
        class function VectorToJson(const Value: TDataType.TVector.TValue): TJSONArray; static;

        // Deserialization helpers for specific data types
        class function JsonToOrdinal(const Json: TJSONValue; const DataType: TDataType): TDataType.TOrdinal.TValue; static;
        class function JsonToFloat(const Json: TJSONValue; const DataType: TDataType): TDataType.TFloat.TValue; static;
        class function JsonToText(const Json: TJSONValue; const DataType: TDataType): TDataType.TText.TValue; static;
        class function JsonToTimestamp(const Json: TJSONValue; const DataType: TDataType): TDataType.TTimestamp.TValue; static;
        class function JsonToDecimal(const Json: TJSONValue; const DataType: TDataType): TDataType.TDecimal.TValue; static;
        class function JsonToEnum(const Json: TJSONValue; const DataType: TDataType): TDataType.TEnum.TValue; static;
        class function JsonToRecord(const Json: TJSONObject; const DataType: TDataType): TDataType.TRecord.TValue; static;
        class function JsonToTuple(const Json: TJSONArray; const DataType: TDataType): TDataType.TTuple.TValue; static;
        class function JsonToArray(const Json: TJSONArray; const DataType: TDataType): TDataType.TArray.TValue; static;
        class function JsonToVector(const Json: TJSONArray; const DataType: TDataType): TDataType.TVector.TValue; static;

    public
        // Serializes a data value to a TJSONValue.
        class function ValueToJson(const Value: TDataType.TValue): TJSONValue;

        // Deserializes a TJSONValue to a data value based on the provided data type.
        class function JsonToValue(const Json: TJSONValue; const DataType: TDataType): TDataType.TValue;
    end;

implementation

uses
    System.Rtti,
    System.TypInfo,
    System.DateUtils,
    System.Math;

class function TDataJsonConverter.OrdinalToJson(const Value: TDataType.TOrdinal.TValue): TJSONValue;
begin
    Result := TJSONNumber.Create(Value.Value);
end;

class function TDataJsonConverter.FloatToJson(const Value: TDataType.TFloat.TValue): TJSONValue;
begin
    Result := TJSONNumber.Create(Value.Value);
end;

class function TDataJsonConverter.TextToJson(const Value: TDataType.TText.TValue): TJSONValue;
begin
    Result := TJSONString.Create(Value.Value);
end;

class function TDataJsonConverter.TimestampToJson(const Value: TDataType.TTimestamp.TValue): TJSONValue;
begin
    Result := TJSONString.Create(DateToISO8601(Value.Value, true));
end;

class function TDataJsonConverter.DecimalToJson(const Value: TDataType.TDecimal.TValue): TJSONValue;
var
    val: Int64;
    scale: Integer;
    s: string;
begin
    // Serialize as string to preserve precision.
    val := Value.Value;
    scale := Value.DataType.Scale;

    if (scale = 0) then
        s := IntToStr(val)
    else
    begin
        s := IntToStr(Abs(val));
        if (Length(s) <= scale) then
            s := StringOfChar('0', scale - Length(s) + 1) + s;

        s := s.Insert(Length(s) - scale, '.');

        if (val < 0) then
            s := '-' + s;
    end;
    Result := TJSONString.Create(s);
end;

class function TDataJsonConverter.EnumToJson(const Value: TDataType.TEnum.TValue): TJSONValue;
begin
    Result := TJSONString.Create(Value.DataType.Identifiers[Value.Value]);
end;

class function TDataJsonConverter.RecordToJson(const Value: TDataType.TRecord.TValue): TJSONObject;
var
    i: Integer;
    recordType: TDataType.TRecord;
begin
    recordType := Value.DataType;
    Result := TJSONObject.Create;
    for i := 0 to recordType.FieldCount - 1 do
        Result.AddPair(recordType.Fields[i].Name, ValueToJson(Value.Items[i]));
end;

class function TDataJsonConverter.TupleToJson(const Value: TDataType.TTuple.TValue): TJSONArray;
var
    i: Integer;
begin
    Result := TJSONArray.Create;
    for i := 0 to Value.ItemCount - 1 do
        Result.AddElement(ValueToJson(Value.Items[i]));
end;

class function TDataJsonConverter.ArrayToJson(const Value: TDataType.TArray.TValue): TJSONArray;
var
    i: Integer;
begin
    Result := TJSONArray.Create;
    for i := 0 to Value.ElementCount - 1 do
        Result.AddElement(ValueToJson(Value.Items[i]));
end;

class function TDataJsonConverter.VectorToJson(const Value: TDataType.TVector.TValue): TJSONArray;
var
    i: Integer;
begin
    Result := TJSONArray.Create;
    for i := 0 to Value.ElementCount - 1 do
        Result.AddElement(ValueToJson(Value.Items[i]));
end;

{ TDataJsonConverter - Deserialization Helpers }

class function TDataJsonConverter.JsonToOrdinal(const Json: TJSONValue; const DataType: TDataType): TDataType.TOrdinal.TValue;
var
    ordinalType: TDataType.TOrdinal;
begin
    Assert(DataType.Kind = dkOrdinal);
    ordinalType := DataType.AsOrdinal;
    Result := ordinalType.CreateValue((Json as TJSONNumber).AsInt64);
end;

class function TDataJsonConverter.JsonToFloat(const Json: TJSONValue; const DataType: TDataType): TDataType.TFloat.TValue;
var
    floatType: TDataType.TFloat;
begin
    Assert(DataType.Kind = dkFloat);
    floatType := DataType.AsFloat;
    Result := floatType.CreateValue((Json as TJSONNumber).AsDouble);
end;

class function TDataJsonConverter.JsonToText(const Json: TJSONValue; const DataType: TDataType): TDataType.TText.TValue;
var
    textType: TDataType.TText;
begin
    Assert(DataType.Kind = dkText);
    textType := DataType.AsText;
    Result := textType.CreateValue((Json as TJSONString).Value);
end;

class function TDataJsonConverter.JsonToTimestamp(const Json: TJSONValue; const DataType: TDataType): TDataType.TTimestamp.TValue;
var
    timestampType: TDataType.TTimestamp;
begin
    Assert(DataType.Kind = dkTimestamp);
    timestampType := DataType.AsTimestamp;
    Result := timestampType.CreateValue(ISO8601ToDate((Json as TJSONString).Value, True));
end;

class function TDataJsonConverter.JsonToDecimal(const Json: TJSONValue; const DataType: TDataType): TDataType.TDecimal.TValue;
var
    val: Int64;
    i: Integer;
    decimalType: TDataType.TDecimal;
begin
    Assert(DataType.Kind = dkDecimal);
    decimalType := DataType.AsDecimal;

    // Handle string representation for precision, with fallback to number for compatibility.
    if (Json is TJSONString) then
    begin
        var s := (Json as TJSONString).Value.Trim;
        var isNegative := s.StartsWith('-');
        if isNegative then
            s := s.Remove(0, 1); // remove sign

        var dotPos := Pos('.', s);
        var numFracDigits: Integer;
        if (dotPos = 0) then
            numFracDigits := 0
        else
        begin
            numFracDigits := Length(s) - dotPos;
            s := s.Remove(dotPos - 1, 1); // remove dot
        end;

        val := StrToInt64(s);

        var scaleDiff := decimalType.Scale - numFracDigits;
        if (scaleDiff > 0) then // Not enough fractional digits
        begin
            for i := 1 to scaleDiff do
                val := val * 10;
        end
        else if (scaleDiff < 0) then // Too many fractional digits
        begin
            for i := 1 to -scaleDiff do
                val := val div 10;
        end;

        if isNegative then
            val := -val;

        Result := decimalType.CreateValue(val);
    end
    else if (Json is TJSONNumber) then
    begin
        // Support numbers for compatibility, but this can lead to precision loss.
        val := Round((Json as TJSONNumber).AsDouble * Power(10, decimalType.Scale));
        Result := decimalType.CreateValue(val);
    end
    else
        raise EConvertError.Create('JSON String or Number expected for Decimal type');
end;

class function TDataJsonConverter.JsonToEnum(const Json: TJSONValue; const DataType: TDataType): TDataType.TEnum.TValue;
var
    idx: Integer;
    enumType: TDataType.TEnum;
begin
    Assert(DataType.Kind = dkEnum);
    enumType := DataType.AsEnum;

    idx := enumType.IndexOf((Json as TJSONString).Value);
    if (idx < 0) then
        raise EConvertError.CreateFmt('Invalid enum identifier "%s" for type %s', [Json.Value, DataType.Name]);
    Result := enumType.CreateValue(idx);
end;

class function TDataJsonConverter.JsonToRecord(const Json: TJSONObject; const DataType: TDataType): TDataType.TRecord.TValue;
var
    items: TArray<IDataValue>;
    i: Integer;
    recordType: TDataType.TRecord;
begin
    Assert(DataType.Kind = dkRecord);
    recordType := DataType.AsRecord;

    SetLength(items, recordType.FieldCount);
    for i := 0 to recordType.FieldCount - 1 do
    begin
        var field := recordType.Fields[i];
        var jsonPair := Json.Get(field.Name);
        if Assigned(jsonPair) then
            items[i] := JsonToValue(jsonPair.JsonValue, field.DataType);
    end;
    Result := recordType.CreateValue(items);
end;

class function TDataJsonConverter.JsonToTuple(const Json: TJSONArray; const DataType: TDataType): TDataType.TTuple.TValue;
var
    items: TArray<IDataValue>;
    i: Integer;
    tupleType: TDataType.TTuple;
begin
    Assert(DataType.Kind = dkTuple);
    tupleType := DataType.AsTuple;

    if (Json.Count <> tupleType.ItemCount) then
        raise EConvertError.CreateFmt('JSON array size (%d) does not match tuple size (%d)', [Json.Count, tupleType.ItemCount]);

    SetLength(items, tupleType.ItemCount);
    for i := 0 to tupleType.ItemCount - 1 do
        items[i] := JsonToValue(Json.Items[i], tupleType.ItemType[i]);
    Result := tupleType.CreateValue(items);
end;

class function TDataJsonConverter.JsonToArray(const Json: TJSONArray; const DataType: TDataType): TDataType.TArray.TValue;
var
    items: TArray<IDataValue>;
    i: Integer;
    arrayType: TDataType.TArray;
begin
    Assert(DataType.Kind = dkArray);
    arrayType := DataType.AsArray;

    SetLength(items, Json.Count);
    for i := 0 to Json.Count - 1 do
        items[i] := JsonToValue(Json.Items[i], arrayType.ElementType);
    Result := arrayType.CreateValue(items);
end;

class function TDataJsonConverter.JsonToVector(const Json: TJSONArray; const DataType: TDataType): TDataType.TVector.TValue;
var
    items: TArray<IDataValue>;
    i: Integer;
    vectorType: TDataType.TVector;
begin
    Assert(DataType.Kind = dkVector);
    vectorType := DataType.AsVector;

    if (Json.Count <> vectorType.ElementCount) then
        raise EConvertError.CreateFmt('JSON array size (%d) does not match vector size (%d)', [Json.Count, vectorType.ElementCount]);

    SetLength(items, Json.Count);
    for i := 0 to Json.Count - 1 do
        items[i] := JsonToValue(Json.Items[i], vectorType.ElementType);
    Result := vectorType.CreateValue(items);
end;

{ TDataJsonConverter - Public Methods }

class function TDataJsonConverter.ValueToJson(const Value: TDataType.TValue): TJSONValue;
begin
    case Value.DataType.Kind of
        dkVoid: Result := TJSONNull.Create;
        dkOrdinal: Result := OrdinalToJson(Value.AsOrdinal);
        dkFloat: Result := FloatToJson(Value.AsFloat);
        dkText: Result := TextToJson(Value.AsText);
        dkTimestamp: Result := TimestampToJson(Value.AsTimestamp);
        dkDecimal: Result := DecimalToJson(Value.AsDecimal);
        dkEnum: Result := EnumToJson(Value.AsEnum);
        dkRecord: Result := RecordToJson(Value.AsRecord);
        dkTuple: Result := TupleToJson(Value.AsTuple);
        dkArray: Result := ArrayToJson(Value.AsArray);
        dkVector: Result := VectorToJson(Value.AsVector);
        dkMethod: raise ENotSupportedException.Create('Cannot serialize a method type to JSON.');
    else
        raise ENotSupportedException.CreateFmt('Unsupported data kind: %s', [GetEnumName(TypeInfo(TDataKind), Ord(Value.DataType.Kind))]);
    end;
end;

class function TDataJsonConverter.JsonToValue(const Json: TJSONValue; const DataType: TDataType): TDataType.TValue;
begin
    case DataType.Kind of
        dkVoid: Result := TDataType.Void.Value;
        dkOrdinal: Result := JsonToOrdinal(Json, DataType);
        dkFloat: Result := JsonToFloat(Json, DataType);
        dkText: Result := JsonToText(Json, DataType);
        dkTimestamp: Result := JsonToTimestamp(Json, DataType);
        dkDecimal: Result := JsonToDecimal(Json, DataType);
        dkEnum: Result := JsonToEnum(Json, DataType);
        dkRecord: Result := JsonToRecord(Json as TJSONObject, DataType);
        dkTuple: Result := JsonToTuple(Json as TJSONArray, DataType);
        dkArray: Result := JsonToArray(Json as TJSONArray, DataType);
        dkVector: Result := JsonToVector(Json as TJSONArray, DataType);
        dkMethod: raise ENotSupportedException.Create('Cannot deserialize a method type from JSON.');
    else
        raise ENotSupportedException.CreateFmt('Unsupported data kind: %s', [GetEnumName(TypeInfo(TDataKind), Ord(DataType.Kind))]);
    end;
end;

end.
