public async Task <BaseAPIResponse> SendSuccess(FGORegion region, int language, TranslationInstallType installType, int groupId, bool success, string errorMessage, bool isAndroid11Install)
        {
            var request = new RestRequest("translate/result")
            {
                Method = Method.POST
            };

            if (!Preferences.ContainsKey("InstallID"))
            {
                Preferences.Set("InstallID", Guid.NewGuid().ToString());
            }

            request.AddHeader("Content-type", "application/json");
            var sendObject = new Dictionary <string, object>()
            {
                { "region", (int)region },
                { "successful", success },
                { "error", errorMessage },
                { "language", language },
                { "group", groupId },
                { "installType", (int)installType },
                { "android11Install", isAndroid11Install },
                { "guid", Preferences.Get("InstallID", "") }
            };

            request.AddParameter("application/json; charset=utf-8", SimpleJson.SerializeObject(sendObject), ParameterType.RequestBody);

            return((await ExecuteAsync <BaseAPIResponse>(request)).Data);
        }
        public async Task Uninstall(FGORegion region)
        {
            _cm.ClearCache();
            bool answer = await DisplayAlert(AppResources.Warning, AppResources.UninstallWarning, AppResources.Yes, AppResources.No);

            if (!answer)
            {
                return;
            }
            SwitchButtons(false);
            HashSet <string> filesToRemove = new HashSet <string>();

            try
            {
                foreach (var game in _installedFgoInstances.Where(w => w.Region == region).Select(s => s.Path))
                {
                    foreach (var basePath in from script
                             in _translations
                             from script2
                             in script.Value.Scripts
                             select script2.Key
                             into file
                             select _android11Access
                            ? $"data/{game}/files/data/d713/{file}"
                            : $"{game}/files/data/d713/{file}")
                    {
                        filesToRemove.Add(basePath);
                    }
                }

                if (filesToRemove.Count == 0)
                {
                    return;
                }


                int i = 0;
                foreach (var file in filesToRemove)
                {
                    i += 1;
                    RevertButton.Text = String.Format(AppResources.UninstallingText, i, filesToRemove.Count);
                    OnPropertyChanged();
                    await _cm.RemoveFileIfExists(_android11Access?ContentType.StorageFramework : ContentType.DirectAccess,
                                                 file, _storageLocation);
                }

                RevertButton.Text = AppResources.UninstallFinished;
                OnPropertyChanged();
                Preferences.Remove($"InstalledScript_{region}");
                await Task.Delay(1000);
            }
            catch (Exception ex)
            {
                await DisplayAlert(AppResources.InternalError,
                                   String.Format(AppResources.InternalErrorDetails, ex.ToString()), AppResources.OK);
            }
            await UpdateTranslationList();
        }
        public async Task <ScriptInstallStatus> InstallScript(ContentType contentType, FGORegion region, List <string> installPaths, string baseInstallPath, int installId, string assetStorageCheck = null,
                                                              ObservableCollection <InstallerPage.TranslationGUIObject> translationGuiObjects = null)
        {
            _cm.ClearCache();
            var gui       = translationGuiObjects != null;
            var guiObject = gui ? translationGuiObjects.First(w => w.BundleID == installId) : null;

            if (guiObject != null)
            {
                guiObject.Status =
                    UIFunctions.GetResourceString("InstallingFetchingHandshake");
                guiObject.ButtonInstallText = UIFunctions.GetResourceString("InstallingText");
                guiObject.TextColor         = Color.Chocolate;
            }
            // fetch new translation list to ensure it is up to date
            var restful         = new RestfulAPI();
            var translationList = await restful.GetHandshakeApiResponse(region, assetStorageCheck);

            if (!translationList.IsSuccessful || translationList.Data.Status != 200)
            {
                return(new ScriptInstallStatus()
                {
                    IsSuccessful = false,
                    ErrorMessage = String.Format(UIFunctions.GetResourceString("InstallHandshakeFailure"), translationList.StatusCode, translationList.Data?.Message)
                });
            }

            if (assetStorageCheck != null)
            {
                switch (translationList.Data.Response.AssetStatus)
                {
                case HandshakeAssetStatus.UpdateRequired:
                    return(new ScriptInstallStatus()
                    {
                        IsSuccessful = false,
                        ErrorMessage = String.Format("AssetStorage.txt out of date, skipping update.")
                    });

                default:
                    break;
                }
            }

            // download required scripts
            var groupToInstall = translationList.Data.Response.Translations.FirstOrDefault(w => w.Group == installId);

            if (groupToInstall?.Scripts == null)
            {
                return(new ScriptInstallStatus()
                {
                    IsSuccessful = false,
                    ErrorMessage = String.Format(UIFunctions.GetResourceString("InstallMissingScriptFailure"), installId)
                });
            }

            if (!gui && groupToInstall.Hidden)
            {
                return(new ScriptInstallStatus()
                {
                    IsSuccessful = false,
                    ErrorMessage = String.Format(UIFunctions.GetResourceString("InstallHiddenScriptFailure"), installId)
                });
            }

            var totalScripts = groupToInstall.Scripts.Count;

            if (guiObject != null)
            {
                guiObject.Status =
                    String.Format(UIFunctions.GetResourceString("InstallDownloadingScripts"), 1, totalScripts);
            }

            ConcurrentDictionary <string, byte[]> scriptDictionary = new ConcurrentDictionary <string, byte[]>();
            object lockObj = new Object();

            try
            {
                await groupToInstall.Scripts.ParallelForEachAsync(async script =>
                {
                    var name             = script.Key;
                    var downloadUrl      = script.Value.DownloadURL;
                    var downloadResponse = await restful.GetScript(downloadUrl);
                    var downloadedScript = downloadResponse.RawBytes;

                    if (!downloadResponse.IsSuccessful)
                    {
                        throw new EndEarlyException(String.Format(UIFunctions.GetResourceString("InstallScriptDownloadFailure"),
                                                                  installId, downloadUrl, downloadResponse.ErrorMessage));
                    }

                    if (downloadedScript == null)
                    {
                        throw new EndEarlyException(String.Format(UIFunctions.GetResourceString("InstallEmptyFileFailure"),
                                                                  installId, downloadUrl));
                    }

                    var scriptSha = ScriptUtil.Sha1(downloadedScript);

                    if (scriptSha != script.Value.TranslatedSHA1)
                    {
                        throw new EndEarlyException(String.Format(UIFunctions.GetResourceString("InstallChecksumFailure"),
                                                                  installId, downloadedScript.Length, downloadUrl));
                    }

                    scriptDictionary.TryAdd(name, downloadedScript);
                    lock (lockObj)
                    {
                        if (guiObject != null)
                        {
                            guiObject.Status =
                                String.Format(UIFunctions.GetResourceString("InstallDownloadingScripts"),
                                              Math.Min(scriptDictionary.Count, totalScripts), totalScripts);
                        }
                    }
                }, maxDegreeOfParallelism : 4);
            }
            catch (EndEarlyException ex)
            {
                return(new ScriptInstallStatus()
                {
                    IsSuccessful = false,
                    ErrorMessage = ex.ToString()
                });
            }

            if (guiObject != null)
            {
                guiObject.Status =
                    String.Format(UIFunctions.GetResourceString("InstallDownloadNewAssetStorage"));
            }

            Dictionary <string, byte[]> newAssetStorages = new Dictionary <string, byte[]>();

            // get new assetstorage.txt
            foreach (var game in installPaths)
            {
                var assetStoragePath = contentType != ContentType.DirectAccess ? $"data/{game}/files/data/d713/{InstallerPage._assetList}"
                    : $"{game}/files/data/d713/{InstallerPage._assetList}";

                var fileContents = await _cm.GetFileContents(contentType, assetStoragePath, baseInstallPath);

                if (!fileContents.Successful || fileContents.FileContents.Length == 0)
                {
                    return(new ScriptInstallStatus()
                    {
                        IsSuccessful = false,
                        ErrorMessage = String.Format(UIFunctions.GetResourceString("InstallEmptyAssetStorage"), installId, assetStoragePath, fileContents.Error)
                    });
                }


                // remove bom
                var base64 = "";
                await using var inputStream = new MemoryStream(fileContents.FileContents);
                using (var reader = new StreamReader(inputStream, Encoding.ASCII))
                {
                    base64 = await reader.ReadToEndAsync();
                }
                var newAssetList = await restful.SendAssetList(base64, installId, region);

                if (!newAssetList.IsSuccessful)
                {
                    return(new ScriptInstallStatus()
                    {
                        IsSuccessful = false,
                        ErrorMessage = string.Format(UIFunctions.GetResourceString("InstallAssetStorageAPIFailure"), installId, newAssetList.StatusCode, newAssetList.Data?.Message)
                    });
                }

                // add bom
                await using var outputStream = new MemoryStream();
                await using (var writer = new StreamWriter(outputStream, Encoding.ASCII))
                {
                    await writer.WriteAsync(newAssetList.Data.Response["data"]);
                }

                newAssetStorages.Add(assetStoragePath, outputStream.ToArray());
            }

            // prepare files

            Dictionary <string, byte[]> filesToWrite = newAssetStorages;

            foreach (var game in installPaths)
            {
                foreach (var asset in scriptDictionary)
                {
                    var assetInstallPath = contentType != ContentType.DirectAccess
                        ? $"data/{game}/files/data/d713/{asset.Key}"
                        : $"{game}/files/data/d713/{asset.Key}";

                    filesToWrite.Add(assetInstallPath, asset.Value);
                }
            }

            // write files
            int j   = 1;
            int tot = filesToWrite.Count;

            foreach (var file in filesToWrite)
            {
                if (file.Key.EndsWith(".bin"))
                {
                    await _cm.RemoveFileIfExists(contentType,
                                                 file.Key, baseInstallPath);
                }
            }
            _cm.ClearCache();

            foreach (var file in filesToWrite)
            {
                if (guiObject != null)
                {
                    guiObject.Status =
                        String.Format(UIFunctions.GetResourceString("InstallWriteFile"), j, tot);
                }

                await _cm.WriteFileContents(contentType, file.Key, baseInstallPath, file.Value);

                j += 1;
            }


            if (guiObject != null)
            {
                guiObject.Status =
                    String.Format(UIFunctions.GetResourceString("InstallFinished"));
                guiObject.TextColor = Color.LimeGreen;
            }
            Preferences.Set($"InstalledScript_{region}", JsonConvert.SerializeObject(groupToInstall));

            return(new ScriptInstallStatus()
            {
                IsSuccessful = true,
                ErrorMessage = ""
            });
        }
 public Task <bool> UninstallScripts(ContentType contentType, FGORegion region, List <string> installPaths, string baseInstallPath)
 {
     throw new System.NotImplementedException(); // implemented locally
 }
        /// <summary>
        /// Get a handshake response
        /// </summary>
        /// <returns>Handshake API struct</returns>
        public async Task <IRestResponse <HandshakeAPIResponse> > GetHandshakeApiResponse(FGORegion region, string assetStorage = null)
        {
            string endpoint;

            if (Preferences.ContainsKey("AuthKey"))
            {
                endpoint = $"translate/scripts/{(int)region}/" + Preferences.Get("AuthKey", "") + "?_ts=" + DateTimeOffset.UtcNow.ToUnixTimeSeconds();
            }
            else
            {
                endpoint = $"translate/scripts/{(int)region}" + "?_ts=" + DateTimeOffset.UtcNow.ToUnixTimeSeconds();
            }
            var request = new RestRequest(endpoint)
            {
                Method = Method.POST
            };

            if (!string.IsNullOrWhiteSpace(assetStorage))
            {
                request.AddHeader("Content-type", "application/json");
                var sendObject = new Dictionary <string, object>()
                {
                    { "assetstorage", assetStorage }
                };
                request.AddParameter("application/json; charset=utf-8", SimpleJson.SerializeObject(sendObject), ParameterType.RequestBody);
            }

            return(await ExecuteAsync <HandshakeAPIResponse>(request));
        }
        /// <summary>
        /// Get new asset list
        /// </summary>
        /// <param name="assetList">Existing asset list</param>
        /// <param name="bundleId">Bundle ID</param>
        /// <returns></returns>
        public async Task <IRestResponse <AssetListAPIResponse> > SendAssetList(string assetList, int bundleId, FGORegion region)
        {
            var request = new RestRequest("translate/update-asset-list")
            {
                Method = Method.POST
            };

            request.AddHeader("Content-type", "application/json");
            var sendObject = new Dictionary <string, object>()
            {
                { "data", assetList },
                { "group", bundleId },
                { "region", (int)region }
            };

            if (Preferences.ContainsKey("AuthKey"))
            {
                sendObject.Add("key", Preferences.Get("AuthKey", ""));
            }
            request.AddParameter("application/json; charset=utf-8", SimpleJson.SerializeObject(sendObject), ParameterType.RequestBody);

            return(await ExecuteAsync <AssetListAPIResponse>(request));
        }
        public async Task Install(FGORegion region, int toInstall)
        {
            _cm.ClearCache();
            bool answer = await DisplayAlert(AppResources.Warning, String.Format(AppResources.InstallWarning, _translations[toInstall].TotalSize.Bytes().Humanize("#.## MB")), AppResources.Yes, AppResources.No);

            if (!answer)
            {
                return;
            }
            SwitchButtons(false);
            var  rest            = new RestfulAPI();
            var  language        = _handshake.Response.Translations.First(f => f.Group == toInstall).Language;
            Task successSendTask = null;

            try
            {
                var installResult = await _sm.InstallScript(
                    _android11Access?ContentType.StorageFramework : ContentType.DirectAccess,
                    region,
                    _installedFgoInstances.Where(w => w.Region == region).Select(s => s.Path).ToList(),
                    _storageLocation,
                    toInstall,
                    null,
                    _guiObjects
                    );

                if (!installResult.IsSuccessful)
                {
                    successSendTask = rest.SendSuccess(region, language, TranslationInstallType.Manual, toInstall,
                                                       false, installResult.ErrorMessage, _android11Access);
                    await DisplayAlert(AppResources.Error,
                                       installResult.ErrorMessage, AppResources.OK);
                }
                else
                {
                    successSendTask = rest.SendSuccess(region, language, TranslationInstallType.Manual, toInstall,
                                                       true, "", _android11Access);
                    await Task.Delay(1000);
                }
            }
            catch (System.UnauthorizedAccessException ex)
            {
                if (!_android11Access)
                {
                    successSendTask = rest.SendSuccess(region, language, TranslationInstallType.Manual, toInstall,
                                                       false, "Unauthorized error handler: " + ex.ToString(), _android11Access);
                    var retry = await DisplayAlert(AppResources.DirectoryPermissionDeniedTitle,
                                                   AppResources.Android11AskToSetup, AppResources.Yes, AppResources.No);

                    if (retry)
                    {
                        await successSendTask;
                        MessagingCenter.Send(Xamarin.Forms.Application.Current, "installer_page_goto_pre_initialize");
                        return;
                    }

                    await successSendTask;
                    return;
                }
                else
                {
                    throw;
                }
            }
            catch (Exception ex)
            {
                successSendTask = rest.SendSuccess(region, language, TranslationInstallType.Manual, toInstall,
                                                   false, ex.ToString(), _android11Access);
                await DisplayAlert(AppResources.InternalError,
                                   String.Format(AppResources.InternalErrorDetails, ex.ToString()), AppResources.OK);
            }

            await successSendTask;

            await UpdateTranslationList();

            //SwitchButtons(true);
        }
        /// <summary>
        /// Processes valid assets to know which buttons to display
        /// </summary>
        /// <returns></returns>
        public async Task ProcessAssets(ContentType storageType, string pathToCheckWith, string storageLocationBase, FGORegion region)
        {
            List <string> validSha = new List <string>();

            _translations?.Clear();
            _guiObjects?.Clear();

            var installedScriptString = Preferences.Get($"InstalledScript_{region}", null);

            if (installedScriptString != null)
            {
                _installedBundle = JsonConvert.DeserializeObject <TranslationList>(installedScriptString);

                foreach (var scriptBundle in _installedBundle.Scripts)
                {
                    validSha.Add(scriptBundle.Value.TranslatedSHA1); // Add existing
                }
            }
            else
            {
                _installedBundle = null;
            }

            foreach (var scriptBundleSet in _handshake.Response.Translations)
            {
                foreach (var scriptBundle in scriptBundleSet.Scripts)
                {
                    validSha.Add(scriptBundle.Value.TranslatedSHA1); // Add all valid sha1s
                }
            }

            if (_handshake.Response.Translations.Count > 0)
            {
                foreach (var scriptBundleSet in _handshake.Response.Translations)
                {
                    ConcurrentBag <Tuple <long, long> > results = new ConcurrentBag <Tuple <long, long> >();

                    await scriptBundleSet.Scripts.ParallelForEachAsync(async scriptBundle =>
                    {
                        // Check hashes
                        var filePath = Path.Combine(pathToCheckWith, scriptBundle.Key);

                        var fileContentsResult = await _cm.GetFileContents(storageType, filePath, storageLocationBase);

                        if (scriptBundle.Key.Contains('/') || scriptBundle.Key.Contains('\\')) // for security purposes, don't allow directory traversal
                        {
                            throw new FileNotFoundException();
                        }


                        TranslationFileStatus status;

                        var fileNotExists = fileContentsResult.Error == FileErrorCode.NotExists;
                        if (!fileNotExists)
                        {
                            var sha1 = ScriptUtil.Sha1(fileContentsResult.FileContents); // SHA of file currently in use

                            if (sha1 == scriptBundle.Value.GameSHA1)                     // Not modified
                            {
                                status = TranslationFileStatus.NotModified;
                            }
                            else if (sha1 == scriptBundle.Value.TranslatedSHA1) // English is installed
                            {
                                status = TranslationFileStatus.Translated;
                            }
                            else if (validSha.Contains(sha1) && (_installedBundle == null || (_installedBundle != null && scriptBundleSet.Group != _installedBundle.Group)))
                            {
                                status = TranslationFileStatus.DifferentTranslation;
                            }
                            else if (_installedBundle != null && scriptBundleSet.Group == _installedBundle.Group)
                            {
                                status = TranslationFileStatus.UpdateAvailable;
                            }
                            else
                            {
                                status = TranslationFileStatus.Invalid;
                            }
                        }
                        else
                        {
                            status = TranslationFileStatus.Missing;
                        }

                        scriptBundle.Value.Status = status;

                        results.Add(new Tuple <long, long>(scriptBundle.Value.LastModified, scriptBundle.Value.Size));
                    }, maxDegreeOfParallelism : 4);

                    long lastModified = results.Max(m => m.Item1);
                    long totalSize    = results.Sum(s => s.Item2);

                    scriptBundleSet.TotalSize = totalSize;
                    _translations.Add(scriptBundleSet.Group, scriptBundleSet);
                    var statusString = InstallerUtil.GenerateStatusString(scriptBundleSet.Scripts);
                    var timespan     = DateTime.UtcNow.Subtract(DateTime.SpecifyKind(DateTimeOffset.FromUnixTimeSeconds((long)lastModified).DateTime,
                                                                                     DateTimeKind.Utc));
                    var  lastUpdated  = InstallerUtil.PeriodOfTimeOutput(timespan);
                    bool enableButton = statusString.Item1 != AppResources.StatusInstalled;
                    var  i1           = scriptBundleSet.Group;

                    if (!scriptBundleSet.Hidden)
                    {
                        _guiObjects.Add(new TranslationGUIObject()
                        {
                            BundleID       = scriptBundleSet.Group,
                            BundleHidden   = scriptBundleSet.Hidden,
                            InstallEnabled = enableButton,
                            InstallClick   = new Command(async() => await Install(region, i1),
                                                         () => enableButton && ButtonsEnabled),
                            Name              = scriptBundleSet.Name,
                            Status            = statusString.Item1,
                            TextColor         = statusString.Item2,
                            LastUpdated       = lastUpdated,
                            ButtonInstallText = statusString.Item1 != AppResources.StatusInstalled
                                ? AppResources.InstallText
                                : AppResources.InstalledText
                        });
                    }
                }

                LoadTranslationList();
            }
            else
            {
                LoadingText.Text = String.Format(AppResources.NoScriptsAvailable);
                SwitchErrorObjects(true);
                return;
            }
        }