﻿#if UNITY_ANDROID
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;

using GooglePlayGames;
using GooglePlayGames.BasicApi;
using GooglePlayGames.BasicApi.SavedGame;

using UnityEngine;

namespace Bayat.SaveSystem.Storage
{

    /// <summary>
    /// The Google Play Games Saved Game storage.
    /// </summary>
    public class GooglePlaySavedGameStorage : StorageBase
    {

        public enum OpenMode
        {
            AutomaticConflictResolution = 0,
            SelectionUI = 1,
            ManualConflictResolution = 2
        }

        #region Events

        public event ConflictCallback ManualConflict;

        #endregion

        #region Fields

        protected bool useBase64 = true;

        protected string writeUiTitle = "Choose a slot to save";
        protected string readUiTitle = "Choose a slot to load";
        protected string deleteUiTitle = "Choose a slot to delete";
        protected uint maxDisplayedSavedGames = 3;
        protected bool showCreateSaveUI = true;
        protected bool showDeleteSaveUI = true;
        protected OpenMode savedGameOpenMode = OpenMode.AutomaticConflictResolution;
        protected DataSource savedGameDataSource = DataSource.ReadCacheOrNetwork;
        protected ConflictResolutionStrategy resolutionStrategy = ConflictResolutionStrategy.UseLongestPlaytime;
        protected bool prefetchDataOnConflict = true;
        protected string metadataDescription;
        protected TimeSpan metadataPlayedTime;
        protected Texture2D metadataPngImage;

        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets the UI title to be used by ShowSelectSavedGameUI when writing
        /// </summary>
        public virtual string WriteUiTitle
        {
            get
            {
                return this.writeUiTitle;
            }
            set
            {
                this.writeUiTitle = value;
            }
        }

        /// <summary>
        /// Gets or sets the UI title to be used by ShowSelectSavedGameUI when reading
        /// </summary>
        public virtual string ReadUiTitle
        {
            get
            {
                return this.readUiTitle;
            }
            set
            {
                this.readUiTitle = value;
            }
        }

        /// <summary>
        /// Gets or sets the UI title to be used by ShowSelectSavedGameUI when deleting
        /// </summary>
        public virtual string DeleteUiTitle
        {
            get
            {
                return this.deleteUiTitle;
            }
            set
            {
                this.deleteUiTitle = value;
            }
        }

        /// <summary>
        /// Gets or sets the max number of saved games to be displayed on the ShowSelectSavedGameUI
        /// </summary>
        public virtual uint MaxDisplayedSavedGames
        {
            get
            {
                return this.maxDisplayedSavedGames;
            }
            set
            {
                this.maxDisplayedSavedGames = value;
            }
        }

        /// <summary>
        /// Gets or sets whether to show the create UI in ShowSelectSavedGameUI
        /// </summary>
        public virtual bool ShowCreateSaveUI
        {
            get
            {
                return this.showCreateSaveUI;
            }
            set
            {
                this.showCreateSaveUI = value;
            }
        }

        /// <summary>
        /// Gets or sets whether to show the delete UI in ShowSelectSavedGameUI
        /// </summary>
        public virtual bool ShowDeleteSaveUI
        {
            get
            {
                return this.showDeleteSaveUI;
            }
            set
            {
                this.showDeleteSaveUI = value;
            }
        }

        /// <summary>
        /// Gets or sets the open mode to be used for openning saved games
        /// </summary>
        public virtual OpenMode SavedGameOpenMode
        {
            get
            {
                return this.savedGameOpenMode;
            }
            set
            {
                this.savedGameOpenMode = value;
            }
        }

        /// <summary>
        /// Gets or sets the data source to be used for saved games
        /// </summary>
        public virtual DataSource SavedGameDataSource
        {
            get
            {
                return this.savedGameDataSource;
            }
            set
            {
                this.savedGameDataSource = value;
            }
        }

        /// <summary>
        /// Gets or sets the conflict resolution strategy to be used for Automatic conflict resolution
        /// </summary>
        public virtual ConflictResolutionStrategy ResolutionStrategy
        {
            get
            {
                return this.resolutionStrategy;
            }
            set
            {
                this.resolutionStrategy = value;
            }
        }

        /// <summary>
        /// Gets or sets whether to prefetch the data on conflict or do it manually
        /// </summary>
        public virtual bool PrefetchDataOnConflict
        {
            get
            {
                return this.prefetchDataOnConflict;
            }
            set
            {
                this.prefetchDataOnConflict = value;
            }
        }

        /// <summary>
        /// Gets or sets the metadata description to be used for the next commit
        /// </summary>
        public virtual string MetadataDescription
        {
            get
            {
                return this.metadataDescription;
            }
            set
            {
                this.metadataDescription = value;
            }
        }

