using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using Net.Eagle0.Shardok.Api;
using Net.Eagle0.Shardok.Common;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using UnityGoDiceInterface;

public class RollPanelController : MonoBehaviour, RollFetcher {
    public DiceConfigurationPanelController diceConfigurationPanelController;
    public Button skipContinueButton;
    public TMP_Text skipContinueButtonText;
    private DiceInterface _diceInterface;

    public TMP_Text tensResultLabel;
    public TMP_Text onesResultLabel;

    public TMP_Text header;
    public TMP_Text reconnectionLabel;
    public TMP_Text runningTotalLabel;

    private RollFetcher.RollResultCallback _rollResultCallback;
    private RollRequest _rollRequest;

    private string logLocation;

    enum OpenEndedState { None, High, Low, RollComplete }
    private OpenEndedState _openEndedState = OpenEndedState.None;

    private readonly int _d100OpenEndedHighThreshold = 96;
    private readonly int _d100OpenEndedLowThreshold = 5;

    private DateTime _lastRollTime = DateTime.MinValue;
    private readonly TimeSpan _disconnectionDelay = TimeSpan.FromMinutes(5);
    private readonly TimeSpan _displayTime = TimeSpan.FromSeconds(3);
    private readonly TimeSpan _openEndedResetTime = TimeSpan.FromSeconds(2);

    public void GetRoll(RollRequest rollRequest, RollFetcher.RollResultCallback cb) {
        // Check for dice enabled
        if (PlayerPrefs.GetInt(SettingsPanelController.UseGoDiceKey, 0) == 0) {
            cb(null);
            return;
        }

        skipContinueButton.gameObject.SetActive(true);
        if (_diceInterface == null) { _diceInterface = GetComponentInParent<DiceInterface>(); }

        _onesResult = null;
        _tensResult = null;
        _runningTotal = 0;
        _openEndedState = OpenEndedState.None;

        skipContinueButtonText.text = "Skip";

        _rollResultCallback = cb;
        _rollRequest = rollRequest;

        SetResultLabels();

        logLocation = NewLogLocation();
        Directory.CreateDirectory(Path.GetDirectoryName(logLocation));

        if (string.IsNullOrEmpty(_tensDieInfo.identifier) ||
            string.IsNullOrEmpty(_onesDieInfo.identifier)) {
            GetConfiguration();
        } else {
            _diceInterface.deviceFoundCallback = DeviceFoundCallback;
            _diceInterface.connectionCallback = ConnectionCallback;
            _diceInterface.rollCallback = RollCallback;
            _diceInterface.logger = LogFromNative;

            // _diceInterface.StartListening();
            SetAlphas();

            if (!_tensDieInfo.connected) { _diceInterface.Connect(_tensDieInfo.identifier); }
            if (!_onesDieInfo.connected) { _diceInterface.Connect(_onesDieInfo.identifier); }
        }

        gameObject.SetActive(true);
    }

    private DieInfo _tensDieInfo;
    private DieInfo _onesDieInfo;

    private int? _tensResult = null;
    private int? _onesResult = null;
    private int _runningTotal = 0;

    private void SetAlphas() {
        tensResultLabel.alpha = _tensDieInfo.connected ? 1.0f : 0.25f;
        onesResultLabel.alpha = _onesDieInfo.connected ? 1.0f : 0.25f;
    }

    void OnEnable() {}

    private void GetConfiguration() {
        diceConfigurationPanelController.GetConfiguration(ConfigurationCallback);
    }

    public void ConfigureClicked() { GetConfiguration(); }

    public void SkipClicked() {
        gameObject.SetActive(false);
        _rollResultCallback(_runningTotal);
        _diceInterface.StopListening();
    }

    void ConfigurationCallback(DieInfo? tensOpt, DieInfo? onesOpt) {
        MainQueue.Q.Enqueue(() => {
            if (tensOpt is DieInfo tens && onesOpt is DieInfo ones) {
                gameObject.SetActive(true);
                _diceInterface.connectionCallback = ConnectionCallback;
                _diceInterface.connectionFailedCallback = ConnectionFailedCallback;
                _diceInterface.disconnectionCallback = DisconnectionCallback;
                _diceInterface.listenerStoppedCallback = ListenerStoppedCallback;
                _diceInterface.rollCallback = RollCallback;

                _tensDieInfo = tens;
                _onesDieInfo = ones;

                reconnectionLabel.gameObject.SetActive(false);

                if (!_tensDieInfo.connected) _diceInterface.Connect(_tensDieInfo.identifier);
                if (!_onesDieInfo.connected) _diceInterface.Connect(_onesDieInfo.identifier);

                SetAlphas();
                SetResultLabels();

                tensResultLabel.color = UnityDieColors.ColorFromDieColor(_tensDieInfo.color);
                onesResultLabel.color = UnityDieColors.ColorFromDieColor(_onesDieInfo.color);
            } else {
                gameObject.SetActive(false);
                _rollResultCallback(null);
            }
        });
    }

