// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.Fonts;
using System;
using System.Collections.Generic;
using System.Linq;
namespace UnicodeTrieGenerator.StateAutomation {
internal class StateMachine
{
private const int InitialState = 1;
private const int FailState = 0;
///
/// Initializes a new instance of the class.
///
/// The state table.
/// The accepting states.
/// The tags.
public StateMachine(int[][] stateTable, bool[] accepting, string[][] tags)
{
this.StateTable = stateTable;
this.Accepting = accepting;
this.Tags = tags;
}
///
/// Gets the state table.
///
public int[][] StateTable { get; }
///
/// Gets the accepting states.
///
public bool[] Accepting { get; }
///
/// Gets the tags.
///
public string[][] Tags { get; }
///
/// Returns an iterable object that yields pattern matches over the input sequence.
///
/// The input sequence.
/// The .
public IEnumerable Match(ReadOnlySpan input)
{
int state = InitialState;
int? startRun = null;
int? lastAccepting = null;
List matches = new(input.Length);
for (int i = 0; i < input.Length; i++)
{
int c = input[i];
int lastState = state;
state = this.StateTable[state][c];
if (state == FailState)
{
// yield the last match if any.
if (startRun != null && lastAccepting != null && lastAccepting >= startRun)
{
matches.Add(new StateMatch()
{
StartIndex = startRun.Value,
EndIndex = lastAccepting.Value,
Tags = this.Tags[lastState]
});
}
// reset the state as if we started over from the initial state
state = this.StateTable[InitialState][c];
startRun = null;
}
// start a run if not in the failure state
if (state != FailState && startRun == null)
{
startRun = i;
}
// if accepting, mark the potential match end
if (this.Accepting[state])
{
lastAccepting = i;
}
// reset the state to the initial state if we get into the failure state
if (state == FailState)
{
state = InitialState;
}
}
// yield the last match if any.
if (startRun != null && lastAccepting != null && lastAccepting >= startRun)
{
matches.Add(new StateMatch()
{
StartIndex = startRun.Value,
EndIndex = lastAccepting.Value,
Tags = this.Tags[state]
});
}
return matches;
}
///
/// For each match over the input sequence, action functions matching
/// the tag definitions in the input pattern are called with the startIndex,
/// length, and the sequence to be sliced.
///
/// The input sequence.
/// The collection of actions.
public void Apply(int[] input, Dictionary>> actions)
{
foreach (StateMatch match in this.Match(input))
{
foreach (string tag in match.Tags)
{
if (actions.TryGetValue(tag, out Action>? action))
{
action(match.StartIndex, match.EndIndex, new ArraySlice(input, match.StartIndex, match.EndIndex + 1 - match.StartIndex));
}
}
}
}
}
internal class StateMatch : IEquatable
{
public int StartIndex { get; set; }
public int EndIndex { get; set; }
public IList Tags { get; set; } = Array.Empty();
public override bool Equals(object? obj) => this.Equals(obj as StateMatch);
public bool Equals(StateMatch? other)
=> other is not null
&& this.StartIndex == other.StartIndex
&& this.EndIndex == other.EndIndex
&& this.Tags.SequenceEqual(other.Tags);
public override int GetHashCode()
=> HashCode.Combine(this.StartIndex, this.EndIndex, this.Tags);
}
}