//
//  PacketStringEnumerator.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System.Collections.Generic;
using System.Text;
using NosSmooth.Packets.Errors;
using Remora.Results;
namespace NosSmooth.Packets;
/// 
/// Enumerator for packet strings.
/// 
public struct PacketStringEnumerator
{
    private readonly EnumeratorData _data;
    private readonly Dictionary _numberOfSeparators;
    private EnumeratorLevel _currentLevel;
    private (char Separator, uint? MaxTokens)? _preparedLevel;
    private PacketToken? _currentToken;
    private bool _readToLast;
    /// 
    /// Initializes a new instance of the  struct.
    /// 
    /// The packet string data.
    /// The separator to use on the highest level.
    public PacketStringEnumerator(string data, char separator = ' ')
    {
        _currentLevel = new EnumeratorLevel(null, separator);
        _data = new EnumeratorData(data);
        _numberOfSeparators = new Dictionary();
        _numberOfSeparators.Add(separator, 1);
        _currentToken = null;
        _preparedLevel = null;
        _readToLast = false;
    }
    /// 
    /// Initializes a new instance of the  struct.
    /// 
    /// The data of the enumerator.
    /// The current enumerator level.
    /// The number of separators.
    private PacketStringEnumerator(EnumeratorData data, EnumeratorLevel level, Dictionary numberOfSeparators)
    {
        _currentLevel = level;
        _data = data;
        // TODO: use something less heavy than copying everything from the dictionary.
        _numberOfSeparators = new Dictionary(numberOfSeparators);
        _currentToken = null;
        _preparedLevel = null;
        _readToLast = false;
    }
    /// 
    /// Sets the separator to search for only once.
    /// 
    /// 
    /// This separator will have the most priority.
    /// 
    /// The separator to look for.
    public void SetAfterSeparatorOnce(char separator)
    {
        _currentToken = null;
        _currentLevel.SeparatorOnce = separator;
    }
    /// 
    /// Sets that the next token should be read to the last entry in the level.
    /// 
    public void SetReadToLast()
    {
        _readToLast = true;
    }
    /// 
    /// Prepare the given level that can be set when needed.
    /// 
    /// The separator of the prepared level.
    /// The maximum number of tokens for the level.
    public void PrepareLevel(char separator, uint? maxTokens = null)
    {
        _preparedLevel = (separator, maxTokens);
    }
    /// 
    /// Reset the prepared level, if there is any.
    /// 
    public void RemovePreparedLevel()
    {
        _preparedLevel = null;
    }
    /// 
    /// Create next level with the separator given in the prepared level.
    /// 
    /// 
    /// Level of the current enumerator will stay the same.
    /// Will return null, if there is not a level prepared.
    /// 
    /// An enumerator with the new level pushed.
    public PacketStringEnumerator? CreatePreparedLevel()
    {
        return _preparedLevel is not null ? CreateLevel(_preparedLevel.Value.Separator, _preparedLevel.Value.MaxTokens) : null;
    }
    /// 
    /// Push next level with the separator given in the prepared level.
    /// 
    /// Whether there is a prepared level present.
    public bool PushPreparedLevel()
    {
        if (_preparedLevel is null)
        {
            return false;
        }
        _currentToken = null;
        _currentLevel = new EnumeratorLevel(_currentLevel, _preparedLevel.Value.Separator, _preparedLevel.Value.MaxTokens)
        {
            ReachedEnd = _currentLevel.ReachedEnd
        };
        if (!_numberOfSeparators.ContainsKey(_preparedLevel.Value.Separator))
        {
            _numberOfSeparators.Add(_preparedLevel.Value.Separator, 0);
        }
        _numberOfSeparators[_preparedLevel.Value.Separator]++;
        return true;
    }
    /// 
    /// Create next level with the given separator and maximum number of tokens.
    /// 
    /// 
    /// Level of the current enumerator will stay the same.
    /// The maximum number of tokens indicates how many tokens can be read ie. in lists,
    /// the enumerator won't allow reading more than that many tokens, error will be thrown if the user tries to read more.
    /// 
    /// The separator of the new level.
    /// The maximum number of tokens to read.
    /// An enumerator with the new level pushed.
    public PacketStringEnumerator CreateLevel(char separator, uint? maxTokens = default)
    {
        _currentToken = null;
        var stringEnumerator = new PacketStringEnumerator(_data, _currentLevel, _numberOfSeparators);
        stringEnumerator.PushLevel(separator, maxTokens);
        return stringEnumerator;
    }
    /// 
    /// Push new separator level to the stack.
    /// 
    /// 
    /// This will change the current enumerator.
    /// It has to be  after parent level should be used.
    /// 
    /// The separator of the new level.
    /// The maximum number of tokens to read.
    public void PushLevel(char separator, uint? maxTokens = default)
    {
        _preparedLevel = null;
        _currentToken = null;
        _currentLevel = new EnumeratorLevel(_currentLevel, separator, maxTokens)
        {
            ReachedEnd = _currentLevel.ReachedEnd
        };
        if (!_numberOfSeparators.ContainsKey(separator))
        {
            _numberOfSeparators.Add(separator, 0);
        }
        _numberOfSeparators[separator]++;
    }
    /// 
    /// Pop the current level.
    /// 
    /// A result that may or may not have succeeded. There will be an error if the current level is the top one.
    public Result PopLevel()
    {
        if (_currentLevel.Parent is null)
        {
            return new InvalidOperationError("The level cannot be popped, the stack is already at the top level.");
        }
        _numberOfSeparators[_currentLevel.Separator]--;
        _currentLevel = _currentLevel.Parent;
        return Result.FromSuccess();
    }
    /// 
    /// Get the next token.
    /// 
    /// Whether to seek the cursor to the end of the token.
    /// The found token.
    public Result GetNextToken(bool seek = true)
    {
        // The token is cached if seek was false to speed things up.
        if (_currentToken != null)
        {
            var cachedToken = _currentToken.Value;
            if (seek)
            {
                UpdateCurrentAndParentLevels(cachedToken);
                _currentLevel.TokensRead++;
                _currentToken = null;
                _data.Cursor += cachedToken.Token.Length + 1;
                _currentLevel.SeparatorOnce = null;
            }
            return cachedToken;
        }
        if (_data.ReachedEnd || (_currentLevel.ReachedEnd ?? false))
        {
            return new PacketEndReachedError(_data.Data, _currentLevel.ReachedEnd ?? false);
        }
        var currentIndex = _data.Cursor;
        char currentCharacter = _data.Data[currentIndex];
        StringBuilder tokenString = new StringBuilder();
        bool? isLast, encounteredUpperLevel;
        // If the current character is a separator, stop, else, add it to the builder.
        // If should read to last, then read until isLast is null or true.
        while (!IsSeparator(currentCharacter, out isLast, out encounteredUpperLevel) || (_readToLast && !(isLast ?? true)))
        {
            tokenString.Append(currentCharacter);
            currentIndex++;
            if (currentIndex == _data.Data.Length)
            {
                isLast = true;
                encounteredUpperLevel = true;
                break;
            }
            currentCharacter = _data.Data[currentIndex];
        }
        _readToLast = false;
        currentIndex++;
        var token = new PacketToken(tokenString.ToString(), isLast, encounteredUpperLevel, _data.ReachedEnd);
        if (seek)
        {
            UpdateCurrentAndParentLevels(token);
            _data.Cursor = currentIndex;
            _currentLevel.TokensRead++;
        }
        else
        {
            _currentToken = token;
        }
        return token;
    }
    /// 
    /// Update fields that are used in the process.
    /// 
    /// The token.
    private void UpdateCurrentAndParentLevels(PacketToken token)
    {
        // If the token is last in the current level, then set reached end of the current level.
        if (_currentLevel.ReachedEnd != true)
        {
            _currentLevel.ReachedEnd = token.IsLast;
        }
        // IsLast is set if parent separator was encountered. The parent needs to be updated.
        if (_currentLevel.Parent is not null && (token.IsLast ?? false))
        {
            var parent = _currentLevel.Parent;
            // Adding TokensRead is supported only one layer currently.
            parent.TokensRead++; // Add read tokens of the parent, because we encountered its separator.
            if (parent.TokensRead >= parent.MaxTokens)
            {
                parent.ReachedEnd = true;
                _currentLevel.ReachedEnd = true;
            }
            _currentLevel.Parent = parent;
        }
        // Encountered Upper Level is set if the reaached separator is not from neither the current level neither the parent
        if ((token.EncounteredUpperLevel ?? false) && _currentLevel.Parent is not null)
        {
            // Just treat it as last, even though that may be incorrect.
            var parent = _currentLevel.Parent;
            parent.ReachedEnd = true;
            _currentLevel.ReachedEnd = true;
            _currentLevel.Parent = parent;
        }
        // The once separator is always used just once, whatever happens.
        _currentLevel.SeparatorOnce = null;
    }
    /// 
    /// Whether the last token of the current level was read.
    /// 
    /// Whether the last token was read. Null if cannot determine (ie. there are multiple levels with the same separator.)
    public bool? IsOnLastToken()
    {
        if (_data.ReachedEnd)
        {
            return true;
        }
        return _currentLevel.ReachedEnd;
    }
    /// 
    /// Checks if the given character is a separator.
    /// 
    /// The character to check.
    /// Whether the separator indicates last separator in this level. True if numberOfSeparators is exactly one and this is the parent's separator.
    /// Whether higher level than the parent was encountered. That could indicate some kind of an error if this is not the last token.
    /// Whether the character is a separator.
    private bool IsSeparator(char c, out bool? isLast, out bool? encounteredUpperLevel)
    {
        isLast = null;
        encounteredUpperLevel = null;
        // Separator once has the highest preference
        if (_currentLevel.SeparatorOnce == c)
        {
            isLast = false;
            return true;
        }
        // The separator is not in any level.
        if (!_numberOfSeparators.ContainsKey(c))
        {
            return false;
        }
        var number = _numberOfSeparators[c];
        if (number == 0)
        { // The separator is not in any level.
            return false;
        }
        // The separator is in one level, we can correctly determine which level it corresponds to.
        // If the number is higher, we cannot know which level the separator corresponds to,
        // thus we have to let encounteredUpperLevel and isLast be null.
        // Typical for arrays that are at the end of packets or of specified length.
        if (number == 1)
        {
            if (_currentLevel.Parent?.Separator == c)
            {
                isLast = true;
                encounteredUpperLevel = false;
            }
            else if (_currentLevel.Separator == c)
            {
                isLast = false;
                encounteredUpperLevel = false;
            }
            else
            {
                encounteredUpperLevel = isLast = true;
            }
        }
        return true;
    }
    private class EnumeratorData
    {
        public EnumeratorData(string data)
        {
            Data = data;
            Cursor = 0;
        }
        public string Data { get; }
        public int Cursor { get; set; }
        public bool ReachedEnd => Cursor >= Data.Length;
    }
    private class EnumeratorLevel
    {
        public EnumeratorLevel(EnumeratorLevel? parent, char separator, uint? maxTokens = default)
        {
            Parent = parent;
            Separator = separator;
            SeparatorOnce = null;
            MaxTokens = maxTokens;
            TokensRead = 0;
            ReachedEnd = false;
        }
        public EnumeratorLevel? Parent { get; set; }
        public char Separator { get; }
        public char? SeparatorOnce { get; set; }
        public uint? MaxTokens { get; set; }
        public uint TokensRead { get; set; }
        public bool? ReachedEnd { get; set; }
    }
}