/************************************************************************
 FAUST Architecture File
 Copyright (C) 2003-2019 GRAME, Centre National de Creation Musicale
 ---------------------------------------------------------------------
 This Architecture section is free software; you can redistribute it
 and/or modify it under the terms of the GNU General Public License
 as published by the Free Software Foundation; either version 3 of
 the License, or (at your option) any later version.
 
 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.
 
 You should have received a copy of the GNU General Public License
 along with this program; If not, see <http://www.gnu.org/licenses/>.
 
 EXCEPTION : As a special exception, you may create a larger work
 that contains this FAUST architecture section and distribute
 that work under terms of your choice, so long as this FAUST
 architecture section is not modified.
 
 ************************************************************************
 ************************************************************************/
 
// @notes MODEL, RELEVANTJSONFILE, PLUGNAME and PLUGINNAME are replaced during processing
using System;
using System.Text;
using System.Runtime.InteropServices;
using System.Collections.Generic;
using System.Globalization;
using UnityEngine;

/* @brief This file contains additionnal codes to communicate with the plugin and build the inspector interface
* @brief It shouldn't be changed
*/
namespace FaustUtilities_MODEL {

    #if UNITY_EDITOR
    using UnityEditor;

    [CustomEditor(typeof(FaustPlugin_MODEL))]
    public class FaustPlugin_Editor: Editor {

        // Initialization of the dsp
        private FaustPlugin_MODEL _dsp;

        // Initialization of the UI related variables
        private string fJSON;
        private FaustUI fUI;
        private int param;

        // @brief All instantiations have to be done in the OnEnable() method.
        private void OnEnable() {
            fJSON = "RELEVANTJSONFILE";
            if (!FaustUI.fJSONParser(ref fJSON, out fUI)) { // Parses the JSON file
                  UnityEngine.Debug.LogError("Error JSON Parser");
            }
            param = fUI.getUI(0).setNumParams(param);   // Sets the parameter number
            _dsp = (FaustPlugin_MODEL) target;          // Sets which component will be edited in the inspector
        }

        // @brief Method called when you click on the inspector
        public override void OnInspectorGUI() {
            GUI.enabled = true;
            GUILayout.BeginVertical(); // Each block of elements needs to be wrap by Begin/EndVertical or Begin/EndHorizontal whether it's a vertical or horizontal group
            GUILayout.BeginHorizontal();
            GUILayout.FlexibleSpace();
            // Button to reset the initial parameter value
            if (GUILayout.Button(new GUIContent("Initialization", "Set initial values"), GUILayout.Width(100), GUILayout.Height(20))) {
                for (int i = 0; i < param; i++) {
                    changeValueParam(0f, fUI.getUI(0).getItem(i).init, i);
                }
            }
            GUILayout.FlexibleSpace();
            GUILayout.EndHorizontal();
            // HelpBox to explain how control the parameter
            EditorGUILayout.HelpBox("To control a parameter using scripts, use the parameter number and the setParameter() method", MessageType.Info);
            // Test if the first item is a group (normally it's always true)
            if (fUI.getUI(0).type == "hgroup" || fUI.getUI(0).type == "vgroup" || fUI.getUI(0).type == "tgroup") {
                EditorGUILayout.BeginVertical();
                // the groupstate variable define if the group is opened or closed
                fUI.getUI(0).groupstate = EditorGUILayout.Foldout(fUI.getUI(0).groupstate, fUI.getUI(0).label);
                EditorGUI.indentLevel++;
                if (fUI.getUI(0).groupstate) {
                    // if the group is opened, the subgroup are displayed
                    for (int i = 0; i < fUI.getUI(0).items.Count; i++) {
                        makeLayoutGrp(fUI.getUI(0).items[i]);
                    }
                    EditorGUILayout.EndVertical();
                }
                EditorGUI.indentLevel--;
            }
            EditorGUI.indentLevel--;
            EditorGUILayout.EndVertical();
        }

