using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using SabreTools.Library.Data;
namespace SabreTools.Library.IO
{
public class ClrMameProReader : IDisposable
{
///
/// Internal stream reader for inputting
///
private StreamReader sr;
///
/// Contents of the current line, unprocessed
///
public string CurrentLine { get; private set; } = string.Empty;
///
/// Get the current line number
///
public long LineNumber { get; private set; } = 0;
///
/// Get if at end of stream
///
public bool EndOfStream
{
get
{
return sr?.EndOfStream ?? true;
}
}
///
/// Contents of the currently read line as an internal item
///
public Dictionary Internal { get; private set; } = new Dictionary();
///
/// Current internal item name
///
public string InternalName { get; private set; } = null;
///
/// Get if we should be making DosCenter exceptions
///
public bool DosCenter { get; set; } = false;
///
/// Get if quotes should surround attribute values
///
///
/// If this is disabled, then a special bit of code will be
/// invoked to deal with unquoted, multi-part names. This can
/// backfire in a lot of circumstances, so don't disable this
/// unless you know what you're doing
///
public bool Quotes { get; set; } = true;
///
/// Current row type
///
public CmpRowType RowType { get; private set; } = CmpRowType.None;
///
/// Contents of the currently read line as a standalone item
///
public KeyValuePair? Standalone { get; private set; } = null;
///
/// Current top-level being read
///
public string TopLevel { get; private set; } = string.Empty;
///
/// Constructor for opening a write from a file
///
public ClrMameProReader(string filename)
{
sr = new StreamReader(filename);
}
///
/// Constructor for opening a write from a stream and encoding
///
public ClrMameProReader(Stream stream, Encoding encoding)
{
sr = new StreamReader(stream, encoding);
}
///
/// Read the next line in the file
///
public bool ReadNextLine()
{
if (!(sr.BaseStream?.CanRead ?? false) || sr.EndOfStream)
return false;
CurrentLine = sr.ReadLine().Trim();
LineNumber++;
// TODO: Act like IniReader here
ProcessLine(CurrentLine);
return true;
}
///
/// Process the current line and extract out values
///
private void ProcessLine(string line)
{
// Standalone (special case for DC dats)
if (line.StartsWith("Name:"))
{
string temp = line.Substring("Name:".Length).Trim();
line = $"Name: {temp}";
}
// Comment
if (line.StartsWith("#"))
{
Internal = null;
InternalName = null;
RowType = CmpRowType.Comment;
Standalone = null;
}
// Top-level
else if (Regex.IsMatch(line, Constants.HeaderPatternCMP))
{
GroupCollection gc = Regex.Match(line, Constants.HeaderPatternCMP).Groups;
string normalizedValue = gc[1].Value.ToLowerInvariant();
Internal = null;
InternalName = null;
RowType = CmpRowType.TopLevel;
Standalone = null;
TopLevel = normalizedValue;
}
// Internal
else if (Regex.IsMatch(line, Constants.InternalPatternCMP))
{
GroupCollection gc = Regex.Match(line, Constants.InternalPatternCMP).Groups;
string normalizedValue = gc[1].Value.ToLowerInvariant();
string[] linegc = SplitLineAsCMP(gc[2].Value);
Internal = new Dictionary();
for (int i = 0; i < linegc.Length; i++)
{
string key = linegc[i].Replace("\"", string.Empty);
if (string.IsNullOrWhiteSpace(key))
continue;
string value = string.Empty;
// Special case for DC-style dats, only a few known fields
if (DosCenter)
{
// If we have a name
if (key == "name")
{
while (++i < linegc.Length
&& linegc[i] != "size"
&& linegc[i] != "date"
&& linegc[i] != "crc")
{
value += $" {linegc[i]}";
}
value = value.Trim();
i--;
}
// If we have a date (split into 2 parts)
else if (key == "date")
{
value = $"{linegc[++i].Replace("\"", string.Empty)} {linegc[++i].Replace("\"", string.Empty)}";
}
// Default case
else
{
value = linegc[++i].Replace("\"", string.Empty);
}
}
// Special case for assumed unquoted values (only affects `name`)
else if (!Quotes && key == "name")
{
while (++i < linegc.Length
&& linegc[i] != "merge"
&& linegc[i] != "size"
&& linegc[i] != "crc"
&& linegc[i] != "md5"
&& linegc[i] != "sha1")
{
value += $" {linegc[i]}";
}
value = value.Trim();
i--;
}
else
{
// Special cases for standalone statuses
if (key == "baddump" || key == "good" || key == "nodump" || key == "verified")
{
value = key;
key = "status";
}
// Special case for standalone sample
else if (normalizedValue == "sample")
{
value = key;
key = "name";
}
// Default case
else
{
value = linegc[++i].Replace("\"", string.Empty);
}
}
Internal[key] = value;
RowType = CmpRowType.Internal;
Standalone = null;
}
InternalName = normalizedValue;
}
// Standalone
else if (Regex.IsMatch(line, Constants.ItemPatternCMP))
{
GroupCollection gc = Regex.Match(line, Constants.ItemPatternCMP).Groups;
string itemval = gc[2].Value.Replace("\"", string.Empty);
Internal = null;
InternalName = null;
RowType = CmpRowType.Standalone;
Standalone = new KeyValuePair(gc[1].Value, itemval);
}
// End section
else if (Regex.IsMatch(line, Constants.EndPatternCMP))
{
Internal = null;
InternalName = null;
RowType = CmpRowType.EndTopLevel;
Standalone = null;
TopLevel = null;
}
// Invalid (usually whitespace)
else
{
Internal = null;
InternalName = null;
RowType = CmpRowType.None;
Standalone = null;
}
}
///
/// Split a line as if it were a CMP rom line
///
/// Line to split
/// Line split
/// Uses code from http://stackoverflow.com/questions/554013/regular-expression-to-split-on-spaces-unless-in-quotes
private string[] SplitLineAsCMP(string s)
{
// Get the opening and closing brace locations
int openParenLoc = s.IndexOf('(');
int closeParenLoc = s.LastIndexOf(')');
// Now remove anything outside of those braces, including the braces
s = s.Substring(openParenLoc + 1, closeParenLoc - openParenLoc - 1);
s = s.Trim();
// Now we get each string, divided up as cleanly as possible
string[] matches = Regex
.Matches(s, Constants.InternalPatternAttributesCMP)
.Cast()
.Select(m => m.Groups[0].Value)
.ToArray();
return matches;
}
///
/// Dispose of the underlying reader
///
public void Dispose()
{
sr.Dispose();
}
}
}