// While this function looks complicated, it's actually quite smooth:
        // The important things to remember is that serialization goes depth first:
        // The first object to get fully serialised is the first nested one, with
        // the parent object being last.
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            var xxxx = serializer.ReferenceLoopHandling;

            if (CancellationToken.IsCancellationRequested)
            {
                return; // Check for cancellation
            }

            /////////////////////////////////////
            // Path one: nulls
            /////////////////////////////////////

            if (value == null)
            {
                return;
            }

            /////////////////////////////////////
            // Path two: primitives (string, bool, int, etc)
            /////////////////////////////////////

            if (value.GetType().IsPrimitive || value is string)
            {
                FirstEntry = false;
                var t = JToken.FromObject(value); // bypasses this converter as we do not pass in the serializer
                t.WriteTo(writer);
                return;
            }

            /////////////////////////////////////
            // Path three: Bases
            /////////////////////////////////////

            if (value is Base && !(value is ObjectReference))
            {
                if (CancellationToken.IsCancellationRequested)
                {
                    return; // Check for cancellation
                }

                var obj = value as Base;

                FirstEntry = false;
                //TotalProcessedCount++;

                // Append to lineage tracker
                Lineage.Add(Guid.NewGuid().ToString());

                var jo            = new JObject();
                var propertyNames = obj.GetDynamicMemberNames();

                var contract = (JsonDynamicContract)serializer.ContractResolver.ResolveContract(value.GetType());

                // Iterate through the object's properties, one by one, checking for ignored ones
                foreach (var prop in propertyNames)
                {
                    if (CancellationToken.IsCancellationRequested)
                    {
                        return; // Check for cancellation
                    }
                    // Ignore properties starting with a double underscore.
                    if (prop.StartsWith("__"))
                    {
                        continue;
                    }

                    var property = contract.Properties.GetClosestMatchProperty(prop);

                    // Ignore properties decorated with [JsonIgnore].
                    if (property != null && property.Ignored)
                    {
                        continue;
                    }


                    // Ignore nulls
                    object propValue = obj[prop];
                    if (propValue == null)
                    {
                        continue;
                    }

                    // Check if this property is marked for detachment: either by the presence of "@" at the beginning of the name, or by the presence of a DetachProperty attribute on a typed property.
                    if (property != null)
                    {
                        var detachableAttributes = property.AttributeProvider.GetAttributes(typeof(DetachProperty), true);
                        if (detachableAttributes.Count > 0)
                        {
                            DetachLineage.Add(((DetachProperty)detachableAttributes[0]).Detachable);
                        }
                        else
                        {
                            DetachLineage.Add(false);
                        }

                        var chunkableAttributes = property.AttributeProvider.GetAttributes(typeof(Chunkable), true);
                        if (chunkableAttributes.Count > 0)
                        {
                            //DetachLineage.Add(true); // NOOPE
                            serializer.Context = new StreamingContext(StreamingContextStates.Other, chunkableAttributes[0]);
                        }
                        else
                        {
                            //DetachLineage.Add(false);
                            serializer.Context = new StreamingContext();
                        }
                    }
                    else if (prop.StartsWith("@")) // Convention check for dynamically added properties.
                    {
                        DetachLineage.Add(true);
                    }
                    else
                    {
                        DetachLineage.Add(false);
                    }

                    // Set and store a reference, if it is marked as detachable and the transport is not null.
                    if (WriteTransports != null && WriteTransports.Count != 0 && propValue is Base && DetachLineage[DetachLineage.Count - 1])
                    {
                        var what = JToken.FromObject(propValue, serializer); // Trigger next.

                        if (CancellationToken.IsCancellationRequested)
                        {
                            return; // Check for cancellation
                        }

                        var refHash = ((JObject)what).GetValue("id").ToString();

                        var reference = new ObjectReference()
                        {
                            referencedId = refHash
                        };
                        TrackReferenceInTree(refHash);
                        jo.Add(prop, JToken.FromObject(reference));
                    }
                    else
                    {
                        jo.Add(prop, JToken.FromObject(propValue, serializer)); // Default route
                    }

                    // Pop detach lineage. If you don't get this, remember this thing moves ONLY FORWARD, DEPTH FIRST
                    DetachLineage.RemoveAt(DetachLineage.Count - 1);
                }

                // Check if we actually have any transports present that would warrant a
                if ((WriteTransports != null && WriteTransports.Count != 0) && RefMinDepthTracker.ContainsKey(Lineage[Lineage.Count - 1]))
                {
                    jo.Add("__closure", JToken.FromObject(RefMinDepthTracker[Lineage[Lineage.Count - 1]]));
                }

                var hash = Models.Utilities.hashString(jo.ToString());
                if (!jo.ContainsKey("id"))
                {
                    jo.Add("id", JToken.FromObject(hash));
                }
                jo.WriteTo(writer);

                if ((DetachLineage.Count == 0 || DetachLineage[DetachLineage.Count - 1]) && WriteTransports != null && WriteTransports.Count != 0)
                {
                    var objString = jo.ToString();
                    var objId     = jo["id"].Value <string>();

                    OnProgressAction?.Invoke("S", 1);

                    foreach (var transport in WriteTransports)
                    {
                        if (CancellationToken.IsCancellationRequested)
                        {
                            continue; // Check for cancellation
                        }

                        transport.SaveObject(objId, objString);
                    }
                }

                // Pop lineage tracker
                Lineage.RemoveAt(Lineage.Count - 1);
                return;
            }

            /////////////////////////////////////
            // Path four: lists/arrays & dicts
            /////////////////////////////////////

            if (CancellationToken.IsCancellationRequested)
            {
                return; // Check for cancellation
            }

            var type = value.GetType();

            // TODO: List handling and dictionary serialisation handling can be sped up significantly if we first check by their inner type.
            // This handles a broader case in which we are, essentially, checking only for object[] or List<object> / Dictionary<string, object> cases.
            // A much faster approach is to check for List<primitive>, where primitive = string, number, etc. and directly serialize it in full.
            // Same goes for dictionaries.
            if (typeof(IEnumerable).IsAssignableFrom(type) && !typeof(IDictionary).IsAssignableFrom(type) && type != typeof(string))
            {
                if (TotalProcessedCount == 0 && FirstEntry)
                {
                    FirstEntry = false;
                    FirstEntryWasListOrDict = true;
                    TotalProcessedCount    += 1;
                    DetachLineage.Add(WriteTransports != null && WriteTransports.Count != 0 ? true : false);
                }

                JArray arr = new JArray();

                // Chunking large lists into manageable parts.
                if (DetachLineage[DetachLineage.Count - 1] && serializer.Context.Context is Chunkable chunkInfo)
                {
                    var maxCount  = chunkInfo.MaxObjCountPerChunk;
                    var i         = 0;
                    var chunkList = new List <DataChunk>();
                    var currChunk = new DataChunk();

                    foreach (var arrValue in ((IEnumerable)value))
                    {
                        if (i == maxCount)
                        {
                            chunkList.Add(currChunk);
                            currChunk = new DataChunk();
                            i         = 0;
                        }
                        currChunk.data.Add(arrValue);
                        i++;
                    }
                    chunkList.Add(currChunk);
                    value = chunkList;
                }

                foreach (var arrValue in ((IEnumerable)value))
                {
                    if (CancellationToken.IsCancellationRequested)
                    {
                        return; // Check for cancellation
                    }

                    if (arrValue == null)
                    {
                        continue;
                    }

                    if (WriteTransports != null && WriteTransports.Count != 0 && arrValue is Base && DetachLineage[DetachLineage.Count - 1])
                    {
                        var what = JToken.FromObject(arrValue, serializer); // Trigger next

                        var refHash = ((JObject)what).GetValue("id").ToString();

                        var reference = new ObjectReference()
                        {
                            referencedId = refHash
                        };
                        TrackReferenceInTree(refHash);
                        arr.Add(JToken.FromObject(reference));
                    }
                    else
                    {
                        arr.Add(JToken.FromObject(arrValue, serializer)); // Default route
                    }
                }

                if (CancellationToken.IsCancellationRequested)
                {
                    return; // Check for cancellation
                }

                arr.WriteTo(writer);

                if (DetachLineage.Count == 1 && FirstEntryWasListOrDict) // are we in a list entry point case?
                {
                    DetachLineage.RemoveAt(0);
                }

                return;
            }

            if (CancellationToken.IsCancellationRequested)
            {
                return; // Check for cancellation
            }

            if (typeof(IDictionary).IsAssignableFrom(type))
            {
                if (TotalProcessedCount == 0 && FirstEntry)
                {
                    FirstEntry = false;
                    FirstEntryWasListOrDict = true;
                    TotalProcessedCount    += 1;
                    DetachLineage.Add(WriteTransports != null && WriteTransports.Count != 0 ? true : false);
                }
                var dict   = value as IDictionary;
                var dictJo = new JObject();
                foreach (DictionaryEntry kvp in dict)
                {
                    if (CancellationToken.IsCancellationRequested)
                    {
                        return; // Check for cancellation
                    }

                    if (kvp.Value == null)
                    {
                        continue;
                    }

                    JToken jToken;
                    if (WriteTransports != null && WriteTransports.Count != 0 && kvp.Value is Base && DetachLineage[DetachLineage.Count - 1])
                    {
                        var what    = JToken.FromObject(kvp.Value, serializer); // Trigger next
                        var refHash = ((JObject)what).GetValue("id").ToString();

                        var reference = new ObjectReference()
                        {
                            referencedId = refHash
                        };
                        TrackReferenceInTree(refHash);
                        jToken = JToken.FromObject(reference);
                    }
                    else
                    {
                        jToken = JToken.FromObject(kvp.Value, serializer); // Default route
                    }
                    dictJo.Add(kvp.Key.ToString(), jToken);
                }
                dictJo.WriteTo(writer);

                if (CancellationToken.IsCancellationRequested)
                {
                    return; // Check for cancellation
                }

                if (DetachLineage.Count == 1 && FirstEntryWasListOrDict) // are we in a dictionary entry point case?
                {
                    DetachLineage.RemoveAt(0);
                }

                return;
            }

            /////////////////////////////////////
            // Path five: everything else (enums?)
            /////////////////////////////////////

            if (CancellationToken.IsCancellationRequested)
            {
                return; // Check for cancellation
            }

            FirstEntry = false;
            var lastCall = JToken.FromObject(value); // bypasses this converter as we do not pass in the serializer

            lastCall.WriteTo(writer);
        }