//
//  $Id$
//
//  SPDataStorage.m
//  sequel-pro
//
//  Created by Rowan Beentje on 10/01/2009.
//  Copyright 2009 Rowan Beentje. All rights reserved.
//
//  This program 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 2 of the License, or
//  (at your option) any later version.
//
//  This program 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 this program; if not, write to the Free Software
//  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
//
//  More info at <http://code.google.com/p/sequel-pro/>

#import "SPDataStorage.h"

@interface SPDataStorage (PrivateAPI)

- (void) _ensureCapacityForAdditionalRowCount:(NSUInteger)numExtraRows;
- (void) _increaseCapacity;

@end

@implementation SPDataStorage

static inline void SPDataStorageEnsureCapacityForAdditionalRowCount(SPDataStorage* self, NSUInteger numExtraRows) 
{
	typedef void (*SPDSEnsureCapacityMethodPtr)(SPDataStorage*, SEL, NSUInteger);
	static SPDSEnsureCapacityMethodPtr SPDSEnsureCapacity;
	if (!SPDSEnsureCapacity) SPDSEnsureCapacity = (SPDSEnsureCapacityMethodPtr)[self methodForSelector:@selector(_ensureCapacityForAdditionalRowCount:)];
	SPDSEnsureCapacity(self, @selector(_ensureCapacityForAdditionalRowCount:), numExtraRows);
}

#pragma mark -
#pragma mark Retrieving rows and cells

/**
 * Return a mutable array containing the data for a specified row.
 */
- (NSMutableArray *) rowContentsAtIndex:(NSUInteger)index
{

	// Throw an exception if the index is out of bounds
	if (index >= numRows) [NSException raise:NSRangeException format:@"Requested storage index (%llu) beyond bounds (%llu)", index, numRows];

	// Construct the NSMutableArray
	NSMutableArray *rowArray = [NSMutableArray arrayWithCapacity:numColumns];
	id *row = dataStorage[index];
	NSUInteger i;
	for (i = 0; i < numColumns; i++) {
		CFArrayAppendValue((CFMutableArrayRef)rowArray, row[i]);
	}

	return rowArray;
}

/**
 * Return the data at a specified row and column index.
 */
- (id) cellDataAtRow:(NSUInteger)rowIndex column:(NSUInteger)columnIndex
{

	// Throw an exception if the row or column index is out of bounds
	if (rowIndex >= numRows || columnIndex >= numColumns) [NSException raise:NSRangeException format:@"Requested storage index (row %llu, col %llu) beyond bounds (%llu, %llu)", rowIndex, columnIndex, numRows, numColumns];

	// Return the content
	return dataStorage[rowIndex][columnIndex];
}

#pragma mark -
#pragma mark Retrieving rows via NSFastEnumeration

/**
 * Implementation of the NSFastEnumeration protocol.
 * Note that this currently doesn't implement mutation guards.
 */
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id *)stackbuf count:(NSUInteger)len
{

	// If the start index is out of bounds, return 0 to indicate end of results
	if (state->state >= numRows) return 0;

	// Determine how many objects to return - 128, len, or all items remaining
	NSUInteger itemsToReturn = 128;
	if (len < 128) itemsToReturn = len;
	if (numRows - state->state < itemsToReturn) {
		itemsToReturn = numRows - state->state;
	}

	// Construct the arrays to return
	NSUInteger i, j;
	NSMutableArray *rowArray;
	id *row;
	for (i = 0; i < itemsToReturn; i++) {
		row = dataStorage[state->state + i];
		rowArray = [NSMutableArray arrayWithCapacity:numColumns];
		for (j = 0; j < numColumns; j++) {
			CFArrayAppendValue((CFMutableArrayRef)rowArray, row[j]);
		}
		stackbuf[i] = rowArray;
	}

	state->state += itemsToReturn;
	state->itemsPtr = stackbuf;
	state->mutationsPtr = (unsigned long *)&numRows;
	return itemsToReturn;
}

#pragma mark -
#pragma mark Adding and amending rows and cells

/**
 * Add a new row to the end of the storage array, supplying an NSArray
 * of objects.  Note that the supplied objects are retained as a reference
 * rather than copied.
 */
