using System.Text;
using System.Text.RegularExpressions;

namespace Inspectron.Epson.Templates.Language;

public class Lexer
{
    private readonly string _source;
    private readonly List<string> _lines;
    private int _lineIndex;
    private int _columnIndex;
    private readonly List<Token> _tokens = new();
    private readonly List<LexerError> _errors = new();
    private bool _inMultiLineComment;
    private SourcePosition _multiLineCommentStart;
    private bool _isCommentOnlyLine;

    private static readonly Regex StyleStartRegex = new(@"^#([a-zA-Z0-9,:]+)#", RegexOptions.Compiled);
    private static readonly Regex BindingRegex = new(@"^\{([^}]+)\}", RegexOptions.Compiled);
    private static readonly Regex ColumnRegex = new(@"^\|(\d+)(?:,(\w+))?\|", RegexOptions.Compiled);

    public Lexer(string source)
    {
        _source = source ?? throw new ArgumentNullException(nameof(source));
        _lines = _source.Split('\n').ToList();
        // Normalize line endings
        for (int i = 0; i < _lines.Count; i++)
        {
            _lines[i] = _lines[i].TrimEnd('\r');
        }
    }

    public LexerResult Tokenize()
    {
        _tokens.Clear();
        _errors.Clear();
        _lineIndex = 0;
        _columnIndex = 0;
        _inMultiLineComment = false;
        _multiLineCommentStart = default;

        while (_lineIndex < _lines.Count)
        {
            _isCommentOnlyLine = false;
            TokenizeLine(_lines[_lineIndex]);

            // Skip NewLine for comment-only lines and lines inside multi-line comments
            if (!_isCommentOnlyLine && !_inMultiLineComment)
            {
                _tokens.Add(new Token(TokenType.NewLine, "\n", new SourcePosition(_lineIndex + 1, _columnIndex + 1)));
            }
            _lineIndex++;
            _columnIndex = 0;
        }

        if (_inMultiLineComment)
        {
            _errors.Add(new LexerError("Unclosed multi-line comment", _multiLineCommentStart));
        }

        _tokens.Add(Token.Eof(_lineIndex + 1));
        return new LexerResult(_tokens.ToList(), _errors.ToList());
    }

    private void TokenizeLine(string line)
    {
        int lineNumber = _lineIndex + 1;

        // Handle multi-line comment state
        if (_inMultiLineComment)
        {
            var closeIndex = line.IndexOf("*@", StringComparison.Ordinal);
            if (closeIndex >= 0)
            {
                // Found comment closer - exit comment mode
                _inMultiLineComment = false;
                var afterCloseIndex = closeIndex + 2;
                if (afterCloseIndex < line.Length && !string.IsNullOrWhiteSpace(line[afterCloseIndex..]))
                {
                    // Process content after the closing *@
                    _columnIndex = afterCloseIndex;
                    TokenizeContent(line, lineNumber);
                }
                else
                {
                    // Comment close line with no content after
                    _isCommentOnlyLine = true;
                    _columnIndex = line.Length;
                }
            }
            else
            {
                // Still in comment - skip entire line (handled by _inMultiLineComment check in Tokenize)
                _columnIndex = line.Length;
            }
            return;
        }

        // Check for comment lines (must be checked before separators)
        var trimmed = line.TrimStart();
        var leadingWhitespace = line.Length - trimmed.Length;

        if (trimmed.StartsWith("@*"))
        {
            // Check for inline comment: @* ... *@
            var closeIndex = trimmed.IndexOf("*@", 2, StringComparison.Ordinal);
            if (closeIndex >= 0)
            {
                // Inline single-line comment - skip entire comment portion
                var afterCloseIndex = leadingWhitespace + closeIndex + 2;
                if (afterCloseIndex < line.Length && !string.IsNullOrWhiteSpace(line[afterCloseIndex..]))
                {
                    // Process content after the closing *@
                    _columnIndex = afterCloseIndex;
                    TokenizeContent(line, lineNumber);
                }
                else
                {
                    // Comment-only line (no content after *@)
                    _isCommentOnlyLine = true;
                    _columnIndex = line.Length;
                }
            }
            else if (trimmed.TrimEnd() == "@*")
            {
                // Start of multi-line comment block
                _inMultiLineComment = true;
                _isCommentOnlyLine = true;
                _multiLineCommentStart = new SourcePosition(lineNumber, leadingWhitespace + 1);
                _columnIndex = line.Length;
            }
            else
            {
                // Single-line comment to end of line
                _isCommentOnlyLine = true;
                _columnIndex = line.Length;
            }
            return;
        }

        // Check for separator lines (entire line)
        if (IsSeparatorLine(line, out var separatorType))
        {
            _tokens.Add(new Token(separatorType, line, new SourcePosition(lineNumber, 1)));
            _columnIndex = line.Length;
            return;
        }

        // Check for directive lines
        if (trimmed.StartsWith("@"))
        {
            if (TryTokenizeDirective(trimmed, leadingWhitespace, lineNumber))
            {
                return;
            }
        }

        // Otherwise, process as content line
        TokenizeContent(line, lineNumber);
    }