        /* @brief Method to display a bargraph
         * @param value Current value of the param in the dsp
         * @param item Which item is displayed 
         */
        private void progressBar(float value, Group item) {
            EditorGUILayout.Space();
            EditorGUILayout.BeginHorizontal();
            if (item.label == "0x00") { // If the name of the parameter haven't been set, the default name 0x00 is erased
                item.label = "";
            }
            Rect rect = GUILayoutUtility.GetRect(18, 18, "TextField");
            EditorGUI.ProgressBar(rect, (value - item.min) / (item.max - item.min), item.label); // The range of the toolbar is between 0 and 1 so the values need to be normalize
            Repaint(); // Updates the bargraph value
            EditorGUILayout.EndHorizontal();
            EditorGUILayout.Space();
        }

        /* @brief Method to display an horizontal slider
         * @param value Current value of the param in the dsp
         * @param item Which item is displayed
         * @return the new value of the param 
         */
        private float hSlider(float value, Group item) {
            EditorGUILayout.BeginHorizontal();
            if (item.label == "0x00") { // If the name of the parameter haven't been set, the default name 0x00 is erased
                item.label = "";
            }
            float newvalue = EditorGUILayout.Slider(new GUIContent(item.label, helpBox(item)), value, item.min, item.max);
            Repaint();
            EditorGUILayout.EndHorizontal();
            return newvalue;
        }

        /* @brief Method to display a numerical entry
         * @param value Current value of the param in the dsp
         * @param item Which item is displayed
         * @return the new value of the entry 
         */
        private float numEntry(float value, Group item) {
            EditorGUILayout.BeginHorizontal();
            if (item.label == "0x00") { // If the name of the parameter haven't been set, the default name 0x00 is erased
                item.label = "";
            }
            float newvalue = EditorGUILayout.FloatField(new GUIContent(item.label, helpBox(item)), value);
            Repaint();
            EditorGUILayout.EndHorizontal();
            return newvalue;
        }

        /* @brief Method to display a checkbox
         * @param value Current value of the param in the dsp
         * @param item Which item is displayed
         * @return the new value of the param
         */
        private float checkBox(float value, Group item) {
            EditorGUILayout.BeginHorizontal();
            if (item.label == "0x00") { // If the name of the parameter haven't been set, the default name 0x00 is erased
                item.label = "";
            }
            // A conversion between bool and float needs to be done (Unity: bool; Faust: float)
            bool temp1 = Convert.ToBoolean(value);
            bool temp2 = EditorGUILayout.Toggle(new GUIContent(item.label, helpBox(item)), temp1);
            float newvalue = Convert.ToSingle(temp2);
            Repaint();
            EditorGUILayout.EndHorizontal();
            return newvalue;
        }

        /* @brief Method to display a button
         * @param value Current value of the param in the dsp
         * @param item Which item is displayed
         * @return the new value if the button
         */
        private float button(Group item) {
            EditorGUILayout.BeginHorizontal();
            if (item.label == "0x00") { // If the name of the parameter haven't been set, the default name 0x00 is erased
                item.label = "";
            }
            if (GUILayout.Button(new GUIContent(item.label, helpBox(item)))) {
                Repaint();
                EditorGUILayout.EndHorizontal();
                return 1;
            } else {
                EditorGUILayout.EndHorizontal();
                return 0;
            }
        }

        /* @brief Method to display a helpbox when the mouse is on the parameter, use the metadata from the json
         * @param item Which item is displayed
         * @return the string to display
         */
        private string helpBox(Group item) {
            string message = "";
            if (item.meta != null) {
                for (int i = 0; i < item.meta.Count; i++) {
                    if (item.meta[i].unit != null) {
                        message += "\nUnit : " + item.meta[i].unit;
                    }
                    if (item.meta[i].scale != null) {
                        message += "\nScale : " + item.meta[i].scale;
                    }
                    if (item.meta[i].tooltip != null) {
                        message += "\nDescription : " + item.meta[i].tooltip;
                    }
                }
                return message;
            } else {
                return null;
            }
        }

