﻿using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System;
using System.Collections.Specialized;
using System.Diagnostics;
using System.IO;
using VRC.Core.BestHTTP;
using VRC.Core.BestHTTP.Authentication;
using VRC.Core.BestHTTP.JSON;
using Debug = UnityEngine.Debug;
using System.Text.RegularExpressions;

namespace VRC.Core
{
    public class ApiFileHelper : MonoBehaviour
    {
        private readonly int kMultipartUploadChunkSize = 10 * 1024 * 1024;
        private readonly int SERVER_PROCESSING_WAIT_TIMEOUT_CHUNK_SIZE = 50 * 1024 * 1024;
        private readonly float SERVER_PROCESSING_WAIT_TIMEOUT_PER_CHUNK_SIZE = 120.0f;
        private readonly float SERVER_PROCESSING_MAX_WAIT_TIMEOUT = 600.0f;
        private readonly float SERVER_PROCESSING_INITIAL_RETRY_TIME = 2.0f;
        private readonly float SERVER_PROCESSING_MAX_RETRY_TIME = 10.0f;

        private readonly Regex[] kUnityPackageAssetNameFilters = new Regex[]
        {
            new Regex(@"/LightingData\.asset$"),                    // lightmap base asset
            new Regex(@"/Lightmap-.*(\.png|\.exr)$"),               // lightmaps
            new Regex(@"/ReflectionProbe-.*(\.exr|\.png)$"),        // reflection probes
            new Regex(@"/Editor/Data/UnityExtensions/")             // anything that looks like part of the Unity installation
        };

        public delegate void OnFileOpSuccess(ApiFile apiFile, string message);
        public delegate void OnFileOpError(ApiFile apiFile, string error);
        public delegate void OnFileOpProgress(ApiFile apiFile, string status, string subStatus, float pct);
        public delegate bool FileOpCancelQuery(ApiFile apiFile);

        public static ApiFileHelper Instance
        {
            get
            {
                CheckInstance();
                return mInstance;
            }
        }

        private static ApiFileHelper mInstance = null;
        const float kPostWriteDelay = 0.75f;

        public bool DebugEnabled
        {
            get { return true; }
        }

        public enum FileOpResult
        {
            Success,
            Unchanged
        }

        public static void UploadFileAsync(string filename, string existingFileId, string friendlyName,
            OnFileOpSuccess onSuccess, OnFileOpError onError, OnFileOpProgress onProgress, FileOpCancelQuery cancelQuery)
        {
            Instance.StartCoroutine(Instance.UploadFile(filename, existingFileId, friendlyName, onSuccess, onError,
                onProgress, cancelQuery));
        }

        public static string GetMimeTypeFromExtension(string extension)
        {
            if (extension == ".vrcw")
                return "application/x-world";
            if (extension == ".vrca")
                return "application/x-avatar";
            if (extension == ".dll")
                return "application/x-msdownload";
            if (extension == ".unitypackage")
                return "application/gzip";
            if (extension == ".gz")
                return "application/gzip";
            if (extension == ".jpg")
                return "image/jpg";
            if (extension == ".png")
                return "image/png";
            if (extension == ".sig")
                return "application/x-rsync-signature";
            if (extension == ".delta")
                return "application/x-rsync-delta";

            Debug.LogWarning("Unknown file extension for mime-type: " + extension);
            return "application/octet-stream";
        }

        public static bool IsGZipCompressed(string filename)
        {
            return GetMimeTypeFromExtension(Path.GetExtension(filename)) == "application/gzip";
        }

