/*
 * This file handles the compression aspect of the quantization and encoding phase of the pipeline.
 */

#include <sstream>
#include <cmath>
#include <Eigen/Core>
#include <valarray>

#ifdef ATC_OPENMP
	#include <omp.h>
#endif

#include "Compressor.hpp"
#include "util.hpp"
#include "QuantizedArrayIterator.hpp"
#include "Breakpoint.hpp"
#include "Lock.hpp"

namespace atc {
	
	// Used to avoid false sharing in quantization scrambling loop (improved scrambling time by ~2.5% in some tests)
	template<typename T>
	struct PaddedValue {
		T val = 0;
	};
	
	// Wrapper method to call the corresponding template method
	template<typename ftype>
	void Compressor<ftype>::quantize(CompressedIO& io, QuantizationTarget target, uint8_t mode) {
		switch (options.int_type) {
			case IntType::uint32:
				quantize<uint32_t>(io, target, mode);
				break;
			case IntType::uint64:
				quantize<uint64_t>(io, target, mode);
				break;
		}
	}
	
	// Wrapper method to call the corresponding template method
	template<typename ftype>
	template<typename itype>
	void Compressor<ftype>::quantize(CompressedIO& io, QuantizationTarget target, uint8_t mode) {
		if (options.ignore_dequantization_correction)
			quantize<itype, true>(io, target, mode);
		else
			quantize<itype, false>(io, target, mode);
	}
	
	// Short-hand function to compute a particular function that is used to track the quantization error up till this point.
	inline void update_sse_reduction(long double& sse_reduction, bool encode, uint8_t bitplane, long double shift, long double block_sse, int64_t block_sse_counter) {
		if (encode)
			sse_reduction = (bitplane > 0 ? shift*(block_sse + shift*((long double) block_sse_counter)) : block_sse_counter);
	}
	
	// Short-hand function to get the lower part of the bits of a quantized coefficient with a pre-computed mask
	template<typename itype>
	long double lower_part(const std::vector<itype>& quantized_data, size_t index, itype mask_lower_bits) {
		return quantized_data[index] & mask_lower_bits;
	}
	
