using System.Collections.Generic;
using System.Globalization;
using System.Linq;
namespace Sozsoft.Platform.Editors;
///
/// Tek bir Script Builder kurali. uzerindeki tarif
/// metotlariyla olusturulur, akici metotlarla kosul ve tetikleyici eklenir.
///
public sealed class EditorScriptRule
{
/// Zorunlu parametreler. Dialogdaki isRuleComplete kontrolunun karsiligi.
private static readonly Dictionary RequiredParams = new Dictionary
{
["multiply"] = new[] { "left", "right", "target" },
["subtract"] = new[] { "left", "right", "target" },
["percent"] = new[] { "amount", "rate", "target" },
["sum"] = new[] { "target" },
["formula"] = new[] { "target", "expression" },
["today"] = new[] { "target" },
["days"] = new[] { "start", "end", "target" },
["hours"] = new[] { "start", "end", "target" },
["copy"] = new[] { "source", "target" },
["setValue"] = new[] { "target", "text" },
["clear"] = new string[0],
["readOnly"] = new[] { "target" },
["notify"] = new[] { "message" },
["ask"] = new[] { "message" },
["openUrl"] = new[] { "url" },
["apiToField"] = new[] { "target", "url" },
["custom"] = new[] { "code" }
};
/// Alan listesi isteyen tarifler.
private static readonly HashSet FieldListRecipes = new HashSet { "sum", "clear" };
///
/// Grup bazli varsayilan tetikleyici. Deger ureten kurallar form acilirken de
/// calisir; kullaniciya bir sey gosteren ya da dis cagri yapan kurallar
/// yalnizca deger degistiginde calisir.
///
private static readonly Dictionary GroupDefaultTrigger =
new Dictionary
{
["multiply"] = EditorScriptTrigger.Both,
["subtract"] = EditorScriptTrigger.Both,
["percent"] = EditorScriptTrigger.Both,
["sum"] = EditorScriptTrigger.Both,
["formula"] = EditorScriptTrigger.Both,
["today"] = EditorScriptTrigger.Both,
["days"] = EditorScriptTrigger.Both,
["hours"] = EditorScriptTrigger.Both,
["copy"] = EditorScriptTrigger.Both,
["setValue"] = EditorScriptTrigger.Both,
["clear"] = EditorScriptTrigger.Both,
["readOnly"] = EditorScriptTrigger.Both,
["notify"] = EditorScriptTrigger.Change,
["ask"] = EditorScriptTrigger.Change,
["openUrl"] = EditorScriptTrigger.Change,
["apiToField"] = EditorScriptTrigger.Change,
["custom"] = EditorScriptTrigger.Change
};
private readonly string _recipe;
private readonly List> _params = new List>();
private readonly List _fields = new List();
private readonly List _conditions = new List();
private string _id;
private bool _anyCondition;
private EditorScriptTrigger _trigger = EditorScriptTrigger.Default;
internal EditorScriptRule(string recipe)
{
_recipe = recipe;
}
/* ---------------------------------------------------------------- *
* Akici yapilandirma
* ---------------------------------------------------------------- */
/// Kural yalnizca verilen kosullarin hepsi saglandiginda calisir.
public EditorScriptRule When(params EditorScriptCondition[] conditions) => AddConditions(conditions, false);
/// Kural verilen kosullardan herhangi biri saglandiginda calisir.
public EditorScriptRule WhenAny(params EditorScriptCondition[] conditions) => AddConditions(conditions, true);
/// Yalnizca alanin degeri degistiginde calissin.
public EditorScriptRule OnChange() => WithTrigger(EditorScriptTrigger.Change);
/// Yalnizca form acilirken calissin (varsayilan deger uretmek icin).
public EditorScriptRule OnOpen() => WithTrigger(EditorScriptTrigger.Open);
/// Hem acilista hem deger degistiginde calissin.
public EditorScriptRule OnOpenAndChange() => WithTrigger(EditorScriptTrigger.Both);
/// Tetikleyiciyi acikca belirler. Default ise tarifin grup varsayilani kullanilir.
public EditorScriptRule WithTrigger(EditorScriptTrigger trigger)
{
_trigger = trigger;
return this;
}
///
/// Kural kimligini sabitler. Verilmezse Build sirasinda tarif adi ve sira
/// numarasindan uretilir (ornek: "multiply_1").
///
public EditorScriptRule WithId(string id)
{
_id = id;
return this;
}
/// Tek kurallik script metni. Birden fazla kural icin kullanin.
public string ToScript() => EditorScript.Build(this);
/// Kurali dogrudan EditorScript alanina atanabilir hale getirir.
public static implicit operator string(EditorScriptRule rule) => rule?.ToScript() ?? string.Empty;
/* ---------------------------------------------------------------- *
* Ic kullanim
* ---------------------------------------------------------------- */
internal EditorScriptRule Set(string key, string value)
{
// Bos parametreler dialogda da yazilmaz; JSON'un birebir esit kalmasi icin atlanir.
if (!string.IsNullOrEmpty(value))
{
_params.Add(new KeyValuePair(key, value));
}
return this;
}
internal EditorScriptRule SetNumber(string key, int? value)
=> value.HasValue ? Set(key, value.Value.ToString(CultureInfo.InvariantCulture)) : this;
internal EditorScriptRule SetFields(IEnumerable fields)
{
if (fields != null)
{
_fields.AddRange(fields.Where(field => !string.IsNullOrEmpty(field)));
}
return this;
}
internal void EnsureId(int order)
{
if (string.IsNullOrEmpty(_id))
{
_id = _recipe + "_" + order.ToString(CultureInfo.InvariantCulture);
}
}
internal EditorScriptTrigger EffectiveTrigger()
{
if (_trigger != EditorScriptTrigger.Default)
{
return _trigger;
}
return GroupDefaultTrigger.TryGetValue(_recipe, out var trigger) ? trigger : EditorScriptTrigger.Change;
}
internal bool IsComplete()
{
if (!RequiredParams.TryGetValue(_recipe, out var required))
{
return false;
}
if (_conditions.Any(condition => !condition.IsComplete()))
{
return false;
}
if (FieldListRecipes.Contains(_recipe) && _fields.Count == 0)
{
return false;
}
return required.All(key => _params.Any(pair => pair.Key == key && pair.Value.Trim().Length > 0));
}
internal string ToJson()
{
var conditions = EffectiveConditions();
var json = "{\"id\":" + EditorScript.Quote(_id) +
",\"recipe\":" + EditorScript.Quote(_recipe) +
",\"condition\":" + conditions[0].ToJson();
if (conditions.Count > 1)
{
json += ",\"conditions\":[" + string.Join(",", conditions.Skip(1).Select(c => c.ToJson())) + "]" +
",\"join\":" + EditorScript.Quote(_anyCondition ? "or" : "and");
}
json += ",\"params\":{" +
string.Join(",", _params.Select(pair => EditorScript.Quote(pair.Key) + ":" + EditorScript.Quote(pair.Value))) +
"}";
json += ",\"fields\":[" + string.Join(",", _fields.Select(EditorScript.Quote)) + "]";
if (_trigger != EditorScriptTrigger.Default)
{
json += ",\"trigger\":" + EditorScript.Quote(TriggerName(_trigger));
}
return json + "}";
}
///
/// Kuralin tek satirlik kodu. runsOnOpen yalnizca script'in tamami acilista da
/// calisiyorsa true gelir; o durumda her kuralin ne zaman calisacagi isReady
/// ile ayristirilir.
///
internal string ToCode(bool runsOnOpen)
{
var body = BuildBody();
if (string.IsNullOrEmpty(body))
{
return string.Empty;
}
var guards = new List();
if (runsOnOpen)
{
var trigger = EffectiveTrigger();
if (trigger == EditorScriptTrigger.Change)
{
guards.Add("!isReady");
}
else if (trigger == EditorScriptTrigger.Open)
{
guards.Add("isReady");
}
}
// readOnly'de kosul dogrudan aksiyonun argumanidir, if ile sarmalanmaz.
if (_recipe != "readOnly")
{
var condition = ConditionExpression();
if (!string.IsNullOrEmpty(condition))
{
guards.Add(condition);
}
}
return guards.Count > 0 ? "if (" + string.Join(" && ", guards) + ") " + body : body;
}
private List EffectiveConditions()
=> _conditions.Count > 0 ? _conditions : new List { EditorScriptCondition.Always() };
private string ConditionExpression()
{
var parts = EffectiveConditions()
.Select(condition => condition.ToExpression())
.Where(expression => !string.IsNullOrEmpty(expression))
.ToList();
if (parts.Count == 0)
{
return string.Empty;
}
if (parts.Count == 1)
{
return parts[0];
}
return string.Join(_anyCondition ? " || " : " && ", parts.Select(part => "(" + part + ")"));
}
private EditorScriptRule AddConditions(EditorScriptCondition[] conditions, bool any)
{
if (conditions != null)
{
_conditions.AddRange(conditions.Where(condition => condition != null));
}
if (_conditions.Count > 1)
{
_anyCondition = any;
}
return this;
}
private string Param(string key)
{
var match = _params.FirstOrDefault(pair => pair.Key == key);
return match.Key == null ? string.Empty : match.Value;
}
private string Digits() => Param("digits").Length > 0 ? Param("digits") : "2";
private string BuildBody()
{
var q = new System.Func(EditorScript.Quote);
var n = new System.Func(EditorScript.NumberOrField);
switch (_recipe)
{
case "multiply":
return "set(" + q(Param("target")) + ", round(" + n(Param("left")) + " * " + n(Param("right")) + ", " + Digits() + "))";
case "subtract":
return "set(" + q(Param("target")) + ", round(" + n(Param("left")) + " - " + n(Param("right")) + ", " + Digits() + "))";
case "percent":
{
var amount = n(Param("amount"));
var portion = amount + " * " + n(Param("rate")) + " / 100";
var expression = Param("mode") == "add"
? amount + " + " + portion
: Param("mode") == "subtract"
? amount + " - " + portion
: portion;
return "set(" + q(Param("target")) + ", round(" + expression + ", " + Digits() + "))";
}
case "sum":
return "set(" + q(Param("target")) + ", round(sum(" + string.Join(", ", _fields.Select(q)) + "), " + Digits() + "))";
case "formula":
return "set(" + q(Param("target")) + ", " + (Param("expression").Length > 0 ? Param("expression") : "null") + ")";
case "today":
{
var offset = Param("offset");
var parsed = int.TryParse(offset, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) ? value : 0;
var start = parsed != 0
? "new Date(Date.now() + " + parsed.ToString(CultureInfo.InvariantCulture) + " * 86400000)"
: "new Date()";
return "set(" + q(Param("target")) + ", " + start + ".toISOString().slice(0, 10))";
}
case "days":
return "set(" + q(Param("target")) + ", days(" + q(Param("start")) + ", " + q(Param("end")) + "))";
case "hours":
return "set(" + q(Param("target")) + ", hours(" + q(Param("start")) + ", " + q(Param("end")) + "))";
case "copy":
return "copy(" + q(Param("source")) + ", " + q(Param("target")) + ")";
case "setValue":
return "set(" + q(Param("target")) + ", tpl(" + q(Param("text")) + "))";
case "clear":
return "clear(" + string.Join(", ", _fields.Select(q)) + ")";
case "readOnly":
{
var expression = ConditionExpression();
if (expression.Length == 0)
{
expression = "true";
}
var flag = Param("invert") == "true" ? "!(" + expression + ")" : expression;
return "readOnly(" + q(Param("target")) + ", " + flag + ")";
}
case "notify":
return "notify(" + q(Param("message")) + ")";
case "ask":
return "if (!ask(" + q(Param("message")) + ")) return";
case "openUrl":
return Param("target").Length > 0 && Param("target") != "_blank"
? "openUrl(" + q(Param("url")) + ", " + q(Param("target")) + ")"
: "openUrl(" + q(Param("url")) + ")";
case "apiToField":
return "set(" + q(Param("target")) + ", await api(" + q(Param("url")) +
(Param("path").Length > 0 ? ", " + q(Param("path")) : string.Empty) + "))";
case "custom":
return Param("code").Trim().TrimEnd(';');
default:
return string.Empty;
}
}
private static string TriggerName(EditorScriptTrigger trigger)
{
switch (trigger)
{
case EditorScriptTrigger.Change: return "change";
case EditorScriptTrigger.Open: return "open";
default: return "both";
}
}
}