        public IEnumerator UploadFile(string filename, string existingFileId, string friendlyName,
            OnFileOpSuccess onSuccess, OnFileOpError onError, OnFileOpProgress onProgress, FileOpCancelQuery cancelQuery)
        {
            Debug.Log("UploadFile: filename: " + filename + ", file id: " +
                      (!string.IsNullOrEmpty(existingFileId) ? existingFileId : "<new>") + ", name: " + friendlyName);

            // validate input file
            Progress(onProgress, null, "Checking file...");

            if (string.IsNullOrEmpty(filename))
            {
                Error(onError, null, "Upload filename is empty!");
                yield break;
            }

            if (!System.IO.Path.HasExtension(filename))
            {
                Error(onError, null, "Upload filename must have an extension: " + filename);
                yield break;
            }

            string whyNot;
            if (!VRC.Tools.FileCanRead(filename, out whyNot))
            {
                Error(onError, null, "Could not read file to upload!", filename + "\n" + whyNot);
                yield break;
            }

            // get or create ApiFile
            Progress(onProgress, null, string.IsNullOrEmpty(existingFileId) ? "Creating file record..." : "Getting file record...");

            bool wait = true;
            bool wasError = false;
            string errorStr = "";

            if (string.IsNullOrEmpty(friendlyName))
                friendlyName = filename;

            string extension = System.IO.Path.GetExtension(filename);
            string mimeType = GetMimeTypeFromExtension(extension);

            ApiFile apiFile = null;

            while (true)
            {
                apiFile = null;
                wait = true;
                errorStr = "";

                if (string.IsNullOrEmpty(existingFileId))
                {
                    ApiFile.Create(friendlyName, mimeType, extension,
                        delegate (ApiFile file)
                        {
                            apiFile = file;
                            wait = false;
                        },
                        delegate (string error)
                        {
                            errorStr = error;
                            wait = false;
                        }
                    );
                }
                else
                {
                    ApiFile.Get(existingFileId,
                        delegate (ApiFile file)
                        {
                            apiFile = file;
                            wait = false;
                        },
                        delegate (string error)
                        {
                            errorStr = error;
                            wait = false;
                        }
                    );
                }

                while (wait)
                {
                    if (CheckCancelled(cancelQuery, onError, null))
                    {
                        yield break;
                    }
                    yield return null;
                }

                if (!string.IsNullOrEmpty(errorStr))
                {
                    if (errorStr.Contains("File not found"))   // TODO: add status code callback for errors
                    {
                        Debug.LogError("Couldn't find file record: " + existingFileId + ", creating new file record");

                        // create a new file
                        existingFileId = "";
                        continue;
                    }

                    string msg = string.IsNullOrEmpty(existingFileId)
                        ? "Failed to create file record."
                        : "Failed to get file record.";
                    Error(onError, null, msg, errorStr);
                    yield break;
                }

                break;
            }

            if (apiFile == null)
                yield break;

            // delay to let write get through servers
            yield return new WaitForSecondsRealtime(kPostWriteDelay);

            // check for server side errors from last upload
            if (apiFile.IsInErrorState())
            {
                Debug.LogWarning("ApiFile: " + apiFile.id + ": server failed to process last uploaded, deleting failed version");

                // delete previous failed version
                Progress(onProgress, apiFile, "Preparing file for upload...", "Cleaning up previous version");

                wait = true;
                errorStr = "";
                apiFile.DeleteLatestVersion(
                    delegate (ApiFile file)
                    {
                        apiFile = file;
                        wait = false;
                    },
                    delegate (string error)
                    {
                        errorStr = error;
                        wait = false;
                    }
                );

                while (wait)
                {
                    if (CheckCancelled(cancelQuery, onError, null))
                    {
                        yield break;
                    }
                    yield return null;
                }

                if (!string.IsNullOrEmpty(errorStr))
                {
                    Error(onError, apiFile, "Failed to delete previous incomplete version!", errorStr);
                    CleanupTempFiles(apiFile.id);
                    yield break;
                }
            }

            // delay to let write get through servers
            yield return new WaitForSecondsRealtime(kPostWriteDelay);

            // verify previous file op is complete
            if (apiFile.HasQueuedOperation())
            {
                Error(onError, apiFile, "A previous upload is still being processed. Please try again later.");
                yield break;
            }

            // prepare file for upload
            Progress(onProgress, apiFile, "Preparing file for upload...", "Optimizing file");

            string uploadFilename = VRC.Tools.GetTempFileName(Path.GetExtension(filename), out errorStr, apiFile.id);
            if (string.IsNullOrEmpty(uploadFilename))
            {
                Error(onError, apiFile, "Failed to optimize file for upload.", "Failed to create temp file: \n" + errorStr);
                yield break;
            }

            wasError = false;
            yield return StartCoroutine(CreateOptimizedFileInternal(filename, uploadFilename,
                delegate (FileOpResult res)
                {
                    if (res == FileOpResult.Unchanged)
                        uploadFilename = filename;
                },
                delegate (string error)
                {
                    Error(onError, apiFile, "Failed to optimize file for upload.", error);
                    CleanupTempFiles(apiFile.id);
                    wasError = true;
                })
            );

            if (wasError)
                yield break;

            // generate md5 and check if file has changed
            Progress(onProgress, apiFile, "Preparing file for upload...", "Generating file hash");

            string fileMD5Base64 = "";
            wait = true;
            errorStr = "";
            VRC.Tools.FileMD5(uploadFilename,
                delegate (byte[] md5Bytes)
                {
                    fileMD5Base64 = Convert.ToBase64String(md5Bytes);
                    wait = false;
                },
                delegate (string error)
                {
                    errorStr = uploadFilename + "\n" + error;
                    wait = false;
                }
            );

            while (wait)
            {
                if (CheckCancelled(cancelQuery, onError, apiFile))
                {
                    CleanupTempFiles(apiFile.id);
                    yield break;
                }
                yield return null;
            }

            if (!string.IsNullOrEmpty(errorStr))
            {
                Error(onError, apiFile, "Failed to generate MD5 hash for upload file.", errorStr);
                CleanupTempFiles(apiFile.id);
                yield break;
            }

            // check if file has been changed
            Progress(onProgress, apiFile, "Preparing file for upload...", "Checking for changes");

            bool isPreviousUploadRetry = false;
            if (apiFile.HasExistingOrPendingVersion())
            {
                // uploading the same file?
                if (string.Compare(fileMD5Base64, apiFile.GetFileMD5(apiFile.GetLatestVersionNumber())) == 0)
                {
                    // the previous operation completed successfully?
                    if (!apiFile.IsWaitingForUpload())
                    {
                        Success(onSuccess, apiFile, "The file to upload is unchanged.");
                        CleanupTempFiles(apiFile.id);
                        yield break;
                    }
                    else
                    {
                        isPreviousUploadRetry = true;

                        Debug.Log("Retrying previous upload");
                    }
                }
                else
                {
                    // the file has been modified
                    if (apiFile.IsWaitingForUpload())
                    {
                        // previous upload failed, and the file is changed

                        // delete previous failed version
                        Progress(onProgress, apiFile, "Preparing file for upload...", "Cleaning up previous version");

                        wait = true;
                        errorStr = "";
                        apiFile.DeleteLatestVersion(
                            delegate (ApiFile file)
                            {
                                apiFile = file;
                                wait = false;
                            },
                            delegate (string error)
                            {
                                errorStr = error;
                                wait = false;
                            }
                        );

                        while (wait)
                        {
                            if (CheckCancelled(cancelQuery, onError, null))
                            {
                                yield break;
                            }
                            yield return null;
                        }

                        if (!string.IsNullOrEmpty(errorStr))
                        {
                            Error(onError, apiFile, "Failed to delete previous incomplete version!", errorStr);
                            CleanupTempFiles(apiFile.id);
                            yield break;
                        }

                        // delay to let write get through servers
                        yield return new WaitForSecondsRealtime(kPostWriteDelay);
                    }
                }
            }

            // generate signature for new file

            Progress(onProgress, apiFile, "Preparing file for upload...", "Generating signature");

            string signatureFilename = VRC.Tools.GetTempFileName(".sig", out errorStr, apiFile.id);
            if (string.IsNullOrEmpty(signatureFilename))
            {
                Error(onError, apiFile, "Failed to generate file signature!", "Failed to create temp file: \n" + errorStr);
                CleanupTempFiles(apiFile.id);
                yield break;
            }

            wasError = false;
            yield return StartCoroutine(CreateFileSignatureInternal(uploadFilename, signatureFilename,
                delegate ()
                {
                    // success!
                },
                delegate (string error)
                {
                    Error(onError, apiFile, "Failed to generate file signature!", error);
                    CleanupTempFiles(apiFile.id);
                    wasError = true;
                })
            );

            if (wasError)
                yield break;

            // generate signature md5 and file size
            Progress(onProgress, apiFile, "Preparing file for upload...", "Generating signature hash");

            string sigMD5Base64 = "";
            wait = true;
            errorStr = "";
            VRC.Tools.FileMD5(signatureFilename,
                delegate (byte[] md5Bytes)
                {
                    sigMD5Base64 = Convert.ToBase64String(md5Bytes);
                    wait = false;
                },
                delegate (string error)
                {
                    errorStr = signatureFilename + "\n" + error;
                    wait = false;
                }
            );

            while (wait)
            {
                if (CheckCancelled(cancelQuery, onError, apiFile))
                {
                    CleanupTempFiles(apiFile.id);
                    yield break;
                }
                yield return null;
            }

            if (!string.IsNullOrEmpty(errorStr))
            {
                Error(onError, apiFile, "Failed to generate MD5 hash for signature file.", errorStr);
                CleanupTempFiles(apiFile.id);
                yield break;
            }

            long sigFileSize = 0;
            if (!VRC.Tools.GetFileSize(signatureFilename, out sigFileSize, out errorStr))
            {
                Error(onError, apiFile, "Failed to generate file signature!", "Couldn't get file size:\n" + errorStr);
                CleanupTempFiles(apiFile.id);
                yield break;
            }

            // download previous version signature (if exists)
            string existingFileSignaturePath = null;
            if (apiFile.HasExistingVersion())
            {
                Progress(onProgress, apiFile, "Preparing file for upload...", "Downloading previous version signature");

                wait = true;
                errorStr = "";
                apiFile.DownloadSignature(
                    delegate (byte[] data)
                    {
                        // save to temp file
                        existingFileSignaturePath = VRC.Tools.GetTempFileName(".sig", out errorStr, apiFile.id);
                        if (string.IsNullOrEmpty(existingFileSignaturePath))
                        {
                            errorStr = "Failed to create temp file: \n" + errorStr;
                            wait = false;
                        }
                        else
                        {
                            try
                            {
                                File.WriteAllBytes(existingFileSignaturePath, data);
                            }
                            catch (Exception e)
                            {
                                existingFileSignaturePath = null;
                                errorStr = "Failed to write signature temp file:\n" + e.Message;
                            }
                            wait = false;
                        }
                    },
                    delegate (string error)
                    {
                        errorStr = error;
                        wait = false;
                    },
                    delegate (int downloaded, int length)
                    {
                        Progress(onProgress, apiFile, "Preparing file for upload...", "Downloading previous version signature", Tools.DivideSafe(downloaded, length));
                    }
                );

                while (wait)
                {
                    if (CheckCancelled(cancelQuery, onError, apiFile))
                    {
                        CleanupTempFiles(apiFile.id);
                        yield break;
                    }
                    yield return null;
                }

                if (!string.IsNullOrEmpty(errorStr))
                {
                    Error(onError, apiFile, "Failed to download previous file version signature.", errorStr);
                    CleanupTempFiles(apiFile.id);
                    yield break;
                }
            }

            // create delta if needed
            string deltaFilename = null;

            if (!string.IsNullOrEmpty(existingFileSignaturePath))
            {
                Progress(onProgress, apiFile, "Preparing file for upload...", "Creating file delta");

                deltaFilename = VRC.Tools.GetTempFileName(".delta", out errorStr, apiFile.id);
                if (string.IsNullOrEmpty(deltaFilename))
                {
                    Error(onError, apiFile, "Failed to create file delta for upload.", "Failed to create temp file: \n" + errorStr);
                    CleanupTempFiles(apiFile.id);
                    yield break;
                }

                wasError = false;
                yield return StartCoroutine(CreateFileDeltaInternal(uploadFilename, existingFileSignaturePath, deltaFilename,
                    delegate ()
                    {
                        // success!
                    },
                    delegate (string error)
                    {
                        Error(onError, apiFile, "Failed to create file delta for upload.", error);
                        CleanupTempFiles(apiFile.id);
                        wasError = true;
                    })
                );

                if (wasError)
                    yield break;
            }

            // upload smaller of delta and new file
            long fullFizeSize = 0;
            long deltaFileSize = 0;
            if (!VRC.Tools.GetFileSize(uploadFilename, out fullFizeSize, out errorStr) ||
                (!string.IsNullOrEmpty(deltaFilename) && !VRC.Tools.GetFileSize(deltaFilename, out deltaFileSize, out errorStr)))
            {
                Error(onError, apiFile, "Failed to create file delta for upload.", "Couldn't get file size: " + errorStr);
                CleanupTempFiles(apiFile.id);
                yield break;
            }

            bool uploadDeltaFile = deltaFileSize > 0 && deltaFileSize < fullFizeSize;
            Debug.Log("Delta size " + deltaFileSize + " (" + ((float)deltaFileSize / (float)fullFizeSize) + " %), full file size " + fullFizeSize + ", uploading " + (uploadDeltaFile ? " DELTA" : " FULL FILE"));

            string deltaMD5Base64 = "";
            if (uploadDeltaFile)
            {
                Progress(onProgress, apiFile, "Preparing file for upload...", "Generating file delta hash");

                wait = true;
                errorStr = "";
                VRC.Tools.FileMD5(deltaFilename,
                    delegate (byte[] md5Bytes)
                    {
                        deltaMD5Base64 = Convert.ToBase64String(md5Bytes);
                        wait = false;
                    },
                    delegate (string error)
                    {
                        errorStr = error;
                        wait = false;
                    }
                );

                while (wait)
                {
                    if (CheckCancelled(cancelQuery, onError, apiFile))
                    {
                        CleanupTempFiles(apiFile.id);
                        yield break;
                    }
                    yield return null;
                }

                if (!string.IsNullOrEmpty(errorStr))
                {
                    Error(onError, apiFile, "Failed to generate file delta hash.", errorStr);
                    CleanupTempFiles(apiFile.id);
                    yield break;
                }
            }

            // validate existing pending version info, if this is a retry
            bool versionAlreadyExists = false;

            if (isPreviousUploadRetry)
            {
                bool isValid = true;

                ApiFile.Version v = apiFile.GetVersion(apiFile.GetLatestVersionNumber());
                if (v != null)
                {
                    if (uploadDeltaFile)
                    {
                        isValid = deltaFileSize == v.delta.sizeInBytes &&
                            deltaMD5Base64.CompareTo(v.delta.md5) == 0 &&
                            sigFileSize == v.signature.sizeInBytes &&
                            sigMD5Base64.CompareTo(v.signature.md5) == 0;
                    }
                    else
                    {
                        isValid = fullFizeSize == v.file.sizeInBytes &&
                            fileMD5Base64.CompareTo(v.file.md5) == 0 &&
                            sigFileSize == v.signature.sizeInBytes &&
                            sigMD5Base64.CompareTo(v.signature.md5) == 0;
                    }
                }
                else
                {
                    isValid = false;
                }

                if (isValid)
                {
                    versionAlreadyExists = true;

                    Debug.Log("Using existing version record");
                }
                else
                {
                    // delete previous invalid version
                    Progress(onProgress, apiFile, "Preparing file for upload...", "Cleaning up previous version");

                    wait = true;
                    errorStr = "";
                    apiFile.DeleteLatestVersion(
                        delegate (ApiFile file)
                        {
                            apiFile = file;
                            wait = false;
                        },
                        delegate (string error)
                        {
                            errorStr = error;
                            wait = false;
                        }
                    );

                    while (wait)
                    {
                        if (CheckCancelled(cancelQuery, onError, null))
                        {
                            yield break;
                        }
                        yield return null;
                    }

                    if (!string.IsNullOrEmpty(errorStr))
                    {
                        Error(onError, apiFile, "Failed to delete previous incomplete version!", errorStr);
                        CleanupTempFiles(apiFile.id);
                        yield break;
                    }

                    // delay to let write get through servers
                    yield return new WaitForSecondsRealtime(kPostWriteDelay);
                }
            }

            // create new version of file
            if (!versionAlreadyExists)
            {
                Progress(onProgress, apiFile, "Creating file version record...");

                wait = true;
                errorStr = "";
                if (uploadDeltaFile)
                {
                    // delta file
                    apiFile.CreateNewVersion(ApiFile.Version.FileType.Delta, deltaMD5Base64, deltaFileSize, sigMD5Base64, sigFileSize,
                        delegate (ApiFile file)
                        {
                            apiFile = file;
                            wait = false;
                        },
                        delegate (string error)
                        {
                            errorStr = error;
                            wait = false;
                        }
                    );
                }
                else
                {
                    // full file
                    apiFile.CreateNewVersion(ApiFile.Version.FileType.Full, fileMD5Base64, fullFizeSize, sigMD5Base64, sigFileSize,
                        delegate (ApiFile file)
                        {
                            apiFile = file;
                            wait = false;
                        },
                        delegate (string error)
                        {
                            errorStr = error;
                            wait = false;
                        }
                    );
                }

                while (wait)
                {
                    if (CheckCancelled(cancelQuery, onError, apiFile))
                    {
                        CleanupTempFiles(apiFile.id);
                        yield break;
                    }
                    yield return null;
                }

                if (!string.IsNullOrEmpty(errorStr))
                {
                    Error(onError, apiFile, "Failed to create file version record.", errorStr);
                    CleanupTempFiles(apiFile.id);
                    yield break;
                }

                // delay to let write get through servers
                yield return new WaitForSecondsRealtime(kPostWriteDelay);
            }

            // upload components

            // upload delta
            if (uploadDeltaFile)
            {
                if (apiFile.GetLatestVersion().delta.status == ApiFile.Status.waiting)
                {
                    Progress(onProgress, apiFile, "Uploading file delta...");

                    wasError = false;
                    yield return StartCoroutine(UploadFileComponentInternal(apiFile,
                        ApiFile.Version.FileDescriptor.Type.delta, deltaFilename, deltaMD5Base64, deltaFileSize,
                        delegate (ApiFile file)
                        {
                            Debug.Log("Successfully uploaded file delta.");
                            apiFile = file;
                        },
                        delegate (string error)
                        {
                            Error(onError, apiFile, "Failed to upload file delta.", error);
                            CleanupTempFiles(apiFile.id);
                            wasError = true;
                        },
                        delegate (long downloaded, long length)
                        {
                            Progress(onProgress, apiFile, "Uploading file delta...", "", Tools.DivideSafe(downloaded, length));
                        },
                        cancelQuery)
                    );

                    if (wasError)
                        yield break;
                }
            }

            // upload file
            else
            {
                if (apiFile.GetLatestVersion().file.status == ApiFile.Status.waiting)
                {
                    Progress(onProgress, apiFile, "Uploading file...");

                    wasError = false;
                    yield return StartCoroutine(UploadFileComponentInternal(apiFile,
                        ApiFile.Version.FileDescriptor.Type.file, uploadFilename, fileMD5Base64, fullFizeSize,
                        delegate (ApiFile file)
                        {
                            Debug.Log("Successfully uploaded file.");
                            apiFile = file;
                        },
                        delegate (string error)
                        {
                            Error(onError, apiFile, "Failed to upload file.", error);
                            CleanupTempFiles(apiFile.id);
                            wasError = true;
                        },
                        delegate (long downloaded, long length)
                        {
                            Progress(onProgress, apiFile, "Uploading file...", "", Tools.DivideSafe(downloaded, length));
                        },
                        cancelQuery)
                    );

                    if (wasError)
                        yield break;
                }
            }

            // upload signature
            if (apiFile.GetLatestVersion().signature.status == ApiFile.Status.waiting)
            {
                Progress(onProgress, apiFile, "Uploading file signature...");

                wasError = false;
                yield return StartCoroutine(UploadFileComponentInternal(apiFile,
                    ApiFile.Version.FileDescriptor.Type.signature, signatureFilename, sigMD5Base64, sigFileSize,
                    delegate (ApiFile file)
                    {
                        Debug.Log("Successfully uploaded file signature.");
                        apiFile = file;
                    },
                    delegate (string error)
                    {
                        Error(onError, apiFile, "Failed to upload file signature.", error);
                        CleanupTempFiles(apiFile.id);
                        wasError = true;
                    },
                    delegate (long downloaded, long length)
                    {
                        Progress(onProgress, apiFile, "Uploading file signature...", "", Tools.DivideSafe(downloaded, length));
                    },
                    cancelQuery)
                );

                if (wasError)
                    yield break;
            }

            // Validate file records queued or complete
            Progress(onProgress, apiFile, "Validating upload...");

            bool isUploadComplete = (uploadDeltaFile
                ? apiFile.GetFileDescriptor(apiFile.GetLatestVersionNumber(), ApiFile.Version.FileDescriptor.Type.delta).status == ApiFile.Status.complete
                : apiFile.GetFileDescriptor(apiFile.GetLatestVersionNumber(), ApiFile.Version.FileDescriptor.Type.file).status == ApiFile.Status.complete);
            isUploadComplete = isUploadComplete &&
                               apiFile.GetFileDescriptor(apiFile.GetLatestVersionNumber(), ApiFile.Version.FileDescriptor.Type.signature).status == ApiFile.Status.complete;

            if (!isUploadComplete)
            {
                Error(onError, apiFile, "Failed to upload file.", "Record status is not 'complete'");
                CleanupTempFiles(apiFile.id);
                yield break;
            }

            bool isServerOpQueuedOrComplete = (uploadDeltaFile
                ? apiFile.GetFileDescriptor(apiFile.GetLatestVersionNumber(), ApiFile.Version.FileDescriptor.Type.file).status != ApiFile.Status.waiting
                : apiFile.GetFileDescriptor(apiFile.GetLatestVersionNumber(), ApiFile.Version.FileDescriptor.Type.delta).status != ApiFile.Status.waiting);

            if (!isServerOpQueuedOrComplete)
            {
                Error(onError, apiFile, "Failed to upload file.", "Record is still in 'waiting' status");
                CleanupTempFiles(apiFile.id);
                yield break;
            }

            // wait for server processing to complete
            Progress(onProgress, apiFile, "Processing upload...");
            float checkDelay = SERVER_PROCESSING_INITIAL_RETRY_TIME;
            float maxDelay = SERVER_PROCESSING_MAX_RETRY_TIME;
            float timeout = GetServerProcessingWaitTimeoutForDataSize(apiFile.GetLatestVersion().file.sizeInBytes);
            double initialStartTime = Time.realtimeSinceStartup;
            double startTime = initialStartTime;
            while (apiFile.HasQueuedOperation())
            {
                // wait before polling again
                Progress(onProgress, apiFile, "Processing upload...", "Checking status in " + Mathf.CeilToInt(checkDelay) + " seconds");
                while (Time.realtimeSinceStartup - startTime < checkDelay)
                {
                    if (CheckCancelled(cancelQuery, onError, apiFile))
                    {
                        CleanupTempFiles(apiFile.id);
                        yield break;
                    }

                    if (Time.realtimeSinceStartup - initialStartTime > timeout)
                    {
                        Error(onError, apiFile, "Timed out waiting for upload processing to complete.");
                        CleanupTempFiles(apiFile.id);
                        yield break;
                    }

                    yield return null;
                }

                // check status
                Progress(onProgress, apiFile, "Processing upload...", "Checking status...");

                wait = true;
                errorStr = "";
                ApiFile.Get(apiFile.id,
                    delegate (ApiFile file)
                    {
                        apiFile = file;
                        wait = false;
                    },
                    delegate (string error)
                    {
                        errorStr = error;
                        wait = false;
                    });

                while (wait)
                {
                    if (CheckCancelled(cancelQuery, onError, apiFile))
                    {
                        CleanupTempFiles(apiFile.id);
                        yield break;
                    }

                    yield return null;
                }

                if (!string.IsNullOrEmpty(errorStr))
                {
                    Error(onError, apiFile, "Checking upload status failed.", errorStr);
                    CleanupTempFiles(apiFile.id);
                    yield break;
                }

                checkDelay = Mathf.Min(checkDelay * 2, maxDelay);
                startTime = Time.realtimeSinceStartup;
            }

            // cleanup and wait for it to finish
            yield return StartCoroutine(CleanupTempFilesInternal(apiFile.id));

            Success(onSuccess, apiFile, "Upload complete!");
        }

