/* Copyright 2020, Michele Santullo
 * This file is part of orotool.
 *
 * Orotool 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.
 *
 * Orotool 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 Orotool.  If not, see <http://www.gnu.org/licenses/>.
 */

//Code based on the public domain example here:
//https://fossies.org/linux/xz/doc/examples/01_compress_easy.c

#include "lzma.hpp"
#include <lzma.h>
#include <stdexcept>
#include <string>
#include <array>

namespace duck {

struct Lzma::LocalData {
	LocalData() :
		stream(LZMA_STREAM_INIT)
	{
	}

	::lzma_stream stream;
};

Lzma::Lzma() :
	m_local(std::make_unique<LocalData>())
{
	constexpr const uint32_t preset = 9 | LZMA_PRESET_EXTREME;

	const lzma_ret init_ret = lzma_easy_encoder(&m_local->stream, preset, LZMA_CHECK_CRC64);
	if (LZMA_OK != init_ret) {
		const char* msg;
		switch (init_ret) {
		case LZMA_MEM_ERROR:
			msg = "memory allocation failed";
			break;
		case LZMA_OPTIONS_ERROR:
			msg = "specified preset is not supported";
			break;
		case LZMA_UNSUPPORTED_CHECK:
			msg = "specified integrity check is not supported";
			break;
		default:
			msg = "unknown error";
			break;
		}
		throw std::runtime_error(std::string("lzma initialisation failed: ") +
			msg + " (" + std::to_string(init_ret) + ")");
	}
}

Lzma::~Lzma() noexcept {
	::lzma_end(&m_local->stream);
}

std::vector<uint8_t> Lzma::compress (std::string_view input) {
	std::array<uint8_t, BUFSIZ> outbuf;
	auto& strm = m_local->stream;

	strm.next_in = reinterpret_cast<const uint8_t*>(input.data());
	strm.avail_in = input.size();
	strm.next_out = outbuf.data();
	strm.avail_out = outbuf.size();

	std::vector<uint8_t> out;

	::lzma_ret code;
	do {
		const ::lzma_action action = (strm.avail_in ? LZMA_RUN : LZMA_FINISH);
		code = ::lzma_code(&strm, action);
		if (strm.avail_out == 0 or LZMA_STREAM_END == code) {
			out.insert(out.end(), outbuf.data(), outbuf.data() + (outbuf.size() - strm.avail_out));
			strm.next_out = outbuf.data();
			strm.avail_out = outbuf.size();
		}
	} while (LZMA_OK == code);

	static const std::string err_prefix("lzma compress failed: ");
	switch (code) {
	case LZMA_OK:
	case LZMA_STREAM_END:
		break;
	case LZMA_MEM_ERROR:
		throw std::runtime_error(err_prefix + "memory allocation failed (" + std::to_string(code) + ")");
	case LZMA_DATA_ERROR:
		throw std::runtime_error(err_prefix + "output size limit exceeded (" + std::to_string(code) + ")");
	default:
		throw std::runtime_error(err_prefix + "unknown error (" + std::to_string(code) + ")");
	}

	return out;
}

} //namespace duck