        /// <summary>
        /// Gets or sets the metadata total played time to be used for the next commit
        /// </summary>
        public virtual TimeSpan MetadataPlayedTime
        {
            get
            {
                return this.metadataPlayedTime;
            }
            set
            {
                this.metadataPlayedTime = value;
            }
        }

        /// <summary>
        /// Gets or sets the metadata PNG image to be used for the next commit
        /// </summary>
        public virtual Texture2D MetadataPngImage
        {
            get
            {
                return this.metadataPngImage;
            }
            set
            {
                this.metadataPngImage = value;
            }
        }

        #endregion

        public GooglePlaySavedGameStorage(string encodingName, bool useBase64) : base(encodingName)
        {
            this.useBase64 = useBase64;
        }

        public override Task<IStorageStream> GetWriteStream(string identifier)
        {
            return Task.FromResult<IStorageStream>(new GooglePlaySavedGameStorageStream(identifier, new MemoryStream()));
        }

        public virtual Task<ISavedGameMetadata> OpenSavedGame(string identifier, string uiTitle)
        {
            var tcs = new TaskCompletionSource<ISavedGameMetadata>();
            switch (this.savedGameOpenMode)
            {
                case OpenMode.SelectionUI:
                    PlayGamesPlatform.Instance.SavedGame.ShowSelectSavedGameUI(uiTitle, this.maxDisplayedSavedGames, this.showCreateSaveUI, this.showDeleteSaveUI, (status, metadata) =>
                    {
                        if (status == SelectUIStatus.SavedGameSelected)
                        {
                            tcs.TrySetResult(metadata);
                        }
                        else
                        {
                            var exception = HandleSelectionUIError(status);
                            tcs.TrySetException(exception);
                        }
                    });
                    break;
                case OpenMode.AutomaticConflictResolution:
                    PlayGamesPlatform.Instance.SavedGame.OpenWithAutomaticConflictResolution(identifier, this.savedGameDataSource, this.resolutionStrategy, (status, metadata) =>
                    {
                        if (status == SavedGameRequestStatus.Success)
                        {
                            tcs.TrySetResult(metadata);
                        }
                        else
                        {
                            var exception = HandleSavedGameError(status);
                            tcs.TrySetException(exception);
                        }
                    });
                    break;
                case OpenMode.ManualConflictResolution:
                    PlayGamesPlatform.Instance.SavedGame.OpenWithManualConflictResolution(identifier, this.savedGameDataSource, this.prefetchDataOnConflict, OnConflict, (status, metadata) =>
                    {
                        if (status == SavedGameRequestStatus.Success)
                        {
                            if (!string.IsNullOrEmpty(metadata.Description))
                            {
                                this.metadataDescription = metadata.Description;
                            }
                            if (metadata.TotalTimePlayed != TimeSpan.Zero)
                            {
                                this.metadataPlayedTime = metadata.TotalTimePlayed;
                            }
                            tcs.TrySetResult(metadata);
                        }
                        else
                        {
                            var exception = HandleSavedGameError(status);
                            tcs.TrySetException(exception);
                        }
                    });
                    break;
            }
            return tcs.Task;
        }

        protected virtual void OnConflict(IConflictResolver resolver, ISavedGameMetadata original, byte[] originalData, ISavedGameMetadata unmerged, byte[] unmergedData)
        {
            ManualConflict?.Invoke(resolver, original, originalData, unmerged, unmergedData);
        }

        protected virtual Exception HandleSavedGameError(SavedGameRequestStatus status)
        {
            Exception exception = null;
            switch (status)
            {
                case SavedGameRequestStatus.TimeoutError:
                    exception = new GooglePlayTimeoutException();
                    break;
                case SavedGameRequestStatus.InternalError:
                    exception = new GooglePlayInternalException();
                    break;
                case SavedGameRequestStatus.AuthenticationError:
                    exception = new GooglePlayAuthenticationException();
                    break;
                case SavedGameRequestStatus.BadInputError:
                    exception = new GooglePlayBadInputException();
                    break;
            }
            return exception;
        }

        protected virtual Exception HandleSelectionUIError(SelectUIStatus status)
        {
            Exception exception = null;
            switch (status)
            {
                case SelectUIStatus.UserClosedUI:
                    exception = new GooglePlayUserClosedUIException();
                    break;
                case SelectUIStatus.InternalError:
                    exception = new GooglePlayInternalException();
                    break;
                case SelectUIStatus.TimeoutError:
                    exception = new GooglePlayTimeoutException();
                    break;
                case SelectUIStatus.AuthenticationError:
                    exception = new GooglePlayAuthenticationException();
                    break;
                case SelectUIStatus.BadInputError:
                    exception = new GooglePlayBadInputException();
                    break;
                case SelectUIStatus.UiBusy:
                    exception = new GooglePlayUIBusyException();
                    break;
            }
            return exception;
        }

