/*
 * scribiu -- read notebooks and voice memos from Livescribe pens
 * Copyright (C) 2015 Javier S. Pedro <javier@javispedro.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/>.
 */

#include <QtCore/QDebug>
#include <QtCore/QDataStream>
#include <QtCore/QDirIterator>
#include "smartpen.h"
#include "paperreplay.h"

namespace
{
bool readUtfString(QDataStream &stream, QString &s)
{
	quint16 len;
	stream >> len;
	QByteArray buffer(len, Qt::Uninitialized);
	if (stream.readRawData(buffer.data(), len) != len) {
		return false;
	}
	s = QString::fromUtf8(buffer);
	return true;
}

QString findPenSerial(const QString &path)
{
	QDir dir(path + "/userdata");
	QStringList entries = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
	if (entries.isEmpty()) {
		return QString();
	} else {
		return entries.first();
	}
}
}

PaperReplay::PaperReplay(QObject *parent) :
    QObject(parent)
{
}

PaperReplay::Session::Session() : d()
{
}

PaperReplay::Session::Session(quint64 id) : d(new SessionData)
{
	d->id = id;
}

PaperReplay::Session::~Session()
{
}

bool PaperReplay::Session::isValid() const
{
	return d;
}

quint64 PaperReplay::Session::id() const
{
	return d ? d->id : 0;
}

QString PaperReplay::Session::name() const
{
	return d->name;
}

QString PaperReplay::Session::fileName() const
{
	return d->file;
}

qint64 PaperReplay::Session::startTime() const
{
	return d->start;
}

qint64 PaperReplay::Session::endTime() const
{
	return d->end;
}

bool PaperReplay::Session::startTimeLess(const Session &a, const Session &b)
{
	return a.d->start < b.d->start;
}

PaperReplay::SessionList::SessionList()
{
}

PaperReplay::SessionList::SessionList(const QMap<qint64, Session> &byTime)
    : _m(byTime)
{
}

QList<PaperReplay::Session> PaperReplay::SessionList::sessionsDuringTime(qint64 time) const
{
	QList<Session> sessions;
	if (_m.isEmpty()) return sessions;
	QMap<qint64, Session>::const_iterator it = _m.lowerBound(time);

	if (it == _m.end()) --it;

	while (it->d->start <= time && time <= it->d->end) {
		sessions.append(*it);
		if (it == _m.begin()) {
			break;
		} else {
			--it;
		}
	}
	return sessions;
}

bool PaperReplay::open(const QString &path, quint64 notebookGuid)
{
	QString penSerial = findPenSerial(path);
	if (penSerial.isEmpty()) {
		qDebug() << "Cannot open paper replay:" << path << "does not contain any pen data";
		return false;
	}

	_dir.setPath(path + QString("/userdata/%1/Paper Replay/99/%2/sessions")
	             .arg(penSerial)
	             .arg(qulonglong(notebookGuid), notebookGuid == 0 ? 1 : 16, 16, QLatin1Char('0')));
	if (!_dir.exists()) {
		qDebug() << "Cannot open paper replay:" << _dir.absolutePath() << "does not exist";
		return false;
	}

	QDirIterator iter(_dir.path(), QStringList("PRS-*"), QDir::Dirs | QDir::NoDotAndDotDot);
	while (iter.hasNext()) {
		bool ok;
		QDir sessionDir(iter.next());
		quint64 sessionId = sessionDir.dirName().mid(4).toULongLong(&ok, 16);

		if (!ok) {
			qWarning() << "Invalid session identifier:" << sessionDir.dirName();
			continue;
		}

		Session session(sessionId);
		if (!parseSessionInfo(session.d, sessionDir.filePath("session.info"))) {
			qWarning() << "Could not parse:" << sessionDir.absoluteFilePath("session.info");
		}
		if (sessionDir.exists("session.pages")) {
			if (!parseSessionPages(session.d, sessionDir.filePath("session.pages"))) {
				qWarning() << "Could not parse:" << sessionDir.absoluteFilePath("session.pages");
			}
		}

		if (!session.d->file.isEmpty()) {
			session.d->file = sessionDir.filePath(session.d->file);
		}

		_sessions.insert(sessionId, session);

		foreach (quint64 page, session.d->pages) {
			_byPageTime[page].insert(session.d->start, session);
		}
	}
	return true;
}