	template<typename ftype>
	template<typename itype, bool ignore_dequantization_correction>
	void Compressor<ftype>::quantize(CompressedIO& io, QuantizationTarget target, uint8_t mode) {
		
		// Construct string used for timers
		std::stringstream ss;
		ss << "compress_quantize";
		if (target == QuantizationTarget::Core)
			ss << "_core";
		else if (target == QuantizationTarget::Factor)
			ss << "_factor_" << mode_to_string(mode);
		std::string timer_string = ss.str();
		
		start_timer(timer_string);
		
		const std::vector<ftype>& data = (target == QuantizationTarget::Factor ? weighted_factors[mode] : core);
		
		// Compute and save maximum (in absolute value) and squared norm of data
		// To compute quantized_data_normsq, (data_normsq - rank_truncation_current_sse)*pow(scale, 2) suffices instead of squared_norm(data) algebraically,
		// however not numerically in very low error cases
		// We recompute the norm here because the loop is pretty much memory-bound anyway so we don't suffer any serious performance penalty
		// Because the array starts with a very large values (the hot corner) followed by many much smaller values,
		// we loop in reverse in order to significantly reduce the error in the sum-of-squares calculation
		start_timer(timer_string + "_calculate_scale_and_norm");
		double maximum = 0;
		long double quantized_data_normsq = 0;
		#ifdef ATC_OPENMP
			omp_set_num_threads(options.threads);
		#endif
		#pragma omp parallel for reduction(max: maximum) reduction(+: quantized_data_normsq)
		for (int64_t i = data.size() - 1; i >= 0; i--) {
			maximum = max(abs(data[i]), maximum);
			long double val = data[i];
			quantized_data_normsq += val*val;
		}
		const uint8_t bitplanes = 8*sizeof(itype);
		double scale = ldexp(1, bitplanes - 1 - ilogb(maximum));
		quantized_data_normsq *= pow(scale, 2);
		io.write(scale);
		size_t total_bytes = sizeof(scale);
		stop_timer();
		
		start_timer(timer_string + "_reorder_and_quantize_data");
		std::vector<itype> quantized_data = reorder_and_quantize_array<itype>(data, scale, target == QuantizationTarget::Factor);
		stop_timer();
		
		// Preliminaries for both normal and unified quantization
		double target_sse = target_sse_left*pow(scale, 2);
		
		// Quantize/encode bit planes
		
		// Preliminaries
		
		// SSE
		start_timer(timer_string + "_scramble_and_encode_bitplanes");
		long double current_sse = quantized_data_normsq;
		long double last_sse_calibration_point = current_sse;
		
		// Bit count tracking
		size_t raw_bits = 0, rle_bits = 0, frequency_table_bits = 0, rle_entropy = 0, leading_bits = 0;
		double size_delta = 0;
		
		// Error tracking (TTHRESH method)
		double epsilon = sqrt(current_sse/quantized_data_normsq);
		double last_eps = 1;
		double eps_delta = 0;
		
		// Error tracking (ATC method)
		double last_sse = current_sse/pow(scale, 2);
		double sse_delta = 1;
		
		// Timings
		double error_recalibration_time = 0;
		double real_scramble_time = 0;
		double real_encoding_time = 0;
		double scramble_time = 0;
		double raw_time = 0;
		double entropy_time = 0;
		
		// Other
		bool done = false;
		bool all_raw = false;
		bool wrote_all_raw = false;
		bool prioritize_leading_bit_encoding = true; // Split bitplane truncation
		
		// For analysis of SSE reduction per bit for both encoding of leading and significant bits
		std::vector<double> leading_sse_reductions, trailing_sse_reductions;
		std::vector<size_t> leading_bit_costs, trailing_bit_costs;
		if (options.print_encoding_sse_reduction_bit_cost) {
			leading_sse_reductions.resize(bitplanes);
			trailing_sse_reductions.resize(bitplanes);
			leading_bit_costs.resize(bitplanes);
			trailing_bit_costs.resize(bitplanes);
		}
		
		// Determine block boundaries in case of core
		size_t full_rank_truncation_estimated_core_size = quantized_data.size();
		if (target == QuantizationTarget::Core) {
			// Estimate the number of coefficients that would be present in the core in the case of full rank truncation based on the ST-HOSVD singular values
			double full_rank_truncation_current_sse = 0;
			double relative_energy_left = 1; // Tracks how much energy is estimated to be left in comparison with what was actually kept during the ST-HOSVD
			for (uint8_t mode_index = 0; mode_index < modes; mode_index++) {
				uint8_t mode = options.mode_processing_order[mode_index];
				double target_mode_truncation_sse = (original_target_sse - full_rank_truncation_current_sse)/(modes - mode_index);
				
				// Determine hypothetical truncation rank
				double current_sse = 0;
				size_t truncation_rank = sq_singular_values[mode].size();
				while (truncation_rank > 1) {
					// To emulate the effect of earlier extra truncation, we multiply the 
					double new_sse = current_sse + sq_singular_values[mode][truncation_rank - 1]*relative_energy_left;
					if (new_sse > target_mode_truncation_sse) {
						// Exceeded target SSE, stop
						full_rank_truncation_current_sse += current_sse;
						break;
					}
					current_sse = new_sse;
					truncation_rank--;
				}
				full_rank_truncation_estimated_core_size = full_rank_truncation_estimated_core_size/svd_sizes[mode]*truncation_rank;
				
				if (mode_index < modes - 1) {
					// Check total energy and how much extra energy we removed compared to the actual ST-HOSVD truncation
					double extra_energy_removed = 0;
					double total_energy_after_actual_truncation = 0;
					for (size_t i = 0; i < sq_singular_values[mode].size(); i++) {
						total_energy_after_actual_truncation += sq_singular_values[mode][i];
						if (i >= truncation_rank && i < svd_sizes[mode])
							extra_energy_removed += sq_singular_values[mode][i];
						else if (i >= svd_sizes[mode] && i < truncation_rank) {
							// This is a special case in case we were to truncate less aggressively for some modes than we did during the actual ST-HOSVD (normally we truncate more though)
							extra_energy_removed -= sq_singular_values[mode][i];
						}
					}
					
					// Track actual energy reduction
					relative_energy_left *= 1 - extra_energy_removed/total_energy_after_actual_truncation;
				}
			}
		}
		// The following constant was picked based on some experiments with Isotropic-PT with RTMSS=0 in order to limit compression overhead arising from excessive amounts of blocks
		// However, this can also increase quantization and encoding times in some cases
		size_t max_blocks = max(size_t(1), full_rank_truncation_estimated_core_size/85000);
		size_t blocks = (target == QuantizationTarget::Core ?
			max(min((options.quantization_blocks == 0 ? size_t(options.threads) : options.quantization_blocks), max_blocks), size_t(1))
			: 1
		);
		size_t quantization_threads = min(blocks, size_t(options.threads));
		size_t last_pass_threads = min(quantization_threads, options.last_pass_max_threads);
		Lock modify_commited_sse_lock;
		std::vector<size_t> block_bounds = calculate_block_bounds(quantized_data.size(), blocks);
		if (target == QuantizationTarget::Core) {
			io.write(uint64_t(blocks));
			total_bytes += sizeof(uint64_t);
		}
		if (target == QuantizationTarget::Core) {
			print_info("Core quantization parallellism information:");
			print_info("- Estimated core size in case of full rank truncation: " + std::to_string(full_rank_truncation_estimated_core_size));
			print_info("- Number of blocks: " + std::to_string(blocks));
			print_info("- Coefficients per block: ~" + std::to_string(block_bounds[1]));
			print_info("- Threads: " + std::to_string(quantization_threads));
			print_info("- Threads for last pass: " + std::to_string(last_pass_threads));
		}
		
		SplitBreakpoint breakpoint(blocks);
		size_t last_total_bytes = total_bytes;
		print_compressed_io_pos(io);
		for (breakpoint.bitplane = bitplanes - 1; ; breakpoint.bitplane--) {
			
			auto time = time_now();
			std::vector<std::vector<bool>> raw_buffers(blocks);
			std::vector<std::vector<uint64_t>> runlengths_buffers(blocks);
			std::vector<size_t> current_runlengths(blocks);
			long double total_leading_sse_reduction = 0, total_trailing_sse_reduction = 0;
			bool encode_leading = true, encode_trailing = true;
			
			// Initialization for error tracking
			const itype mask_higher_bits = (itype(-1) << breakpoint.bitplane) << 1; // << bitplanes does nothing, so two shifts are needed
			const itype mask_current_bit = itype(1) << breakpoint.bitplane;
			const itype mask_lower_bits = ~(mask_higher_bits | mask_current_bit); // ~ is bitwise NOT
			const long double sse_shift = ignore_dequantization_correction ?
				double(itype(1) << breakpoint.bitplane)
				: (breakpoint.bitplane > 0 ? double(itype(1) << (breakpoint.bitplane - 1)) : 0);
			size_t updates_interval = 100; // Default value, only used in case of single-pass processing, which is only used when there's only one quantization block so no synchronization overhead
			
			// The following code is executed once for most bitplanes, potentially twice for the last bitplane in the case of split bitplane encoding
			for (uint8_t pass = 0; pass < 2; pass++) {
				auto pass_time = time_now();
				const bool check_sse_reduction = ((blocks == 1 || pass == 1) && target == QuantizationTarget::Core && target_sse > 0);
				const long double target_sse_reduction = (target_sse > 0 ? current_sse - target_sse : std::numeric_limits<long double>::infinity()); // If target_sse is 0, force all threads to encode everything without stopping
				if (target == QuantizationTarget::Core) {
					print_status("Processing core bit plane " + std::to_string(uint64_t(breakpoint.bitplane)) + (check_sse_reduction ? (" while tracking error reduction (checking every " + std::to_string(updates_interval) + " SSE updates)") : ""), false);
					if (options.verbose)
						std::cout << std::flush;
				}
				
				bool stop = false;
				long double pass_sse_reduction = 0;
				size_t total_leading_sse_updates = 0, total_trailing_sse_updates = 0;
				size_t pass_threads = (pass == 1 ? last_pass_threads : quantization_threads);
				#ifdef ATC_OPENMP
					omp_set_num_threads(pass_threads);
				#endif
				std::vector<PaddedValue<long double>> committed_sse_reductions(pass_threads); // Assumption: OpenMP will never allocate more threads than assigned with omp_set_num_threads
				#pragma omp parallel for reduction(+:scramble_time,total_leading_sse_reduction,total_trailing_sse_reduction,total_leading_sse_updates,total_trailing_sse_updates)
				for (size_t block = 0; block < blocks; block++) {
					
					size_t leading_index = block_bounds[block], trailing_index = block_bounds[block];
					size_t committed_leading_index = leading_index, committed_trailing_index = trailing_index;
					if (stop) {
						if (encode_leading)
							breakpoint.blocks[block].leading_index = committed_leading_index;
						if (encode_trailing)
							breakpoint.blocks[block].trailing_index = committed_trailing_index;
						continue;
					}
					
					auto local_time = time_now();
					size_t thread_index = 0;
					#ifdef ATC_OPENMP
						thread_index = omp_get_thread_num();
					#endif
					
					// Private variables which are not needed afterwards
					long double trailing_plane_sse = 0, leading_plane_sse = 0; // Counts the added value within the adjusted SSE term
					int64_t trailing_sse_counter = 0, leading_sse_counter = 0; // Counts the number of times 2^(bitplane - 1) must be added/subtracted inside the adjusted SSE term
					size_t trailing_updates_since_last_check = 0, leading_updates_since_last_check = 0; // Counts the number of SSE updates so far since the last check
					
					// Variables which are needed afterwards, but are allocated as private variables first to prevent sharing and caching problems
					// To be sure, we even use separate vectors, although most writes occur in the vectors' contents, not their metadata (which would be stored close to other metadata)
					std::vector<bool> raw_buffer;
					std::vector<uint64_t> runlengths;
					size_t current_runlength = 0;
					size_t committed_raw_length = 0, committed_runlengths = 0;
					long double committed_leading_sse_reduction = 0, committed_trailing_sse_reduction = 0;
					
					auto commit_sse_update = [&] () {
						committed_raw_length = raw_buffer.size();
						committed_runlengths = runlengths.size();
						committed_leading_index = leading_index;
						committed_trailing_index = trailing_index;
						update_sse_reduction(committed_leading_sse_reduction, encode_leading, breakpoint.bitplane, sse_shift, leading_plane_sse, leading_sse_counter);
						update_sse_reduction(committed_trailing_sse_reduction, encode_trailing, breakpoint.bitplane, sse_shift, trailing_plane_sse, trailing_sse_counter);
						if (check_sse_reduction) {
							committed_sse_reductions[thread_index].val = ((!all_raw && options.split_bitplane_truncation) ?
								(pass == 0 ?
									(prioritize_leading_bit_encoding ? committed_leading_sse_reduction : committed_trailing_sse_reduction)
									: (encode_leading ? committed_leading_sse_reduction : committed_trailing_sse_reduction)
								) : committed_leading_sse_reduction + committed_trailing_sse_reduction
							);
						}
					};
					
					for (size_t index = block_bounds[block]; index < block_bounds[block + 1]; index++) {
						
						// Encode bit or not
						// If bit is not encoded (or it is an encoded leading 0), immediately continue to the next iteration for efficiency
						bool current_bit = quantized_data[index] & mask_current_bit;
						bool leading = ((quantized_data[index] & mask_higher_bits) == 0);
						if (leading && !all_raw) {
							if (encode_leading) {
								// Feed to RLE
								if (!current_bit) {
									current_runlength++;
									continue;
								} else {
									runlengths.push_back(current_runlength);
									current_runlength = 0;
									leading_index = index + 1;
								}
							} else {
								continue;
							}
						} else {
							if (encode_trailing) {
								// Feed to raw stream
								raw_buffer.push_back(current_bit);
								trailing_index = index + 1;
							} else {
								continue;
							}
						}
						
						// Update SSE variables
						// This custom code turned out to be significantly faster than just subtracting the old squared error and adding the new squared error
						// This should also be more precise because we're not performing operations on numbers with different magnitudes very often
						if constexpr (ignore_dequantization_correction) {
							if (current_bit) {
								// We don't include a special case for bitplane == 0 here, because it's a very rare case
								// so the cost of the extra check is probably higher than removing the redundant arithmetic in this case
								if (leading) {
									if (encode_leading) {
										leading_plane_sse += 2*lower_part(quantized_data, index, mask_lower_bits);
										leading_sse_counter++;
										leading_updates_since_last_check++;
									}
								} else if (encode_trailing) {
									trailing_plane_sse += 2*lower_part(quantized_data, index, mask_lower_bits);
									trailing_sse_counter++;
									trailing_updates_since_last_check++;
								}
							}
						} else {
							if (leading) {
								if (encode_leading && current_bit) {
									// SSE is unchanged for leading 0's
									// Leading 1 bit, if bitplane == 0 we simply subtract 1 (tracked by leading_sse_counter)
									if (breakpoint.bitplane > 0) {
										leading_plane_sse += 6*lower_part(quantized_data, index, mask_lower_bits);
										leading_sse_counter += 3;
									} else {
										leading_sse_counter++;
									}
									leading_updates_since_last_check++;
								}
							} else if (encode_trailing) {
								if (!current_bit) {
									// Trailing 0 bit
									if (breakpoint.bitplane > 0) {
										trailing_plane_sse -= 2*lower_part(quantized_data, index, mask_lower_bits);
										trailing_sse_counter += 3;
									} else {
										trailing_sse_counter++;
									}
									trailing_updates_since_last_check++;
								} else if (breakpoint.bitplane > 0) {
									// Trailing 1, only changes SSE if bitplane > 0
									trailing_plane_sse += 2*lower_part(quantized_data, index, mask_lower_bits);
									trailing_sse_counter -= 1;
									trailing_updates_since_last_check++;
								}
							}
						}
						
						// Check SSE regularly
						if (check_sse_reduction && leading_updates_since_last_check + trailing_updates_since_last_check >= updates_interval) {
							
							total_leading_sse_updates += leading_updates_since_last_check;
							total_trailing_sse_updates += trailing_updates_since_last_check;
							leading_updates_since_last_check = 0;
							trailing_updates_since_last_check = 0;
							
							// Check if target SSE reduction has already been achieved by improvements in other threads
							if (pass_threads > 1)
								modify_commited_sse_lock.set();
							if (!stop) {
								
								// If not, commit the changes made by this thread locally
								
								// Update local errors first
								commit_sse_update();
								
								// Then check total SSE reduction across all threads to see if we can stop
								long double global_sse_reduction = 0;
								for (auto& padded_val : committed_sse_reductions)
									global_sse_reduction += padded_val.val;
								global_sse_reduction += pass_sse_reduction;
								if (global_sse_reduction >= target_sse_reduction)
									stop = true;
								
							}
							if (pass_threads > 1)
								modify_commited_sse_lock.unset();
							
							// We cannot break out of a critical section
							if (stop)
								break;
						}
						
					}
					
					total_leading_sse_updates += leading_updates_since_last_check;
					total_trailing_sse_updates += trailing_updates_since_last_check;
					
					// If this thread wasn't interrupted, it means it ran until the end of the block and should do a final error update
					// If this thread was interrupted by itself, it already did this update and doesn't need to do it again
					// If this thread was interrupted by another thread, its changes shouldn't be committed
					if (!stop)
						commit_sse_update();
					
					// If all-raw encoding was active, the leading index breakpoint can simply be considered equal to the trailing index breakpoint
					if (all_raw)
						committed_leading_index = committed_trailing_index;
					
					// Move private variables back to shared buffers
					// In the case of a second pass, don't overwrite a buffer from the first pass which needs to be kept
					if (encode_leading) {
						// Leading bit data can be transferred
						runlengths.resize(committed_runlengths);
						runlengths_buffers[block] = std::move(runlengths);
						current_runlengths[block] = current_runlength;
						breakpoint.blocks[block].leading_index = committed_leading_index;
						total_leading_sse_reduction += committed_leading_sse_reduction; // Doesn't need to be atomic, because we use an OMP reduction
					}
					if (encode_trailing) {
						raw_buffer.resize(committed_raw_length);
						raw_buffers[block] = std::move(raw_buffer);
						breakpoint.blocks[block].trailing_index = committed_trailing_index;
						total_trailing_sse_reduction += committed_trailing_sse_reduction; // Doesn't need to be atomic, because we use an OMP reduction
					}
					
					// pass_sse_reduction only needs to be kept up-to-date if we are actively checking the SSE reduction while processing the bit plane
					if (check_sse_reduction) {
						// Transfer committed SSE to total so we don't need to keep re-adding this in the future
						// This should only be done when no thread is currently evaluating the total SSE reduction in the encoding loop, therefore we need a lock!
						if (pass_threads > 1)
							modify_commited_sse_lock.set();
						pass_sse_reduction += committed_sse_reductions[thread_index].val;
						committed_sse_reductions[thread_index].val = 0;
						if (pass_threads > 1)
							modify_commited_sse_lock.unset();
					}
					
					scramble_time += elapsed_time(local_time);
					
				}
				
				bool second_pass_required = false;
				if (pass == 0) {
					current_sse -= total_leading_sse_reduction + total_trailing_sse_reduction;
					// We need to explicitly include a condition based on stop, else round-off errors can lead us to change our initial decision about stopping,
					// which would then lead to buggy output
					if (target == QuantizationTarget::Core && target_sse > 0 && (stop || total_leading_sse_reduction + total_trailing_sse_reduction >= target_sse_reduction)) {
						// Target will be met this bitplane
						done = true;
						bool undo_leading_bit_encoding = false, undo_trailing_bit_encoding = false;
						bool prioritized_sse_reduction_is_sufficient = ((prioritize_leading_bit_encoding ? total_leading_sse_reduction : total_trailing_sse_reduction) >= target_sse_reduction);
						if (check_sse_reduction) {
							if (!all_raw && options.split_bitplane_truncation) {
								// Because of split bitplane truncation a second pass may be needed
								// Undo the encoding of the unprioritized bit category
								(prioritize_leading_bit_encoding ? undo_trailing_bit_encoding : undo_leading_bit_encoding) = true;
								// Perform second pass if necessary
								second_pass_required = !prioritized_sse_reduction_is_sufficient;
							}
						} else {
							// By default, both categories will need to be re-encoded, unless we're in a specific case later
							undo_leading_bit_encoding = true;
							undo_trailing_bit_encoding = true;
							if (!all_raw && options.split_bitplane_truncation) {
								if (prioritized_sse_reduction_is_sufficient) {
									// One bit category is prioritized, so if that category alone satisfies the target SSE reduction, we can undo encoding in both categories and disable encoding in the unprioritized bit category
									(prioritize_leading_bit_encoding ? encode_trailing : encode_leading) = false;
								} else {
									// If we need both bit categories, we should only undo and re-encode the unprioritized bit category
									(prioritize_leading_bit_encoding ? undo_leading_bit_encoding : undo_trailing_bit_encoding) = false;
								}
							}
							// If we didn't track the error during this bit plane, always perform second pass, because we still need to find where
							second_pass_required = true;
						}
						double last_leading_sse_reduction = total_leading_sse_reduction, last_trailing_sse_reduction = total_trailing_sse_reduction; // Needed later, after they might have been reset
						// If we don't undo a certain bit category, we should disable its encoding for the second pass
						if (undo_leading_bit_encoding) {
							for (size_t block = 0; block < blocks; block++) {
								runlengths_buffers[block].clear();
								breakpoint.blocks[block].leading_index = block_bounds[block];
								current_runlengths[block] = 0;
							}
							current_sse += total_leading_sse_reduction;
							total_leading_sse_reduction = 0;
						} else {
							encode_leading = false;
						}
						if (undo_trailing_bit_encoding) {
							for (size_t block = 0; block < blocks; block++) {
								raw_buffers[block].clear();
								breakpoint.blocks[block].trailing_index = block_bounds[block];
							}
							current_sse += total_trailing_sse_reduction;
							total_trailing_sse_reduction = 0;
						} else {
							encode_trailing = false;
						}
						if (second_pass_required) {
							// Estimate decent quantization SSE updates check interval
							// We want 1 - sqrt((original_target_sse - updates_interval*approximate_unscaled_sse_reduction_per_update)/original_target_sse) = quantization_rel_error_margin
							// => (1 - quantization_rel_error_margin)^2 = 1 - updates_interval*approximate_sse_reduction_per_update/original_target_sse
							// => updates_interval = (1 - (1 - quantization_rel_error_margin)^2)*original_target_sse/approximate_sse_reduction_per_update
							double relevant_sse_reduction = 0;
							size_t relevant_sse_updates = 0;
							if (encode_leading) {
								relevant_sse_reduction += last_leading_sse_reduction;
								relevant_sse_updates += total_leading_sse_updates;
							}
							if (encode_trailing) {
								relevant_sse_reduction += last_trailing_sse_reduction;
								relevant_sse_updates += total_trailing_sse_updates;
							}
							double approximate_unscaled_sse_reduction_per_update = relevant_sse_reduction/(relevant_sse_updates*pow(scale, 2));
							updates_interval = max(1, floor((1 - pow(1 - options.quantization_rel_error_margin, 2))*original_target_sse/approximate_unscaled_sse_reduction_per_update));
						}
					}
				} else {
					// Second pass, definitely stop
					// Only bit category should have been encoded in the second pass
					current_sse -= encode_leading ? total_leading_sse_reduction : total_trailing_sse_reduction;
				}
				
				if (target == QuantizationTarget::Core && options.verbose)
					std::cout << ", elapsed scrambling time: " + std::to_string(elapsed_time(pass_time)) + " s" << std::endl;
				if (!second_pass_required)
					break;
				
			}
			real_scramble_time += elapsed_time(time);
			
			if (options.print_encoding_sse_reduction_bit_cost) {
				leading_sse_reductions[breakpoint.bitplane] = total_leading_sse_reduction/pow(scale, 2);
				trailing_sse_reductions[breakpoint.bitplane] = total_trailing_sse_reduction/pow(scale, 2);
			}
			
			// Optionally recalibrate error
			if (target == QuantizationTarget::Core && !done) {
					
				// Check if SSE recalibration might be useful
				// The following estimate is based on Robertazzi and Schwartz assuming the distribution of summed values is exponential
				
				// 63 is the number of significant bits in the 80-bit floating point mantissa, which is used for the initialization
				// For 32-bit quantization, the truncation error becomes dominant and needs to be considered too, so we take the minimum of 63 and bitplanes as a rough estimate
				// However, after the first recalibration we are working with the quantized-data-based SSE again, so there is no more reason to consider the number of bitplanes
				// This ignores the error introduced by quantizing the floating-point data to 32/64 bits but this is usually negligible
				// If it is not, it is the responsibility of the user to switch to 64-bit quantization
				uint8_t precision = (last_sse_calibration_point == quantized_data_normsq ? std::min<uint8_t>(63, bitplanes) : 63);
				
				long double sse_summation_error_stdev = sqrt((long double)(0.13)*quantized_data.size()/12)*pow((long double)(2), (long double)(-precision))*last_sse_calibration_point;
				long double summation_error_margin = sse_summation_error_stdev; // Seems to be sufficiently wide in practice, sse_summation_error_stdev is probably not the real standard deviation
				long double relative_sse_reduction_margin = 1.0/16; // Typically SSE is reduced by a factor of 4 each bitplane, but extreme cases of a factor of ~14 have been observed
				long double desired_error_control_precision = 0.001;
				bool actual_sse_might_reach_target_sse_next_bitplane = (current_sse*relative_sse_reduction_margin - summation_error_margin <= target_sse);
				bool current_sse_might_be_too_inaccurate_to_trust = (summation_error_margin/current_sse >= 0.1);
				// We square the desired error control because we want to control the final error, not SSE
				bool recalibration_might_significantly_improve_error_control = (summation_error_margin/target_sse >= pow(desired_error_control_precision, 2));
				
				if ((actual_sse_might_reach_target_sse_next_bitplane || current_sse_might_be_too_inaccurate_to_trust) && recalibration_might_significantly_improve_error_control) {
					
					time = time_now();
					long double recalibrated_sse = 0;
					const itype encoded_bits_mask = (~itype(0)) << breakpoint.bitplane;
					const itype unencoded_bits_mask = ~encoded_bits_mask;
					const long double dequantization_correction = (long double)(itype(1) << (breakpoint.bitplane - 1));
					#pragma omp parallel for reduction(+: recalibrated_sse)
					for (size_t i = 0; i < quantized_data.size(); i++) {
						long double val = (long double)(quantized_data[i] & unencoded_bits_mask) - ((quantized_data[i] & encoded_bits_mask) > 0 ? dequantization_correction : (long double)(0));
						recalibrated_sse += val*val;
					}
					current_sse = recalibrated_sse;
					last_sse_calibration_point = recalibrated_sse;
					print_info("Recalibrated quantization error tracker");
					error_recalibration_time += elapsed_time(time);
					
				}
			}
			
			time = time_now();
			
			// Make sure the number of threads is set to the default value again
			#ifdef ATC_OPENMP
				omp_set_num_threads(quantization_threads);
			#endif
			
			// Calculate frequency tables for all blocks in parallel
			std::vector<std::map<uint64_t, uint64_t>> frequency_tables(blocks);
			size_t total_symbols_amount = 0;
			#pragma omp parallel for reduction(+: total_symbols_amount)
			for (size_t block = 0; block < blocks; block++) {
				frequency_tables[block] = CompressedIO::calculate_frequency_table(runlengths_buffers[block]);
				total_symbols_amount += runlengths_buffers[block].size();
			}
			auto frequencies = CompressedIO::merge_frequency_tables(frequency_tables);
			auto encoding_frequency_map = CompressedIO::calculate_encoding_frequency_map(frequencies, options.entropy_coding_method);
			bool write_rle = (!all_raw && frequencies.size() > 0);
			size_t leading_bit_cost = 8;
			io.write<uint8_t>(write_rle);
			total_bytes += 1;
			if (write_rle) {
				io.enable_bit_mode();
				size_t frequency_table_size = io.write_frequency_table(frequencies);
				io.disable_bit_mode();
				frequency_table_bits += frequency_table_size;
				leading_bit_cost += frequency_table_size;
				total_bytes += 8*((frequency_table_size - 1)/64 + 1);
				
				// Optional, compute theoretical entropy of runlengths
				double bitplane_rle_entropy = 0;
				for (const auto& entry : frequencies)
					bitplane_rle_entropy += entry.second*log2(total_symbols_amount/entry.second);
				rle_entropy += size_t(ceil(bitplane_rle_entropy));
			}
			
			// Compress data for each block
			std::vector<std::vector<char>> compressed_blocks(blocks);
			size_t trailing_bit_cost = 0;
			size_t new_raw_bits = 0;
			size_t new_sign_bits = 0;
			#pragma omp parallel for reduction(+:new_raw_bits,new_sign_bits,leading_bits,rle_bits,trailing_bit_cost,leading_bit_cost,raw_time,entropy_time)
			for (size_t block = 0; block < blocks; block++) {
				
				CompressedIO block_io;
				block_io.open_write();
				block_io.enable_bit_mode();
				
				// Write raw bits for this plane
				uint64_t raw_size = raw_buffers[block].size();
				block_io.write_bits(raw_size);
				auto local_time = time_now();
				for (size_t i = 0; i < raw_size; i++)
					block_io.write_bit(raw_buffers[block][i]);
				force_clear(raw_buffers[block]);
				raw_time += elapsed_time(local_time);
				new_raw_bits += raw_size;
				trailing_bit_cost += raw_size;
				
				if (write_rle) {
					
					// Write RLE/AC leading bits for this bitplane
					local_time = time_now();
					size_t rle_size = block_io.entropy_encode(encoding_frequency_map, runlengths_buffers[block], options.entropy_coding_method);
					// runlengths_buffers[block].size() is the amount of new significant coefficients on this bitplane, so the number of new sign bits
					// These sign bits have to be included in the encoding cost to represent the SSE reduction achieved by leading bit encoding
					leading_bit_cost += rle_size + runlengths_buffers[block].size();
					new_sign_bits += runlengths_buffers[block].size();
					force_clear(runlengths_buffers[block]);
					entropy_time += elapsed_time(local_time);
					rle_bits += rle_size;
					
					// Calculate encoded leading bits
					// First term is the amount of encoded leading 1's, second term the amount of encoded leading 0's
					leading_bits += runlengths_buffers[block].size() + (std::accumulate(runlengths_buffers[block].begin(), runlengths_buffers[block].end(), 0) + current_runlengths[block]);
				
				}
				
				// Extract compressed block stream
				block_io.disable_bit_mode();
				block_io.close();
				compressed_blocks[block] = block_io.get_and_clear_buffer();
				
			}
			raw_bits += new_raw_bits;
			
			// Write compressed blocks to output sequentially
			for (size_t block = 0; block < blocks; block++) {
				io.write(uint64_t(compressed_blocks[block].size()));
				io.write(compressed_blocks[block]);
				total_bytes += sizeof(uint64_t) + compressed_blocks[block].size();
				force_clear(compressed_blocks[block]);
			}
			
			real_encoding_time += elapsed_time(time);
			
			// Do work using total bit statistics
			if (options.print_encoding_sse_reduction_bit_cost) {
				trailing_bit_costs[breakpoint.bitplane] = trailing_bit_cost;
				leading_bit_costs[breakpoint.bitplane] = leading_bit_cost;
			}
			if (!all_raw) {
				// Compare efficiency of RLE encoding of leading bits vs raw encoding of trailing bits
				prioritize_leading_bit_encoding = (total_leading_sse_reduction/leading_bit_cost >= total_trailing_sse_reduction/trailing_bit_cost);
			}
			
			// Update/check stopping criterion metrics for factors, only if target SSE left for core was strictly positive, else try to perform lossless compression
			if (target_sse_left > 0) {
				if (options.tthresh_factor_stop_criterion) {
					// TTHRESH factor stopping criterion
					epsilon = sqrt(double(current_sse/quantized_data_normsq));
					if (last_total_bytes > 0) {
						if (target == QuantizationTarget::Core) {
							size_delta = (total_bytes - last_total_bytes) / double(last_total_bytes);
							eps_delta = (last_eps - epsilon) / epsilon;
						} else {
							if ((total_bytes/double(core_total_bytes)) / (epsilon/core_eps) >= core_price)
								done = true;
						}
					}
					last_eps = epsilon;
				} else {
					// Factor stopping criterion with adjusted normalization
					double current_unscaled_sse = current_sse/pow(scale, 2); // Reverts scaling
					if (target == QuantizationTarget::Core) {
						if (current_unscaled_sse < last_sse) {
							// Only update metrics if some improvement was made, else an exceptional bitplane
							// e.g. with negative error improvement could be used as baseline
							size_delta = total_bytes - last_total_bytes;
							sse_delta = last_sse - current_unscaled_sse;
						}
					} else {
						if (current_unscaled_sse >= last_sse) {
							// No improvement made this bitplane, continue unless we already encoded everything exactly
							done = (current_sse <= 0);
						} else if ((total_bytes - last_total_bytes)/(last_sse - current_unscaled_sse) >= core_price) {
							// Price per SSE improvement became higher than at core breakpoint
							done = true;
						}
					}
					last_sse = current_unscaled_sse;
				}
			}
			last_total_bytes = total_bytes;
			
			// If the cost of encoding the leading bits (ignoring the associated sign bits, which need to be encoded anyway) is larger than the number of actual leading bits being encoded,
			// switch to all-raw encoding (bypassing runlength and entropy coding)
			if (leading_bit_cost - new_sign_bits > leading_bits)
				all_raw = true;
			
			// Finishing off one bitplane iteration
			if (breakpoint.bitplane > 0) {
				if (!wrote_all_raw) {
					io.write(uint8_t(all_raw));
					total_bytes++;
					if (all_raw) {
						if (!done && target == QuantizationTarget::Core)
							print_info("Reached all-raw threshold, all following bitplanes will be encoded using only raw encoding.");
						wrote_all_raw = true;
					}
				}
				io.write(uint8_t(done));
				total_bytes++;
			} else {
				warn("Encoded all bitplanes of " + (target == QuantizationTarget::Core ? "core" : "factor " + std::to_string(uint64_t(mode))) + " without satisfying stopping criterion.");
				warn("As a result, the desired precision may not be reached.");
				if (std::is_same<itype, uint32_t>::value)
					warn("Considering using uint64 as integer quantization type for higher precision.");
				break;
			}
			
			// If done, break now to prevent bitplane from being decremented again
			if (done)
				break;
			
		}
		print_compressed_io_pos(io);
		stop_timer();
		
		if (options.timings) {
			results.timings[timer_string + "_scramble_realtime"] = real_scramble_time;
			results.timings[timer_string + "_scramble_sum_of_threads"] = scramble_time;
			results.timings[timer_string + "_encode_realtime"] = real_encoding_time;
			results.timings[timer_string + "_encode_raw_sum_of_threads"] = raw_time;
			results.timings[timer_string + "_encode_entropy_sum_of_threads"] = entropy_time;
			if (target == QuantizationTarget::Core) {
				results.timings[timer_string + "_error_recalibration_time"] = error_recalibration_time;
				if (options.verbose) {
					print_info("Core quantization loop timings:");
					print_info("- Real-time:");
					print_info("  - Scrambling time: " + std::to_string(real_scramble_time) + " s");
					print_info("  - Encoding time: " + std::to_string(real_encoding_time) + " s");
					if (error_recalibration_time > 0)
						print_info("  - Error recalibration time: " + std::to_string(error_recalibration_time) + " s");
					print_info("- Sum of real-times for all threads:");
					print_info("  - Scrambling time: " + std::to_string(scramble_time) + " s");
					print_info("  - Raw encoding time: " + std::to_string(raw_time) + " s");
					print_info("  - Entropy encoding time: " + std::to_string(entropy_time) + " s");
				}
			}
		}
		if (target == QuantizationTarget::Core) {
			print_breakpoint_info(breakpoint, block_bounds, true, current_sse/pow(scale, 2), target_sse/pow(scale, 2));
		} else {
			print_info("Factor " + mode_to_string(mode) + " encoding breakpoint: bitplane " + std::to_string(uint64_t(breakpoint.bitplane)));
		}
		if (options.print_encoding_sse_reduction_bit_cost && options.verbose) {
			print_info("SSE reductions:");
			print_info("- Leading bits: ", false); print_vector(leading_sse_reductions);
			print_info("- Trailing bits: ", false); print_vector(trailing_sse_reductions);
			print_info("Bit costs:");
			print_info("- Leading bits: ", false); print_vector(leading_bit_costs);
			print_info("- Trailing bits: ", false); print_vector(trailing_bit_costs);
			print_info("Efficiency:");
			print_info("- Leading bits: ", false); print_vector(divide_vectors(leading_sse_reductions, leading_bit_costs));
			print_info("- Trailing bits: ", false); print_vector(divide_vectors(trailing_sse_reductions, trailing_bit_costs));
		}
		
		// If quantizing core, calculate metrics for factor stopping criterion
		if (target == QuantizationTarget::Core) {
			if (options.tthresh_factor_stop_criterion) {
				core_price = size_delta/eps_delta;
				core_eps = epsilon;
				core_total_bytes = total_bytes;
			} else {
				core_price = size_delta/sse_delta;
			}
		}
		
		if (target == QuantizationTarget::Core && !options.leading_bit_positions_path.empty())
			write_leading_bit_positions(quantized_data, breakpoint, block_bounds);
		
		// If quantizing core, first calculate and transmit compressed core slice norms and ranks 
		if (target == QuantizationTarget::Core) {
			
			if (options.use_quantized_core_slice_norms) {
				start_timer(timer_string + "_calculate_slice_norms");
				update_core_slice_norms(quantized_data, scale, breakpoint, block_bounds);
				stop_timer();
				print_info("Quantized core sizes: " + sizes_to_string(quantized_sizes));
				write_sizes(io, quantized_sizes);
			} else {
				update_core_slice_norms();
			}
			
			if (options.print_significant_coefficients_per_slice) {
				for (uint8_t mode = 0; mode < modes; mode++) {
					print_info("Core slice norms (mode " + mode_to_string(mode) + "): ", false);
					print_vector(core_slice_norms[mode]);
				}
			}
			
			// Write tensor ranks and core slice norms
			for (uint8_t mode = 0; mode < modes; mode++)
				io.write(core_slice_norms[mode]);
			
			print_compressed_io_pos(io);
			
		}
		
		// Save signs of significant coefficients
		start_timer(timer_string + "_encode_signs");
		size_t sign_bits = 0;
		itype mask_till_current = itype(-1) << breakpoint.bitplane;
		itype mask_till_previous = mask_till_current << 1;
		std::vector<std::vector<double>> significant_coefficients_per_slice_fraction(modes, std::vector<double>());
		if (target == QuantizationTarget::Core && options.print_significant_coefficients_per_slice) {
			for (uint8_t mode = 0; mode < modes; mode++)
				significant_coefficients_per_slice_fraction[mode].resize(svd_sizes[mode], 0);
		}
		QuantizedArrayIterator it(target == QuantizationTarget::Factor, svd_sizes, quantized_data.size(), current_mode_order, options.core_flattening, options.print_significant_coefficients_per_slice);
		io.enable_bit_mode();
		for (size_t block = 0; block < blocks; block++) {
			// Iterate for index in range block_bounds[block] to block_bounds[block + 1]
			for (; it.get_reordered_index() < block_bounds[block + 1]; it.advance()) {
				size_t index = it.get_reordered_index();
				itype val = quantized_data[index];
				if ((val & mask_till_current) > 0 && (index < breakpoint.blocks[block].leading_index || (val & mask_till_previous) > 0)) {
					io.write_bit(data[it.get_original_index()] > 0);
					sign_bits++;
					if (options.print_significant_coefficients_per_slice && target == QuantizationTarget::Core) {
						for (uint8_t mode_index = 0; mode_index < modes; mode_index++)
							significant_coefficients_per_slice_fraction[current_mode_order[mode_index]][it.get_multi_index(mode_index)] += double(svd_sizes[current_mode_order[mode_index]])/quantized_data.size();
					}
				}
			}
		}
		io.disable_bit_mode();
		total_bytes += 8*((int64_t(sign_bits) - 1)/64 + 1); // Compensates for overhead of CompressedIO's internal 64-bit buffer
		print_compressed_io_pos(io);
		stop_timer();
		if (target == QuantizationTarget::Core && options.print_significant_coefficients_per_slice) {
			for (uint8_t mode = 0; mode < modes; mode++) {
				print_info("Ratio of significant coefficients per slice (mode " + mode_to_string(mode) + "): ", false);
				print_vector(significant_coefficients_per_slice_fraction[mode]);
			}
		}
	
		stop_timer();
		
		if (options.verbose) {
			size_t total_bits = 8*total_bytes;
			if (target == QuantizationTarget::Core) {
				print_info("Core encoding bits:");
				print_info("- Total: " + std::to_string(total_bits));
				print_info("- Sign bits: " + std::to_string(raw_bits));
				print_info("- Raw-encoded non-sign bits: " + std::to_string(raw_bits));
				print_info("- Runlength bits: " + std::to_string(rle_bits + frequency_table_bits));
				std::stringstream ss;
				ss << "  - Entropy-coded stream bits: " << rle_bits << " (" << std::fixed << std::setprecision(1)
					<< (100.0*(rle_bits - rle_entropy)/rle_entropy) << "% less efficient than entropy limit)";
				print_info(ss.str());
				ss.str("");
				ss << "  - Frequency table bits: " << frequency_table_bits << " (" << std::fixed << std::setprecision(1)
					<< (100.0*frequency_table_bits/(rle_bits + frequency_table_bits)) << "% overhead)";
				print_info(ss.str());
				ss.str("");
				ss << "  - Encoded leading bits: " << leading_bits << " (compression factor: " << std::fixed << std::setprecision(1)
					<< (double(leading_bits)/(rle_bits + frequency_table_bits)) << ")";
				print_info(ss.str());
				ss.str("");
			} else {
				print_info("Factor " + mode_to_string(mode) + " encoding bits: " + std::to_string(total_bits));
			}
		}
		
	}
	