        protected override async Task CommitWriteStreamInternal(IStorageStream stream)
        {
            var googleStream = (GooglePlaySavedGameStorageStream)stream;

            var metadata = await OpenSavedGame(stream.Identifier, this.writeUiTitle);
            var data = googleStream.MemoryStream.ToArray();

            await CommitUpdateSavedGame(metadata, data);
        }

        public virtual Task<ISavedGameMetadata> CommitUpdateSavedGame(ISavedGameMetadata metadata, byte[] data)
        {
            var tcs = new TaskCompletionSource<ISavedGameMetadata>();
            var builder = new SavedGameMetadataUpdate.Builder();
            if (!string.IsNullOrEmpty(this.metadataDescription))
            {
                builder.WithUpdatedDescription(this.metadataDescription);
            }
            if (this.metadataPlayedTime != null && this.metadataPlayedTime != TimeSpan.Zero)
            {
                builder.WithUpdatedPlayedTime(this.metadataPlayedTime);
            }
            if (this.metadataPngImage != null)
            {
                var pngData = this.metadataPngImage.EncodeToPNG();
                builder.WithUpdatedPngCoverImage(pngData);
            }
            var updatedMetadata = builder.Build();

            PlayGamesPlatform.Instance.SavedGame.CommitUpdate(metadata, updatedMetadata, data, (status, newMetadata) =>
            {
                if (status == SavedGameRequestStatus.Success)
                {
                    tcs.TrySetResult(metadata);
                }
                else
                {
                    var exception = HandleSavedGameError(status);
                    tcs.TrySetException(exception);
                }
            });
            return tcs.Task;
        }

        public override async Task<IStorageStream> GetReadStream(string identifier)
        {
            var tcs = new TaskCompletionSource<IStorageStream>();
            var metadata = await OpenSavedGame(identifier, this.readUiTitle);

            PlayGamesPlatform.Instance.SavedGame.ReadBinaryData(metadata, (status, data) =>
            {
                if (status == SavedGameRequestStatus.Success)
                {
                    var stream = new GooglePlaySavedGameStorageStream(identifier, new MemoryStream(data));
                    tcs.TrySetResult(stream);
                }
                else
                {
                    var exception = HandleSavedGameError(status);
                    tcs.TrySetException(exception);
                }
            });
            await tcs.Task;
            return tcs.Task.Result;
        }

        protected override async Task WriteAllTextInternal(string identifier, string data)
        {
            var metadata = await OpenSavedGame(identifier, this.writeUiTitle);

            byte[] buffer;
            if (this.useBase64)
            {
                buffer = Convert.FromBase64String(Convert.ToBase64String(this.textEncoding.GetBytes(data)));
            }
            else
            {
                buffer = this.textEncoding.GetBytes(data);
            }

            await CommitUpdateSavedGame(metadata, buffer);
        }

        public override async Task<string> ReadAllText(string identifier)
        {
            var tcs = new TaskCompletionSource<string>();
            var metadata = await OpenSavedGame(identifier, this.readUiTitle);

            PlayGamesPlatform.Instance.SavedGame.ReadBinaryData(metadata, (status, data) =>
            {
                if (status == SavedGameRequestStatus.Success)
                {
                    string result;
                    if (this.useBase64)
                    {
                        result = this.textEncoding.GetString(Convert.FromBase64String(Convert.ToBase64String(data)));
                    }
                    else
                    {
                        result = this.textEncoding.GetString(data);
                    }
                    tcs.TrySetResult(result);
                }
                else
                {
                    var exception = HandleSavedGameError(status);
                    tcs.TrySetException(exception);
                }
            });
            await tcs.Task;
            return tcs.Task.Result;
        }

        protected override async Task WriteAllBytesInternal(string identifier, byte[] data)
        {
            var metadata = await OpenSavedGame(identifier, this.writeUiTitle);
            await CommitUpdateSavedGame(metadata, data);
        }

        public override async Task<byte[]> ReadAllBytes(string identifier)
        {
            var metadata = await OpenSavedGame(identifier, this.readUiTitle);
            var tcs = new TaskCompletionSource<byte[]>();
            PlayGamesPlatform.Instance.SavedGame.ReadBinaryData(metadata, (status, data) =>
            {
                if (status == SavedGameRequestStatus.Success)
                {
                    tcs.TrySetResult(data);
                }
                else
                {
                    var exception = HandleSavedGameError(status);
                    tcs.TrySetException(exception);
                }
            });
            await tcs.Task;
            return tcs.Task.Result;
        }

