using UnityEngine;


using HoloToolkit.Unity;
using HoloToolkit.Unity.InputModule;
using KeyJ;

using System.Collections.Generic;
using System.Collections;
using System;
using System.Linq;
using System.IO;
using System.ComponentModel;
using System.Threading;

#if NETFX_CORE
using System.Threading.Tasks;
using Windows.Storage;
using Windows.System.Threading;
#else
using System.Threading.Tasks;
#endif

public class PersistenceManager : Singleton<PersistenceManager>
{

    // The object to be instantiated when loading an image
    private GameObject polaroidPrefab;

    // Overview of all pictures in the scene
    private Dictionary<string, Texture2D> picturesDictionary;
    // The folder path for saving the polaroid files
    string folderPath;

    // Defines if existing files should be overwritten while saving
    [SerializeField]
    private bool overrideFiles = false;

    private void Start()
    {
        // Set the corresponding image folder for the different platform
#if NETFX_CORE
            folderPath = KnownFolders.CameraRoll.Path;
            rawImageStructList = new List<RawImageStruct>();
#else
        folderPath = Application.persistentDataPath;
#endif

        // Get the polaroid prefab which will be instantiated while loading
        if (PhotoAppCapture.Instance != null)
        {
            polaroidPrefab = PhotoAppCapture.Instance.GetPolaroidPrefab();
        }

        //ONLY FOR DEBUGGING PURPOSES: DELETES ALL WORLD ANCHOR POSITIONS FROM STORE
        //WorldAnchorStore.GetAsync(new WorldAnchorStore.GetAsyncDelegate((WorldAnchorStore store) =>
        //{
        //    store.Clear();
        //}));
    }

    // Some random textures to preview saving/loading caüability
    public Texture2D[] randomTextures;

    // placeholder texture until the final image is loaded
    [SerializeField]
    private Texture2D loadingTexture;

    public void SetRandomImage()
    {
        // Randomize the polaroid's position and image to let you test loading/saving capability
        GameObject[] pictures = GameObject.FindGameObjectsWithTag("Picture");
        for (int i = 0; i < pictures.Length; i++)
        {
            DestroyImmediate(pictures[i].GetComponent<UnityEngine.VR.WSA.WorldAnchor>());
            pictures[i].GetComponent<Renderer>().material.mainTexture = randomTextures[(int)UnityEngine.Random.Range(0, randomTextures.Length)];
            pictures[i].transform.Translate(new Vector3(UnityEngine.Random.Range(-0.2f, 0.2f), UnityEngine.Random.Range(-0.2f, 0.2f), UnityEngine.Random.Range(-0.2f, 0.2f)));
            pictures[i].AddComponent<UnityEngine.VR.WSA.WorldAnchor>();
        }
    }

    GameObject[] GetCurrentPictureGameObjects()
    {
        return GameObject.FindGameObjectsWithTag("Picture");
    }

    Dictionary<string, Texture2D> GetCurrentPicturesDictionary()
    {
        // Get all pictures in the current scene and save to dictionary
        Dictionary<string, Texture2D> dicTexPictures = new Dictionary<string, Texture2D>();
        GameObject[] pictures = GetCurrentPictureGameObjects();

        for (var i = 0; i < pictures.Length; i++)
        {
            Texture2D tex2D = (Texture2D)pictures[i].GetComponent<Renderer>().material.mainTexture;
            dicTexPictures.Add(pictures[i].name.ToString(), tex2D);
        }

        return dicTexPictures;
    }

    public void Save()
    {

        UnityEngine.VR.WSA.Persistence.WorldAnchorStore.GetAsync((new UnityEngine.VR.WSA.Persistence.WorldAnchorStore.GetAsyncDelegate((UnityEngine.VR.WSA.Persistence.WorldAnchorStore store) => {

            // At first we save the world anchor positions of the polaroid objects
            SaveWorldAnchors(store);

            // After that the polaroid images will be saved to disk
            SavePolaroidFiles(overrideFiles);
        })));

    }