    void DeviceFoundCallback(string identifier) {
        MainQueue.Q.Enqueue(() => {
            if ((_tensDieInfo.identifier == identifier && !_tensDieInfo.connected) ||
                (_onesDieInfo.identifier == identifier && !_onesDieInfo.connected)) {
                _diceInterface.Connect(identifier);
            }
        });
    }

    void ConnectionCallback(string identifier, string deviceName) {
        MainQueue.Q.Enqueue(() => {
            if (identifier == _tensDieInfo.identifier) {
                _tensDieInfo.connected = true;
            } else if (identifier == _onesDieInfo.identifier) {
                _onesDieInfo.connected = true;
            }

            SetAlphas();

            if (_tensDieInfo.connected && _onesDieInfo.connected) {
                reconnectionLabel.gameObject.SetActive(false);
                _diceInterface.StopListening();
            }
        });
    }

    void ConnectionFailedCallback(string identifier) {
        MainQueue.Q.Enqueue(() => {
            if (identifier == _onesDieInfo.identifier || identifier == _tensDieInfo.identifier) {
                reconnectionLabel.gameObject.SetActive(true);
                _diceInterface.StartListening();
            } else {
                Debug.Log("Got connection failed for unknown identifier");
            }
        });
    }

    void DisconnectionCallback(string identifier) {
        MainQueue.Q.Enqueue(() => {
            if (identifier == _tensDieInfo.identifier) {
                _tensDieInfo.connected = false;
                SetAlphas();
                _diceInterface.Connect(identifier);
            } else if (identifier == _onesDieInfo.identifier) {
                _onesDieInfo.connected = false;
                SetAlphas();
                _diceInterface.Connect(identifier);
            } else {
                Debug.Log("Got disconnection callback for unknown identifier");
            }
        });
    }

    void ListenerStoppedCallback() {
        MainQueue.Q.Enqueue(() => { reconnectionLabel.text = "Unable to connect."; });
    }

    private readonly Dictionary<CommandType, string> _baseHeaderText = new() {
        { CommandType.ArcheryCommand, "Archery Attack" },
        { CommandType.BuildBridgeCommand, "Build Bridge" },
        { CommandType.ChargeCommand, "Charge" },
        { CommandType.BraveWaterCommand, "Brave Water" },
        { CommandType.MeleeCommand, "Melee Attack" }
    };

    private string BaseHeaderText(CommandType commandType) {
        if (_baseHeaderText.ContainsKey(commandType)) { return _baseHeaderText[commandType]; }

        return "Action";
    }

    void SetResultLabels() {
        var baseText = BaseHeaderText(_rollRequest.CommandType);
        string fullText = "";

        if (_openEndedState == OpenEndedState.High) {
            fullText = $"You rolled OPEN ENDED HIGH! Keep rolling for {baseText}!";
        } else if (_openEndedState == OpenEndedState.Low) {
            fullText = $"You rolled OPEN ENDED LOW! Keep rolling for {baseText}!";
        } else {
            switch (_rollRequest.RollType) {
                case RollType.D100: fullText = $"Roll D100 for {baseText}"; break;
                case RollType.D100OpenEnded:
                    fullText = $"Roll Open Ended D100 for {baseText}";
                    break;
                case RollType.D100OpenEndedHigh:
                    fullText = $"Roll Open Ended High D100 for {baseText}";
                    break;
                case RollType.D100OpenEndedLow:
                    fullText = $"Roll Open Ended Low D100 for {baseText}";
                    break;
                case RollType.Unspecified:
                default: throw new ArgumentException("Invalid roll type");
            }
        }

        if (_rollRequest.Odds is OddsView odds) {
            int minRoll = 101 - odds.SuccessChance;
            fullText += $" ({minRoll}+ to succeed)";
        }

        header.text = fullText;

        if (_tensResult is int tr) {
            tensResultLabel.text = tr.ToString();
        } else {
            tensResultLabel.text = "?";
        }

        if (_onesResult is int or) {
            onesResultLabel.text = or.ToString();
        } else {
            onesResultLabel.text = "?";
        }

        runningTotalLabel.text = $"Running total: {_runningTotal}";
        runningTotalLabel.gameObject.SetActive(_runningTotal != 0);
    }