        public IEnumerator CreateOptimizedFileInternal(string filename, string outputFilename, Action<FileOpResult> onSuccess, Action<string> onError)
        {
            Debug.Log("CreateOptimizedFile: " + filename + " => " + outputFilename);

            // assume it's a .gz, or a .unitypackage
            // else nothing to do

            if (!IsGZipCompressed(filename))
            {
                Debug.Log("CreateOptimizedFile: (not gzip compressed, done)");
                // nothing to do
                if (onSuccess != null)
                    onSuccess(FileOpResult.Unchanged);
                yield break;
            }

            bool isUnityPackage = string.Compare(Path.GetExtension(filename), ".unitypackage", true) == 0;

            yield return null;

            // open file
            const int kGzipBufferSize = 256 * 1024;
            Stream inStream = null;
            try
            {
                inStream = new DotZLib.GZipStream(filename, kGzipBufferSize);
            }
            catch (Exception e)
            {
                if (onError != null)
                    onError("Couldn't read file: " + filename + "\n" + e.Message);
                yield break;
            }

            yield return null;

            // create output
            DotZLib.GZipStream outStream = null;    
            try
            {
                outStream = new DotZLib.GZipStream(outputFilename, DotZLib.CompressLevel.Best, true, kGzipBufferSize);    // this lib supports rsyncable output
            }
            catch (Exception e)
            {
                if (inStream != null)
                    inStream.Close();
                if (onError != null)
                    onError("Couldn't create output file: " + outputFilename + "\n" + e.Message);
                yield break;
            }

            yield return null;

            // copy / filter file
            if (isUnityPackage)
            {
                try
                {
                    // discard files in the package we don't need

                    // scan package and make list of asset guids we don't want
                    List<string> assetGuidsToStrip = new List<string>();
                    {
                        byte[] filenameBuf = new byte[4096];
                        ICSharpCode.SharpZipLib.Tar.TarInputStream tarInputStream = new ICSharpCode.SharpZipLib.Tar.TarInputStream(inStream);
                        ICSharpCode.SharpZipLib.Tar.TarEntry tarEntry = tarInputStream.GetNextEntry();
                        while (tarEntry != null)
                        {
                            if (tarEntry.Size > 0 && tarEntry.Name.EndsWith("/pathname", StringComparison.OrdinalIgnoreCase))
                            {
                                int bytesRead = tarInputStream.Read(filenameBuf, 0, (int)tarEntry.Size);
                                if (bytesRead > 0)
                                {
                                    string assetFilename = System.Text.ASCIIEncoding.ASCII.GetString(filenameBuf, 0, bytesRead);
                                    if (kUnityPackageAssetNameFilters.Any(r => r.IsMatch(assetFilename)))
                                    {
                                        string assetGuid = assetFilename.Substring(0, assetFilename.IndexOf('/'));
                                        // Debug.Log("-- stripped file from package: " + assetGuid + " - " + assetFilename);
                                        assetGuidsToStrip.Add(assetGuid);
                                    }
                                }
                            }

                            tarEntry = tarInputStream.GetNextEntry();
                        }

                        tarInputStream.Close();
                    }

                    // rescan input .tar and copy only entries we want to the output
                    {
                        inStream.Close();
                        inStream = new DotZLib.GZipStream(filename, kGzipBufferSize);

                        ICSharpCode.SharpZipLib.Tar.TarOutputStream tarOutputStream = new ICSharpCode.SharpZipLib.Tar.TarOutputStream(outStream);

                        ICSharpCode.SharpZipLib.Tar.TarInputStream tarInputStream = new ICSharpCode.SharpZipLib.Tar.TarInputStream(inStream);
                        ICSharpCode.SharpZipLib.Tar.TarEntry tarEntry = tarInputStream.GetNextEntry();
                        while (tarEntry != null)
                        {
                            string assetGuid = tarEntry.Name.Substring(0, tarEntry.Name.IndexOf('/'));
                            bool strip = assetGuidsToStrip.Any(s => string.Compare(s, assetGuid) == 0);
                            if (!strip)
                            {
                                tarOutputStream.PutNextEntry(tarEntry);
                                tarInputStream.CopyEntryContents(tarOutputStream);
                                tarOutputStream.CloseEntry();
                            }

                            tarEntry = tarInputStream.GetNextEntry();
                        }

                        tarInputStream.Close();
                        tarOutputStream.Close();
                    }
                }
                catch (Exception e)
                {
                    if (inStream != null)
                        inStream.Close();
                    if (outStream != null)
                        outStream.Close();
                    if (onError != null)
                        onError("Failed to strip and recompress file." + "\n" + e.Message);
                    yield break;
                }
            }
            else
            {
                // not a unitypackage 

                // straight stream copy
                try
                {
                    const int bufSize = 256 * 1024;
                    byte[] buf = new byte[bufSize];
                    ICSharpCode.SharpZipLib.Core.StreamUtils.Copy(inStream, outStream, buf);
                }
                catch (Exception e)
                {
                    if (inStream != null)
                        inStream.Close();
                    if (outStream != null)
                        outStream.Close();
                    if (onError != null)
                        onError("Failed to recompress file." + "\n" + e.Message);
                    yield break;
                }
            }

            yield return null;

            if (inStream != null)
                inStream.Close();
            inStream = null;
            if (outStream != null)
                outStream.Close();
            outStream = null;

            yield return null;

            if (onSuccess != null)
                onSuccess(FileOpResult.Success);
        }