    void SaveWorldAnchors(UnityEngine.VR.WSA.Persistence.WorldAnchorStore store)
    {
        // Get all the polaroids in the scene
        picturesDictionary = GetCurrentPicturesDictionary();

        foreach (KeyValuePair<string, Texture2D> picture in picturesDictionary)
        {
            SaveWorldAnchor(store, picture);
        }
    }

    void SaveWorldAnchor(UnityEngine.VR.WSA.Persistence.WorldAnchorStore store, KeyValuePair<string, Texture2D> picture)
    {
        GameObject go = GameObject.Find(picture.Key);

        if (go != null)
        {
            // Delete the old position of the specific polaroid
            store.Delete(picture.Key);

            // Save the new position of the polaroid in the store
            UnityEngine.VR.WSA.WorldAnchor wa = go.GetComponent<UnityEngine.VR.WSA.WorldAnchor>();

            if (wa != null)
            {
                store.Save(picture.Key, wa);
            }
        }
    }

    public void SaveWorldAnchor(KeyValuePair<string, Texture2D> picture)
    {
        // Replace "UnityEngine.VR..." with "UnityEngine.XR..." in future versions
        // Do not use Unity 2017.2 because some HoloLens interactions are broken in
        // Unity 2017.2 version which will be fixed in future versions of Unity
        UnityEngine.VR.WSA.Persistence.WorldAnchorStore.GetAsync(new UnityEngine.VR.WSA.Persistence.WorldAnchorStore.GetAsyncDelegate((UnityEngine.VR.WSA.Persistence.WorldAnchorStore store) =>
        {
            GameObject go = GameObject.Find(picture.Key);

            if (go != null)
            {
                // Delete the old position of the specific polaroid
                store.Delete(picture.Key);

                // Save the new position of the polaroid in the store
                UnityEngine.VR.WSA.WorldAnchor wa = go.GetComponent<UnityEngine.VR.WSA.WorldAnchor>();

                if (wa != null)
                {
                    store.Save(picture.Key, wa);
                }
            }
        }));
    }

    private void SavePolaroidFiles(bool overrideFiles)
    {
        // Get an overview of all existing polaroids in the scene
        picturesDictionary = GetCurrentPicturesDictionary();

        // Save them to disk afterwards
        foreach (KeyValuePair<string, Texture2D> picture in picturesDictionary)
        {
            SavePolaroidFile(picture, overrideFiles);
        }
    }

    public async void SavePolaroidFile(KeyValuePair<string, Texture2D> picture, bool overrideFiles)
    {
        Texture2D tex2D = (Texture2D)picture.Value;

        string dest = Path.Combine(folderPath, picture.Key + ".jpg");
        FileInfo fi = new FileInfo(dest);

        // if the image doesn't exist on the hard drive then save it
        if (!fi.Exists || overrideFiles)
        {
            // This method should be replaced by a better performing function
            // because it can only be called from the main thread
            byte[] texJPG = tex2D.EncodeToJPG();

            await Task.Run(() =>
            {
                // Write the file to harddrive. Note: "WriteAllBytes" takes care
                // of closing the file after being written to harddrive
                File.WriteAllBytes(dest, texJPG);
            });
        }
    }

    public void SavePolaroidFile(KeyValuePair<string, Texture2D> picture)
    {
        SavePolaroidFile(picture, overrideFiles);
    }

