using Gherkin.Ast;

namespace Gherkin;

public class AstBuilder<T> : IAstBuilder<T>
{
    private readonly Stack<AstNode> _stack = new();
    private AstNode CurrentNode => _stack.Peek();
    private readonly List<Comment> _comments = new();

    public AstBuilder()
    {
        Reset();
    }

    public void Reset()
    {
        _stack.Clear();
        _stack.Push(new AstNode(RuleType.None));
        _comments.Clear();
    }

    public void Build(Token token)
    {
        if (token.MatchedType == TokenType.Comment)
        {
            _comments.Add(CreateComment(GetLocation(token), token.MatchedText));
        }
        else
        {
            CurrentNode.Add((RuleType)token.MatchedType, token);
        }
    }

    public void StartRule(RuleType ruleType)
    {
        _stack.Push(new AstNode(ruleType));
    }

    public void EndRule(RuleType ruleType)
    {
        var node = _stack.Pop();
        object transformedNode = GetTransformedNode(node);
        CurrentNode.Add(node.RuleType, transformedNode);
    }

    public T GetResult()
    {
        return CurrentNode.GetSingle<T>(RuleType.GherkinDocument);
    }

    private object GetTransformedNode(AstNode node)
    {
        switch (node.RuleType)
        {
            case RuleType.Step:
                {
                    var stepLine = node.GetToken(TokenType.StepLine);
                    var stepArg = node.GetSingle<StepArgument>(RuleType.DataTable) ??
                                  node.GetSingle<StepArgument>(RuleType.DocString);
                    var keywordType = GetKeywordType(stepLine);
                    return CreateStep(GetLocation(stepLine), stepLine.MatchedKeyword, keywordType, stepLine.MatchedText, stepArg, node);
                }
            case RuleType.DocString:
                {
                    var separatorToken = node.GetTokens(TokenType.DocStringSeparator).First();
                    var contentType = separatorToken.MatchedText.Length == 0 ? null : separatorToken.MatchedText;
                    var lineTokens = node.GetTokens(TokenType.Other);
                    var content = string.Join(Environment.NewLine, lineTokens.Select(lt => lt.MatchedText));
                    var delimiter = separatorToken.MatchedKeyword.Length == 0 ? null : separatorToken.MatchedKeyword;

                    return CreateDocString(GetLocation(separatorToken), contentType, content, delimiter, node);
                }
            case RuleType.DataTable:
                {
                    var rows = GetTableRows(node);
                    return CreateDataTable(rows, node);
                }
            case RuleType.Background:
                {
                    var backgroundLine = node.GetToken(TokenType.BackgroundLine);
                    var description = GetDescription(node);
                    var steps = GetSteps(node);
                    return CreateBackground(GetLocation(backgroundLine), backgroundLine.MatchedKeyword, backgroundLine.MatchedText, description, steps, node);
                }
            case RuleType.ScenarioDefinition:
                {
                    var tags = GetTags(node);

                    var scenarioNode = node.GetSingle<AstNode>(RuleType.Scenario);
                    var scenarioLine = scenarioNode.GetToken(TokenType.ScenarioLine);

                    var description = GetDescription(scenarioNode);
                    var steps = GetSteps(scenarioNode);
                    var examples = scenarioNode.GetItems<Examples>(RuleType.ExamplesDefinition).ToArray();
                    return CreateScenario(tags, GetLocation(scenarioLine), scenarioLine.MatchedKeyword, scenarioLine.MatchedText, description, steps, examples, node);
                }
            case RuleType.ExamplesDefinition:
                {
                    var tags = GetTags(node);
                    var examplesNode = node.GetSingle<AstNode>(RuleType.Examples);
                    var examplesLine = examplesNode.GetToken(TokenType.ExamplesLine);
                    var description = GetDescription(examplesNode);

                    var allRows = examplesNode.GetSingle<TableRow[]>(RuleType.ExamplesTable);
                    var header = allRows != null ? allRows.First() : null;
                    var rows = allRows != null ? allRows.Skip(1).ToArray() : null;
                    return CreateExamples(tags, GetLocation(examplesLine), examplesLine.MatchedKeyword, examplesLine.MatchedText, description, header, rows, node);
                }
            case RuleType.ExamplesTable:
                {
                    return GetTableRows(node);
                }
            case RuleType.Description:
                {
                    var lineTokens = node.GetTokens(TokenType.Other);

                    // Trim trailing empty lines
                    lineTokens = lineTokens.Reverse().SkipWhile(t => string.IsNullOrWhiteSpace(t.MatchedText)).Reverse();

                    return string.Join(Environment.NewLine, lineTokens.Select(lt => lt.MatchedText));
                }
            case RuleType.Feature:
                {
                    var header = node.GetSingle<AstNode>(RuleType.FeatureHeader);
                    if (header == null) return null;
                    var tags = GetTags(header);
                    var featureLine = header.GetToken(TokenType.FeatureLine);
                    if (featureLine == null) return null;
                    var children = new List<IHasLocation>();
                    var background = node.GetSingle<Background>(RuleType.Background);
                    if (background != null)
                    {
                        children.Add(background);
                    }
                    var childrenEnumerable = children.Concat(node.GetItems<IHasLocation>(RuleType.ScenarioDefinition))
                                                     .Concat(node.GetItems<IHasLocation>(RuleType.Rule));
                    var description = GetDescription(header);
                    if (featureLine.MatchedGherkinDialect == null) return null;
                    var language = featureLine.MatchedGherkinDialect.Language;

                    return CreateFeature(tags, GetLocation(featureLine), language, featureLine.MatchedKeyword, featureLine.MatchedText, description, childrenEnumerable.ToArray(), node);
                }
            case RuleType.Rule:
                {
                    var header = node.GetSingle<AstNode>(RuleType.RuleHeader);
                    if (header == null) return null;
                    var tags = GetTags(header);
                    var ruleLine = header.GetToken(TokenType.RuleLine);
                    if (ruleLine == null) return null;
                    var children = new List<IHasLocation>();
                    var background = node.GetSingle<Background>(RuleType.Background);
                    if (background != null)
                    {
                        children.Add(background);
                    }
                    var childrenEnumerable = children.Concat(node.GetItems<IHasLocation>(RuleType.ScenarioDefinition));
                    var description = GetDescription(header);
                    if (ruleLine.MatchedGherkinDialect == null) return null;

                    return CreateRule(tags, GetLocation(ruleLine), ruleLine.MatchedKeyword, ruleLine.MatchedText, description, childrenEnumerable.ToArray(), node);
                }
            case RuleType.GherkinDocument:
                {
                    var feature = node.GetSingle<Feature>(RuleType.Feature);

                    return CreateGherkinDocument(feature, _comments.ToArray(), node);
                }
        }