        public IEnumerator CreateFileSignatureInternal(string filename, string outputSignatureFilename, Action onSuccess, Action<string> onError)
        {
            Debug.Log("CreateFileSignature: " + filename + " => " + outputSignatureFilename);

            yield return null;

            Stream inStream = null;
            FileStream outStream = null;
            byte[] buf = new byte[64 * 1024];
            IAsyncResult asyncRead = null;
            IAsyncResult asyncWrite = null;

            try
            {
                inStream = librsync.net.Librsync.ComputeSignature(File.OpenRead(filename));
            }
            catch (Exception e)
            {
                if (onError != null)
                    onError("Couldn't open input file: " + e.Message);
                yield break;
            }

            try
            {
                outStream = File.Open(outputSignatureFilename, FileMode.Create, FileAccess.Write);
            }
            catch (Exception e)
            {
                if (onError != null)
                    onError("Couldn't create output file: " + e.Message);
                yield break;
            }

            while (true)
            {
                try
                {
                    asyncRead = inStream.BeginRead(buf, 0, buf.Length, null, null);
                }
                catch (Exception e)
                {
                    if (onError != null)
                        onError("Couldn't read file: " + e.Message);
                    yield break;
                }

                while (!asyncRead.IsCompleted)
                    yield return null;

                int read = 0;
                try
                {
                    read = inStream.EndRead(asyncRead);
                }
                catch (Exception e)
                {
                    if (onError != null)
                        onError("Couldn't read file: " + e.Message);
                    yield break;
                }

                if (read <= 0)
                    break;

                try
                {
                    asyncWrite = outStream.BeginWrite(buf, 0, read, null, null);
                }
                catch (Exception e)
                {
                    if (onError != null)
                        onError("Couldn't write file: " + e.Message);
                    yield break;
                }

                while (!asyncWrite.IsCompleted)
                    yield return null;

                try
                {
                    outStream.EndWrite(asyncWrite);
                }
                catch (Exception e)
                {
                    if (onError != null)
                        onError("Couldn't write file: " + e.Message);
                    yield break;
                }
            }

            inStream.Close();
            outStream.Close();

            yield return null;

            if (onSuccess != null)
                onSuccess();
        }