	template<typename ftype>
	template<typename itype>
	itype Compressor<ftype>::quantize_value(const ftype& val, double scale) {
		return itype(min(round(abs(val)*scale), std::numeric_limits<itype>::max()));
	}
	
	template<typename ftype>
	template<typename itype>
	std::vector<itype> Compressor<ftype>::reorder_and_quantize_array(const std::vector<ftype>& data, double scale, bool is_factor) {
		std::vector<itype> quantized_data(data.size());
		for (QuantizedArrayIterator it(is_factor, svd_sizes, data.size(), current_mode_order, options.core_flattening); it.unfinished(); it.advance())
			quantized_data[it.get_reordered_index()] = quantize_value<itype>(data[it.get_original_index()], scale);
		return quantized_data;
	}
	
	template<typename ftype>
	void Compressor<ftype>::weight_factors() {
		
		weighted_factors.resize(modes);
		for (uint8_t mode = 0; mode < modes; mode++) {
			size_t rows = dct_sizes[mode];
			size_t cols = quantized_sizes[mode];
			if (options.householder) {
				// Arrange scaled coefficients into a single long vector
				size_t total_size = cols*(2*rows - cols - 1)/2;
				weighted_factors[mode].resize(total_size);
				Eigen::Map<vtype> weighted_factor_vector(weighted_factors[mode].data(), total_size);
				size_t index = total_size;
				for (size_t col = std::min(cols, rows - 1); col-- > 0;) {
					// Process columns from last to first, adding core slice norms as we go
					size_t reflector_length = rows - col - 1;
					index -= reflector_length;
					weighted_factor_vector.segment(index, reflector_length) = factors[mode].block(col + 1, col, reflector_length, 1);
				}
				weighted_factor_vector.array() *= calculate_householder_weights(mode).array();
			} else {
				weighted_factors[mode].resize(rows*cols);
				Eigen::Map<mtype> weighted_factor_matrix(weighted_factors[mode].data(), rows, cols);
				for (size_t col = 0; col < cols; col++)
					weighted_factor_matrix.col(col) = factors[mode].col(col)*core_slice_norms[mode][col];
			}
			
		}
		
	}
	
