using Gherkin.CucumberMessages.Types;

// ReSharper disable PossibleMultipleEnumeration

namespace Gherkin.CucumberMessages.Pickles;

public class PickleCompiler(IIdGenerator idGenerator)
{
    protected StepKeywordType CurrentStepKeywordType { get; set; }

    public List<Pickle> Compile(GherkinDocument gherkinDocument)
    {
        var pickles = new List<Pickle>();
        var feature = gherkinDocument.Feature;
        if (feature == null)
        {
            return pickles;
        }

        var language = feature.Language;
        var tags = feature.Tags;

        BuildFeature(pickles, language, tags, Enumerable.Empty<PickleStep>, feature.Children, gherkinDocument.Uri);

        return pickles;
    }

    protected virtual void BuildFeature(List<Pickle> pickles, string language, IEnumerable<Tag> tags,
        Func<IEnumerable<PickleStep>> backgroundStepsFactory, IEnumerable<FeatureChild> children,
        string gherkinDocumentUri)
    {
        if (children == null)
            return;

        foreach (var child in children)
        {
            if (child.Background != null)
            {
                backgroundStepsFactory = BuildBackground(child.Background, backgroundStepsFactory);
            }
            else if (child.Rule != null)
            {
                var mergedRuleTags = tags.Concat(child.Rule.Tags);
                BuildRule(pickles, language, mergedRuleTags, backgroundStepsFactory, child.Rule.Children, gherkinDocumentUri);
            }
            else if (child.Scenario != null)
            {
                BuildScenario(pickles, language, tags, backgroundStepsFactory, gherkinDocumentUri, child.Scenario);
            }
        }
    }

    protected virtual void BuildRule(List<Pickle> pickles, string language, IEnumerable<Tag> tags,
        Func<IEnumerable<PickleStep>> backgroundStepsFactory, IEnumerable<RuleChild> children,
        string gherkinDocumentUri)
    {
        if (children == null)
            return;

        foreach (var child in children)
        {
            if (child.Background != null)
            {
                backgroundStepsFactory = BuildBackground(child.Background, backgroundStepsFactory);
            }
            else if (child.Scenario != null)
            {
                BuildScenario(pickles, language, tags, backgroundStepsFactory, gherkinDocumentUri, child.Scenario);
            }
        }
    }

    private Func<IEnumerable<PickleStep>> BuildBackground(Background background, Func<IEnumerable<PickleStep>> backgroundStepsFactory)
    {
        var previousFactory = backgroundStepsFactory;
        backgroundStepsFactory = () => previousFactory().Concat(PickleSteps(background.Steps));
        return backgroundStepsFactory;
    }

    private void BuildScenario(List<Pickle> pickles, string language, IEnumerable<Tag> tags, Func<IEnumerable<PickleStep>> backgroundStepsFactory, string gherkinDocumentUri, Scenario scenario)
    {
        CurrentStepKeywordType = StepKeywordType.Unspecified;
        if (!scenario.Examples.Any())
        {
            CompileScenario(pickles, backgroundStepsFactory, scenario, tags, language, gherkinDocumentUri);
        }
        else
        {
            CompileScenarioOutline(pickles, backgroundStepsFactory, scenario, tags, language, gherkinDocumentUri);
        }
    }

    protected virtual void CompileScenario(List<Pickle> pickles,
        Func<IEnumerable<PickleStep>> backgroundStepsFactory, Scenario scenario, IEnumerable<Tag> featureTags,
        string language, string gherkinDocumentUri)
    {
        var steps = new List<PickleStep>();
        if (scenario.Steps.Any())
            steps.AddRange(backgroundStepsFactory());

        var scenarioTags = new List<Tag>();
        scenarioTags.AddRange(featureTags);
        scenarioTags.AddRange(scenario.Tags);

        steps.AddRange(PickleSteps(scenario.Steps));

        Pickle pickle = new Pickle(
                idGenerator.GetNewId(),
                gherkinDocumentUri,
                scenario.Name,
                language,
                steps,
                PickleTags(scenarioTags),
                new[] { scenario.Id }
        );
        pickles.Add(pickle);
    }

