/*
 * This file is part of OpenTTD.
 * OpenTTD 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, version 2.
 * OpenTTD 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 OpenTTD. If not, see <http://www.gnu.org/licenses/>.
 */

/** @file smallmatrix_type.hpp Simple matrix class that allows allocating an item without the need to copy this->data needlessly. */

#ifndef SMALLMATRIX_TYPE_HPP
#define SMALLMATRIX_TYPE_HPP

#include "alloc_func.hpp"
#include "mem_func.hpp"

/**
 * Simple matrix template class.
 *
 * Allocating a matrix in one piece reduces overhead in allocations compared
 * with allocating a vector of vectors and saves some pointer dereferencing.
 * However, you can only get rectangular matrixes like this and if you're
 * changing their height very often performance will probably be worse than
 * with a vector of vectors, due to more frequent copying of memory blocks.
 *
 * No iterators are provided as iterating the columns would require persistent
 * column objects. Those do not exist. Providing iterators with transient
 * column objects would tie each iterator to a column object, thus replacing
 * previously retrieved columns when iterating and defeating the point of
 * iteration.
 *
 * It's expected that the items don't need to be constructed or deleted by the
 * container. Only memory allocation and deallocation is performed. This is the
 * same for all openttd "SmallContainer" classes.
 *
 * @tparam T The type of the items stored
 */
template <typename T>
class SmallMatrix {
protected:
	T *data;       ///< The pointer to the first item
	uint width;    ///< Number of items over first axis
	uint height;   ///< Number of items over second axis
	uint capacity; ///< The available space for storing items

public:

	SmallMatrix() : data(nullptr), width(0), height(0), capacity(0) {}

	/**
	 * Copy constructor.
	 * @param other The other matrix to copy.
	 */
	SmallMatrix(const SmallMatrix &other) : data(nullptr), width(0), height(0), capacity(0)
	{
		this->Assign(other);
	}

	~SmallMatrix()
	{
		free(this->data);
	}

	/**
	 * Assignment.
	 * @param other The other matrix to assign.
	 */
	SmallMatrix &operator=(const SmallMatrix &other)
	{
		this->Assign(other);
		return *this;
	}

	/**
	 * Assign items from other vector.
	 */
	inline void Assign(const SmallMatrix &other)
	{
		if (&other == this) return;

		this->height = other.Height();
		this->width = other.Width();
		uint num_items = this->width * this->height;
		if (num_items > this->capacity) {
			this->capacity = num_items;
			free(this->data);
			this->data = MallocT<T>(num_items);
			MemCpyT(this->data, other[0], num_items);
		} else if (num_items > 0) {
			MemCpyT(this->data, other[0], num_items);
		}
	}

	/**
	 * Remove all rows from the matrix.
	 */
	inline void Clear()
	{
		/* In fact we just reset the width avoiding the need to
		 * probably reallocate the same amount of memory the matrix was
		 * previously using. */
		this->width = 0;
	}

	/**
	 * Remove all items from the list and free allocated memory.
	 */
	inline void Reset()
	{
		this->height = 0;
		this->width = 0;
		this->capacity = 0;
		free(this->data);
		this->data = nullptr;
	}

	/**
	 * Compact the matrix down to the smallest possible size.
	 */
	inline void Compact()
	{
		uint capacity = this->height * this->width;
		if (capacity >= this->capacity) return;
		this->capacity = capacity;
		this->data = ReallocT(this->data, this->capacity);
	}

	/**
	 * Erase a column, replacing it with the last one.
	 * @param x Position of the column.
	 */
	void EraseColumn(uint x)
	{
		if (x < --this->width) {
			MemCpyT<T>(this->data + x * this->height,
					this->data + this->width * this->height,
					this->height);
		}
	}

	/**
	 * Remove columns from the matrix while preserving the order of other columns.
	 * @param x First column to remove.
	 * @param count Number of consecutive columns to remove.
	 */
	void EraseColumnPreservingOrder(uint x, uint count = 1)
	{
		if (count == 0) return;
		assert(x < this->width);
		assert(x + count <= this->width);
		this->width -= count;
		uint to_move = (this->width - x) * this->height;
		if (to_move > 0) {
			MemMoveT(this->data + x * this->height,
					this->data + (x + count) * this->height, to_move);
		}
	}