    public void Load()
    {
        UnityEngine.VR.WSA.Persistence.WorldAnchorStore.GetAsync(new UnityEngine.VR.WSA.Persistence.WorldAnchorStore.GetAsyncDelegate((UnityEngine.VR.WSA.Persistence.WorldAnchorStore store) => {
            if (store.anchorCount > 0)
            {
                int counter = 0;
                // for every world anchor in the world anchor store try to load the position and image
                foreach (string id in store.GetAllIds())
                {
                    // we get the file's name from the world anchor store
                    System.IO.FileInfo fileInfo = new FileInfo(Path.Combine(folderPath, id + ".jpg"));

                    // check first, if the image file exists
                    if (fileInfo.Exists)
                    {
                        // Optional: assign placeholder loading texture
                        AssignLoadingTexture(id);

                        // load the corresponding world anchor for the gameobject
                        LoadWorldAnchor(store, id);

                        // load the image file from disk
                        LoadPolaroidFromFile(id);

                        ++counter;
                    }
                }

                // Run tasks in queue, Second Attribute defines how many tasks can run simultaneously
                RunTasksInQueue(taskQueue, 1);
            }
        }));
    }

    void LoadWorldAnchor(UnityEngine.VR.WSA.Persistence.WorldAnchorStore store, string id)
    {
        GameObject go = GameObject.Find(id);

        // If the gameobject for this world anchor doesn't exist create it
        if (go == null)
        {
            go = Instantiate(polaroidPrefab);
            go.name = id;
        }

        // Read the world anchor from the world anchor store to get object position
        store.Load(go.name, go);
    }

    void LoadPolaroidFromFile(string id)
    {
        string goName = id;

        // After that the appropriate image file will be loaded from the harddrive
        Task task = new Task(
            () =>
            {
                FileInfo currentFileInfo = new FileInfo(Path.Combine(folderPath, id + ".jpg"));
                byte[] binaryImage;


                // if the image file exists load the image
                if (currentFileInfo.Exists)
                {
                    // Read the corresponding image file from the harddrive
                    binaryImage = File.ReadAllBytes(currentFileInfo.FullName);

                    // Decode the image file to a raw image
                    NanoJPEG nanoJPEG = new NanoJPEG();
                    nanoJPEG.njDecode(binaryImage);
                    int width = nanoJPEG.njGetWidth();
                    int height = nanoJPEG.njGetHeight();

                    bool scaleDown = true;

                    if (scaleDown == true)
                    {
                        // Add the image to an image list from which the raw image will be loaded into a texture
                        // To improve loading speed we load with one fourth of the original resolution
                        RawImageStruct loadedRawImage = new RawImageStruct(nanoJPEG.njGetImage(), goName, width, height);
                        rawImageStructList.Add(FlipImageX(loadedRawImage, true, 4));
                    }
                    else
                    {
                        // Add the image to an image list from which the raw image will be loaded into a texture
                        RawImageStruct loadedRawImage = new RawImageStruct(nanoJPEG.njGetImage(), goName, width, height);
                        rawImageStructList.Add(FlipImageX(loadedRawImage, false));
                    }

                    // check constantly if images are ready to be applied to the polaroid
                    CheckForImageRepeatedly(1, true);
                }
                else
                {
                    UnityEngine.VR.WSA.Persistence.WorldAnchorStore.GetAsync(new UnityEngine.VR.WSA.Persistence.WorldAnchorStore.GetAsyncDelegate((UnityEngine.VR.WSA.Persistence.WorldAnchorStore store) =>
                    {
                        // If the image file doesn't exist, delete the image from the World Anchor Store
                        store.Delete(id);
                    }));
                }
            }, TaskCreationOptions.PreferFairness);

        // Add to task queue
        taskQueue.Enqueue(task);
    }

    void AssignLoadingTexture(string id)
    {
        GameObject go = GameObject.Find(id);
        if (go != null)
        {
            go.GetComponent<Renderer>().material.mainTexture = loadingTexture;
        }
    }

