/*
    PIP - Platform Independent Primitives
    Binary markup serializator
    Ivan Pelipenko peri4ko@yandex.ru

    This program 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.

    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 Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "pichunkstream.h"

//! \class PIChunkStream pichunkstream.h
//! \details
//! \~english \section PIChunkStream_sec0 Synopsis
//! \~russian \section PIChunkStream_sec0 Краткий обзор
//! \~english
//! This class provides very handly mechanism to store and restore values to and from
//! \a PIByteArray. The main advantage of using this class is that your binary data
//! become independent from order and collection of your values.
//!
//! \~russian
//! Этот класс предоставляет очень удобный механизм для сохранения и извлечения значений
//! в/из \a PIByteArray. Главным плюсом является то, что данные не будут зависеть от порядка
//! и наличия значений.
//!
//! \~english \section PIChunkStream_sec1 Mechanism
//! \~russian \section PIChunkStream_sec1 Механизм
//! \~english
//! %PIChunkStream works with items called "chunk". Chunk is an ID, size and any value that
//! can be stored and restored to/from %PIChunkStream with stream operators << and >>.
//!
//! To construct %PIChunkStream for writing data use any non-default constructor. Empty constructor
//! creates internal buffer that can be accessed by function \a data().
//! Non-empty constructor works with given byte array.
//!
//! To read chunks from byte array use function \a read() that returns ID of
//! readed chunk. Then you can get value of this chunk with functions \a getData() or \a get(),
//! but you should definitely know type of this value. You can read from byte array
//! while \a atEnd() if false.
//!
//! \~russian
//! %PIChunkStream работает с элементами под названием "чанк". Чанк имеет ID, размер и значение,
//! и может быть записан или прочитан в/из %PIChunkStream с помощью операторов << и >>.
//!
//! Для создания потока на запись используется любой не-умолчальный конструктор. Пустой конструктор
//! создает внутренний буфер, который можно получить с помощью метода \a data().
//! Непустой конструктор работает с переданным байтовым массивом.
//!
//! Для чтения чанков из байтового массива используется метод \a read(), который возвращает
//! ID прочитанного чанка. Получить значение этого чанка далее можно с помощью методов \a getData() или get(),
//! но тип значения должен быть известен. Читать из потока можно пока метод \a atEnd() возвращает ложь.
//!
//! \~english \section PIChunkStream_sec2 Examples
//! \~russian \section PIChunkStream_sec2 Пример
//!
//! \~english Using simple operator and cascade serialization:
//! \~russian Использование простого оператора и каскадная сериализация:
//!
//! \~english Prepare your structs to work with %PIChunkStream:
//! \~russian Подготовка своей структуры для работы с %PIChunkStream:
//! \~\snippet pichunkstream.cpp struct
//! \~english Old-style writing to %PIChunkStream:
//! \~russian Старый стиль использования %PIChunkStream:
//! \~\snippet pichunkstream.cpp write
//! \~english Fastest reading from %PIChunkStream:
//! \~russian Самое быстрое чтение из %PIChunkStream:
//! \~\snippet pichunkstream.cpp read
//!
//! \~english And next code show how to serialize your struct with %PIChunkStream:
//! \~russian Следующий код показывает, как сериализовать свою структуру в %PIChunkStream:
//! \~\snippet pichunkstream.cpp write_new
//!
//! \~english ... and deserialize:
//! \~russian ... и десериализовать:
//! \~\snippet pichunkstream.cpp read_new
//!


void PIChunkStream::setSource(const PIByteArray & data) {
	data_ = const_cast<PIByteArray *>(&data);
	_init();
}


void PIChunkStream::setSource(PIByteArray * data) {
	data_ = (data ? data : &tmp_data);
	_init();
}


PIByteArray PIChunkStream::data() const {
	if (first_byte_taken) {
		PIByteArray ret(*data_);
		ret.prepend((uchar)(0x80 | version_));
		return ret;
	}
	return *data_;
}


int PIChunkStream::read() {
	switch (version_) {
	case Version_1: (*data_) >> last_id >> last_data; break;
	case Version_2:
		last_id = readVInt(*data_);
		last_data.resize(readVInt(*data_));
		// piCout << last_id << last_data.size();
		(*data_) >> PIMemoryBlock(last_data.data(), last_data.size_s());
		break;
	default: break;
	}
	return last_id;
}


int PIChunkStream::peekVInt(Version version_, uchar * data_, int sz, uint & ret) {
	switch (version_) {
	case Version_1: memcpy(&ret, data_, 4); return 4;
	case Version_2: {
		PIByteArray hdr(data_, piMini(5, sz));
		hdr.resize(5);
		uchar hsz = 0;
		ret       = readVInt(hdr, &hsz);
		return hsz;
	}
	default: break;
	}
	return 0;
}


void PIChunkStream::replaceChunk(int id, const PIByteArray & v) {
	if (!data_map.contains(id)) return;
	auto & pos(data_map[id]);
	PIByteArray nsba;
	writeVInt(nsba, v.size());
	int size_mod = (v.size_s() + nsba.size_s()) - (pos.length + pos.size_bytes);
	pos.length   = v.size_s();
	if (size_mod != 0) {
		auto it = data_map.makeIterator();
		while (it.next()) {
			if (it.value().start > pos.start) {
				it.value().start += size_mod;
			}
		}
		if (size_mod > 0) {
			data_->insert(pos.start, PIByteArray(size_mod));
		} else {
			data_->remove(pos.start, -size_mod);
		}
	}
	memcpy(data_->data(pos.start - pos.size_bytes), nsba.data(), nsba.size());
	pos.start += nsba.size_s() - pos.size_bytes;
	memcpy(data_->data(pos.start), v.data(), pos.length);
}


void PIChunkStream::readAll() {
	data_map.clear();
	if (!data_) return;
	int pos = 0, sz = data_->size_s(), hsz = 0;
	uint csz = 0, cid = 0;
	while (pos < sz) {
		pos += peekVInt((Version)version_, data_->data(pos), data_->size_s() - pos, cid);
		hsz = peekVInt((Version)version_, data_->data(pos), data_->size_s() - pos, csz);
		pos += hsz;
		data_map[cid] = CacheEntry(pos, csz, hsz);
		pos += csz;
	}
}


PIChunkStream::~PIChunkStream() {}


void PIChunkStream::_init() {
	first_byte_taken = false;
	last_id          = -1;
	last_data.clear();
	if (!data_->isEmpty()) {
		uchar v = data_->at(0);
		if ((v & 0x80) == 0x80) {
			v &= 0x7f;
			switch (v) {
			case 2:
				version_ = (uchar)Version_2;
				data_->pop_front();
				first_byte_taken = true;
				break;
			default: version_ = Version_1; break;
			}
		} else {
			version_ = Version_1;
		}
	}
}


uint PIChunkStream::readVInt(PIByteArray & s, uchar * bytes_cnt) {
	if (s.isEmpty()) return 0;
	uchar bytes[4];
	uchar abc;
	s >> bytes[0];
	if (bytes[0] == 0xf0) {
		abc = 3;
		for (int i = 0; i < 4; ++i)
			s >> bytes[i];
		if (bytes_cnt) *bytes_cnt = 5;
	} else {
		for (abc = 0; abc < 3; ++abc) {
			uchar mask = (0x80 >> abc);
			if ((bytes[0] & mask) == mask) {
				bytes[0] &= (mask - 1);
				s >> bytes[abc + 1];
			} else
				break;
		}
		if (bytes_cnt) *bytes_cnt = (abc + 1);
	}
	uint ret = 0;
	for (int i = 0; i <= abc; ++i) {
		ret += (bytes[i] << (8 * ((int)abc - i)));
	}
	return ret;
}


void PIChunkStream::writeVInt(PIByteArray & s, uint val) {
	if (val <= 0x7f) {
		s << uchar(val);
	} else if (val <= 0x3fff) {
		s << uchar((val >> 8) | 0x80) << uchar(val & 0xff);
	} else if (val <= 0x1fffff) {
		s << uchar((val >> 16) | 0xc0) << uchar((val >> 8) & 0xff) << uchar(val & 0xff);
	} else if (val <= 0xfffffff) {
		s << uchar((val >> 24) | 0xe0) << uchar((val >> 16) & 0xff) << uchar((val >> 8) & 0xff) << uchar(val & 0xff);
	} else {
		s << uchar(0xf0) << uchar((val >> 24) & 0xff) << uchar((val >> 16) & 0xff) << uchar((val >> 8) & 0xff) << uchar(val & 0xff);
	}
}