        /* @brief Method to add the different elements on the inspector and deals with the varition of the parameter
         * @param item Which item will be displayed 
         */
        private void addComponent(Group item) {
            int numparam = item.numparam;
            // Tests the type of the item and checks if there is a variation of the parameter
            if (item.type == "vslider" || item.type == "hslider") {
                float value = _dsp.getParameter(numparam);
                float newvalue = hSlider(value, item);
                changeValueParam(value, newvalue, numparam);
            } else if (item.type == "nentry") {
                float value = _dsp.getParameter(numparam);
                float newvalue = numEntry(value, item);
                changeValueParam(value, newvalue, numparam);
            } else if (item.type == "checkbox") {
                float value = _dsp.getParameter(numparam);
                float newvalue = checkBox(value, item);
                changeValueParam(value, newvalue, numparam);
            } else if (item.type == "button") {
                float value = _dsp.getParameter(numparam);
                float newvalue = button(item);
                changeValueParam(value, newvalue, numparam);
            } else if (item.type == "hbargraph" || item.type == "vbargraph") {
                float value = _dsp.getParameter(numparam);
                progressBar(value, item);
            }
        }

        /* @brief Recursive method to organize the layout of the inspector
         * @param item Which item is displayed 
         */
        private void makeLayoutGrp(Group item) {
            // If the items.items is null, the item is actually an item (slider, etc) and not a group because it doesn't contain any item
            if (item.items != null) {
                // If not, the item is a group of items
                if (item.type == "hgroup" || item.type == "vgroup" || item.type == "tgroup") {
                    EditorGUILayout.BeginVertical();
                    // Display the group
                    item.groupstate = EditorGUILayout.Foldout(item.groupstate, item.label);
                    // if the group is opened the method checks the items in the group
                    if (item.groupstate) {
                        EditorGUI.indentLevel++;
                        for (int i = 0; i < item.items.Count; i++) {
                            makeLayoutGrp(item.items[i]); // Call the same method for each item in the group
                        }
                        EditorGUI.indentLevel--;
                    }
                    EditorGUILayout.EndVertical();
                }
            } else {
                addComponent(item); // Add the component to the inspector
            }
        }

        /* @brief Method to change the value of a param in the dsp
         * @param value Value of the param in the dsp
         * @param newvalue Current value of the param in the inspector 
         */
        private void changeValueParam(float value, float newvalue, int param) {
            if (newvalue != value) {
                _dsp.setParameter(param, newvalue);
            }
        }
    }

    #endif // UNITY_EDITOR

    /* @brief This class is the interface between the native plugin and the Unity environment
     */
    public class Faust_Context {

        private IntPtr _context;

        #if UNITY_EDITOR_OSX || UNITY_EDITOR_WIN || UNITY_STANDALONE_OSX || UNITY_STANDALONE_WIN || UNITY_WSA || UNITY_WSA_10_0
        const string _dllName = "PLUGNAME";
        #elif UNITY_IOS || UNITY_VISIONOS
        const string _dllName = "__Internal";
        #elif UNITY_EDITOR || UNITY_ANDROID || UNITY_STANDALONE_LINUX
        const string _dllName = "PLUGINNAME";
        #else
        Debug.LogError("Architecture not supported by the plugin");
        #endif

        // Imports all c++ function to intialize and process the dsp. The methods need to be private
        [DllImport(_dllName)]
        private static extern IntPtr Faust_contextNew(int buffersize, int nvoices);

        [DllImport(_dllName)]
        private static extern void Faust_contextInit(IntPtr ctx, int samplerate);

        [DllImport(_dllName)]
        private static extern void Faust_process(IntPtr ctx, [In] float[] inbuffer, [Out] float[] outbuffer, int numframes, int channels);

        [DllImport(_dllName)]
        private static extern void Faust_delete(IntPtr ctx);

        [DllImport(_dllName)]
        private static extern int Faust_getSampleRate(IntPtr ctx);

        [DllImport(_dllName)]
        private static extern int Faust_getNumInputChannels(IntPtr ctx);

        [DllImport(_dllName)]
        private static extern int Faust_getNumOutputChannels(IntPtr ctx);

        [DllImport(_dllName)]
        private static extern void Faust_setParameterValue(IntPtr ctx, int param, float value);

        [DllImport(_dllName)]
        private static extern float Faust_getParameterValue(IntPtr ctx, int param);