    public void DeleteAllPolaroids()
    {
        // In the prototype version of the app, the image files won't be wiped from the disk
        // They just won't show up in the app anymore and are still accessible from the gallery
        // Feel free to implement this feature if needed

        GameObject[] gameObjects;

        gameObjects = GameObject.FindGameObjectsWithTag("Picture");

        UnityEngine.VR.WSA.Persistence.WorldAnchorStore.GetAsync(new UnityEngine.VR.WSA.Persistence.WorldAnchorStore.GetAsyncDelegate((UnityEngine.VR.WSA.Persistence.WorldAnchorStore store) =>
        {
            for (var i = 0; i < gameObjects.Length; i++)
            {
                if (gameObjects[i] != null)
                {
                    // Delete the old position of the specific polaroid
                    store.Delete(gameObjects[i].name);
                }
                Destroy(gameObjects[i]);
            }
        }));
    }

    public void DeleteCurrentPolaroid()
    {
        GameObject hitObject;
        hitObject = GazeManager.Instance.HitObject;

        if (hitObject != null)
        {
            if (hitObject.tag == "Picture")
            {
                UnityEngine.VR.WSA.Persistence.WorldAnchorStore.GetAsync(new UnityEngine.VR.WSA.Persistence.WorldAnchorStore.GetAsyncDelegate((UnityEngine.VR.WSA.Persistence.WorldAnchorStore store) =>
                {
                    // Delete the old position of the specific polaroid
                    store.Delete(hitObject.name);

                    Destroy(hitObject);
                }));
            }
        }
    }

        int maxSimultaneousTasks = 3;
    int currentlyRunningTasks = 0;
    Queue<Task> taskQueue = new Queue<Task>();
    // A simple task planning function which allows to execute a certain amount of tasks simultaneously 
    public async void RunTasksInQueue(Queue<Task> tasksToRun, int maxTasksToRunInParallel)
    {
        List<Task> tasks = tasksToRun.ToList();
        maxSimultaneousTasks = maxTasksToRunInParallel;

        foreach (Task task in tasks) {
            if (currentlyRunningTasks < maxSimultaneousTasks)
            {
                if (taskQueue.Count > 0)
                {
                    Task t = taskQueue.Dequeue();
                    t.Start();
                    ++currentlyRunningTasks;
                    await t.ContinueWith(tsk => { --currentlyRunningTasks; RunTasksInQueue(taskQueue, maxTasksToRunInParallel); });
                }
            }
        }
    }

    List<RawImageStruct> rawImageStructList;

    struct RawImageStruct
    {
        public byte[] rawImage;
        public string polaroidName;
        public int width;
        public int height;

        public RawImageStruct(byte[] bImage, string polaroid, int rawWidth, int rawHeight) : this()
        {
            this.rawImage = bImage;
            this.polaroidName = polaroid;
            this.width = rawWidth;
            this.height = rawHeight;
        }
    }

    RawImageStruct FlipImageX(RawImageStruct imageStruct, bool scaleDown)
    {
        if (!scaleDown)
        {
            return FlipImageX(imageStruct, scaleDown, 1);
        }
        else
        {
            return FlipImageX(imageStruct, scaleDown, 4);
        }
    }

    RawImageStruct FlipImageX(RawImageStruct imageStruct, bool scaleDown, int divisor)
    {
        if (!scaleDown)
        {
            byte[] rawImageFlippedX = new byte[imageStruct.rawImage.Length];
            for (int y = 0; y < imageStruct.height; y++)
            {
                for (int x = 0; x < imageStruct.width; x++)
                {
                    for (int i = 0; i < 3; i++)
                    {
                        rawImageFlippedX[y * 3 * imageStruct.width + 3 * x + i] = imageStruct.rawImage[(imageStruct.height - y - 1) * 3 * imageStruct.width + 3 * x + i];
                    }
                }
            }
            return new RawImageStruct(rawImageFlippedX, imageStruct.polaroidName, imageStruct.width, imageStruct.height);
        }
        else
        {
            byte[] rawImageFlippedX = new byte[imageStruct.rawImage.Length / divisor];
            for (int y = 0; y < imageStruct.height; y += divisor)
            {
                for (int x = 0; x < imageStruct.width; x += divisor)
                {
                    for (int i = 0; i < 3; i++)
                    {
                        rawImageFlippedX[y / divisor * 3 * imageStruct.width / divisor + 3 * x / divisor + i] = imageStruct.rawImage[(imageStruct.height - y - 1) * 3 * imageStruct.width + 3 * x + i];
                    }
                }
            }
            return new RawImageStruct(rawImageFlippedX, imageStruct.polaroidName, imageStruct.width / divisor, imageStruct.height / divisor);
        }
    }

