package net.cakenet.jsaton.script;
import net.cakenet.jsaton.nativedef.WindowManager;
import net.cakenet.jsaton.nativedef.WindowReference;
import net.cakenet.jsaton.script.debug.BreakInformation;
import net.cakenet.jsaton.script.debug.Breakpoint;
import net.cakenet.jsaton.script.debug.DebugBreakListener;
import net.cakenet.jsaton.script.ruby.RubyScript;
import net.cakenet.jsaton.util.CompressionUtil;
import net.cakenet.jsaton.util.TimeUtil;
import javax.script.ScriptException;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.*;
import java.util.LinkedList;
import java.util.List;
import java.util.TreeMap;
public abstract class Script {
public static final int MAGIC = ('N' << 24) | ('U' << 16) | ('B' << 8) | 'S';
private List<PropertyChangeListener> propertyListeners = new LinkedList<>();
private List<DebugBreakListener> debugListeners = new LinkedList<>();
private Thread thread;
private long startTime, suspendStart, suspendAccum;
private ScriptState state = ScriptState.STOPPED;
private String game;
private String name;
private String author;
private String description;
private String script;
private String path;
private boolean debug;
protected BreakInformation breakInfo;
private volatile boolean cleanExit, exception;
private WindowReference target = WindowManager.getDesktop();
public final TreeMap<Integer, Breakpoint> breakpoints = new TreeMap<>();
public final ScriptLanguage language;
protected Script(ScriptLanguage lang) {
name = "untitled";
this.language = lang;
}
public void save(String path) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream daos = new DataOutputStream(baos);
writeString(daos, game);
writeString(daos, name);
writeString(daos, author);
writeString(daos, description);
writeString(daos, script);
daos.writeInt(breakpoints.size());
for (int line : breakpoints.keySet()) {
daos.writeInt(line);
breakpoints.get(line).save(daos);
}
byte[] data = baos.toByteArray();
daos.close();
baos.close();
byte[] compressed = CompressionUtil.deflate(data, true);
FileOutputStream fos = new FileOutputStream(path);
try {
daos = new DataOutputStream(fos);
daos.writeInt(MAGIC);
daos.writeInt(compressed.length);
daos.write(compressed);
daos.close();
this.path = path;
} finally {
fos.close();
}
}
protected void fireDebugBreak(BreakInformation info) {
for (int i = 0; i < debugListeners.size(); i++) {
DebugBreakListener dl = debugListeners.get(i);
if (dl == null)
continue;
dl.onBreak(this, info);
}
}
protected void prepare() {
// Do nothing by default...
}
protected abstract void execute() throws Exception;
public final void start(final boolean debug) throws ScriptException {
if (state != ScriptState.STOPPED)
throw new ScriptException("Cannot have two instances of the same script alive at the same time!");
this.debug = debug;
setState(ScriptState.INITIALISING);
prepare();
thread = new Thread(ScriptSecurityManager.SecureThreadGroup, new Runnable() {
public void run() {
cleanExit = false;
exception = false;
try {
setState(ScriptState.RUNNING);
startTime = System.currentTimeMillis();
execute();
cleanExit = true;
finished();
} catch (Throwable t) {
if (!(t instanceof ThreadDeath)) {
if (!(t.getCause() instanceof ThreadDeath)) {
// Todo: handle exceptions in scripts (for prettier output...)
t.printStackTrace();
exception = true;
}
finished();
}
}
}
});
thread.setPriority(1);
thread.setDaemon(true);
thread.start();
}
public final void stop() {
if (thread == null)
return;
thread.stop();
exception = cleanExit = false;
finished();
}
public final void suspend() {
if (state != ScriptState.RUNNING)
return;
setState(ScriptState.SUSPENDED);
suspendStart = System.currentTimeMillis();
thread.suspend();
}
public final void resume() {
if (state != ScriptState.SUSPENDED)
return;
setState(ScriptState.RUNNING);
thread.resume();
suspendAccum += System.currentTimeMillis() - suspendStart;
}
private void finished() {
thread = null;
System.out.println("Script " + (cleanExit ? "finished in" :
("terminated " + (exception ? "unexpectedly " : "") + "after")) + " " + getElapsedTime());
setState(ScriptState.STOPPED);
startTime = -1;
suspendAccum = 0;
}
public abstract void stepInto();
public abstract void stepOver();
public final void addDebugListener(DebugBreakListener listener) {
debugListeners.add(listener);
}
public final void removeDebugListener(DebugBreakListener listener) {
debugListeners.remove(listener);
}
public final void addPropertyChangeListener(PropertyChangeListener pcl) {
propertyListeners.add(pcl);
}
public final void removePropertyChangeListener(PropertyChangeListener pcl) {
propertyListeners.remove(pcl);
}
public String getGame() {
return game;
}
public void setGame(String game) {
String oldGame = this.game;
this.game = game;
firePropertyChange("game", oldGame, game);
}
public String getName() {
return name;
}
public void setName(String name) {
String oldName = this.name;
this.name = name;
firePropertyChange("name", oldName, name);
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
String oldAuthor = this.author;
this.author = author;
firePropertyChange("author", oldAuthor, author);
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
String oldDesc = this.description;
this.description = description;
firePropertyChange("description", oldDesc, description);
}
public String getPath() {
return path;
}
public String getScript() {
return script;
}
public void setScript(String script) {
String oldScript = this.script;
this.script = script;
firePropertyChange("script", oldScript, script);
}
public WindowReference getTarget() {
return target;
}
public void setTarget(WindowReference target) {
WindowReference oldTarget = this.target;
this.target = target;
firePropertyChange("target", oldTarget, target);
}
public ScriptState getState() {
return state;
}
public boolean isDebug() {
return debug;
}
public BreakInformation getBreakInfo() {
return breakInfo;
}
private String getElapsedTime() {
if (startTime == -1)
return "not running";
return TimeUtil.timeString((int) (System.currentTimeMillis() - (startTime + suspendAccum)));
}
private void setState(ScriptState state) {
ScriptState oldState = this.state;
this.state = state;
firePropertyChange("state", oldState, state);
}
public void fireStateChange() {
firePropertyChange("state", state, state);
}
private void firePropertyChange(String property, Object oldValue, Object value) {
if (propertyListeners.isEmpty())
return;
PropertyChangeEvent pce = new PropertyChangeEvent(this, property, oldValue, value);
for (int i = 0; i < propertyListeners.size(); i++) {
PropertyChangeListener l = propertyListeners.get(i);
if (l == null)
continue;
try {
l.propertyChange(pce);
} catch (Throwable t) {
t.printStackTrace();
}
}
}
private static void writeString(DataOutputStream os, String s) throws IOException {
if (s != null) {
byte[] data = s.getBytes();
os.writeInt(data.length);
os.write(data);
} else
os.writeInt(-1);
}
private static String readString(DataInputStream is) throws IOException {
int len = is.readInt();
if(len == -1)
return null;
byte[] data = new byte[len];
is.readFully(data);
return new String(data);
}
public static Script create(ScriptLanguage lang) {
if(lang == null)
throw new RuntimeException("null");
switch(lang) {
case RUBY:
return new RubyScript();
default:
throw new RuntimeException("No support for " + lang + " in load script...");
}
}
public static Script open(String path) throws IOException {
String ext = path.substring(path.lastIndexOf('.') + 1);
ScriptLanguage lang = null;
for (ScriptLanguage l : ScriptLanguage.values()) {
if (l.extension.equalsIgnoreCase(ext))
lang = l;
}
if (lang == null)
throw new IOException("Invalid script (unknown extension)");
Script script = create(lang);
DataInputStream dis = new DataInputStream(new FileInputStream(path));
try {
if (dis.readInt() != MAGIC)
throw new IOException("Not a valid script");
int compressed_length = dis.readInt();
byte[] data = new byte[compressed_length];
dis.readFully(data);
dis.close();
data = CompressionUtil.inflate(data, true);
dis = new DataInputStream(new ByteArrayInputStream(data));
script.game = readString(dis);
script.name = readString(dis);
script.author = readString(dis);
script.description = readString(dis);
script.script = readString(dis);
int breakpoints = dis.readInt();
for(int i = 0; i < breakpoints; i++) {
int line = dis.readInt();
script.breakpoints.put(line, Breakpoint.read(dis));
}
script.path = path;
return script;
} finally {
dis.close();
}
}
}