        [DllImport(_dllName)]
        private static extern float Faust_getParamMin(IntPtr ctx, int param);

        [DllImport(_dllName)]
        private static extern float Faust_getParamMax(IntPtr ctx, int param);
        
        [DllImport(_dllName)]
        private static extern void Faust_keyOn(IntPtr ctx, int channel, int pitch, int velocity);
           
        [DllImport(_dllName)]
        private static extern void Faust_keyOff(IntPtr ctx, int channel, int pitch, int velocity);
   
        [DllImport(_dllName)]
        private static extern void Faust_ctrlChange(IntPtr ctx, int channel, int ctrl, int value); 
   
        [DllImport(_dllName)]
        private static extern void Faust_allNotesOff(IntPtr ctx);

        public Faust_Context(int buffersize) {
            _context = Faust_contextNew(buffersize, 16);
        }

        ~Faust_Context() {
            Faust_delete(_context);
        }

        public void context_init(int samplerate) {
            Faust_contextInit(_context, samplerate);
        }

        public int getSampleRate() {
            return Faust_getSampleRate(_context);
        }

        public int getNumInputChannels() {
            return Faust_getNumInputChannels(_context);
        }

        public int getNumOutputChannels() {
            return Faust_getNumOutputChannels(_context);
        }

        public void process(float[] buffer, int numframes, int channels) {
            Faust_process(_context, buffer, buffer, numframes, channels);
        }

        public void setParameterValue(int param, float value) {
            Faust_setParameterValue(_context, param, value);
        }

        public float getParameterValue(int param) {
            return Faust_getParameterValue(_context, param);
        }

        public float getParamMin(int param) {
            return Faust_getParamMin(_context, param);
        }

        public float getParamMax(int param) {
            return Faust_getParamMax(_context, param);
        }
        
        public void keyOn(int channel, int pitch, int velocity) {
            Faust_keyOn(_context, channel, pitch, velocity);
        }
        
        public void keyOff(int channel, int pitch, int velocity) {
            Faust_keyOff(_context, channel, pitch, velocity);
        }
        
        public void ctrlChange(int channel, int ctrl, int value) {
            Faust_ctrlChange(_context, channel, ctrl, value);
        }
        
        public void allNotesOff() {
            Faust_allNotesOff(_context);
        }

    }

    /* @brief Class to parse the JSON
     * @brief These attributes represent the global datas except the global metadatas (useless in this context)
     * @brief Also contains all methods to parse the JSON
     */
    public class FaustUI {
        public string name;
        public int inputs;
        public int outputs;
        public List < Group > ui;

        public Group getUI(int i) {
            return ui[i];
        }

        public static bool fJSONParser(ref string fJSON, out FaustUI faustUI) {
            faustUI = new FaustUI();
            bool success = false;
            StringBuilder key,
            value;
            if (parseChar(ref fJSON, '{')) {
                do {
                    if (parseDQString(ref fJSON, out key) && parseChar(ref fJSON, ':')) {
                        switch (key.ToString()) {
                        case "name":
                            success = parseDQString(ref fJSON, out value);
                            faustUI.name = value.ToString();
                            break;
                        case "filename":            // Don't use the result
                            success = parseDQString(ref fJSON, out value);
                            break;
                        case "version":             // Don't use the result
                            success = parseDQString(ref fJSON, out value);
                            break;
                        case "compile_options":     // Don't use the result
                            success = parseDQString(ref fJSON, out value);
                            break;
                        case "library_list":        // Don't use the result
                            success = parseGlobalMetaData(ref fJSON);
                            break;
                        case "include_pathnames":   // Don't use the result
                            success = parseGlobalMetaData(ref fJSON);
                            break;
                        case "inputs":
                            success = parseNum(ref fJSON,  out value);
                            faustUI.inputs = Convert.ToInt32(value.ToString());
                            break;
                        case "outputs":
                            success = parseNum(ref fJSON,  out value);
                            faustUI.outputs = Convert.ToInt32(value.ToString());
                            break;
                        case "meta":
                            success = parseGlobalMetaData(ref fJSON);
                            break;
                        case "ui":
                            faustUI.ui = new List < Group > ();
                            int numitems = 0;
                            success = parseUI(ref fJSON, ref faustUI.ui, ref numitems);
                            break;
                        default:
                            // Parse DQString or Num
                            var fJsonCache = fJSON;
                            if (success = parseDQString(ref fJSON, out value)) continue;
                            fJSON = fJsonCache;
                            success = parseNum(ref fJSON,  out value);
                            break;
                        }
                    }
                } while (parseChar(ref fJSON, ','));
            }
            parseChar(ref fJSON, '}');
            return success;
        }

