/*
* Copyright 2016 Cel Skeggs
*
* This file is part of the CCRE, the Common Chicken Runtime Engine.
*
* The CCRE is free software: you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* The CCRE is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
* details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with the CCRE. If not, see <http://www.gnu.org/licenses/>.
*/
package ccre.recording;
import java.util.List;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
/**
* A class that handles data replaying from an arbitrary InputStream, as an
* approximate mirror to {@link Recorder}.
*
* @author skeggsc
*/
public class Replayer {
private final StreamDecoder decoder;
/**
* A decoded channel, including the name, recorded type, and all decoded
* samples.
*
* @author skeggsc
*/
public static final class ReplayChannel {
/**
* The name of the extracted channel. Note that channel names should not
* be expected to be consistent between different recordings or CCRE
* versions - they may change without warning.
*/
public final String name;
/**
* The type of this recording.
*/
public final Recorder.RawType type;
/**
* The list of samples.
*/
public final ArrayList<ReplaySample> samples = new ArrayList<>();
private ReplayChannel(String name, Recorder.RawType type) {
this.name = name;
this.type = type;
}
private byte getSnapshotType() {
switch (this.type) {
case BOOLEAN:
return RecordSnapshot.T_BYTE;
case EVENT:
return RecordSnapshot.T_NULL;
case FLOAT:
return RecordSnapshot.T_INT;
case OUTPUT_STREAM:
return RecordSnapshot.T_BYTES;
case DISCRETE:
return RecordSnapshot.T_BYTES;
default:
throw new RuntimeException();
}
}
}
/**
* A specific sample from a {@link ReplayChannel}.
*
* @author skeggsc
*/
public static final class ReplaySample {
/**
* The timestamp for when this sample was collected, in units of 10
* microseconds, from the time of the first recorded sample.
*/
public final long timestamp;
/**
* The value in this sample, not decoded.
*/
public final long value;
/**
* The data array included in this sample, if it's the right type for
* that. Possibly null if not.
*/
public final byte[] data;
private ReplaySample(RecordSnapshot snapshot) {
this.timestamp = snapshot.timestamp;
this.value = snapshot.value;
this.data = snapshot.data;
}
}
/**
* Creates a new Replayer from an input stream. The input stream will not be
* read from until you call {@link Replayer#decode()}.
*
* @param in the input stream.
* @throws IOException if the stream is malformed.
*/
public Replayer(InputStream in) throws IOException {
decoder = new StreamDecoder(in);
}
private final HashMap<Integer, ReplayChannel> channels = new HashMap<>();
private final ArrayList<ReplayChannel> allChannels = new ArrayList<>();
/**
* Decodes everything from the input stream, including sorting into
* channels.
*
* @return the list of extracted channels.
* @throws IOException if the stream is malformed.
*/
public List<ReplayChannel> decode() throws IOException {
while (true) {
RecordSnapshot snapshot = decoder.decode(this::extractType);
if (snapshot == null) {
break;
}
if (snapshot.channel == 0) {
processMetaUpdate(snapshot);
} else {
ReplayChannel rc = channels.get(snapshot.channel);
// must not be null because we got the type via extractType
rc.samples.add(new ReplaySample(snapshot));
}
}
return allChannels;
}
private void processMetaUpdate(RecordSnapshot snapshot) throws IOException {
String d = new String(snapshot.data);
if (d.isEmpty()) {
throw new IOException("Invalid meta update of length 0!");
}
if (d.equals("\2ENDOFSTREAM")) {
// we actually don't care in this decoder.
return;
}
if (d.charAt(0) == '\0') {
// init channel
String[] strs = d.split("\0", 4);
int new_channel_number;
try {
new_channel_number = Integer.parseInt(strs[1]);
} catch (NumberFormatException ex) {
throw new IOException("Invalid init channel meta update: invalid channel number format.");
}
Recorder.RawType rt;
try {
rt = Recorder.RawType.valueOf(strs[2]);
} catch (IllegalArgumentException ex) {
throw new IOException("Invalid init channel meta update: raw type name " + strs[2]);
}
String name = strs[3];
if (channels.containsKey(new_channel_number)) {
throw new IOException("Attempt to reinit channel!");
}
ReplayChannel rc = new ReplayChannel(name, rt);
channels.put(new_channel_number, rc);
allChannels.add(rc);
} else if (d.charAt(0) == '\1') {
// destroy channel
int new_channel_number;
try {
new_channel_number = Integer.parseInt(d.substring(1));
} catch (NumberFormatException ex) {
throw new IOException("Invalid init channel meta update: invalid channel number format.");
}
if (channels.remove(new_channel_number) == null) {
throw new IOException("Attempt to deinit nonexistent channel.");
}
} else {
throw new IOException("Invalid meta update with initial byte " + d.charAt(0));
}
}
private Byte extractType(int channel) {
if (channel == 0) {
return RecordSnapshot.T_BYTES;
} else {
ReplayChannel rc = channels.get(channel);
return rc == null ? null : rc.getSnapshotType();
}
}
}