using MultiTerm.Core.Types;
using System.Collections.ObjectModel;
using System.Text;
namespace MultiTerm.Core.Model;
///
/// A string that contains symbols of multiple formats such as a standard character as well as hexadecimal or binary parts.
///
public class MultiFormatString: ObservableCollection
{
private const int charsPerHexBundle = 2;
private const int charsPerBinBundle = 8;
#region Insertion / Removal of Characters
///
/// Inserts item at given index after checking if its valid using .
///
/// index where to add item, usually items.Count when using Add method.
/// item to insert
protected override void InsertItem(int index, IFormattedCharacter item)
{
int offset = 0;
bool insertSpacingAfter = false;
if(item is FormattedCharacter formattedChar)
{
//check if valid, only insert then
if(ValidateValue(formattedChar.Format, formattedChar.Character) == false)
{
return;
}
// check if spacing is required and insert
if(IsSpacingCharacterRequiredBeforeNewCharacter(this, index + offset, formattedChar))
{
base.InsertItem(index, new SpacingCharacter());
offset++; // insert item with offset of 1
}
if(IsSpacingCharacterRequiredAfterNewCharacter(this, index + offset, formattedChar))
{
insertSpacingAfter = true;
}
}
base.InsertItem(index + offset, item);
if (insertSpacingAfter)
{
base.InsertItem(index + offset + 1, new SpacingCharacter());
}
}
protected override void RemoveItem(int index)
{
// if formatted character => just remove
base.RemoveItem(index);
// if previous item is a spacing character => remove it too
if(this.ElementAt(index-1) is SpacingCharacter)
{
base.RemoveAt(index - 1);
}
}
private static bool IsSpacingCharacterRequiredBeforeNewCharacter(IEnumerable collection, int indexOfInsertion, IFormattedCharacter newCharacter)
{
bool spacingCharRequired = false;
// no spacing required if new character is a spacing character
if (newCharacter is SpacingCharacter) { return false; }
// no handling implemented for items that are not a FormattedCharacter
if (newCharacter is not FormattedCharacter newFormattedCharacter)
{ throw new NotImplementedException($"'{nameof(IsSpacingCharacterRequiredBeforeNewCharacter)}()' does not implement handling for type {newCharacter.GetType()}"); }
// switch spacing required by type of format
spacingCharRequired = newFormattedCharacter.Format switch
{
FormatType.Character => CheckSpacingRequiredBecauseChangedFormat(collection, indexOfInsertion, newFormattedCharacter.Format),
FormatType.Hexadecimal => CheckSpacingRequiredBecauseChangedFormat(collection, indexOfInsertion, newFormattedCharacter.Format) ||
CheckSpacingRequiredBecauseBundleMaxCountReached(collection, indexOfInsertion, charsPerHexBundle, newFormattedCharacter.Format),
FormatType.Binary => CheckSpacingRequiredBecauseChangedFormat(collection, indexOfInsertion, newFormattedCharacter.Format) ||
CheckSpacingRequiredBecauseBundleMaxCountReached(collection, indexOfInsertion, charsPerBinBundle, newFormattedCharacter.Format),
_ => throw new NotImplementedException($"'{nameof(IsSpacingCharacterRequiredBeforeNewCharacter)}()' does not implement handling for format {newFormattedCharacter.Format}"),
};
return spacingCharRequired;
static bool CheckSpacingRequiredBecauseChangedFormat(IEnumerable collection, int indexOfInsertion, FormatType newCharFormat)
{
// check if previous item is of other type
if (collection.Any())
{
int count = 1;
FormattedCharacter? prevChar = null;
while (prevChar is null)
{
try
{
prevChar = collection.ElementAt(indexOfInsertion - count++) as FormattedCharacter;
// format of previous is not equal to new => spacer required
if (prevChar != null && prevChar.Format != newCharFormat)
{
return true;
}
}
catch { break; } // end of collection => break while
}
}
return false;
}
static bool CheckSpacingRequiredBecauseBundleMaxCountReached(IEnumerable collection, int indexOfInsertion, int charsPerBundle, FormatType newCharFormat)
{
// spacing required if previous x characters are of same format
if (collection.Count() >= charsPerBundle)
{
try
{
// get last x elements of collection
var lastXElements = collection.Take(new Range(new Index(indexOfInsertion - charsPerBundle), new Index(indexOfInsertion)));
// get elements of last x elements where the same format is used
var sameFormatElements = lastXElements.Where(x => x is FormattedCharacter formChar && formChar.Format == newCharFormat);
// elements of same Format are more (impossible) or equal to amount required for bundle
if (sameFormatElements.Count() >= charsPerBundle)
{
return true;
}
}
catch { } // catch any exception, ignore it and return default
}
return false;
}
}
private static bool IsSpacingCharacterRequiredAfterNewCharacter(IEnumerable collection, int indexOfInsertion, IFormattedCharacter newCharacter)
{
bool spacingCharRequired = false;
// no spacing required if new character is a spacing character
if (newCharacter is SpacingCharacter) { return false; }
// no handling implemented for items that are not a FormattedCharacter
if (newCharacter is not FormattedCharacter newFormattedCharacter)
{ throw new NotImplementedException($"'{nameof(IsSpacingCharacterRequiredAfterNewCharacter)}()' does not implement handling for type {newCharacter.GetType()}"); }
// if there is anything in the collection
if (collection.Any())
{
// check if next character in collection is a FormattedCharacter
try
{
var nextChar = collection.ElementAt(indexOfInsertion) as FormattedCharacter;
// format of next is not equal to new => spacer required
if (nextChar != null && nextChar.Format != newFormattedCharacter.Format)
{
return true;
}
}
catch { } // catch any exception and return default result
}
return spacingCharRequired;
}
#endregion
///
/// Validates wether a certain is valid for the given .
///
/// true if valid
public static bool ValidateValue(FormatType format, string value)
{
// invalid if more than one character
if(value.Length > 1)
{ return false; }
// extract character
var character = value.First();
switch (format)
{
case FormatType.Character:
// accept only ascii, not extended ascii or unicode
return char.IsAscii(character);
case FormatType.Hexadecimal:
return (character >= '0' && character <= '9' ||
character >= 'A' && character <= 'F' ||
character >= 'a' && character <= 'f');
case FormatType.Binary:
return (character == '0' || character == '1');
default:
throw new NotImplementedException($"'{nameof(ValidateValue)}()' does not implement validation for format {format}");
}
}
#region ToString
///
/// Converts the to a string that only contains Unicode (UTF-16) characters.
///
/// converted string
public override string ToString() // TODO Implement Unit Tests!
{
StringBuilder stringBuilder = new();
string hexConversionCharacters = String.Empty, binaryConversionCharacters = String.Empty;
// Internal function to check and finalize a Hexadecimal Conversion
// parameter == null will force ending
void finalizeHexConversion(FormattedCharacter? formattedChar)
{
// hex conversion ongoing?
if (String.IsNullOrEmpty(hexConversionCharacters) == false)
{
// ending conversion or reached limit of characters?
if (formattedChar == null ||
formattedChar.Format != FormatType.Hexadecimal ||
hexConversionCharacters.Length == charsPerHexBundle)
{
// finalize conversion
stringBuilder.Append(FromHexString(hexConversionCharacters));
// reset ongoing conversion
hexConversionCharacters = String.Empty;
}
}
}
// Internal function to check and finalize a Binary Conversion
// parameter == null will force ending
void finalizeBinaryConversion(FormattedCharacter? formattedChar)
{
// binary conversion ongoing?
if (String.IsNullOrEmpty(binaryConversionCharacters) == false)
{
// ending conversion or reached limit of characters?
if (formattedChar == null ||
formattedChar.Format != FormatType.Binary ||
binaryConversionCharacters.Length == charsPerBinBundle)
{
// finalize conversion
stringBuilder.Append(FromBinaryString(binaryConversionCharacters));
// reset ongoing conversion
binaryConversionCharacters = String.Empty;
}
}
}
// go through every character
foreach (var character in this.Items)
{
// can only handle FormattedCharacters
if (character is not FormattedCharacter formattedCharacter) { continue; }
// finalize ongoing conversions if there are any
finalizeHexConversion(formattedCharacter);
finalizeBinaryConversion(formattedCharacter);
switch (formattedCharacter.Format) // switch by format
{
case FormatType.Character:
stringBuilder.Append(formattedCharacter.Character);
break;
case FormatType.Hexadecimal:
hexConversionCharacters += formattedCharacter.Character;
break;
case FormatType.Binary:
binaryConversionCharacters += formattedCharacter.Character;
break;
default:
throw new NotImplementedException($"'{nameof(ToString)}()' does not implement conversion for format {formattedCharacter.Format}");
}
}
// fully finalize after the loop, finish all unfinished conversions
finalizeHexConversion(null);
finalizeBinaryConversion(null);
return stringBuilder.ToString();
}
// from: https://stackoverflow.com/questions/724862/converting-from-hex-to-string
// test with: returns: "Hello world" for "48656C6C6F20776F726C64"
// see https://www.fileformat.info/info/charset/UTF-16/list.htm
private static string FromHexString(string hexString)
{
// Added PadLeft so strings with one character do not get ignored
string internalHexString = hexString.PadLeft(charsPerHexBundle, '0');
var bytes = new byte[internalHexString.Length / 2];
for (var i = 0; i < bytes.Length; i++)
{
bytes[i] = Convert.ToByte(internalHexString.Substring(i * 2, 2), 16);
}
return Encoding.ASCII.GetString(bytes);
}
private static string FromBinaryString(string binaryString)
{
string internalBinaryString = binaryString.PadLeft(charsPerBinBundle, '0');
var bytes = new byte[internalBinaryString.Length / 8];
for (var i = 0; i < bytes.Length; i++)
{
bytes[i] = Convert.ToByte(internalBinaryString.Substring(i * 8, 8), 2);
}
return Encoding.ASCII.GetString(bytes);
}
#endregion
}