    protected virtual void CompileScenarioOutline(List<Pickle> pickles,
        Func<IEnumerable<PickleStep>> backgroundStepsFactory, Scenario scenarioOutline,
        IEnumerable<Tag> featureTags, string language, string gherkinDocumentUri)
    {
        foreach (var examples in scenarioOutline.Examples)
        {
            if (examples.TableHeader == null) continue;
            var variableCells = examples.TableHeader.Cells;
            foreach (var values in examples.TableBody)
            {
                var valueCells = values.Cells;

                var steps = new List<PickleStep>();
                if (scenarioOutline.Steps.Any())
                    steps.AddRange(backgroundStepsFactory());

                var tags = new List<Tag>();
                tags.AddRange(featureTags);
                tags.AddRange(scenarioOutline.Tags);
                tags.AddRange(examples.Tags);

                foreach (var scenarioOutlineStep in scenarioOutline.Steps)
                {
                    string stepText = Interpolate(scenarioOutlineStep.Text, variableCells, valueCells);

                    PickleStep pickleStep = CreatePickleStep(
                            scenarioOutlineStep,
                            stepText,
                            CreatePickleArgument(scenarioOutlineStep, variableCells, valueCells),
                            new[] { scenarioOutlineStep.Id, values.Id }
                    );
                    steps.Add(pickleStep);
                }

                Pickle pickle = new Pickle(
                        idGenerator.GetNewId(),
                        gherkinDocumentUri,
                        Interpolate(scenarioOutline.Name, variableCells, valueCells),
                        language,
                        steps,
                        PickleTags(tags),
                        new[] { scenarioOutline.Id, values.Id }
                );

                pickles.Add(pickle);
            }
        }
    }

    protected virtual PickleStep CreatePickleStep(Step step, string text, PickleStepArgument argument, IEnumerable<string> astNodeIds)
    {
        CurrentStepKeywordType = GetKeywordType(step, CurrentStepKeywordType);
        return new PickleStep(argument, astNodeIds, idGenerator.GetNewId(), text, CurrentStepKeywordType);
    }

    protected virtual StepKeywordType GetKeywordType(Step step, StepKeywordType lastStepKeywordType)
    {
        var stepKeywordType = step.KeywordType;
        switch (stepKeywordType)
        {
            case StepKeywordType.Context:
            case StepKeywordType.Action:
            case StepKeywordType.Outcome:
            case StepKeywordType.Unknown:
                return stepKeywordType;
            case StepKeywordType.Conjunction:
                return lastStepKeywordType == StepKeywordType.Unspecified ||
                       lastStepKeywordType == StepKeywordType.Unknown
                    ? StepKeywordType.Context
                    : lastStepKeywordType;
            default:
                return StepKeywordType.Unspecified;
        }
    }


    protected virtual PickleStepArgument CreatePickleArgument(Step argument)
    {
        var noCells = Enumerable.Empty<TableCell>();
        return CreatePickleArgument(argument, noCells, noCells);
    }

    protected virtual PickleStepArgument CreatePickleArgument(Step step, IEnumerable<TableCell> variableCells, IEnumerable<TableCell> valueCells)
    {
        if (step.DataTable != null)
        {
            var t = step.DataTable;
            var rows = t.Rows;
            var newRows = new List<PickleTableRow>(rows.Count());
            foreach (var row in rows)
            {
                var cells = row.Cells;
                var newCells = new List<PickleTableCell>();
                foreach (var cell in cells)
                {
                    newCells.Add(
                            new PickleTableCell(
                                    Interpolate(cell.Value, variableCells, valueCells)
                            )
                    );
                }
                newRows.Add(new PickleTableRow(newCells));
            }
            return new PickleStepArgument
            {
                DataTable = new PickleTable(newRows)
            };
        }

        if (step.DocString != null)
        {
            var ds = step.DocString;
            return
                new PickleStepArgument
                {
                    DocString = new PickleDocString(
                        Interpolate(ds.Content, variableCells, valueCells),
                        ds.MediaType == null ? null : Interpolate(ds.MediaType, variableCells, valueCells))
                };
        }

        return null;
    }

    protected virtual PickleStep[] PickleSteps(IEnumerable<Step> steps)
    {
        var result = new List<PickleStep>();
        foreach (var step in steps)
        {
            result.Add(PickleStep(step));
        }
        return result.ToArray();
    }

    protected virtual PickleStep PickleStep(Step step)
    {
        return CreatePickleStep(
                step,
                step.Text,
                CreatePickleArgument(step),
                new[] { step.Id }
        );
    }

    protected virtual string Interpolate(string name, IEnumerable<TableCell> variableCells, IEnumerable<TableCell> valueCells)
    {
        int col = 0;
        foreach (var variableCell in variableCells)
        {
            var valueCell = valueCells.ElementAt(col++);
            string header = variableCell.Value;
            string value = valueCell.Value;
            name = name.Replace("<" + header + ">", value);
        }
        return name;
    }

    protected virtual List<PickleTag> PickleTags(List<Tag> tags)
    {
        var result = new List<PickleTag>();
        foreach (var tag in tags)
        {
            result.Add(PickleTag(tag));
        }
        return result;
    }

    protected virtual PickleTag PickleTag(Tag tag)
    {
        return new PickleTag(tag.Name, tag.Id);
    }
}