        public override Task<StorageClearOperationResult> Clear()
        {
            var tcs = new TaskCompletionSource<StorageClearOperationResult>();
            PlayGamesPlatform.Instance.SavedGame.FetchAllSavedGames(this.savedGameDataSource, async (status, list) =>
            {
                if (status == SavedGameRequestStatus.Success)
                {
                    var removedKeys = new List<string>();
                    for (int i = 0; i < list.Count; i++)
                    {
                        var deleteResult = await Delete(list[i].Filename);
                        if (deleteResult.Succeed)
                        {
                            removedKeys.Add(list[i].Filename);
                        }
                    }
                    tcs.TrySetResult(new StorageClearOperationResult(true, removedKeys.ToArray()));
                }
                else
                {
                    var exception = HandleSavedGameError(status);
                    tcs.TrySetException(exception);
                }
            });
            return tcs.Task;
        }

        protected override async Task<StorageDeleteOperationResult> DeleteInternal(string identifier)
        {
            var metadata = await OpenSavedGame(identifier, this.deleteUiTitle);
            if (metadata != null)
            {
                PlayGamesPlatform.Instance.SavedGame.Delete(metadata);
                return new StorageDeleteOperationResult(true);
            }
            else
            {
                return new StorageDeleteOperationResult(false);
            }
        }

        public override Task<bool> Exists(string identifier)
        {
            var tcs = new TaskCompletionSource<bool>();
            PlayGamesPlatform.Instance.SavedGame.FetchAllSavedGames(this.savedGameDataSource, (status, list) =>
            {
                if (status == SavedGameRequestStatus.Success)
                {
                    var match = list.Find(x => x.Filename == identifier);
                    tcs.TrySetResult(match != null);
                }
                else
                {
                    var exception = HandleSavedGameError(status);
                    tcs.TrySetException(exception);
                }
            });
            return tcs.Task;
        }

        protected override async Task<StorageMoveOperationResult> MoveInternal(string oldIdentifier, string newIdentifier, bool replace)
        {
            string data = await ReadAllText(oldIdentifier);
            await WriteAllText(newIdentifier, data);
            StorageDeleteOperationResult deleteOp = await Delete(oldIdentifier);
            return new StorageMoveOperationResult(deleteOp.Succeed, newIdentifier);
        }

        protected override async Task<StorageCopyOperationResult> CopyInternal(string fromIdentifier, string toIdentifier, bool replace)
        {
            string data = await ReadAllText(fromIdentifier);
            await WriteAllText(toIdentifier, data);
            return new StorageCopyOperationResult(true, toIdentifier);
        }

        public override Task<string[]> List(string identifier, StorageListOptions options)
        {
            var tcs = new TaskCompletionSource<string[]>();
            PlayGamesPlatform.Instance.SavedGame.FetchAllSavedGames(this.savedGameDataSource, (status, list) =>
            {
                if (status == SavedGameRequestStatus.Success)
                {
                    var items = new string[list.Count];
                    for (int i = 0; i < list.Count; i++)
                    {
                        items[i] = list[i].Filename;
                    }
                    tcs.TrySetResult(items);
                }
                else
                {
                    var exception = HandleSavedGameError(status);
                    tcs.TrySetException(exception);
                }
            });
            return tcs.Task;
        }

        public override Task<string[]> ListAll()
        {
            return List(string.Empty, new StorageListOptions()
            {
                Recurse = true
            });
        }

    }

    /// <summary>
    /// The PlayFab user data storage stream wrapper.
    /// </summary>
    public class GooglePlaySavedGameStorageStream : StorageStream
    {

        protected readonly MemoryStream memoryStream;

        public virtual MemoryStream MemoryStream
        {
            get
            {
                return this.memoryStream;
            }
        }

        public GooglePlaySavedGameStorageStream(string identifier, MemoryStream memoryStream) : base(identifier, memoryStream)
        {
            this.memoryStream = memoryStream;
        }

    }


    [Serializable]
    public class GooglePlayException : Exception
    {
        public GooglePlayException() { }
        public GooglePlayException(string message) : base(message) { }
        public GooglePlayException(string message, Exception inner) : base(message, inner) { }
        protected GooglePlayException(
          System.Runtime.Serialization.SerializationInfo info,
          System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
    }


    [Serializable]
    public class GooglePlayBadInputException : GooglePlayException
    {
    }


    [Serializable]
    public class GooglePlayAuthenticationException : GooglePlayException
    {
    }


    [Serializable]
    public class GooglePlayInternalException : GooglePlayException
    {
    }


    [Serializable]
    public class GooglePlayTimeoutException : GooglePlayException
    {
    }


    [Serializable]
    public class GooglePlayUIBusyException : GooglePlayException
    {
    }


    [Serializable]
    public class GooglePlayUserClosedUIException : GooglePlayException
    {
    }

}
#endif