        public IEnumerator CreateFileDeltaInternal(string newFilename, string existingFileSignaturePath, string outputDeltaFilename, Action onSuccess, Action<string> onError)
        {
            Debug.Log("CreateFileDelta: " + newFilename + " (delta) " + existingFileSignaturePath + " => " + outputDeltaFilename);

            yield return null;

            Stream inStream = null;
            FileStream outStream = null;
            byte[] buf = new byte[64 * 1024];
            IAsyncResult asyncRead = null;
            IAsyncResult asyncWrite = null;

            try
            {
                inStream = librsync.net.Librsync.ComputeDelta(File.OpenRead(existingFileSignaturePath), File.OpenRead(newFilename));
            }
            catch (Exception e)
            {
                if (onError != null)
                    onError("Couldn't open input file: " + e.Message);
                yield break;
            }

            try
            {
                outStream = File.Open(outputDeltaFilename, FileMode.Create, FileAccess.Write);
            }
            catch (Exception e)
            {
                if (onError != null)
                    onError("Couldn't create output file: " + e.Message);
                yield break;
            }

            while (true)
            {
                try
                {
                    asyncRead = inStream.BeginRead(buf, 0, buf.Length, null, null);
                }
                catch (Exception e)
                {
                    if (onError != null)
                        onError("Couldn't read file: " + e.Message);
                    yield break;
                }

                while (!asyncRead.IsCompleted)
                    yield return null;

                int read = 0;
                try
                {
                    read = inStream.EndRead(asyncRead);
                }
                catch (Exception e)
                {
                    if (onError != null)
                        onError("Couldn't read file: " + e.Message);
                    yield break;
                }

                if (read <= 0)
                    break;

                try
                {
                    asyncWrite = outStream.BeginWrite(buf, 0, read, null, null);
                }
                catch (Exception e)
                {
                    if (onError != null)
                        onError("Couldn't write file: " + e.Message);
                    yield break;
                }

                while (!asyncWrite.IsCompleted)
                    yield return null;

                try
                {
                    outStream.EndWrite(asyncWrite);
                }
                catch (Exception e)
                {
                    if (onError != null)
                        onError("Couldn't write file: " + e.Message);
                    yield break;
                }
            }

            inStream.Close();
            outStream.Close();

            yield return null;

            if (onSuccess != null)
                onSuccess();
        }

