using System;
using System.Collections.Generic;
using Cthangover.Core.Factories.Impls;
using Cthangover.Core.Localization;
using Cthangover.Mods.Dialog;
using Cthangover.Mods.Dialog.Action.Impls;
using Cthangover.Core.UI;
using Cthangover.Mods.Interface.Base.Lists;
using Cthangover.Core.UI.Dialog;
using Cthangover.Core.UI.Event;
using Cthangover.Core.UI.Executable;
using Cthangover.Core.Scenarios;
using Cthangover.Core.Scenes;
using Cthangover.Core.Interactive;
namespace Cthangover.Mods.Dialog.Scenarios
{
///
/// Parses scenario DSL script text into a of dialog actions.
/// The pipeline works in two phases: first the text is split into lines, then each
/// non-empty, non-comment line is tokenized and dispatched to the matching
/// via .
/// Special handling exists for :label anchor points, select/option
/// blocks (accumulated into ), and -> arrow targets.
///
public static class ScenarioParser
{
///
/// Parses a complete scenario script text and returns the resulting dialog queue.
///
/// Raw scenario DSL content.
/// Optional localization provider for key= text lookups.
public static DialogQueue Parse(string text, ILocalizationProvider locale = null)
{
var dlg = new DialogQueue();
var lines = text.Split('\n');
ActionSelect pendingSelect = null;
for (var lineIndex = 0; lineIndex < lines.Length; lineIndex++)
{
var lineNumber = lineIndex + 1;
var line = lines[lineIndex].Trim();
if (line.Length == 0 || line.StartsWith('#'))
{
FinalizePendingSelect(dlg, ref pendingSelect);
continue;
}
if (line.StartsWith(':'))
{
FinalizePendingSelect(dlg, ref pendingSelect);
dlg.Point(line.Substring(1)).SourceLine = lineNumber;
continue;
}
var positional = new List();
var named = new Dictionary();
var cmd = SplitLine(line, positional, named, out var arrowTarget);
if (cmd == null) continue;
if (cmd == "select")
{
FinalizePendingSelect(dlg, ref pendingSelect);
pendingSelect = new ActionSelect { SourceLine = lineNumber };
var promptText = positional.Count > 0 ? positional[0] : null;
if (named.TryGetValue("key", out var key) && locale != null)
{
var loc = locale.Get(key);
if (loc != null) promptText = loc;
}
if (promptText != null)
pendingSelect.Text = promptText;
if (named.TryGetValue("first", out var first))
pendingSelect.FirstAvatar = first;
if (named.TryGetValue("second", out var second))
pendingSelect.SecondAvatar = second;
continue;
}
if (cmd == "option")
{
if (pendingSelect == null) continue;
var optText = positional.Count > 0 ? positional[0] : "";
if (named.TryGetValue("key", out var key) && locale != null)
{
var loc = locale.Get(key);
if (loc != null) optText = loc;
}
var goTo = arrowTarget;
if (string.IsNullOrEmpty(goTo))
{
if (named.TryGetValue("goto", out var gt))
goTo = gt;
else if (positional.Count > 1 && positional[1].StartsWith(':'))
goTo = positional[1].TrimStart(':');
}
if (pendingSelect.Variants == null)
pendingSelect.Variants = new List();
var variant = SelectVariant.New(optText, goTo);
if (named.TryGetValue("if", out var condition))
variant.Condition = condition;
pendingSelect.Variants.Add(variant);
continue;
}
FinalizePendingSelect(dlg, ref pendingSelect);
var strategy = ScenarioCommandStrategyFactory.Get(cmd);
if (strategy == null)
continue;
var firstNew = dlg.Queue.Count;
strategy.Execute(dlg, positional, named, arrowTarget, locale);
for (var i = firstNew; i < dlg.Queue.Count; i++)
{
if (dlg.Queue[i] != null && dlg.Queue[i].SourceLine == 0)
dlg.Queue[i].SourceLine = lineNumber;
}
}
FinalizePendingSelect(dlg, ref pendingSelect);
return dlg;
}
///
/// Splits one trimmed, non-empty DSL line the way does:
/// returns the lowercase command word (null for an empty line) and fills
/// the positional tokens, the name=value pairs (names lowercased)
/// and the -> :label target. Tools that read scenarios without
/// running them (the scenario studio) use it to agree with the parser.
///
public static string SplitLine(string line, List positional, Dictionary named, out string arrowTarget)
{
arrowTarget = "";
var tokens = Tokenize(line);
if (tokens.Count == 0)
return null;
var cmd = tokens[0].ToLowerInvariant();
var spec = ScenarioCommandStrategyFactory.Get(cmd) as IScenarioCommandSpec;
for (var i = 1; i < tokens.Count; i++)
{
var t = tokens[i];
if (t == "->")
{
if (i + 1 < tokens.Count)
{
arrowTarget = tokens[i + 1].TrimStart(':');
i++;
}
continue;
}
var eqIdx = t.IndexOf('=');
if (eqIdx > 0)
{
var name = t.Substring(0, eqIdx);
if (IsValidIdent(name))
{
named[spec?.PreserveNamedArgumentCase == true ? name : name.ToLowerInvariant()] = t.Substring(eqIdx + 1);
continue;
}
}
positional.Add(t);
}
return cmd;
}
///
/// Extracts YAML-like front-matter metadata from script text. Lines before
/// a --- separator or the first non-key:value line are parsed as
/// key: value pairs. Returns both the metadata dictionary and the
/// remaining script content after the metadata block.
///
public static (Dictionary metadata, string remainingText) ParseMetadata(string text)
{
var metadata = new Dictionary(StringComparer.OrdinalIgnoreCase);
var lines = text.Split('\n');
var contentStart = 0;
var inMetadata = true;
for (var i = 0; i < lines.Length; i++)
{
var line = lines[i].Trim();
if (!inMetadata)
{
continue;
}
if (line == "---")
{
contentStart = i + 1;
break;
}
if (string.IsNullOrEmpty(line))
{
contentStart = i;
break;
}
var colonIdx = line.IndexOf(':');
if (colonIdx > 0)
{
var key = line.Substring(0, colonIdx).Trim();
var val = line.Substring(colonIdx + 1).Trim();
metadata[key] = val;
}
else
{
contentStart = i;
break;
}
}
var remaining = string.Join("\n", lines, contentStart, lines.Length - contentStart);
return (metadata, remaining);
}
private static void FinalizePendingSelect(DialogQueue dlg, ref ActionSelect pending)
{
if (pending == null) return;
if (pending.Variants == null || pending.Variants.Count == 0)
{
pending = null;
return;
}
dlg.Queue.Add(pending);
pending = null;
}
private static List Tokenize(string line)
{
var tokens=new List();
var i=0;
while(i