        private static void skipBlank(ref string s) {
            while (Char.IsWhiteSpace(s.ToCharArray()[0])) {
                s = skipChar(ref s, 1);
            }
        }

        private static bool parseChar(ref string s, char x) {
            skipBlank(ref s);
            if (s.ToCharArray()[0] == x) {
                s = skipChar(ref s, 1);
                return true;
            } else {
                return false;
            }
        }

        private static string skipChar(ref string s, int count) {
            return s.Remove(0, count);
        }

        private static bool parseString(ref string s, char quote, out StringBuilder name) {
            bool valid = false;
            name = new StringBuilder();
            skipBlank(ref s);
            char[] c = s.ToCharArray();
            if (c[0] == quote) {
                int i = 1;
                while (c[i] != quote) {
                    name.Append(c[i]);
                    i++;
                }
                valid = true;
                s = skipChar(ref s, name.ToString().Length + 2);
            }
            return valid;
        }

        private static bool parseNum(ref string s,  out StringBuilder name) {
            bool valid = false;
            name = new StringBuilder();
            char[] c = s.ToCharArray();
            if (c[0] == ' ') {
                int i = 1;
                while (c[i] != ' ' && c[i] != ',' && c[i] != '}' && c[i] != ']') {
                    name.Append(c[i]);
                    i++;
                }
                valid = true;
                s = skipChar(ref s, name.ToString().Length + 1);
            }
            return valid;
        }

        private static bool parseDQString(ref string s, out StringBuilder word) {
            return parseString(ref s, '"', out word);
        }

        private static bool parseItemMetaData(ref string s, List < Meta > metadatas) {
            StringBuilder metaKey,
            metaValue;
            if (parseChar(ref s, ':') && parseChar(ref s, '[')) {
                do {
                    if (parseChar(ref s, '{')
                        && parseDQString(ref s, out metaKey)
                        && parseChar(ref s, ':')
                        && parseDQString(ref s, out metaValue)
                        && parseChar(ref s, '}')) {
                        switch (metaKey.ToString()) {
                        case "unit":
                            Meta meta1 = new Meta();
                            meta1.unit = metaValue.ToString();
                            metadatas.Add(meta1);
                            break;
                        case "tooltip":
                            Meta meta2 = new Meta();
                            meta2.tooltip = metaValue.ToString();
                            metadatas.Add(meta2);
                            break;
                        case "scale":
                            Meta meta3 = new Meta();
                            meta3.scale = metaValue.ToString();
                            metadatas.Add(meta3);
                            break;
                        default:
                            break;
                        }
                    }
                } while (parseChar(ref s , ','));
                return parseChar(ref s, ']');
            } else {
                return false;
            }
        }

        private static bool parseGlobalMetaData(ref string s) {
            if (parseChar(ref s, '[')) {
                while (!parseChar(ref s, ']')) {
                    s = skipChar(ref s, 1);
                }
                return true;
            } else {
                return false;
            }
        }