- (void) addRowWithContents:(NSArray *)row
{
	
	// Ensure that sufficient capacity is available
	SPDataStorageEnsureCapacityForAdditionalRowCount(self, 1);

	// Add an empty row array to the data store
	id *newRow = (id *)malloc(columnPointerByteSize);
	dataStorage[numRows] = newRow;
	numRows++;

	// Copy over references to the array contents, and retain the objects
	NSUInteger cellsCopied = 0;
	for (id cellData in row) {
		if (cellData) newRow[cellsCopied] = (id)CFRetain(cellData);
		else newRow[cellsCopied] = nil;
		if (++cellsCopied == numColumns) break;
	}

	// If an array shorter than the row width was added, pad with nils
	if (cellsCopied < numColumns) {
		for ( ; cellsCopied <= numColumns; cellsCopied++) newRow[cellsCopied] = nil;
	}
}

/**
 * Insert a new row into the storage array at a specified point, pushing
 * all later rows the next index.  Note that the supplied objects within the
 * array are retained as a reference rather than copied.
 */
- (void) insertRowContents:(NSArray *)row atIndex:(NSUInteger)index
{

	// Throw an exception if the index is out of bounds
	if (index > numRows) [NSException raise:NSRangeException format:@"Requested storage index (%llu) beyond bounds (%llu)", index, numRows];

	// If "inserting" at the end of the array just add a row
	if (index == numRows) return SPDataStorageAddRow(self, row);

	// Ensure that sufficient capacity is available to hold all the rows
	SPDataStorageEnsureCapacityForAdditionalRowCount(self, 1);

	// Renumber the specified index, and all subsequent indices, to create a gap
	for (NSUInteger j = numRows - 1; j >= index; j--) {
		dataStorage[j + 1] = dataStorage[j];
	}

	// Add a new instantiated row array to the data store at the specified point
	id *newRow = (id *)malloc(columnPointerByteSize);
	dataStorage[index] = newRow;
	numRows++;

	// Copy over references to the array contents, and retain the objects
	NSUInteger cellsCopied = 0;
	for (id cellData in row) {
		if (cellData) newRow[cellsCopied] = (id)CFRetain(cellData);
		else newRow[cellsCopied] = nil;
		if (++cellsCopied == numColumns) break;
	}

	// If an array shorter than the row width was inserted, pad with nils
	if (cellsCopied < numColumns) {
		for ( ; cellsCopied <= numColumns; cellsCopied++) newRow[cellsCopied] = nil;
	}
}

/**
 * Replace a row with contents of the supplied NSArray.
 */
- (void) replaceRowAtIndex:(NSUInteger)index withRowContents:(NSArray *)row
{
	NSUInteger cellsProcessed = 0;

	// Throw an exception if the index is out of bounds
	if (index >= numRows) [NSException raise:NSRangeException format:@"Requested storage index (%llu) beyond bounds (%llu)", index, numRows];

	id *storageRow = dataStorage[index];

	// Iterate through the row replacing the objects
	for (id cellData in row) {
		if (storageRow[cellsProcessed]) CFRelease(storageRow[cellsProcessed]);
		if (cellData) storageRow[cellsProcessed] = (id)CFRetain(cellData);
		else storageRow[cellsProcessed] = nil;
		if (++cellsProcessed == numColumns) break;
	}

	// Ensure all cells are correctly updated if an array shorter than the row width was supplied
	if (cellsProcessed < numColumns) {
		for ( ; cellsProcessed <= numColumns; cellsProcessed++) {
			if (storageRow[cellsProcessed]) CFRelease(storageRow[cellsProcessed]);
			storageRow[cellsProcessed] = nil;
		}
	}
}

/**
 * Replace the contents of a single cell with a supplied object.
 */
- (void) replaceObjectInRow:(NSUInteger)rowIndex column:(NSUInteger)columnIndex withObject:(id)object
{

	// Throw an exception of either index is out of bounds
	if (rowIndex >= numRows || columnIndex >= numColumns) [NSException raise:NSRangeException format:@"Requested storage index (row %llu, col %llu) beyond bounds (%llu, %llu)", rowIndex, columnIndex, numRows, numColumns];

	// Release the old object and retain the new one
	if (dataStorage[rowIndex][columnIndex]) CFRelease(dataStorage[rowIndex][columnIndex]);
	if (object) dataStorage[rowIndex][columnIndex] = (id)CFRetain(object);
	else dataStorage[rowIndex][columnIndex] = nil;
}

/**
 * Remove a row, renumbering all elements beyond index.
 */