        protected static void Success(OnFileOpSuccess onSuccess, ApiFile apiFile, string message)
        {
            if (apiFile == null)
                apiFile = new ApiFile();

            Debug.Log("ApiFile " + apiFile.ToStringBrief() + ": Operation Succeeded!");
            if (onSuccess != null)
                onSuccess(apiFile, message);
        }

        protected static void Error(OnFileOpError onError, ApiFile apiFile, string error, string moreInfo = "")
        {
            if (apiFile == null)
                apiFile = new ApiFile();

            Debug.LogError("ApiFile " + apiFile.ToStringBrief() + ": Error: " + error + "\n" + moreInfo);
            if (onError != null)
                onError(apiFile, error);
        }

        protected static void Progress(OnFileOpProgress onProgress, ApiFile apiFile, string status, string subStatus = "", float pct = 0.0f)
        {
            if (apiFile == null)
                apiFile = new ApiFile();

            Debug.Log("ApiFile " + apiFile.ToStringBrief() + ": " + status + " (" + subStatus + ") " + (pct * 100).ToString("g2") + "%");
            if (onProgress != null)
                onProgress(apiFile, status, subStatus, pct);
        }

        protected static bool CheckCancelled(FileOpCancelQuery cancelQuery, OnFileOpError onError, ApiFile apiFile)
        {
            if (apiFile == null)
                apiFile = new ApiFile();

            if (cancelQuery != null && cancelQuery(apiFile))
            {
                Debug.Log("ApiFile " + apiFile.ToStringBrief() + ": Operation cancelled");
                if (onError != null)
                    onError(apiFile, "Cancelled by user.");
                return true;
            }
            return false;
        }

        protected static void CleanupTempFiles(string subFolderName)
        {
            Instance.StartCoroutine(Instance.CleanupTempFilesInternal(subFolderName));
        }

        protected IEnumerator CleanupTempFilesInternal(string subFolderName)
        {
            if (!string.IsNullOrEmpty(subFolderName))
            {
                string folder = VRC.Tools.GetTempFolderPath(subFolderName);

                while (Directory.Exists(folder))
                {
                    try
                    {
                        if (Directory.Exists(folder))
                            Directory.Delete(folder, true);
                    }
                    catch (System.Exception)
                    {
                    }

                    yield return null;
                }
            }
        }