	/**
	 * Erase a row, replacing it with the last one.
	 * @param y Position of the row.
	 */
	void EraseRow(uint y)
	{
		if (y < this->height - 1) {
			for (uint x = 0; x < this->width; ++x) {
				this->data[x * this->height + y] =
						this->data[(x + 1) * this->height - 1];
			}
		}
		this->Resize(this->width, this->height - 1);
	}

	/**
	 * Remove columns from the matrix while preserving the order of other columns.
	 * @param y First column to remove.
	 * @param count Number of consecutive columns to remove.
	 */
	void EraseRowPreservingOrder(uint y, uint count = 1)
	{
		if (this->height > count + y) {
			for (uint x = 0; x < this->width; ++x) {
				MemMoveT(this->data + x * this->height + y,
						this->data + x * this->height + y + count,
						this->height - count - y);
			}
		}
		this->Resize(this->width, this->height - count);
	}

	/**
	 * Append rows.
	 * @param to_add Number of rows to append.
	 */
	inline void AppendRow(uint to_add = 1)
	{
		this->Resize(this->width, to_add + this->height);
	}

	/**
	 * Append rows.
	 * @param to_add Number of rows to append.
	 */
	inline void AppendColumn(uint to_add = 1)
	{
		this->Resize(to_add + this->width, this->height);
	}

	/**
	 * Set the size to a specific width and height, preserving item positions
	 * as far as possible in the process.
	 * @param new_width Target width.
	 * @param new_height Target height.
	 */
	inline void Resize(uint new_width, uint new_height)
	{
		uint new_capacity = new_width * new_height;
		T *new_data = nullptr;
		void (*copy)(T *dest, const T *src, size_t count) = nullptr;
		if (new_capacity > this->capacity) {
			/* If the data doesn't fit into current capacity, resize and copy ... */
			new_data = MallocT<T>(new_capacity);
			copy = &MemCpyT<T>;
		} else {
			/* ... otherwise just move the columns around, if necessary. */
			new_data = this->data;
			copy = &MemMoveT<T>;
		}
		if (this->height != new_height || new_data != this->data) {
			if (this->height > 0) {
				if (new_height > this->height) {
					/* If matrix is growing, copy from the back to avoid
					 * overwriting uncopied data. */
					for (uint x = this->width; x > 0; --x) {
						if (x * new_height > new_capacity) continue;
						(*copy)(new_data + (x - 1) * new_height,
								this->data + (x - 1) * this->height,
								std::min(this->height, new_height));
					}
				} else {
					/* If matrix is shrinking copy from the front. */
					for (uint x = 0; x < this->width; ++x) {
						if ((x + 1) * new_height > new_capacity) break;
						(*copy)(new_data + x * new_height,
								this->data + x * this->height,
								std::min(this->height, new_height));
					}
				}
			}
			this->height = new_height;
			if (new_data != this->data) {
				free(this->data);
				this->data = new_data;
				this->capacity = new_capacity;
			}
		}
		this->width = new_width;
	}

	inline uint Height() const
	{
		return this->height;
	}

	inline uint Width() const
	{
		return this->width;
	}

	/**
	 * Get item x/y (const).
	 *
	 * @param x X-position of the item.
	 * @param y Y-position of the item.
	 * @return Item at specified position.
	 */
	inline const T &Get(uint x, uint y) const
	{
		assert(x < this->width && y < this->height);
		return this->data[x * this->height + y];
	}

	/**
	 * Get item x/y.
	 *
	 * @param x X-position of the item.
	 * @param y Y-position of the item.
	 * @return Item at specified position.
	 */
	inline T &Get(uint x, uint y)
	{
		assert(x < this->width && y < this->height);
		return this->data[x * this->height + y];
	}

	/**
	 * Get column "number" (const)
	 *
	 * @param x Position of the column.
	 * @return Column at "number".
	 */
	inline const T *operator[](uint x) const
	{
		assert(x < this->width);
		return this->data + x * this->height;
	}

	/**
	 * Get column "number" (const)
	 *
	 * @param x Position of the column.
	 * @return Column at "number".
	 */
	inline T *operator[](uint x)
	{
		assert(x < this->width);
		return this->data + x * this->height;
	}
};

#endif /* SMALLMATRIX_TYPE_HPP */