    private bool IsSeparatorLine(string line, out TokenType separatorType)
    {
        var trimmed = line.Trim();
        separatorType = TokenType.Text;

        if (trimmed.Length >= 3)
        {
            if (trimmed.All(c => c == '-'))
            {
                separatorType = TokenType.DashSeparator;
                return true;
            }
            if (trimmed.All(c => c == '='))
            {
                separatorType = TokenType.EqualsSeparator;
                return true;
            }
            if (trimmed.All(c => c == '*'))
            {
                separatorType = TokenType.StarSeparator;
                return true;
            }
            if (trimmed.All(c => c == '~'))
            {
                separatorType = TokenType.TildeSeparator;
                return true;
            }
        }

        return false;
    }

    private bool TryTokenizeDirective(string trimmed, int leadingWhitespace, int lineNumber)
    {
        var parts = trimmed.Split(' ', StringSplitOptions.RemoveEmptyEntries);
        if (parts.Length == 0) return false;

        var directive = parts[0].ToLowerInvariant();
        var value = parts.Length > 1 ? string.Join(" ", parts.Skip(1)) : string.Empty;
        var position = new SourcePosition(lineNumber, leadingWhitespace + 1);

        switch (directive)
        {
            case "@if":
                _tokens.Add(new Token(TokenType.If, value, position));
                _columnIndex = trimmed.Length + leadingWhitespace;
                return true;

            case "@elseif":
                _tokens.Add(new Token(TokenType.ElseIf, value, position));
                _columnIndex = trimmed.Length + leadingWhitespace;
                return true;

            case "@else":
                _tokens.Add(new Token(TokenType.Else, string.Empty, position));
                _columnIndex = trimmed.Length + leadingWhitespace;
                return true;

            case "@end":
                _tokens.Add(new Token(TokenType.End, string.Empty, position));
                _columnIndex = trimmed.Length + leadingWhitespace;
                return true;

            case "@foreach":
                _tokens.Add(new Token(TokenType.Foreach, value, position));
                _columnIndex = trimmed.Length + leadingWhitespace;
                return true;

            case "@row":
                _tokens.Add(new Token(TokenType.Row, value, position));
                _columnIndex = trimmed.Length + leadingWhitespace;
                return true;

            case "@endrow":
                _tokens.Add(new Token(TokenType.EndRow, string.Empty, position));
                _columnIndex = trimmed.Length + leadingWhitespace;
                return true;
        }

        return false;
    }

    private void TokenizeContent(string line, int lineNumber)
    {
        var textBuffer = new StringBuilder();
        int textStartColumn = 1;
        bool inStyle = false;

        while (_columnIndex < line.Length)
        {
            var remaining = line[_columnIndex..];
            var currentColumn = _columnIndex + 1;

            // Check for style start: #styles#
            if (!inStyle)
            {
                var styleMatch = StyleStartRegex.Match(remaining);
                if (styleMatch.Success)
                {
                    FlushTextBuffer(textBuffer, lineNumber, textStartColumn);
                    _tokens.Add(new Token(TokenType.StyleStart, styleMatch.Groups[1].Value,
                        new SourcePosition(lineNumber, currentColumn)));
                    _columnIndex += styleMatch.Length;
                    textStartColumn = _columnIndex + 1;
                    inStyle = true;
                    continue;
                }
            }

            // Check for style end: trailing #
            if (inStyle && remaining.StartsWith("#"))
            {
                FlushTextBuffer(textBuffer, lineNumber, textStartColumn);
                _tokens.Add(new Token(TokenType.StyleEnd, "#",
                    new SourcePosition(lineNumber, currentColumn)));
                _columnIndex++;
                textStartColumn = _columnIndex + 1;
                inStyle = false;
                continue;
            }

            // Check for binding: {path} or {path:format}
            var bindingMatch = BindingRegex.Match(remaining);
            if (bindingMatch.Success)
            {
                FlushTextBuffer(textBuffer, lineNumber, textStartColumn);
                _tokens.Add(new Token(TokenType.Binding, bindingMatch.Groups[1].Value,
                    new SourcePosition(lineNumber, currentColumn)));
                _columnIndex += bindingMatch.Length;
                textStartColumn = _columnIndex + 1;
                continue;
            }

            // Check for column: |width| or |width,align|
            var columnMatch = ColumnRegex.Match(remaining);
            if (columnMatch.Success)
            {
                FlushTextBuffer(textBuffer, lineNumber, textStartColumn);
                var width = columnMatch.Groups[1].Value;
                var align = columnMatch.Groups[2].Success ? columnMatch.Groups[2].Value : "left";
                _tokens.Add(new Token(TokenType.Column, $"{width},{align}",
                    new SourcePosition(lineNumber, currentColumn)));
                _columnIndex += columnMatch.Length;
                textStartColumn = _columnIndex + 1;
                continue;
            }

            // Regular text character
            if (textBuffer.Length == 0)
            {
                textStartColumn = currentColumn;
            }
            textBuffer.Append(line[_columnIndex]);
            _columnIndex++;
        }

        // Flush remaining text
        FlushTextBuffer(textBuffer, lineNumber, textStartColumn);

        // If style wasn't closed on this line, add error
        if (inStyle)
        {
            _errors.Add(new LexerError("Unclosed style block", new SourcePosition(lineNumber, _columnIndex)));
        }
    }

    private void FlushTextBuffer(StringBuilder buffer, int line, int startColumn)
    {
        if (buffer.Length > 0)
        {
            _tokens.Add(new Token(TokenType.Text, buffer.ToString(),
                new SourcePosition(line, startColumn)));
            buffer.Clear();
        }
    }
}

public record LexerResult(List<Token> Tokens, List<LexerError> Errors)
{
    public bool HasErrors => Errors.Count > 0;
}

public record LexerError(string Message, SourcePosition Position);
