/*
DroidFish - An Android chess program.
Copyright (C) 2011-2013 Peter Ă–sterlund, peterosterlund2@gmail.com
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.petero.droidfish.activities;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import org.petero.droidfish.R;
import org.petero.droidfish.gamelogic.Pair;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.widget.Toast;
public class PGNFile {
private final File fileName;
public PGNFile(String fileName) {
this.fileName = new File(fileName);
}
public final String getName() {
return fileName.getAbsolutePath();
}
public static final class GameInfo {
public String info = "";
public long startPos;
public long endPos;
final GameInfo setNull(long currPos) {
info = null;
startPos = currPos;
endPos = currPos;
return this;
}
final boolean isNull() { return info == null; }
public String toString() {
if (info == null)
return "--";
return info;
}
}
private final static class HeaderInfo {
int gameNo;
String event = "";
String site = "";
String date = "";
String round = "";
String white = "";
String black = "";
String result = "";
HeaderInfo(int gameNo) {
this.gameNo = gameNo;
}
public String toString() {
StringBuilder info = new StringBuilder(128);
info.append(gameNo);
info.append(". ");
info.append(white);
info.append(" - ");
info.append(black);
if (date.length() > 0) {
info.append(' ');
info.append(date);
}
if (round.length() > 0) {
info.append(' ');
info.append(round);
}
if (event.length() > 0) {
info.append(' ');
info.append(event);
}
if (site.length() > 0) {
info.append(' ');
info.append(site);
}
info.append(' ');
info.append(result);
return info.toString();
}
}
public static enum GameInfoResult {
OK,
CANCEL,
NOT_PGN,
OUT_OF_MEMORY;
}
private static class BytesToString {
private byte[] buf = new byte[256];
private int len = 0;
public void write(int c) {
if (len < 256)
buf[len++] = (byte)c;
}
public void reset() {
len = 0;
}
public String toString() {
return new String(buf, 0, len);
}
}
private static class BufferedInput {
private byte buf[] = new byte[8192];
private int bufLen = 0;
private int pos = 0;
private InputStream is;
public BufferedInput(InputStream is) {
this.is = is;
}
public int read() throws IOException {
if (pos >= bufLen) {
int len = is.read(buf);
if (len <= 0)
return -1;
pos = 0;
bufLen = len;
}
return buf[pos++] & 0xff;
}
public void close() {
try {
is.close();
} catch (IOException ex) {
}
}
}
/** Return info about all PGN games in a file. */
public final Pair<GameInfoResult,ArrayList<GameInfo>> getGameInfo(Activity activity,
final ProgressDialog progress) {
return getGameInfo(activity, progress, -1);
}
/** Return info about all PGN games in a file. */
public final Pair<GameInfoResult,ArrayList<GameInfo>> getGameInfo(Activity activity,
final ProgressDialog progress,
int maxGames) {
ArrayList<GameInfo> gamesInFile = new ArrayList<GameInfo>();
gamesInFile.clear();
long fileLen = 0;
BufferedInput f = null;
try {
int percent = -1;
{
RandomAccessFile raf = new RandomAccessFile(fileName, "r");
fileLen = raf.length();
raf.close();
}
f = new BufferedInput(new FileInputStream(fileName));
GameInfo gi = null;
HeaderInfo hi = null;
boolean inHeader = false;
boolean inHeaderSection = false;
long filePos = 0;
long nRead = 0;
int gameNo = 1;
final int INITIAL = 0;
final int NORMAL = 1;
final int BRACE_COMMENT = 2;
final int LINE_COMMENT = 3;
final int STRING = 4;
final int STRING_ESCAPE = 5;
final int HEADER = 6;
final int HEADER_SYMBOL = 7;
final int EOF = 8;
int state = INITIAL;
boolean firstColumn = true;
BytesToString lastSymbol = new BytesToString();
BytesToString lastString = new BytesToString();
while (state != EOF) {
filePos = nRead;
int c = f.read();
nRead++;
if (c == -1) {
state = EOF;
continue;
}
if (firstColumn) { // Handle % escape mechanism
if (c == '%') {
state = LINE_COMMENT;
continue;
}
}
firstColumn = (c == '\n' || c == '\r');
switch (state) {
case BRACE_COMMENT:
if (c == '}')
state = NORMAL;
break;
case LINE_COMMENT:
if (c == '\n' || c == '\r')
state = NORMAL;
break;
case STRING:
if (c == '"')
state = NORMAL;
else if (c == '\\')
state = STRING_ESCAPE;
else
lastString.write(c);
break;
case STRING_ESCAPE:
lastString.write(c);
state = STRING;
break;
case HEADER_SYMBOL:
switch (c) {
case '"':
state = STRING;
lastString.reset();
break;
case ' ': case '\n': case '\r': case '\t': case 160: case ']':
state = NORMAL;
break;
default:
lastSymbol.write(c);
break;
}
break;
case HEADER:
case INITIAL:
case NORMAL:
switch (c) {
case -1:
state = EOF;
break;
case '[':
state = HEADER;
inHeader = true;
break;
case ']':
if (inHeader) {
inHeader = false;
String tag = lastSymbol.toString();
String value = lastString.toString();
if ("Event".equals(tag)) {
hi.event = value.equals("?") ? "" : value;
} else if ("Site".equals(tag)) {
hi.site = value.equals("?") ? "" : value;
} else if ("Date".equals(tag)) {
hi.date = value.equals("?") ? "" : value;
} else if ("Round".equals(tag)) {
hi.round = value.equals("?") ? "" : value;
} else if ("White".equals(tag)) {
hi.white = value;
} else if ("Black".equals(tag)) {
hi.black = value;
} else if ("Result".equals(tag)) {
if (value.equals("1-0")) hi.result = "1-0";
else if (value.equals("0-1")) hi.result = "0-1";
else if ((value.equals("1/2-1/2")) || (value.equals("1/2"))) hi.result = "1/2-1/2";
else hi.result = "*";
}
}
state = NORMAL;
break;
case '.':
case '*':
case '(':
case ')':
case '$':
inHeaderSection = false;
break;
case '{':
state = BRACE_COMMENT;
inHeaderSection = false;
break;
case ';':
state = LINE_COMMENT;
inHeaderSection = false;
break;
case '"':
state = STRING;
lastString.reset();
break;
case ' ': case '\n': case '\r': case '\t': case 160:
break;
default:
if (inHeader) {
state = HEADER_SYMBOL;
lastSymbol.reset();
lastSymbol.write(c);
} else {
inHeaderSection = false;
}
break;
}
}
if (state == HEADER) {
if (!inHeaderSection) { // Start of game
inHeaderSection = true;
if (gi != null) {
gi.endPos = filePos;
gi.info = hi.toString();
gamesInFile.add(gi);
if ((maxGames > 0) && gamesInFile.size() >= maxGames) {
gi = null;
break;
}
final int newPercent = (int)(filePos * 100 / fileLen);
if (newPercent > percent) {
percent = newPercent;
if (progress != null) {
activity.runOnUiThread(new Runnable() {
public void run() {
progress.setProgress(newPercent);
}
});
}
}
if (Thread.currentThread().isInterrupted())
return new Pair<GameInfoResult,ArrayList<GameInfo>>(GameInfoResult.CANCEL, null);
}
gi = new GameInfo();
gi.startPos = filePos;
gi.endPos = -1;
hi = new HeaderInfo(gameNo++);
}
}
}
if (gi != null) {
gi.endPos = filePos;
gi.info = hi.toString();
gamesInFile.add(gi);
}
} catch (IOException e) {
} catch (OutOfMemoryError e) {
gamesInFile.clear();
gamesInFile = null;
return new Pair<GameInfoResult,ArrayList<GameInfo>>(GameInfoResult.OUT_OF_MEMORY, null);
} finally {
if (f != null)
f.close();
}
if ((gamesInFile.size() == 0) && (fileLen > 0))
return new Pair<GameInfoResult,ArrayList<GameInfo>>(GameInfoResult.NOT_PGN, null);
return new Pair<GameInfoResult,ArrayList<GameInfo>>(GameInfoResult.OK, gamesInFile);
}
private final void mkDirs() {
File dirFile = fileName.getParentFile();
dirFile.mkdirs();
}
/** Read one game defined by gi. Return null on failure. */
final String readOneGame(GameInfo gi) {
try {
RandomAccessFile f = new RandomAccessFile(fileName, "r");
byte[] pgnData = new byte[(int) (gi.endPos - gi.startPos)];
f.seek(gi.startPos);
f.readFully(pgnData);
f.close();
return new String(pgnData);
} catch (IOException e) {
}
return null;
}
/** Append PGN to the end of this PGN file. */
public final void appendPGN(String pgn, Context context) {
try {
mkDirs();
FileWriter fw = new FileWriter(fileName, true);
fw.write(pgn);
fw.close();
Toast.makeText(context, R.string.game_saved, Toast.LENGTH_SHORT).show();
} catch (IOException e) {
if (context != null)
Toast.makeText(context, R.string.failed_to_save_game, Toast.LENGTH_SHORT).show();
}
}
final boolean deleteGame(GameInfo gi, Context context, ArrayList<GameInfo> gamesInFile) {
try {
File tmpFile = new File(fileName + ".tmp_delete");
RandomAccessFile fileReader = new RandomAccessFile(fileName, "r");
RandomAccessFile fileWriter = new RandomAccessFile(tmpFile, "rw");
copyData(fileReader, fileWriter, gi.startPos);
fileReader.seek(gi.endPos);
copyData(fileReader, fileWriter, fileReader.length() - gi.endPos);
fileReader.close();
fileWriter.close();
if (!tmpFile.renameTo(fileName))
throw new IOException();
// Update gamesInFile
if (gamesInFile != null) {
gamesInFile.remove(gi);
final int nGames = gamesInFile.size();
final long delta = gi.endPos - gi.startPos;
for (int i = 0; i < nGames; i++) {
GameInfo tmpGi = gamesInFile.get(i);
if (tmpGi.startPos > gi.startPos) {
tmpGi.startPos -= delta;
tmpGi.endPos -= delta;
}
}
}
return true;
} catch (IOException e) {
if (context != null)
Toast.makeText(context, R.string.failed_to_delete_game, Toast.LENGTH_SHORT).show();
}
return false;
}
final boolean replacePGN(String pgnToSave, GameInfo gi, Context context) {
try {
File tmpFile = new File(fileName + ".tmp_delete");
RandomAccessFile fileReader = new RandomAccessFile(fileName, "r");
RandomAccessFile fileWriter = new RandomAccessFile(tmpFile, "rw");
copyData(fileReader, fileWriter, gi.startPos);
fileWriter.write(pgnToSave.getBytes());
fileReader.seek(gi.endPos);
copyData(fileReader, fileWriter, fileReader.length() - gi.endPos);
fileReader.close();
fileWriter.close();
if (!tmpFile.renameTo(fileName))
throw new IOException();
Toast.makeText(context, R.string.game_saved, Toast.LENGTH_SHORT).show();
return true;
} catch (IOException e) {
if (context != null)
Toast.makeText(context, R.string.failed_to_save_game, Toast.LENGTH_SHORT).show();
}
return false;
}
private final static void copyData(RandomAccessFile fileReader,
RandomAccessFile fileWriter,
long nBytes) throws IOException {
byte[] buffer = new byte[8192];
while (nBytes > 0) {
int nRead = fileReader.read(buffer, 0, Math.min(buffer.length, (int)nBytes));
if (nRead > 0) {
fileWriter.write(buffer, 0, nRead);
nBytes -= nRead;
}
}
}
final boolean delete() {
return fileName.delete();
}
}