        return node;
    }

    protected virtual StepKeywordType GetKeywordType(Token stepLine)
    {
        var stepKeywordType = stepLine.MatchedGherkinDialect.GetStepKeywordType(stepLine.MatchedKeyword);
        if (stepKeywordType == null || stepKeywordType == StepKeywordType.Unspecified)
            return StepKeywordType.Unspecified;
        return stepKeywordType.Value;
    }

    protected virtual Background CreateBackground(Location location, string keyword, string name, string description, Step[] steps, AstNode node)
    {
        return new Background(location, keyword, name, description, steps);
    }

    protected virtual DataTable CreateDataTable(TableRow[] rows, AstNode node)
    {
        return new DataTable(rows);
    }

    protected virtual Comment CreateComment(Location location, string text)
    {
        return new Comment(location, text);
    }

    protected virtual Examples CreateExamples(Tag[] tags, Location location, string keyword, string name, string description, TableRow header, TableRow[] body, AstNode node)
    {
        return new Examples(tags, location, keyword, name, description, header, body);
    }

    protected virtual Scenario CreateScenario(Tag[] tags, Location location, string keyword, string name, string description, Step[] steps, Examples[] examples, AstNode node)
    {
        return new Scenario(tags, location, keyword, name, description, steps, examples);
    }

    protected virtual DocString CreateDocString(Location location, string contentType, string content, string delimiter, AstNode node)
    {
        return new DocString(location, contentType, content, delimiter);
    }

    protected virtual Step CreateStep(Location location, string keyword, StepKeywordType keywordType, string text, StepArgument argument, AstNode node)
    {
        return new Step(location, keyword, keywordType, text, argument);
    }

    protected virtual GherkinDocument CreateGherkinDocument(Feature feature, Comment[] gherkinDocumentComments, AstNode node)
    {
        return new GherkinDocument(feature, gherkinDocumentComments);
    }

    protected virtual Feature CreateFeature(Tag[] tags, Location location, string language, string keyword, string name, string description, IHasLocation[] children, AstNode node)
    {
        return new Feature(tags, location, language, keyword, name, description, children);
    }

    protected virtual Rule CreateRule(Tag[] tags, Location location, string keyword, string name, string description, IHasLocation[] children, AstNode node)
    {
        return new Rule(tags, location, keyword, name, description, children);
    }

    protected virtual Tag CreateTag(Location location, string name, AstNode node)
    {
        return new Tag(location, name);
    }

    protected virtual Location CreateLocation(int line, int column)
    {
        return new Location(line, column);
    }

    protected virtual TableRow CreateTableRow(Location location, TableCell[] cells, AstNode node)
    {
        return new TableRow(location, cells);
    }

    protected virtual TableCell CreateTableCell(Location location, string value)
    {
        return new TableCell(location, value);
    }

    private Location GetLocation(Token token, int column = 0)
    {
        return column == 0 ? token.Location : CreateLocation(token.Location.Line, column);
    }

    private Tag[] GetTags(AstNode node)
    {
        var tagsNode = node.GetSingle<AstNode>(RuleType.Tags);
        if (tagsNode == null)
            return [];

        return tagsNode.GetTokens(TokenType.TagLine)
            .SelectMany(t => t.MatchedItems, (t, tagItem) =>
                CreateTag(GetLocation(t, tagItem.Column), tagItem.Text, tagsNode))
            .ToArray();
    }

    private TableRow[] GetTableRows(AstNode node)
    {
        var rows = node.GetTokens(TokenType.TableRow).Select(token => CreateTableRow(GetLocation(token), GetCells(token), node)).ToArray();
        CheckCellCountConsistency(rows);
        return rows;
    }

    protected virtual void CheckCellCountConsistency(TableRow[] rows)
    {
        if (rows.Length == 0)
            return;

        int cellCount = rows[0].Cells.Count();
        foreach (var row in rows)
        {
            if (row.Cells.Count() != cellCount)
            {
                HandleAstError("inconsistent cell count within the table", row.Location);
            }
        }
    }

    protected virtual void HandleAstError(string message, Location location)
    {
        throw new AstBuilderException(message, location);
    }

    private TableCell[] GetCells(Token tableRowToken)
    {
        return tableRowToken.MatchedItems
            .Select(cellItem => CreateTableCell(GetLocation(tableRowToken, cellItem.Column), cellItem.Text))
            .ToArray();
    }

    private static Step[] GetSteps(AstNode scenarioDefinitionNode)
    {
        return scenarioDefinitionNode.GetItems<Step>(RuleType.Step).ToArray();
    }

    private static string GetDescription(AstNode scenarioDefinitionNode)
    {
        return scenarioDefinitionNode.GetSingle<string>(RuleType.Description);
    }
}
