using System.Globalization;
namespace OpenNest.Data;
///
/// The drawing library's Filter dialog: a customer and inclusive ranges of thickness, size,
/// holes and saved date, all optional. Thickness and sizes are inches, whatever units each
/// drawing is in. Size and hole limits match only drawings the server has measured (see
/// ). The browse session sends it with every query.
///
public sealed record DrawingLibraryFilter
{
public static DrawingLibraryFilter None { get; } = new();
/// Exact customer, compared by . Blank means any.
public string Customer { get; init; } = "";
public double? MinThickness { get; init; }
public double? MaxThickness { get; init; }
///
/// Whether drawings saved without a thickness (0, which suits any thickness) still match a
/// thickness range. False also leaves them out when no range is set.
///
public bool IncludeBlankThickness { get; init; } = true;
/// Limits on the longer side of the bounding box.
public double? MinLength { get; init; }
public double? MaxLength { get; init; }
/// Limits on the shorter side of the bounding box.
public double? MinWidth { get; init; }
public double? MaxWidth { get; init; }
public int? MinHoles { get; init; }
public int? MaxHoles { get; init; }
/// First and last day saved, inclusive, in the server's local dates.
public DateOnly? SavedFrom { get; init; }
public DateOnly? SavedTo { get; init; }
public bool IsEmpty => this == None;
/// True when a size or hole limit is set, so only measured drawings can match.
public bool NeedsGeometry =>
MinLength is not null || MaxLength is not null || MinWidth is not null || MaxWidth is not null
|| MinHoles is not null || MaxHoles is not null;
/// The first invalid range, or null when the filter may be sent.
public string? GetValidationError() =>
RangeError("thickness", MinThickness, MaxThickness)
?? RangeError("length", MinLength, MaxLength)
?? RangeError("width", MinWidth, MaxWidth)
?? RangeError("holes", MinHoles, MaxHoles)
?? (SavedFrom is { } from && SavedTo is { } to && from > to ? "The saved-from date must not be after the saved-to date." : null)
?? DrawingLibraryRules.TextError("customer", Customer ?? "", DrawingLibraryRules.MaxTextLength);
/// A one-line summary for the library's filter label, for example "Thickness 0.0598-0.1875 in; Holes 2+".
public string Describe()
{
var parts = new List();
if (!string.IsNullOrWhiteSpace(Customer))
parts.Add($"Customer {Customer.Trim()}");
if (Range(MinThickness, MaxThickness, " in") is { } thickness)
parts.Add($"Thickness {thickness}" + (IncludeBlankThickness ? " (or none)" : ""));
else if (!IncludeBlankThickness)
parts.Add("Has a thickness");
if (Range(MinLength, MaxLength, " in") is { } length)
parts.Add($"Length {length}");
if (Range(MinWidth, MaxWidth, " in") is { } width)
parts.Add($"Width {width}");
if (Range(MinHoles, MaxHoles, "") is { } holes)
parts.Add($"Holes {holes}");
if (SavedFrom is not null || SavedTo is not null)
parts.Add("Saved " + (SavedFrom, SavedTo) switch
{
({ } from, { } to) => $"{Date(from)} to {Date(to)}",
({ } from, null) => $"on or after {Date(from)}",
(null, { } to) => $"on or before {Date(to)}",
_ => "",
});
return string.Join("; ", parts);
}
internal static string? RangeError(string name, double? min, double? max)
{
if (min is { } low && (!double.IsFinite(low) || low < 0))
return $"The lowest {name} must be a finite number, zero or greater.";
if (max is { } high && (!double.IsFinite(high) || high < 0))
return $"The highest {name} must be a finite number, zero or greater.";
return min > max ? $"The lowest {name} must not be above the highest." : null;
}
internal static string? RangeError(string name, int? min, int? max)
{
if (min < 0 || max < 0)
return $"The {name} limits must be zero or greater.";
return min > max ? $"The lowest {name} must not be above the highest." : null;
}
private static string? Range(double? min, double? max, string unit) => (min, max) switch
{
({ } low, { } high) when low == high => $"{Number(low)}{unit}",
({ } low, { } high) => $"{Number(low)}-{Number(high)}{unit}",
({ } low, null) => $"{Number(low)}{unit}+",
(null, { } high) => $"up to {Number(high)}{unit}",
_ => null,
};
private static string? Range(int? min, int? max, string unit) =>
Range(min is { } low ? low : null, max is { } high ? high : (double?)null, unit);
private static string Number(double value) => value.ToString("0.####", CultureInfo.CurrentCulture);
private static string Date(DateOnly date) => date.ToString("d", CultureInfo.CurrentCulture);
}