using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using NcProgramManager.Cnc.Models;
namespace NcProgramManager.Cnc
{
/// Validates NC program content per-manufacturer before upload.
public sealed class NcProgramValidator : INcProgramValidator
{
private const int MaxBlockLength = 80;
private const int MaxCommentLength = 32;
private static readonly Regex FanucNamePattern = new Regex(@"^O\d{4}$", RegexOptions.Compiled);
private static readonly Regex FanucFirstLinePattern = new Regex(@"^O\d{4}", RegexOptions.Compiled);
private readonly CncManufacturer _manufacturer;
public NcProgramValidator(CncManufacturer manufacturer)
{
_manufacturer = manufacturer;
}
public ValidationResult Validate(string programName, string content)
{
// Mazak EIA: full passthrough. Not Fanuc-restrictive — filenames preserved
// verbatim, content sent as-is. No name/content/forbidden-char/length checks.
if (_manufacturer == CncManufacturer.Mazak)
return ValidationResult.Ok();
var errors = new List();
// Name-format check (uses just the bare filename without path)
string name = ExtractName(programName);
ValidateName(name, errors);
if (string.IsNullOrEmpty(content))
return errors.Count == 0 ? ValidationResult.Ok() : ValidationResult.Fail(errors);
// Normalise: strip CR, split on LF
string normalized = content.Replace("\r", "");
string[] lines = normalized.Split('\n');
// First-line check (Fanuc/Mitsubishi).
// FanucProgram.Standardize() prepends a leading '\n' to the program body,
// so the O#### header is not necessarily on lines[0]. Scan the first
// non-blank line instead of assuming index 0.
if (_manufacturer == CncManufacturer.Fanuc || _manufacturer == CncManufacturer.Mitsubishi)
{
string first = null;
for (int li = 0; li < lines.Length; li++)
{
string t = lines[li].Trim();
if (t.Length > 0) { first = t; break; }
}
if (first == null || !FanucFirstLinePattern.IsMatch(first))
errors.Add(new ValidationError(1, "First line must be O#### program number", ValidationSeverity.Error));
}
bool restrictive = IsRestrictiveDialect();
for (int i = 0; i < lines.Length; i++)
{
int lineNum = i + 1;
string line = lines[i];
if (restrictive)
{
// Tab check
if (line.IndexOf('\t') >= 0)
errors.Add(new ValidationError(lineNum, "Tab character not allowed", ValidationSeverity.Error));
// Lowercase check
for (int ci = 0; ci < line.Length; ci++)
{
if (char.IsLower(line[ci]))
{
errors.Add(new ValidationError(lineNum, "Lowercase letters not allowed", ValidationSeverity.Error));
break;
}
}
// Block length
if (line.Length > MaxBlockLength)
errors.Add(new ValidationError(lineNum,
string.Format("Block length {0} exceeds 80 chars", line.Length),
ValidationSeverity.Error));
// Nested parens + comment length
ValidateParensAndComments(line, lineNum, errors);
}
// Forbidden chars per manufacturer
ValidateForbiddenChars(line, lineNum, errors);
}
bool hasErrors = false;
for (int i = 0; i < errors.Count; i++)
if (errors[i].Severity == ValidationSeverity.Error) { hasErrors = true; break; }
if (hasErrors)
return ValidationResult.Fail(errors);
// Preserve warnings even when overall result is Success
return errors.Count == 0 ? ValidationResult.Ok() : ValidationResult.OkWithWarnings(errors);
}
// ── private helpers ───────────────────────────────────────────────────
private bool IsRestrictiveDialect()
{
return _manufacturer == CncManufacturer.Fanuc
|| _manufacturer == CncManufacturer.Mitsubishi;
}
private void ValidateName(string name, List errors)
{
if (string.IsNullOrEmpty(name)) return;
switch (_manufacturer)
{
case CncManufacturer.Fanuc:
case CncManufacturer.Mitsubishi:
if (!FanucNamePattern.IsMatch(name))
errors.Add(new ValidationError(0,
string.Format("Program name '{0}' must match O####", name),
ValidationSeverity.Error));
break;
case CncManufacturer.Siemens:
if (!name.EndsWith(".MPF"))
errors.Add(new ValidationError(0,
string.Format("Program name '{0}' must end with .MPF", name),
ValidationSeverity.Error));
break;
case CncManufacturer.Heidenhain:
if (!name.EndsWith(".H"))
errors.Add(new ValidationError(0,
string.Format("Program name '{0}' must end with .H", name),
ValidationSeverity.Error));
break;
case CncManufacturer.Fagor:
// Estrai stem ed estensione (solo ultima estensione conta)
int dotIdx = name.LastIndexOf('.');
string stem = dotIdx >= 0 ? name.Substring(0, dotIdx) : name;
// Lunghezza: regola applicata sul nome completo (incluso eventuale .ext)
if (name.Length > 24)
errors.Add(new ValidationError(0,
string.Format("Nome programma '{0}' supera 24 caratteri", name),
ValidationSeverity.Error));
if (name.IndexOf('\u00f1') >= 0)
errors.Add(new ValidationError(0,
"Carattere '\u00f1' non ammesso nel nome programma Fagor",
ValidationSeverity.Error));
// Stem deve contenere solo lettere/cifre/spazi
for (int i = 0; i < stem.Length; i++)
{
char c = stem[i];
if (!(char.IsLetterOrDigit(c) || c == ' '))
{
errors.Add(new ValidationError(0,
string.Format("Carattere '{0}' non ammesso nel nome programma Fagor (solo lettere/cifre/spazi)", c),
ValidationSeverity.Error));
break;
}
}
break;
}
}
private void ValidateParensAndComments(string line, int lineNum, List errors)
{
int depth = 0;
int commentStart = -1;
for (int i = 0; i < line.Length; i++)
{
char c = line[i];
if (c == '(')
{
depth++;
if (depth > 1)
{
errors.Add(new ValidationError(lineNum, "Nested parentheses not allowed", ValidationSeverity.Error));
// Don't keep adding the same error for this line
return;
}
commentStart = i;
}
else if (c == ')')
{
if (depth == 1 && commentStart >= 0)
{
// Extract comment content between the parens
int len = i - commentStart - 1;
if (len > MaxCommentLength)
errors.Add(new ValidationError(lineNum,
string.Format("Comment length {0} exceeds 32 chars", len),
ValidationSeverity.Error));
}
if (depth > 0) depth--;
commentStart = -1;
}
}
}
private void ValidateForbiddenChars(string line, int lineNum, List errors)
{
switch (_manufacturer)
{
case CncManufacturer.Fanuc:
case CncManufacturer.Mitsubishi:
for (int i = 0; i < line.Length; i++)
{
char c = line[i];
if (c == ';' || c == '[' || c == ']')
{
errors.Add(new ValidationError(lineNum,
string.Format("Forbidden char '{0}' for Fanuc/Mitsubishi", c),
ValidationSeverity.Error));
}
else if (c == '#')
{
errors.Add(new ValidationError(lineNum,
"Macro variable '#' detected — verify macro support on target CNC",
ValidationSeverity.Warning));
}
}
break;
case CncManufacturer.Siemens:
for (int i = 0; i < line.Length; i++)
{
if (line[i] == ';')
errors.Add(new ValidationError(lineNum,
"Forbidden char ';' for Siemens (use () for comments)",
ValidationSeverity.Error));
}
break;
case CncManufacturer.Heidenhain:
for (int i = 0; i < line.Length; i++)
{
char c = line[i];
if (c == '%' || c == ';')
errors.Add(new ValidationError(lineNum,
string.Format("Forbidden char '{0}' for Heidenhain", c),
ValidationSeverity.Error));
}
break;
case CncManufacturer.Fagor:
// Fagor: nessuna restrizione su contenuto programma (ISO + linguaggio alto livello)
break;
}
}
/// Extracts bare filename from a path (strips directory prefix).
private static string ExtractName(string programName)
{
if (string.IsNullOrEmpty(programName)) return programName;
int slash = programName.LastIndexOf('/');
int backslash = programName.LastIndexOf('\\');
int sep = slash > backslash ? slash : backslash;
return sep >= 0 ? programName.Substring(sep + 1) : programName;
}
}
}