/*
 * This file defines the atc::CompressorBase class and declares its members.
 */

#pragma once

#include <cstdint>
#include <memory>
#include <stack>
#include <chrono>
#include <iomanip>
#include <numeric>

#include "Options.hpp"
#include "Results.hpp"
#include "DataType.hpp"
#include "CompressedIO.hpp"
#include "Breakpoint.hpp"

namespace atc {
	
	/*
	 * This class contains various utility functions for the atc::Compressor class
	 * that do not depend on the floating-point type used for arithmetic.
	 * Furthermore, this class may be instantiated of its own to handle certain simple interface calls such as read_data_type,
	 * since this does not depend on the floating-point type used.
	 */
	class CompressorBase {
	public:
		// Extracts the data type of the original data stored in the compressed output object managed by the given compressed_io object.
		DataType read_data_type(CompressedIO& compressed_io);
		/*
		 * Extracts the original sizes stored in the compressed output object managed by the given compressed_io object
		 * and computes the decompressed tensor size resulting from a hypothetical decompression with the given options.
		 * See calculate_decompressed_size concerning the effects of autocrop.
		 */
		size_t read_decompressed_size(CompressedIO& compressed_io, const Options& options, bool suppress_autocrop_warnings);
		/*
		 * Computes the decompressed tensor size resulting from a hypothetical decompression with the given original sizes and options.
		 * If autocrop is used, an upper bound is returned assuming no cropping is performed.
		 * If suppress_autocrop_warnings, no warnings will be printed about this.
		 */
		size_t calculate_decompressed_size(const std::vector<size_t>& sizes, const Options& options, bool suppress_autocrop_warnings);
		// Moves the results, effectively returning the stored results and then clearing the storage.
		Results get_and_clear_results();
	protected:
		
		// Types
		typedef uint16_t flag_type; // The type of the bit field that handles all compression flags that must be stored in the compressed output.
		
		// Constants
		// Flag bitfield
		// The values indicate the setting of a particular option during compression, such that the same setting can be used during decompression.
		// We define these constants to avoid duplicating magic values elsewhere.
		const flag_type flag_truncate_st_hosvd = 1 << 0;
		const flag_type flag_long_bitplane_encoding = 1 << 1;
		const flag_type flag_msi_storage_order = 1 << 2;
		const flag_type flag_use_quantized_core_slice_norms = 1 << 3;
		const flag_type flag_householder = 1 << 4;
		const flag_type flag_ANS = 1 << 5;
		const flag_type flag_dct = 1 << 6;
		const flag_type flag_zigzag_flattening = 1 << 7; // This field has three options, so it takes up two bits
		const flag_type flag_z_order_flattening = 2 << 7; // This field has three options, so it takes up two bits
		const flag_type flag_hwm_core_slice_norms = 1 << 9;
		
		Options options; // The locally-stored options, which is copied from the given options object such that it can be modified to make things a bit easier.
		uint8_t data_type_id; // The integer representation of the data type of the original data, stored during compression such that we can check during decompression if that decompressed data type matches when trying to calculate the decompression error.
		uint8_t modes; // Short-hand for original_sizes.size(), used very often throughout the code.
		// Sizes are stored using least-significant-index first ordering, regardless of the user-defined storage order.
		// Instead, we transform the given sizes to the internal order and transform these internal sizes to the user-defined order when displaying them to the user.
		std::vector<size_t>
			original_sizes, // Original sizes
			dct_sizes, // After T-HODCT pre-processing
			svd_sizes, // After ST-HOSVD
			quantized_sizes, // After quantization and removal of 0-slices
			decompressed_sizes // Original sizes
		;
		double data_normsq; // The squared Frobenius/Euclidean norm of the original data
		double data_min, data_max; // The bounds of the original data
		std::vector<uint8_t> current_mode_order, decompression_mode_processing_order; // Mode orders are stored using least-significant-index first ordering
		std::vector<bool> apply_dct_to_mode; // When applying the T-HODCT, stores whether or not a DCT should be applied explicitly after ST-HOSVD decompression or if this was already implicitly done by merging it with the corresponding ST-HOSVD factor matrix.
		size_t decompression_mode_processing_order_cost; // The heuristic cost value used to determine the pseudo-optimal ST-HOSVD decompression mode processing order. Stored such that we can check later if we have improved our old mode processing order when performing a second search.
		double original_target_sse, target_sse_left, rank_truncation_target_sse, rank_truncation_current_sse = 0; // SSE values which are used to control the error during compression.
		Results results; // The internal results object, which accumulates the results stored in it until get_and_clear_results is called.
		
		// General methods
		void print_banner(const std::string& message); // If options.verbose, prints a large banner with the given message in the middle
		void print_info(const std::string& message, bool newline = true); // If options.verbose, prints an aligned message with an [Info] tag.
		void print_status(const std::string& message, bool newline = true); // If options.verbose, prints an aligned message with a [Status] tag.
		void warn(const std::string& message); // Unless options.suppress_warnings, prints an aligned message with a [Warning] tag.
		void print_tagged_message(const std::string& tag, const std::string& message, bool newline = true); // If options.verbose, prints an aligned message with the given tag.
		void print_breakpoint_info(const SplitBreakpoint& breakpoint, const std::vector<size_t>& block_bounds, bool encoding, double current_sse = -1, double target_sse = -1); // Prints various tagged messages concerning the given quantization breakpoint.
		void print_compressed_io_pos(const CompressedIO& io); // If DNDEBUG is undefined and options.verbose, prints a status message showing the current CompressedIO position (byte and bit positions).
		void verify_mode_permutation(const std::vector<uint8_t>& permutation, const std::string& name); // Verifies if the given mode order is a valid permutation and throws appropriate exceptions in the case of errors.
		void write_header(CompressedIO& io); // Writes compressed output header
		void read_header(CompressedIO& io); // Reads compressed output header
		std::string mode_to_string(uint8_t mode); // Converts the given mode to its string representation, taking the user-defined storage order into account.
		std::string sizes_to_string(const std::vector<size_t>& sizes); // Converts the given sizes to a string based on the user-defined storage order.
		std::string mode_order_to_string(const std::vector<uint8_t>& mode_order); // Converts the mode order (in internal LSI representation) to a string based on the user-defined storage order.
		void write_sizes(CompressedIO& io, const std::vector<size_t>& sizes); // Writes the given sizes to the given IO object.
		std::vector<size_t> read_sizes(CompressedIO& io); // Reads sizes from the given IO object using the stored number of modes.
		void verify_slices(); // Verifies the validity of the given slices stored in options.cutout.
		void update_decompression_mode_processing_order(); // Determines the quasi-optimal ST-HOSVD decompression mode processing order based on current and future mode sizes.
		size_t calculate_required_size_core(bool include_dct); // Determines the minimum required size for the temporary buffer to store the core during each step of the ST-HOSVD (and potentially the T-HODCT).
		
		// Timings
		std::stack<std::pair<std::string, std::chrono::high_resolution_clock::time_point>> timers; // Stack of timers that allows for easy timing of section hierarchies.
		void start_timer(std::string message); // Starts a timer with a given name and puts it on the timer stack.
 		void stop_timer(); // Removes the top timer from the stack and stores its results in results.timings. If options.verbose, also prints a tagged message displaying the timing.
		
	};
	
}