- (void) removeRowAtIndex:(NSUInteger)index
{

	// Throw an exception if the index is out of bounds
	if (index >= numRows) [NSException raise:NSRangeException format:@"Requested storage index (%llu) beyond bounds (%llu)", index, numRows];

	// Free the row
	NSUInteger j = numColumns;
	id *row = dataStorage[index];
	while (j > 0) {
		if (row[--j]) CFRelease(row[j]);
	}
	free(row);
	numRows--;

	// Renumber all subsequent indices to fill the gap
	for (j = index; j < numRows; j++) {
		dataStorage[j] = dataStorage[j + 1];
	}
	dataStorage[numRows] = NULL;
}

/**
 * Remove all rows in the specified range, renumbering all elements
 * beyond the end of the range.
 */
- (void) removeRowsInRange:(NSRange)rangeToRemove
{

	// Throw an exception if the range is out of bounds
	if (rangeToRemove.location + rangeToRemove.length > numRows) [NSException raise:NSRangeException format:@"Requested storage index (%llu) beyond bounds (%llu)", (rangeToRemove.location + rangeToRemove.length), numRows];

	// Free rows in the range
	NSUInteger i, j = numColumns;
	id *row;
	for (i = rangeToRemove.location; i < rangeToRemove.location + rangeToRemove.length; i++) {
		row = dataStorage[i];
		while (j > 0) {
			if (row[--j]) CFRelease(row[j]);
		}
		free(row);	
	}
	numRows -= rangeToRemove.length;

	// Renumber all subsequent indices to fill the gap
	for (i = rangeToRemove.location + rangeToRemove.length - 1; i < numRows; i++) {
		dataStorage[i] = dataStorage[i + rangeToRemove.length];
	}
	for (i = numRows; i < numRows + rangeToRemove.length; i++) {
		dataStorage[i] = NULL;
	}
}

/**
 * Remove all rows from the array, and free their associated memory.
 */
- (void) removeAllRows
{
	NSUInteger j;
	id *row;

	// Free all the data
	while (numRows > 0) {
		row = dataStorage[--numRows];
		j = numColumns;
		while (j > 0) {
			if (row[--j]) CFRelease(row[j]);
		}
		free(row);
	}

	numRows = 0;
}

#pragma mark -
#pragma mark Basic information

/**
 * Returns the number of rows currently held in data storage.
 */
- (NSUInteger) count
{
	return numRows;
}

/**
 * Set the number of columns represented by the data storage.
 */
- (void) setColumnCount:(NSUInteger)columnCount
{
	columnPointerByteSize = columnCount * sizeof(id);

	// If there are rows present in the storage, and the number of
	// columns has changed, amend the existing rows to match.
	if (columnCount != numColumns && numRows) {
		NSUInteger i = numRows, j;
		id *row;

		// If the new column count is higher than the old count, iterate through the existing rows
		// and pad with nils
		if (columnCount > numColumns) {
			while (i-- > 0) {
				dataStorage[i] = (id *)realloc(dataStorage[i], columnPointerByteSize);
				j = numColumns;
				while (j < columnCount) {
					dataStorage[i][j++] = nil;
				}
			}

		// If the new column count is lower than the old count, iterate through the existing rows
		// freeing any extra objects
		} else {
			while (i > 0) {
				row = dataStorage[--i];
				j = numColumns;
				while (j > columnCount) {
					if (row[--j]) CFRelease(row[j]);
				}
				dataStorage[i] = (id *)realloc(row, columnPointerByteSize);
			}
		}
	}

	// Update the column count
	numColumns = columnCount;
}

/**
 * Return the number of columns represented by the data storage.
 */
- (NSUInteger) columnCount
{
	return numColumns;
}

/**
 * Setup and teardown
 */
#pragma mark -

- (id) init {
	if ((self = [super init])) {
		numColumns = 0;
		columnPointerByteSize = 0;
		numRows = 0;

		// Initialise the array, initially with space for 100 rows
		numRowsCapacity = 100;
		dataStorage = (id **)malloc(numRowsCapacity * sizeof(id *));
	}
	return self;
}

- (void) dealloc {
	[self removeAllRows];
	free(dataStorage);
	[super dealloc];
}

@end

@implementation SPDataStorage (PrivateAPI)

/**
 * Private method to ensure the array always has sufficient capacity
 * to store any additional rows required.
 */
- (void) _ensureCapacityForAdditionalRowCount:(NSUInteger)numExtraRows
{
	while (numRows + numExtraRows > numRowsCapacity) [self _increaseCapacity];
}

/**
 * Private method to increase the storage available for the array;
 * currently doubles the capacity as boundaries are reached.
 */
- (void) _increaseCapacity
{
	numRowsCapacity *= 2;
	dataStorage = (id **)realloc(dataStorage, numRowsCapacity * sizeof(id *));
}

@end