    void RollCallback(string identifier, sbyte x, sbyte y, sbyte z) {
        MainQueue.Q.Enqueue(() => {
            if (gameObject.activeSelf) {
                var d10Roll = DiceVectors.D10Value(x, y, z);

                if (identifier == _tensDieInfo.identifier) {
                    if (_tensResult == null) { _tensResult = d10Roll; }
                } else if (identifier == _onesDieInfo.identifier) {
                    if (_onesResult == null) { _onesResult = d10Roll; }
                }

                SetResultLabels();
                CheckCompletion();
            }
        });
    }

    void HandleOneRoll(int newD100Result) {
        if (_openEndedState == OpenEndedState.Low) {
            _runningTotal -= newD100Result;
        } else {
            _runningTotal += newD100Result;
        }

        // Rolls in the middle always end the open ended state.
        if (newD100Result > _d100OpenEndedLowThreshold &&
            newD100Result < _d100OpenEndedHighThreshold) {
            _openEndedState = OpenEndedState.RollComplete;
            return;
        }

        // For a LOW roll, if we are currently in a None state, move to OpenEnded
        // Low so we can roll again, but if we were already there, this ends the roll.
        if (newD100Result <= _d100OpenEndedLowThreshold) {
            if ((_rollRequest.RollType == RollType.D100OpenEnded ||
                 _rollRequest.RollType == RollType.D100OpenEndedLow) &&
                _openEndedState == OpenEndedState.None) {
                _openEndedState = OpenEndedState.Low;
            } else {
                _openEndedState = OpenEndedState.RollComplete;
            }

            return;
        }

        // At this point we know the current roll is high. If we were already in OpenEndedLow,
        // stay there and keep subtracting, otherwise move into High.
        if (_openEndedState == OpenEndedState.Low) {
            _openEndedState = OpenEndedState.Low;
        } else {
            _openEndedState = OpenEndedState.High;
        }
    }

    void CheckCompletion() {
        if (_onesResult is int ones && _tensResult is int tens) {
            // FIXME: open ended rolls
            var currentResult = tens * 10 + ones;

            HandleOneRoll(currentResult);
            // TODO: flash the dice here for high rolls

            if (_openEndedState == OpenEndedState.RollComplete) {
                skipContinueButtonText.text = "Dismiss";
                SetResultLabels();
                StartCoroutine(AfterDelay(_displayTime, () => {
                    if (gameObject.activeSelf) {
                        gameObject.SetActive(false);
                        _rollResultCallback(_runningTotal);
                    }
                }));

                // Disconnect only after _disconnectionDelay time
                _lastRollTime = DateTime.Now;
                StartCoroutine(AfterDelay(_disconnectionDelay, () => {
                    if (DateTime.Now - _lastRollTime > _disconnectionDelay) {
                        _diceInterface.StopListening();

                        _tensDieInfo.connected = false;
                        _onesDieInfo.connected = false;
                        _diceInterface.Disconnect(_tensDieInfo.identifier);
                        _diceInterface.Disconnect(_onesDieInfo.identifier);
                    }
                }));
            } else {
                // We keep rolling, so reset the dice and keep going.
                StartCoroutine(AfterDelay(_openEndedResetTime, () => {
                    _tensResult = null;
                    _onesResult = null;
                    SetResultLabels();
                }));
            }
        }
    }

    IEnumerator AfterDelay(TimeSpan timeSpan, Action action) {
        yield return new WaitForSeconds(timeSpan.Seconds);
        action();
    }

    void LogFromNative(string log) {
        MainQueue.Q.Enqueue(() => {
            Debug.Log(log);
            File.AppendAllText(logLocation, log);
        });
    }

    string NewLogLocation() {
        return Path.Combine(
                Application.persistentDataPath,
                "eagle0",
                "Resources",
                "RollPanelLogs",
                DateTime.Now.ToString("yyyy-MM-dd-HH-mm-ss") + ".log");
    }
}