        private static bool parseUI(ref string s, ref List < Group > uiItems, ref int numItems) {
            CultureInfo culture = new CultureInfo("en-US");
            if (parseChar(ref s, '[')) {

                StringBuilder label;
                StringBuilder value;

                do {
                    if (parseChar(ref s, '{')) {
                        do {
                            if (parseDQString(ref s, out label)) {

                                switch (label.ToString()) {
                                case "type":
                                    if (uiItems.Count != 0) {
                                        numItems++;
                                    }
                                    if (parseChar(ref s, ':') && parseDQString(ref s, out value)) {
                                        Group item = new Group();
                                        item.type = value.ToString();
                                        uiItems.Add(item);
                                    }
                                    break;
                                case "label":
                                    if (parseChar(ref s, ':') && parseDQString(ref s, out value)) {
                                        uiItems[numItems].label = value.ToString();
                                    }
                                    break;
                                case "shortname":
                                    if (parseChar(ref s, ':') && parseDQString(ref s, out value)) {
                                        uiItems[numItems].shortname = value.ToString();
                                    }
                                    break;
                                case "address":
                                    if (parseChar(ref s, ':') && parseDQString(ref s, out value)) {
                                        uiItems[numItems].address = value.ToString();
                                    }
                                    break;
                                case "url":
                                    if (parseChar(ref s, ':') && parseDQString(ref s, out value)) {
                                        uiItems[numItems].url = value.ToString();
                                    }
                                    break;
                                case "varname":
                                    if (parseChar(ref s, ':') && parseDQString(ref s, out value)) {
                                        uiItems[numItems].varname = value.ToString();
                                    }
                                    break;
                                case "meta":
                                    uiItems[numItems].meta = new List < Meta > ();
                                    if (!parseItemMetaData(ref s, uiItems[numItems].meta)) {
                                        return false;
                                    }
                                    break;
                                case "init":
                                    if (parseChar(ref s, ':') && parseNum(ref s, out value)) {
                                        float x = Convert.ToSingle(value.ToString(), culture);
                                        uiItems[numItems].init = x;
                                    }
                                    break;
                                case "min":
                                    if (parseChar(ref s, ':') && parseNum(ref s, out value)) {
                                        float x = Convert.ToSingle(value.ToString(), culture);
                                        uiItems[numItems].min = x;
                                    }
                                    break;
                                case "max":
                                    if (parseChar(ref s, ':') && parseNum(ref s, out value)) {
                                        float x = Convert.ToSingle(value.ToString(), culture);
                                        uiItems[numItems].max = x;
                                    }
                                    break;
                                case "step":
                                    if (parseChar(ref s, ':') && parseNum(ref s, out value)) {
                                        float x = Convert.ToSingle(value.ToString(), culture);
                                        uiItems[numItems].step = x;
                                    }
                                    break;
                                case "items":
                                    uiItems[numItems].items = new List < Group > ();
                                    int numItems2 = 0;
                                    if (parseChar(ref s, ':') && !parseUI(ref s, ref uiItems[numItems].items, ref numItems2)) return false;
                                    break;
                                default:
                                    return false;
                                }
                            } else {
                                return false;
                            }
                        } while (parseChar(ref s , ','));
                        parseChar(ref s, '}');
                    }
                } while (parseChar(ref s , ','));
                return parseChar(ref s, ']');
            } else {
                return false;
            }
        }
    }

    /* @brief Class representing an item (slider, etc) or a group
     * @brief if it's an item List<Group> is null 
     */
    public class Group {
        public string type;
        public string label;
        public string shortname;
        public string address;
        public string url;
        public string varname;
        public List < Meta > meta;
        public List < Group > items;
        public float init;
        public float min;
        public float max;
        public float step;     
        public int numparam;
        public bool groupstate = true;

        /* @brief Recursive method to set the number of a parameter
         * @brief it gives a unique ID to the parameters and uses it to communicate with the plugin
         */
        public int setNumParams(int param) {
            if (items != null) {
                for (int i = 0; i < items.Count; i++) {
                    param = items[i].setNumParams(param);
                }
                return param;
            } else {
                numparam = param;
                param++;
                return param;
            }
        }

        public Group getItem(int param) {
            Group result = null;
            if (items != null) {
                for (int i = 0; i < items.Count; i++) {
                    Group item = items[i].getItem(param);
                    if (item != null) {
                        result = item;
                    }
                }
                return result;
            } else {
                if (this.numparam == param) {
                    return this;
                } else {
                    return null;
                }
            }
        }
    }

    /* @brief Class to parse the metadatas of an item (not the global metadatas)
     */
    public class Meta {
        public string unit;
        public string scale;
        public string tooltip;
    }
}