        private static void CheckInstance()
        {
            if (mInstance == null)
            {
                GameObject go = new GameObject("ApiFileHelper");
                mInstance = go.AddComponent<ApiFileHelper>();

                try
                {
                    GameObject.DontDestroyOnLoad(go);
                }
                catch
                {
                }
            }
        }

        private IEnumerator UploadFileComponentInternal(ApiFile apiFile, ApiFile.Version.FileDescriptor.Type fileDescriptorType, string filename, string md5Base64, long fileSize, Action<ApiFile> onSuccess, Action<string> onError, Action<long, long> onProgess, FileOpCancelQuery cancelQuery)
        {
            Debug.Log("UploadFileComponent: " + fileDescriptorType + " (" + apiFile.id + "): " + filename);
            var fileDesc = apiFile.GetFileDescriptor(apiFile.GetLatestVersionNumber(), fileDescriptorType);

            // validate
            {
                if (fileDesc.status != ApiFile.Status.waiting)
                {
                    // nothing to do (might be a retry)
                    Debug.Log("UploadFileComponent: (file record not in waiting status, done)");
                    if (onSuccess != null)
                        onSuccess(apiFile);
                    yield break;
                }

                if (fileSize != fileDesc.sizeInBytes)
                {
                    if (onError != null)
                        onError("File size does not match version descriptor");
                    yield break;
                }
                if (string.Compare(md5Base64, fileDesc.md5) != 0)
                {
                    if (onError != null)
                        onError("File MD5 does not match version descriptor");
                    yield break;
                }

                // make sure file is right size
                long tempSize = 0;
                string errorStr = "";
                if (!VRC.Tools.GetFileSize(filename, out tempSize, out errorStr))
                {
                    if (onError != null)
                        onError("Couldn't get file size");
                    yield break;
                }
                if (tempSize != fileSize)
                {
                    if (onError != null)
                        onError("File size does not match input size");
                    yield break;
                }
            }

            // simple upload
            if (fileDesc.category == ApiFile.Category.simple)
            {
                OnFileOpError onCancelFunc = delegate (ApiFile file, string s)
                {
                    if (onError != null)
                        onError(s);
                };

                string uploadUrl = "";
                {
                    bool wait = true;
                    string errorStr = "";
                    apiFile.StartSimpleUpload(fileDescriptorType,
                        delegate (string url)
                        {
                            uploadUrl = url;
                            wait = false;
                        },
                        delegate (string error)
                        {
                            errorStr = "Failed to start upload: " + error;
                            wait = false;
                        });

                    while (wait)
                    {
                        if (CheckCancelled(cancelQuery, onCancelFunc, apiFile))
                        {
                            yield break;
                        }
                        yield return null;
                    }

                    if (!string.IsNullOrEmpty(errorStr))
                    {
                        if (onError != null)
                            onError(errorStr);
                        yield break;
                    }

                    // delay to let write get through servers
                    yield return new WaitForSecondsRealtime(kPostWriteDelay);
                }

                // PUT file
                {
                    bool wait = true;
                    string errorStr = "";
                    VRC.HttpRequest req = ApiFile.PutSimpleFileToURL(uploadUrl, filename, GetMimeTypeFromExtension(Path.GetExtension(filename)), md5Base64,
                        delegate ()
                        {
                            wait = false;
                        },
                        delegate (string error)
                        {
                            errorStr = "Failed to upload file: " + error;
                            wait = false;
                        },
                        delegate (long uploaded, long length)
                        {
                            if (onProgess != null)
                                onProgess(uploaded, length);
                        }
                    );

                    while (wait)
                    {
                        if (CheckCancelled(cancelQuery, onCancelFunc, apiFile))
                        {
                            if (req != null)
                                req.Abort();
                            yield break;
                        }
                        yield return null;
                    }

                    if (!string.IsNullOrEmpty(errorStr))
                    {
                        if (onError != null)
                            onError(errorStr);
                        yield break;
                    }
                }

                // finish upload
                {
                    bool wait = true;
                    string errorStr = "";
                    apiFile.FinishUpload(fileDescriptorType, null,
                        delegate (ApiFile file)
                        {
                            apiFile = file;
                            wait = false;
                        },
                        delegate (string error)
                        {
                            errorStr = "Failed to finish upload: " + error;
                            wait = false;
                        });

                    while (wait)
                    {
                        if (CheckCancelled(cancelQuery, onCancelFunc, apiFile))
                        {
                            yield break;
                        }
                        yield return null;
                    }

                    if (!string.IsNullOrEmpty(errorStr))
                    {
                        if (onError != null)
                            onError(errorStr);
                        yield break;
                    }

                    // delay to let write get through servers
                    yield return new WaitForSecondsRealtime(kPostWriteDelay);
                }
            }

            // multipart upload
            else if (fileDesc.category == ApiFile.Category.multipart)
            {
                FileStream fs = null;
                OnFileOpError onCancelFunc = delegate (ApiFile file, string s)
                {
                    fs.Close();
                    if (onError != null)
                        onError(s);
                };

                // query multipart upload status.
                // we might be resuming a previous upload
                ApiFile.UploadStatus uploadStatus = null;
                {
                    bool wait = true;
                    string errorStr = "";
                    apiFile.GetUploadStatus(apiFile.GetLatestVersionNumber(), fileDescriptorType,
                       delegate (ApiFile.UploadStatus status)
                       {
                           uploadStatus = status;
                           wait = false;

                           Debug.Log("Found existing multipart upload status (next part = " + uploadStatus.nextPartNumber + ")");
                       },
                       delegate (string error)
                       {
                           if (!error.Contains("NoSuchUpload"))
                           {
                               errorStr = "Failed to query multipart upload status: " + error;
                               wait = false;
                           }
                       });

                    while (wait)
                    {
                        if (CheckCancelled(cancelQuery, onCancelFunc, apiFile))
                        {
                            yield break;
                        }
                        yield return null;
                    }

                    if (!string.IsNullOrEmpty(errorStr))
                    {
                        if (onError != null)
                            onError(errorStr);
                        yield break;
                    }
                }

                // split file into chunks
                try
                {
                    fs = File.OpenRead(filename);
                }
                catch (Exception e)
                {
                    if (onError != null)
                        onError("Couldn't open file: " + e.Message);
                    yield break;
                }

                byte[] buffer = new byte[kMultipartUploadChunkSize * 2];

                long totalBytesUploaded = 0;
                List<string> etags = new List<string>();
                if (uploadStatus != null)
                    etags = uploadStatus.etags.ToList();

                int numParts = Mathf.Max(1, Mathf.FloorToInt((float)fs.Length / (float)kMultipartUploadChunkSize));
                for (int partNumber = 1; partNumber <= numParts; partNumber++)
                {
                    // read chunk
                    int bytesToRead = partNumber < numParts ? kMultipartUploadChunkSize : (int)(fs.Length - fs.Position);
                    int bytesRead = 0;
                    try
                    {
                        bytesRead = fs.Read(buffer, 0, bytesToRead);
                    }
                    catch (Exception e)
                    {
                        fs.Close();
                        if (onError != null)
                            onError("Couldn't read file: " + e.Message);
                        yield break;
                    }

                    if (bytesRead != bytesToRead)
                    {
                        fs.Close();
                        if (onError != null)
                            onError("Couldn't read file: read incorrect number of bytes from stream");
                        yield break;
                    }

                    // check if this part has been upload already
                    // NOTE: uploadStatus.nextPartNumber == number of parts already uploaded
                    if (uploadStatus != null && partNumber <= uploadStatus.nextPartNumber)
                    {
                        totalBytesUploaded += bytesRead;
                        continue;
                    }

                    // start upload
                    string uploadUrl = "";
                    {
                        bool wait = true;
                        string errorStr = "";
                        apiFile.StartMultipartUpload(fileDescriptorType, partNumber,
                            delegate (string url)
                            {
                                uploadUrl = url;
                                wait = false;
                            },
                            delegate (string error)
                            {
                                errorStr = "Failed to start part upload: " + error;
                                wait = false;
                            });

                        while (wait)
                        {
                            if (CheckCancelled(cancelQuery, onCancelFunc, apiFile))
                            {
                                yield break;
                            }
                            yield return null;
                        }

                        if (!string.IsNullOrEmpty(errorStr))
                        {
                            fs.Close();
                            if (onError != null)
                                onError(errorStr);
                            yield break;
                        }

                        // delay to let write get through servers
                        yield return new WaitForSecondsRealtime(kPostWriteDelay);
                    }

                    // PUT file part
                    {
                        bool wait = true;
                        string errorStr = "";
                        VRC.HttpRequest req = ApiFile.PutMultipartDataToURL(uploadUrl, buffer, bytesRead, GetMimeTypeFromExtension(Path.GetExtension(filename)),
                            delegate (string etag)
                            {
                                if (!string.IsNullOrEmpty(etag))
                                    etags.Add(etag);
                                totalBytesUploaded += bytesRead;
                                wait = false;
                            },
                            delegate (string error)
                            {
                                errorStr = "Failed to upload data: " + error;
                                wait = false;
                            },
                            delegate (long uploaded, long length)
                            {
                                if (onProgess != null)
                                    onProgess(totalBytesUploaded + uploaded, fileSize);
                            }
                        );

                        while (wait)
                        {
                            if (CheckCancelled(cancelQuery, onCancelFunc, apiFile))
                            {
                                if (req != null)
                                    req.Abort();
                                yield break;
                            }
                            yield return null;
                        }

                        if (!string.IsNullOrEmpty(errorStr))
                        {
                            fs.Close();
                            if (onError != null)
                                onError(errorStr);
                            yield break;
                        }
                    }
                }

                // finish upload
                {
                    bool wait = true;
                    string errorStr = "";
                    apiFile.FinishUpload(fileDescriptorType, etags,
                        delegate (ApiFile file)
                        {
                            apiFile = file;
                            wait = false;
                        },
                        delegate (string error)
                        {
                            errorStr = "Failed to finish upload: " + error;
                            wait = false;
                        });

                    while (wait)
                    {
                        if (CheckCancelled(cancelQuery, onCancelFunc, apiFile))
                        {
                            yield break;
                        }
                        yield return null;
                    }

                    if (!string.IsNullOrEmpty(errorStr))
                    {
                        fs.Close();
                        if (onError != null)
                            onError(errorStr);
                        yield break;
                    }

                    // delay to let write get through servers
                    yield return new WaitForSecondsRealtime(kPostWriteDelay);
                }

                fs.Close();
            }
            else
            {
                if (onError != null)
                    onError("Unknown file category type: " + fileDesc.category);
                yield break;
            }

            // verify status of file record
            {
                OnFileOpError onCancelFunc = delegate (ApiFile file, string s)
                {
                    if (onError != null)
                        onError(s);
                };

                float initialStartTime = Time.realtimeSinceStartup;
                float startTime = initialStartTime;
                float timeout = GetServerProcessingWaitTimeoutForDataSize(fileDesc.sizeInBytes);
                float waitDelay = SERVER_PROCESSING_INITIAL_RETRY_TIME;
                float maxDelay = SERVER_PROCESSING_MAX_RETRY_TIME;
                while (true)
                {
                    var desc = apiFile.GetFileDescriptor(apiFile.GetLatestVersionNumber(), fileDescriptorType);
                    if (desc == null)
                    {
                        if (onError != null)
                            onError("File descriptor is null ('" + fileDescriptorType + "')");
                        yield break;
                    }

                    if (desc.status != ApiFile.Status.waiting)
                    {
                        // upload completed or is processing
                        break;
                    }

                    // wait for next poll 
                    while (Time.realtimeSinceStartup - startTime < waitDelay)
                    {
                        if (CheckCancelled(cancelQuery, onCancelFunc, apiFile))
                        {
                            yield break;
                        }

                        if (Time.realtimeSinceStartup - initialStartTime > timeout)
                        {
                            if (onError != null)
                                onError("Couldn't verify upload status: Timed out wait for server processing");
                            yield break;
                        }

                        yield return null;
                    }

                    bool wait = true;
                    string errorStr = "";
                    apiFile.Refresh(
                        delegate (ApiFile file)
                        {
                            wait = false;
                        },
                        delegate (string error)
                        {
                            errorStr = "Couldn't verify upload status: " + error;
                            wait = false;
                        });

                    while (wait)
                    {
                        if (CheckCancelled(cancelQuery, onCancelFunc, apiFile))
                        {
                            yield break;
                        }
                        yield return null;
                    }

                    if (!string.IsNullOrEmpty(errorStr))
                    {
                        if (onError != null)
                            onError(errorStr);
                        yield break;
                    }

                    waitDelay = Mathf.Min(waitDelay * 2, maxDelay);
                    startTime = Time.realtimeSinceStartup;
                }
            }

            if (onSuccess != null)
                onSuccess(apiFile);
        }

        private float GetServerProcessingWaitTimeoutForDataSize(int size)
        {
            float timeoutMultiplier = Mathf.Ceil((float)size / (float)SERVER_PROCESSING_WAIT_TIMEOUT_CHUNK_SIZE);
            return Mathf.Clamp(timeoutMultiplier * SERVER_PROCESSING_WAIT_TIMEOUT_PER_CHUNK_SIZE, SERVER_PROCESSING_WAIT_TIMEOUT_PER_CHUNK_SIZE, SERVER_PROCESSING_MAX_WAIT_TIMEOUT);
        }
    }
}
