// Only uncomment one of the following:
#define M_HOLOLENS_CODE_WITH_OR_WITHOUT_HEADSET
//#define M_VIVE_OR_DESKTOP // in this code, distinguish between desktop or headset with GC.isInDesktopMode

#if M_HOLOLENS_CODE_WITH_OR_WITHOUT_HEADSET
#if UNITY_EDITOR // for code running within the unity editor
#define M_HOLOLENS_CODE_WITHOUT_HEADSET // hololens codebase, but running in unity player, without headset (e.g. for debugging network stuff)
#elif WINDOWS_UWP // for code running on the Hololens headset
#define M_HOLOLENS_CODE_WITH_HEADSET
#endif
#endif

#if UNITY_EDITOR || WINDOWS_UWP
#define M_UNITY_API
#endif


//using System.Collections;
using System.Collections.Generic;
using System.IO;
//using System.Linq;
//using System.Text;
using System.Xml.Serialization;

#if M_UNITY_API
using UnityEngine;
#endif
#if M_HOLOLENS_CODE_WITH_HEADSET
using System;
using System.Threading.Tasks;
using Windows.Storage;
#endif


//namespace McGuffinsOfflineNetworkWorkshop {

    public class BooleanWrapper {
        public bool v; // value
        public BooleanWrapper( bool value ) { v = value; }
    }
    public class PairOfInts {
        public int a;
        public int b;
        public PairOfInts(int x,int y) { a=x; b=y; }
    }
    // This is part of an ugly hack to make it easy to read/write a string using XML serialization.
    public class ClassContainingAString {
        // Data members that you want serialized into XML need to be public.
        // Data members that should NOT be serialized need to be private or marked with [System.Xml.Serialization.XmlIgnoreAttribute]
        public string s;
    }


    public class MUtil {
        public static string fullInfoAboutException( System.Exception exception ) {
            // from https://stackoverflow.com/questions/4272579/how-to-print-full-stack-trace-in-exception
            var stringBuilder = new System.Text.StringBuilder();
            while ( exception != null ) {
                stringBuilder.AppendLine( exception.Message );
                stringBuilder.AppendLine( exception.StackTrace );
                exception = exception.InnerException;
            }
            return stringBuilder.ToString();
        }

        public static void shiftLastToFirst<T>( List<T> ls, int numItemsToShift ) {
            int indexOfLast = ls.Count - 1;
            if ( indexOfLast <= 0 ) return;
            for ( int i = 0; i < numItemsToShift; ++i ) {
                T item = ls[ indexOfLast ];
                ls.RemoveAt( indexOfLast );
                // This operation takes linear time, and the whole routine could be made much more efficient, but it's not worth bothering to optimize
                ls.Insert( 0, item );
            }
        }
        public static void shiftFirstToLast<T>( List<T> ls, int numItemsToShift ) {
            for ( int i = 0; i < numItemsToShift; ++i ) {
                T item = ls[ 0 ];
                // This operation takes linear time, and the whole routine could be made much more efficient, but it's not worth bothering to optimize
                ls.RemoveAt( 0 );
                ls.Add( item );
            }
        }
        public static void shuffleList<T>( List<T> l, System.Random r ) {
            int size = l.Count;
            for ( int i = 0; i < size-1 /*skip last iteration*/; ++i ) {
                // a random integer in [0,size-i-1]
                int j = r.Next(size-i);
                j += i; // now j is in [i,size-1]
                MLogger.Assert( i <= j && j <= size-1, "6k85597hmi" );

                // Swap elements i and j.
                // Note that i and j may be equal, which is okay
                // (we're not required to generate a derangement of the input).
                // Usually, however, i and j won't be equal,
                // hence we don't bother checking for it.
                T tmp = l[i];
                l[i] = l[j];
                l[j] = tmp;
            }
        }


        // This should work well if the input orientations (passed in as frame axis vectors)
        // are close to each other.
        // (You can compute a quaternion with Quaternion.LookRotation( averageForward.asUnityVector3(), averageUp.asUnityVector3() ); )
        // If instead you want to average orientations that may be very different,
        // and you're lucky enough to only have two orientations to average,
        // use a quaternion slerp with an interpolation parameter of 0.5.
        // If you have more than two quaternions to average, and they may be very different
        // from each other, try reading
        //   https://stackoverflow.com/questions/12374087/average-of-multiple-quaternions
        // ... or use the below method at your risk, because the result may be degenerate.
        public static void averageOrientation(
            List<MVector3> forwardVectors,
            List<MVector3> upVectors,
            out MVector3 averageForward,
            out MVector3 averageUp
        ) {
            averageForward = new MVector3();
            averageUp = new MVector3();
            for ( int i = 0; i < forwardVectors.Count; ++i ) {
                averageForward += forwardVectors[i];
                averageUp += upVectors[i];
            }
            // Now ensure that the vectors are orthonormal.
            averageForward = averageForward.normalized();
            // The following variable name assumes a right-handed coordinate system,
            // but the resulting up should be be orthogonal to forward
            // even if the coordinate system is left-handed.
            MVector3 right = MVector3.cross( averageForward, averageUp );
            averageUp = MVector3.cross( right, averageForward ).normalized();
        }
#if M_UNITY_API
        // From https://answers.unity.com/questions/31622/is-it-possible-to-find-the-width-of-a-text-mesh.html
        public static float GetWidthOfTextMesh( TextMesh mesh ) {
            float width = 0;
            foreach ( char symbol in mesh.text ) {
                CharacterInfo info;
                if ( mesh.font.GetCharacterInfo( symbol, out info, mesh.fontSize, mesh.fontStyle ) ) {
                    width += info.advance;
                }
            }
            return width * mesh.characterSize * 0.1f;
        }


        public static Quaternion averageOrientation(
            List<Quaternion> quaternions
        ) {
            if ( quaternions.Count == 0 ) return new Quaternion();
            if ( quaternions.Count == 1 ) return quaternions[0];
            if ( quaternions.Count == 2 ) return Quaternion.Slerp(quaternions[0],quaternions[1],0.5f);

            // This only works well if the quaternions are close to each other.
            List<MVector3> forwardList = new List<MVector3>();
            List<MVector3> upList = new List<MVector3>();
            for ( int i = 0; i < quaternions.Count; ++i ) {
                forwardList.Add( new MVector3(quaternions[i] * Vector3.forward) );
                upList.Add( new MVector3(quaternions[i] * Vector3.up) );
            }
            MVector3 f;
            MVector3 u;
            averageOrientation(forwardList,upList,out f,out u);
            return Quaternion.LookRotation(f.asUnityVector3(),u.asUnityVector3());
        }
        public static Quaternion defaultOrientation() { return Quaternion.LookRotation(Vector3.forward); }
#endif


        // http://blog.danskingdom.com/saving-and-loading-a-c-objects-data-to-an-xml-json-or-binary-file/
#if !M_UNITY_API
        // This version is for standalone (without Unity).
        public static string folder = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
        public static void WriteXml<T>(string fileName, T objectToWrite) where T : new() {
            MLogger.Log("Folder: " + folder);
            string filePath = Path.Combine(folder, fileName);
            TextWriter tw = null;
            try {
                tw = new StreamWriter(filePath, false);
                var serializer = new XmlSerializer(typeof(T));
                serializer.Serialize(tw, objectToWrite);
            }
            finally {
                if (tw != null)
                    tw.Close();
            }
        }
#endif
#if M_HOLOLENS_CODE_WITHOUT_HEADSET || M_VIVE_OR_DESKTOP
        // This version of the code is for Unity preview mode, not on HoloLens.
        public static string folder; // = Application.persistentDataPath; // maps to C:\Users\(userid)\AppData\LocalLow\DefaultCompany\(app name)\
        public static void WriteXml<T>(string fileName, T objectToWrite) where T : new() {
            MLogger.Log("Folder: " + folder);
            string filePath = Path.Combine(folder, fileName);
            //FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read);
            FileStream fs = new FileStream(filePath, FileMode.Create );
            if (fs != null) {
                StreamWriter sw = new StreamWriter(fs);
                if (sw != null) {
                    //sw.WriteLine("Livin' the baller life! #AR #HoloLens");
                    try {
                        var serializer = new XmlSerializer(typeof(T));
                        serializer.Serialize(sw, objectToWrite);
                    }
                    finally {
                        sw.Close();
                    }
                }
                else {
                    MLogger.Log("Creating StreamWriter failed");
                }
            }
            else {
                MLogger.Log("Creating FileStream failed");
            }
        }
#endif
#if M_HOLOLENS_CODE_WITH_HEADSET
        // This version of the code is for HoloLens.
        public static StorageFolder folder = Windows.Storage.ApplicationData.Current.LocalFolder; // can be accessed from the web portal under File Explorer / User Files / LocalAppData / (program name) / LocalState
        async public static void WriteXml<T>(string fileName, T objectToWrite) where T : new() {

            StorageFile file = await folder.CreateFileAsync(fileName, CreationCollisionOption.ReplaceExisting);
            MLogger.Log("Trying to write file...");
            using (Stream stream = await file.OpenStreamForWriteAsync()) {
                MLogger.Log("(writing step:2)");
                using (StreamWriter sw = new StreamWriter(stream)) {
                    //sw.WriteLine("Livin' the baller life! #AR #HoloLens");
                    MLogger.Log("(writing step:3)");
                    try {
                        MLogger.Log("(writing step:4)");
                        var serializer = new XmlSerializer(typeof(T));
                        serializer.Serialize(sw, objectToWrite);
                        MLogger.Log("...successfully wrote file");
                    }
                    finally {
                    }
                }
            }
        }
#endif



#if !M_UNITY_API

        public static T ReadXml<T>(string fileName) where T : new() {
            MLogger.Log("Folder: " + folder);
            string filePath = Path.Combine(folder, fileName);
            TextReader tr = null;
            try {
                tr = new StreamReader(filePath);
                var serializer = new XmlSerializer(typeof(T));
                return (T)serializer.Deserialize(tr);
            }
            finally {
                if (tr != null)
                    tr.Close();
            }
        }
#endif
#if M_HOLOLENS_CODE_WITHOUT_HEADSET || M_VIVE_OR_DESKTOP
        public static T ReadXml<T>(string fileName) where T : new() {
            MLogger.Log("Folder: " + folder);
            string filePath = Path.Combine(folder, fileName);
            FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read);
            //FileStream fs = new FileStream(filePath, FileMode.Create );
            if (fs != null) {
                StreamReader sr = new StreamReader(fs);
                if (sr != null) {
                    try {
                        var serializer = new XmlSerializer(typeof(T));
                        return (T)serializer.Deserialize(sr);
                    }
                    finally {
                        sr.Close();
                    }
                }
                else {
                    MLogger.Log("Creating StreamReader failed");
                }
            }
            else {
                MLogger.Log("Creating FileStream failed");
            }
            return default(T);
        }