	template<typename ftype>
	Eigen::Matrix<ftype, Eigen::Dynamic, 1> Compressor<ftype>::calculate_householder_weights(uint8_t mode, bool allow_zero_weights) {
		// Arrange quantization scaling factors for all Householder coefficients into a vector
		size_t rows = dct_sizes[mode];
		size_t cols = quantized_sizes[mode];
		size_t total_size = cols*(2*rows - cols - 1)/2;
		vtype weights(total_size);
		mtype X;
		if (options.householder_weighting_method == HouseholderWeightingMethod::Exact) {
			X = mtype::Zero(rows, cols);
			for (size_t col = 0; col < cols; col++) {
				// If rows == cols, we will skip the last column in the next loop so we should already apply the last Householder transformation
				// Because the last Householder reflector is always e_rows, the transformation is just a sign flip
				X(col, col) = (rows == cols ? -1 : 1)*core_slice_norms[mode][col];
			}
		}
		double total_sq_core_slice_norms = 0;
		size_t index = total_size;
		for (size_t col = std::min(cols, rows - 1); col-- > 0;) {
			// Process columns from last to first
			size_t truncated_reflector_length = rows - col - 1;
			size_t full_reflector_length = truncated_reflector_length + 1;
			index -= truncated_reflector_length;
			double new_sq_core_slice_norm = pow(core_slice_norms[mode][col], 2);
			switch (options.householder_weighting_method) {
			case HouseholderWeightingMethod::Exact: {
				// Uses "secret" information from compression, can only be used for research
				Eigen::Map<vtype> vj(&factors_copy[mode](col, col), full_reflector_length);
				const auto& block = X.block(col, col, rows - col, cols - col);
				vtype vjtX = vj.transpose() * block;
				double vjtX_sq_norm = vjtX.squaredNorm();
				for (size_t row = col + 1; row < rows; row++) {
					double sq_ratio = pow(vj(row - col)/vj(0), 2);
					weights(index + row - col - 1) = 2*sqrt(X.block(row, col + 1, 1, cols - col - 1).squaredNorm() + sq_ratio*new_sq_core_slice_norm + (1 + sq_ratio)*vjtX_sq_norm);
				}
				if (col > 0) {
					// Update X for the next step, multiply with I - 2*v*v^T
					X.block(col, col, rows - col, cols - col) -= 2*vj*vjtX;
				}
				break;
			}
			case HouseholderWeightingMethod::Approximation: {
				// Normal approximation
				double expected_sq_vjj = 0.5 + 1/(2*sqrt(full_reflector_length));
				double expected_sq_vij = 1/(2*sqrt(full_reflector_length)*(sqrt(full_reflector_length) + 1));
				double expected_sq_ratio = expected_sq_vij/expected_sq_vjj;
				weights.segment(index, truncated_reflector_length).fill(2*sqrt(total_sq_core_slice_norms/(full_reflector_length - 1) + expected_sq_ratio*new_sq_core_slice_norm
					+ (1 + expected_sq_ratio)*(expected_sq_vjj*new_sq_core_slice_norm + expected_sq_vij*total_sq_core_slice_norms)));
				total_sq_core_slice_norms += new_sq_core_slice_norm;
				break;
			}
			case HouseholderWeightingMethod::CoreSliceNorms: {
				// Just use the core slice norms like without Householder, very simple and without real mathematical basis
				double norm = core_slice_norms[mode][col];
				weights.segment(index, truncated_reflector_length).fill(allow_zero_weights || norm > 0 ? norm : 1);
				break;
			}
			case HouseholderWeightingMethod::Uniform:
				// Use the same weights everywhere
				weights.segment(index, truncated_reflector_length).fill(1e6);
				break;
			}
		}
		
		return weights;
	}
	