    byte[] FlipImageX(byte[] rawImage, int width, int height, bool scaleDown)
    {
        if (!scaleDown)
        {
            byte[] rawImageFlippedX = new byte[rawImage.Length];
            for (int y = 0; y < height; y++)
            {
                for (int x = 0; x < width; x++)
                {
                    for (int i = 0; i < 3; i++)
                    {
                        rawImageFlippedX[y * 3 * width + 3 * x + i] = rawImage[(height - y - 1) * 3 * width + 3 * x + i];
                    }
                }
            }
            return rawImageFlippedX;
        }
        else
        {
            int divisor = 2;
            byte[] rawImageFlippedX = new byte[rawImage.Length / divisor];
            for (int y = 0; y < height; y += divisor)
            {
                for (int x = 0; x < width; x += divisor)
                {
                    for (int i = 0; i < 3; i++)
                    {
                        rawImageFlippedX[y / divisor * 3 * (width / divisor) + 3 * x / divisor + i] = rawImage[((height / 2) - y - 1) * 3 * (width / 2) + 3 * x + i];
                    }
                }
            }
            return rawImageFlippedX;
        }
    }

    Texture2D LoadTextureDataToTexture(byte[] data, int width, int height)
    {
        // Create Texture
        Texture2D texture = new Texture2D(width, height, TextureFormat.RGB24, false);
        // Load the raw data into Unity
        texture.LoadRawTextureData(data);
        // Update the texture visualization
        texture.Apply();
        // Return Texture
        return texture;
    }

    // Timer variables which are needed to check for new images
    float timerIntervall = 4;
    float currentTimerValue = 0;
    bool checkForImages = false;

    private void Update()
    {
        currentTimerValue += Time.deltaTime;
        if (currentTimerValue > timerIntervall && checkForImages)
        {
            currentTimerValue = 0;
            CheckForImage();
        }
    }

    void CheckForImageRepeatedly(int seconds, bool setActive)
    {
        checkForImages = setActive;
        timerIntervall = seconds;
    }

    void CheckForImage()
    {
        // check if there are images to be applied as texture
        if (rawImageStructList.Count > 0)
        {
            
            RawImageStruct[] rawImageStructs = rawImageStructList.ToArray();
            for (int i = 0; i < rawImageStructs.Length; ++i)
            {
                // check if the current image file was loaded correctly
                if (rawImageStructs[i].rawImage != null)
                {
                    // Look for the gameobject where the image file should be applied to
                    GameObject go = GameObject.Find(rawImageStructs[i].polaroidName);
                    if (go != null)
                    {
                        // Apply the new texture to the gameobject
                        go.GetComponent<Renderer>().material.mainTexture = LoadTextureDataToTexture(rawImageStructs[i].rawImage, rawImageStructs[i].width, rawImageStructs[i].height);
                        
                        rawImageStructList.Remove(rawImageStructs[i]);
                    }
                    else
                    {
                        // if there is no gameobject for the texture, delete it from list
                        rawImageStructList.Remove(rawImageStructs[i]);
                    }
                }
                else
                {
                    // if the raw image is null or broken delete it from list
                    rawImageStructList.Remove(rawImageStructs[i]);
                }
            }
        }

        // stop checking for images if loading is completed
        if (rawImageStructList.Count == 0)
        {
            checkForImages = false;
        }
    }

}