/*
 * This file contains the implementation of the atc::Slice class.
 * 
 * The contents of this file are based on TTHRESH code, with permission from the author, Rafael Ballester-Ripoll.
 * TTHRESH can be found here: https://github.com/rballester/tthresh/blob/
 */

#include <string>
#include <sstream>
#include <cmath>
#include <stdexcept>
#include <iostream>

#include "Slice.hpp"

namespace atc {

	Slice::Slice(int64_t start, int64_t stop, int64_t step, Reduction reduction) {
		if (step == 0)
			throw std::invalid_argument("Invalid slice arguments: step size cannot be 0");
		this->start = start;
		this->stop = stop;
		this->step = step;
		this->reduction = reduction;
	}

	Slice::Slice(const std::string& description) {
		
		// Check last character to determine reduction
		char last_char = description.back();
		size_t description_length = description.length() - 1;
		if (last_char == 'b') {
			reduction = Reduction::Box;
		} else if (last_char == 'l') {
			reduction = Reduction::Lanczos;
		} else {
			description_length = description.length();
		}
		
		if (description.substr(0, description_length).empty())
			throw std::invalid_argument("Invalid slice description \"" + description + "\": description cannot be empty (ignoring reduction identifier)");
		
		// Get values
		std::string token;
		size_t last = 0; size_t next = 0;
		size_t parts = 0;
		do {
			// Get value
			next = description.find(":", last);
			std::string token = description.substr(last, std::min(next, description_length)  - last);
			int64_t val = (token.empty() ? UNDEFINED : stoi(token));
			last = next + 1;
			// Store token
			if (parts == 2 && val == 0)
				throw std::invalid_argument("Invalid slice description \"" + description + "\": step size cannot be 0");
			if (parts >= 3)
				throw std::invalid_argument("Invalid slice description \"" + description + "\": description cannot contain more than 3 parts");
			(parts == 0 ? start : (parts == 1 ? stop : step)) = val;
			parts++;
		} while (next != std::string::npos);
		
		if (parts == 1) {
			// If only one part is given, we only consider this single index, not a slicing
			// so we set stop to the value which gives us an equivalent slicing
			// In case the chosen index is -1, setting stop to 0 won't work
			// Instead, we need to leave it undefined and let the mode size update handle it later
			if (start != -1)
				stop = start + 1;
		}
		
	}
	
	// The following two functions are based on a Python implementation of Python slicing found here:
	// https://stackoverflow.com/questions/12173856/implementing-python-slice-notation
	
	int64_t adjust_endpoint(size_t size, int64_t endpoint, int64_t step) {
		if (endpoint < 0) {
			endpoint += size;
			if (endpoint < 0)
				endpoint = (step > 0 ? 0 : -1);
		} else if (size_t(endpoint) >= size) {
			endpoint = (step > 0 ? size : size - 1);
		}
		return endpoint;
	}
	
	void Slice::update_mode_size(size_t size) {
		
		if (size == 0)
			throw std::invalid_argument("Mode size given to Slice::update_mode_size cannot be 0!");
		
		if (mode_size == 0) {
			if (step == UNDEFINED)
				step = 1;
			
			if (start == UNDEFINED)
				start = (step > 0 ? 0 : size - 1);
			else
				start = adjust_endpoint(size, start, step);
			
			if (stop == UNDEFINED)
				stop = (step > 0 ? size : -1);
			else
				stop = adjust_endpoint(size, stop, step);
			mode_size = size;
		}
		
	}

	size_t Slice::get_size() const {
		if (mode_size == 0)
			throw std::runtime_error("Slice::get_size should only be called after calling Slice::update_mode_size!");
		return std::max<int64_t>(0, ceil(double(stop - start)/step));
	}

	bool Slice::is_standard() const {
		if (mode_size == 0)
			throw std::runtime_error("Slice::is_standard should only be called after calling Slice::update_mode_size!");
		return step > 0 && get_size() == mode_size;
	}
	
	const int64_t& Slice::get_start() const { return start; }
	const int64_t& Slice::get_stop() const { return stop; }
	const int64_t& Slice::get_step() const { return step; }
	const Reduction& Slice::get_reduction() const { return reduction; }

	std::ostream& operator<<(std::ostream& stream, const Slice& slice) {
		stream << slice.get_start() << ":" << slice.get_stop();
		if (slice.get_step() != 1)
			std::cout << ":" << slice.get_step();
		if (slice.get_reduction() == Reduction::Box)
			stream << " (box filter)";
		else if (slice.get_reduction() == Reduction::Lanczos)
			stream << " (Lanczos filter)";
		return stream;
	}
	
}