void PaperReplay::close()
{
	_byPageTime.clear();
	_sessions.clear();
	_dir.setPath(QString());
}

QList<PaperReplay::Session> PaperReplay::sessions() const
{
	return _sessions.values();
}

PaperReplay::SessionList PaperReplay::sessions(quint64 pageAddress) const
{
	return SessionList(_byPageTime[pageAddress]);
}

bool PaperReplay::parseSessionInfo(SessionData *session, const QString &path)
{
	QFile f(path);
	if (f.open(QIODevice::ReadOnly)) {
		return parseSessionInfo(session, &f);
	} else {
		return false;
	}
}

bool PaperReplay::parseSessionInfo(SessionData *session, QIODevice *dev)
{
	unsigned char magic[2];
	if (dev->read(reinterpret_cast<char*>(magic), 2) != 2 ||
	        magic[0] != 0xFA || magic[1] != 0xCE) {
		qWarning() << "Invalid magic";
		return false;
	}

	char version = 0;
	if (!dev->getChar(&version)) {
		qWarning() << "Short read while getting version number";
		return false;
	}

	switch (version) {
	case 3:
		return parseSessionInfoV3(session, dev);
	default:
		qWarning() << "Unknown version:" << version;
		return false;
	}
}

bool PaperReplay::parseSessionInfoV3(SessionData *session, QIODevice *dev)
{
	QDataStream s(dev);
	if (s.skipRawData(5) != 5) return false;

	qint64 startTime, endTime, creationTime;
	QString name;

	s >> startTime >> endTime >> creationTime;
	if (!readUtfString(s, name)) {
		return false;
	}

	session->name = name;
	session->start = startTime;
	session->end = endTime;

	qDebug() << "Session:" << name << Smartpen::fromPenTime(session->start) << Smartpen::fromPenTime(session->end);

	quint16 num_clips;
	s >> num_clips;

	Q_ASSERT(num_clips == 1); // TODO: We do not yet know how to handle this scenario

	for (uint i = 0; i < num_clips; ++i) {
		QString file;
		if (!readUtfString(s, file)) {
			return false;
		}
		s >> startTime >> endTime;

		qDebug() << " Clip:" << file << Smartpen::fromPenTime(startTime) << Smartpen::fromPenTime(endTime);
		session->file = file;
	}

	quint16 num_strokes;
	s >> num_strokes;

	// TODO:
	for (uint i = 0; i < num_strokes; ++i) {
		quint64 a, b, c;
		quint32 d;
		QString nbGuid;
		quint8 f, g;
		s >> a >> b >> c >> d;
		if (!readUtfString(s, nbGuid)) {
			return false;
		}
		s >> f >> g;
		qDebug() << " Stroke:" << a << b << c << d << nbGuid << f << g;
	}

	return true;
}

bool PaperReplay::parseSessionPages(SessionData *session, const QString &path)
{
	QFile f(path);
	if (f.open(QIODevice::ReadOnly)) {
		return parseSessionPages(session, &f);
	} else {
		return false;
	}
}

bool PaperReplay::parseSessionPages(SessionData *session, QIODevice *dev)
{
	unsigned char magic[2];
	if (dev->read(reinterpret_cast<char*>(magic), 2) != 2 ||
	        magic[0] != 0xCA || magic[1] != 0xBE) {
		qWarning() << "Invalid magic";
		return false;
	}

	char version = 0;
	if (!dev->getChar(&version)) {
		qWarning() << "Short read while getting version number";
		return false;
	}

	switch (version) {
	case 1:
		return parseSessionPagesV1(session, dev);
	default:
		qWarning() << "Unknown version:" << version;
		return false;
	}
}

bool PaperReplay::parseSessionPagesV1(SessionData *session, QIODevice *dev)
{
	QDataStream s(dev);
	if (s.skipRawData(1) != 1) return false;

	quint16 num_pages = 0;
	s >> num_pages;
	session->pages.reserve(session->pages.size() + num_pages);

	for (uint i = 0; i < num_pages; ++i) {
		quint64 address;
		qint64 time;
		s >> address >> time;

		session->pages.append(address);
		qDebug() << " Page:" << address << time << Smartpen::fromPenTime(time);
	}

	return true;
}