	template<typename ftype>
	void Compressor<ftype>::update_core_slice_norms() {
		update_core_slice_norms(std::vector<uint32_t>(), 1, SplitBreakpoint(1), std::vector<size_t>(), false);
	}
	
	template<typename ftype>
	template<typename itype>
	void Compressor<ftype>::update_core_slice_norms(const std::vector<itype>& quantized_core, double scale, const SplitBreakpoint& breakpoint, const std::vector<size_t>& block_bounds, bool use_quantized_data) {
		
		// Initialization
		// indices represent coordinates in the current mode order
		// e.g. indices (x, y, z) with current mode order (1, 2, 0) represent indices (z, x, y) in the default mode order
		std::vector<size_t> indices(modes, 0);
		core_slice_norms.resize(modes);
		for (uint8_t mode = 0; mode < modes; mode++)
			core_slice_norms[mode] = std::vector<double>(svd_sizes[mode], 0);
		
		// Calculate slice norms
		
		// Masks to filter relevant bits before and after breakpoint index
		itype mask_till_current = itype(-1) << breakpoint.bitplane;
		itype mask_till_previous = mask_till_current << 1;
		// Extra bits to add to significant coefficients for dequantization correction
		itype current_bitplane_bit = 1 << breakpoint.bitplane;
		itype next_bitplane_bit = current_bitplane_bit >> 1;
		QuantizedArrayIterator it(svd_sizes, current_mode_order, options.core_flattening, true);
		for (size_t block = 0; block < breakpoint.blocks.size(); block++) {
			for (; it.get_reordered_index() < block_bounds[block + 1]; it.advance()) {
				double val = 0;
				if (use_quantized_data) {
					
					size_t index = it.get_reordered_index();
					itype bits_till_current = quantized_core[index] & mask_till_current;
					itype bits_till_previous = quantized_core[index] & mask_till_previous;
					// Check whether the dequantized value will be 0, or will be corrected with an extra bit on the current or next bitplane
					// The following code is a shortened, less intuitive version of a longer if-else tree
					if (bits_till_current > 0) {
						if (index >= breakpoint.blocks[block].trailing_index && bits_till_previous > 0)
							val = double(bits_till_previous | current_bitplane_bit);
						else if (index < breakpoint.blocks[block].leading_index || bits_till_previous > 0)
							val = double(bits_till_current | next_bitplane_bit); // If bitplane == 0, then no bit will be added, so this case is fine too
					}
					
				} else {
					// Calculate core slice norms based on full floating-point data
					val = core[it.get_original_index()];
				}
				val = pow(val, 2);
				if (val > 0)
					for (uint8_t mode_index = 0; mode_index < modes; mode_index++)
						core_slice_norms[current_mode_order[mode_index]][it.get_multi_index(mode_index)] += val;
			}
		}
		
		// Finish core slice norms and derive core tensor ranks
		quantized_sizes = std::vector<size_t>(modes, 1);
		for (uint8_t mode = 0; mode < modes; mode++) {
			bool truncated = false;
			for (size_t i = svd_sizes[mode]; i-- > 0; ) {
				if (!truncated && (core_slice_norms[mode][i] > 0 || i == 0)) {
					// Reached end of zero slices, stop
					truncated = true;
					quantized_sizes[mode] = i + 1;
					core_slice_norms[mode].resize(i + 1);
				}
				if (truncated) {
					// Back slices are already truncated, store slice norms
					core_slice_norms[mode][i] = sqrt(core_slice_norms[mode][i])/scale;
				}
			}
		}
		
	}
	
