using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using FMOD;
using FMOD.Studio;
using FMODUnity;
using Ktyl.Util;
using UnityEngine;
using UnityEngine.Animations.Rigging;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Users;
using UnityEngine.Networking;
using Debug = UnityEngine.Debug;
using STOP_MODE = FMOD.Studio.STOP_MODE;
#if UNITY_EDITOR
using UnityEditor;

#endif

[CreateAssetMenu(menuName = "KernelPanic/Dialogue/Dialogue System")]
public partial class DialogueSystem : ScriptableObject
{
    [SerializeField] private DialogueSettings _settings;

    // https://stackoverflow.com/questions/2282476/actiont-vs-delegate-event 
    public event EventHandler<DialogueLine> onDialogueLine;

    // a list of dialogue keys that have already been spoken
    private readonly List<string> _usedClips = new List<string>();

    private EVENT_CALLBACK _dialogueCallback;
    private EventInstance _currentInstance;
    private string _currentControlScheme;

    private void OnEnable()
    {
        _usedClips.Clear();

        _dialogueCallback = DialogueEventCallback;
    }

    [AOT.MonoPInvokeCallback(typeof(EVENT_CALLBACK))]
    private static RESULT DialogueEventCallback(EVENT_CALLBACK_TYPE type, IntPtr instancePtr, IntPtr parameterPtr)
    {
        var instance = new EventInstance(instancePtr);

        // retrieve user data
        instance.getUserData(out IntPtr stringPtr);

        // get string obejct
        var stringHandle = GCHandle.FromIntPtr(stringPtr);
        var key = stringHandle.Target as string;

        switch (type)
        {
            case EVENT_CALLBACK_TYPE.CREATE_PROGRAMMER_SOUND:
            {
                MODE soundMode = MODE.LOOP_NORMAL | MODE.CREATECOMPRESSEDSAMPLE | MODE.NONBLOCKING;
                var parameter =
                    (PROGRAMMER_SOUND_PROPERTIES) Marshal.PtrToStructure(parameterPtr,
                        typeof(PROGRAMMER_SOUND_PROPERTIES));

                if (key.Contains("."))
                {
                    Sound dialogueSound;
                    var soundResult = RuntimeManager.CoreSystem.createSound(Application.streamingAssetsPath + "/" + key,
                        soundMode, out dialogueSound);
                    if (soundResult == RESULT.OK)
                    {
                        parameter.sound = dialogueSound.handle;
                        parameter.subsoundIndex = -1;
                        Marshal.StructureToPtr(parameter, parameterPtr, false);
                    }
                }
                else
                {
                    SOUND_INFO dialogueSoundInfo;
                    var keyResult = RuntimeManager.StudioSystem.getSoundInfo(key, out dialogueSoundInfo);
                    if (keyResult != RESULT.OK)
                    {
                        break;
                    }

                    Sound dialogueSound;
                    var soundResult = RuntimeManager.CoreSystem.createSound(dialogueSoundInfo.name_or_data,
                        soundMode | dialogueSoundInfo.mode, ref dialogueSoundInfo.exinfo, out dialogueSound);
                    if (soundResult == RESULT.OK)
                    {
                        parameter.sound = dialogueSound.handle;
                        parameter.subsoundIndex = dialogueSoundInfo.subsoundindex;
                        Marshal.StructureToPtr(parameter, parameterPtr, false);
                    }
                }

                break;
            }
            case EVENT_CALLBACK_TYPE.DESTROY_PROGRAMMER_SOUND:
            {
                var parameter =
                    (PROGRAMMER_SOUND_PROPERTIES) Marshal.PtrToStructure(parameterPtr,
                        typeof(PROGRAMMER_SOUND_PROPERTIES));
                var sound = new Sound(parameter.sound);
                sound.release();
                
                break;
            }
            case EVENT_CALLBACK_TYPE.DESTROYED:
            {
                stringHandle.Free();
                break;
            }
        }

        return RESULT.OK;
    }

    // noRepeat locks this key off from further use. further attempts to use the key will be discarded
    public void PlayLine(string key, bool noRepeat = true)
    {
        if (noRepeat)
        {
            if (_usedClips.Contains(key)) return;
            
            _usedClips.Add(key);
        }

        if ( _currentInstance.isValid() )
        {
            _currentInstance.stop( STOP_MODE.IMMEDIATE );
            _currentInstance.release();
        }
        
        _currentInstance = RuntimeManager.CreateInstance(_settings.RadioDialogueKey);

        GCHandle stringHandle = GCHandle.Alloc(key, GCHandleType.Pinned);
        _currentInstance.setUserData(GCHandle.ToIntPtr(stringHandle));

        DialogueLine dl;
        var line = DialogueDatabase.ReadDialogue(key);
        // process templates in string
        line = ProcessTemplates(line);
        dl.text = line;
        
        var clip = _settings.GetDialogueClip(key);
        dl.duration = clip.length;

        _currentInstance.setCallback(_dialogueCallback);
        _currentInstance.start();
        
        onDialogueLine?.Invoke(this, dl);
    }

    private string ProcessTemplates(string text)
    {
        var spriteIndices = _currentControlScheme == InputSchemes.GAMEPAD
            ? _settings.GamepadInputPrompts
            : _settings.KeyboardInputPrompts;
        
        text = text.Replace("[BLINK]", $"<sprite index={spriteIndices.blink}>");
        text = text.Replace("[BOOST]", $"<sprite index={spriteIndices.boost}>");
        text = text.Replace("[TIME_FREEZE]", $"<sprite index={spriteIndices.timeFreeze}>");

        return text;
    }

    public void UpdateControlPrompts(PlayerInput playerInput)
    {
        var controlScheme = playerInput.currentControlScheme;
        if (controlScheme != InputSchemes.PCMR && controlScheme != InputSchemes.GAMEPAD)
        {
            Debug.LogError($"could not set unknown control scheme {controlScheme}");
            return;
        }
        
        _currentControlScheme = playerInput.currentControlScheme;
    }
}

public struct DialogueLine
{
    public string text;
    public float duration;
}

#region Editor

#if UNITY_EDITOR

[CustomEditor(typeof(DialogueSystem))]
public class DialogueSystemEditor : Editor
{
    private DialogueSystem _dialogue;
    private string _key;

    private void OnEnable()
    {
        _dialogue = target as DialogueSystem;
    }

    public override void OnInspectorGUI()
    {
        base.OnInspectorGUI();

        _key = EditorGUILayout.TextField("key", _key);

        if (GUILayout.Button("Play Line"))
        {
            _dialogue.PlayLine(_key);
        }
    }
}

#endif

#endregion