#endif
#if M_HOLOLENS_CODE_WITH_HEADSET
        async public static Task<T> ReadXml<T>(string fileName) where T : new() {
            //Stream stream = await folder.OpenStreamForReadAsync(filename);
            StorageFile file = await folder.GetFileAsync(fileName);
            //var stream = await file.OpenAsync(FileAccessMode.Read); // doesn't return a stream of the correct type
            var inputStream = await file.OpenReadAsync();
            var classicStream = inputStream.AsStreamForRead();
            if ( classicStream != null ) {
                using (StreamReader sr = new StreamReader(classicStream)) {
                    try {
                        var serializer = new XmlSerializer(typeof(T));
                        return (T)serializer.Deserialize(sr);
                    }
                    finally {
                    }
                }
            }
            return default(T);
        }
#endif

        // If you have an XML file (or other text file) that you want to use as read-only input for your Unity project,
        // and want it to be deployed automatically with your project, you can place the file in your Unity Assets folder,
        // and then declare the below line in a class that derives from MonoBehaviour
        //      public TextAsset textFile1;
        // and then, in the Unity Editor, when you select a GameObject with the corresponding script attached,
        // there will be a field for textFil1 into which you can drag the xml file from your assets.
        // ( https://answers.unity.com/questions/59466/loading-xml-file-from-resources-file-after-build.html ).
        // Then, at runtime, retrieve the contents of the file as a string as textFile1.text
        // and, if you like, pass it into the below routine to reconstruct the object.
        // Other approaches:
        //    https://answers.unity.com/questions/916489/where-to-put-xml-files-in-unity-project.html
        //    https://forums.hololens.com/discussion/1862/how-to-deploy-and-read-data-file-with-app
        //
        // Code below adapted from:
        // https://stackoverflow.com/questions/2347642/deserialize-from-string-instead-textreader
        public static T DeserializeXmlStringToObject<T>(string input) where T : new() {
            var serializer = new XmlSerializer(typeof(T));
            try {
                using (TextReader reader = new StringReader(input)) {
                    return (T)serializer.Deserialize(reader);
                }
            }
            finally {
            }
            // return default(T);
        }
    }

//} // namespace