	template<typename ftype>
	void Compressor<ftype>::print_encoded_array_statistics(QuantizationTarget target, size_t total_bits, size_t raw_bits, size_t sign_bits, size_t rle_bits, size_t leading_bits, uint8_t mode) {
		
		if (options.verbose) {
			print_info("Bits for ", false);
			if (target == QuantizationTarget::Core)
				std::cout << "core";
			else if (target == QuantizationTarget::Factor)
				std::cout << "factor " << mode_to_string(mode);
			std::cout << ": total: " << total_bits
				<< ", raw: " << raw_bits
				<< ", sign: " << sign_bits
				<< ", RLE: " << rle_bits
				<< ", leading: " << leading_bits
				<< ", rest: " << total_bits - raw_bits - rle_bits - sign_bits << std::endl;
		}
		
	}
	
	template<typename ftype>
	template<typename itype>
	void Compressor<ftype>::write_leading_bit_positions(const std::vector<itype>& quantized_data, const SplitBreakpoint& breakpoint, const std::vector<size_t>& block_bounds) {
		const uint8_t bitplanes = sizeof(itype)*8;
		const itype highest_bit_mask = itype(1) << (bitplanes - 1);
		std::ofstream stream(options.leading_bit_positions_path, std::ios::out | std::ios::binary);
		// Print the number of modes and the mode sizes in the current mode order
		stream.put(modes);
		for (uint8_t mode = 0; mode < modes; mode++)
			stream.write(reinterpret_cast<char*>(&svd_sizes[current_mode_order[mode]]), sizeof(svd_sizes[0]));
		std::vector<uint8_t> leading_bit_positions(product(svd_sizes));
		QuantizedArrayIterator it(false, svd_sizes, quantized_data.size(), current_mode_order, options.core_flattening);
		for (size_t block = 0; block < breakpoint.blocks.size(); block++) {
			for (; it.get_reordered_index() < block_bounds[block + 1]; it.advance()) {
				size_t reordered_index = it.get_reordered_index();
				itype val = quantized_data[reordered_index];
				const uint8_t bitplane_breakpoint = breakpoint.bitplane + (reordered_index >= breakpoint.blocks[block].leading_index);
				for (uint8_t leading_bit_position = bitplanes - 1; ; leading_bit_position--) {
					if (val & highest_bit_mask) {
						leading_bit_positions[it.get_original_index()] = leading_bit_position + 1;
						break;
					}
					if (leading_bit_position <= bitplane_breakpoint) {
						// This coefficient is insignificant, so encode its leading bit position as if its last leading zero is on the breakpoint bitplane
						leading_bit_positions[it.get_original_index()] = breakpoint.bitplane;
						break;
					}
					val <<= 1; // If we continue, go to the next bit
				}
			}
		}
		stream.write(reinterpret_cast<char*>(leading_bit_positions.data()), sizeof(leading_bit_positions[0])*leading_bit_positions.size());
	}
	
	#include "explicit_instantiations/Compressor.txt"
	
}
