//
//  BCDataMatrix.m
//  BioCocoa
//
//  Created by Scott Christley on 7/25/08.
//  Copyright (c) 2003-2009 The BioCocoa Project.
//  All rights reserved.
//
//  Redistribution and use in source and binary forms, with or without
//  modification, are permitted provided that the following conditions
//  are met:
//  1. Redistributions of source code must retain the above copyright
//  notice, this list of conditions and the following disclaimer.
//  2. Redistributions in binary form must reproduce the above copyright
//  notice, this list of conditions and the following disclaimer in the
//  documentation and/or other materials provided with the distribution.
//  3. The name of the author may not be used to endorse or promote products
//  derived from this software without specific prior written permission.
//
//  THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
//  IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
//  OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
//  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
//  INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
//  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
//  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
//  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
//  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
//  THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//

#import "BCDataMatrix.h"
#import "BCInternal.h"

char * const BCidEncode = @encode(id);
char * const BCintEncode = @encode(int);
char * const BCdoubleEncode = @encode(double);
char * const BClongEncode = @encode(long);
char * const BCboolEncode = @encode(BOOL);

NSString * const BCParseColumnNames = @"parseColumnNames";
NSString * const BCParseRowNames = @"parseRowNames";
NSString * const BCColumnNames = @"columnNames";
NSString * const BCRowNames = @"rowNames";
NSString * const BCDataLayout = @"dataLayout";
NSString * const BCMatrixFormat = @"matrixFormat";
NSString * const BCListFormat = @"listFormat";
NSString * const BCSeparatorCharacterSet = @"separatorCharacterSet";

@implementation BCDataMatrix

+ (BCDataMatrix *)emptyDataMatrixWithRows: (unsigned int)rows andColumns: (unsigned int)cols andEncode: (char *)anEncode
{
	return [[self alloc] initEmptyDataMatrixWithRows: rows andColumns: cols andEncode: anEncode];
}

+ (BCDataMatrix *)dataMatrixWithContentsOfFile: (NSString *)aFile andEncode: (char *)anEncode
{
	return [[self alloc] initWithContentsOfFile: aFile andEncode: anEncode];
}

+ (BCDataMatrix *)dataMatrixWithContentsOfFile: (NSString *)aFile andEncode: (char *)anEncode andFormat: (NSDictionary *)format
{
	return [[self alloc] initWithContentsOfFile: aFile andEncode: anEncode andFormat: format];
}

+ (BCDataMatrix *)dataMatrixWithRowMajorMatrix: (void *)aMatrix numberOfRows: (unsigned int)rows
	andColumns: (unsigned int)cols andEncode: (char *)anEncode
{
	return [[self alloc] initWithRowMajorMatrix: aMatrix numberOfRows: rows andColumns: cols andEncode: anEncode];
}

+ (BCDataMatrix *)dataMatrixWithColumnMajorMatrix: (void *)aMatrix numberOfRows: (unsigned int)rows
	andColumns: (unsigned int)cols andEncode: (char *)anEncode
{
	return [[self alloc] initWithColumnMajorMatrix: aMatrix numberOfRows: rows andColumns: cols andEncode: anEncode];
}	

- (BCDataMatrix *)initEmptyDataMatrixWithRows: (unsigned int)rows andColumns: (unsigned int)cols andEncode: (char *)anEncode
{
	[super init];
	
	numOfRows = rows;
	numOfCols = cols;
	encode = anEncode;
	isColumnMajor = NO;
	
	int i, j;
	if (!strcmp(encode, BCidEncode)) {
		dataMatrix = malloc(sizeof(id) * numOfRows * numOfCols);
		id (*grid)[numOfRows][numOfCols] = dataMatrix;
		for (i = 0;i < numOfRows; ++i)
			for (j = 0;j < numOfCols; ++j)
				(*grid)[i][j] = nil;
	} else if (!strcmp(encode, BCintEncode)) {
		dataMatrix = malloc(sizeof(int) * numOfRows * numOfCols);
		int (*grid)[numOfRows][numOfCols] = dataMatrix;
		for (i = 0;i < numOfRows; ++i)
			for (j = 0;j < numOfCols; ++j)
				(*grid)[i][j] = 0;
	} else if (!strcmp(encode, BClongEncode)) {
		dataMatrix = malloc(sizeof(long) * numOfRows * numOfCols);
		long (*grid)[numOfRows][numOfCols] = dataMatrix;
		for (i = 0;i < numOfRows; ++i)
			for (j = 0;j < numOfCols; ++j)
				(*grid)[i][j] = 0;
	} else if (!strcmp(encode, BCdoubleEncode)) {
		dataMatrix = malloc(sizeof(double) * numOfRows * numOfCols);
		double (*grid)[numOfRows][numOfCols] = dataMatrix;
		for (i = 0;i < numOfRows; ++i)
			for (j = 0;j < numOfCols; ++j)
				(*grid)[i][j] = 0.0;
	} else if (!strcmp(encode, BCboolEncode)) {
		dataMatrix = malloc(sizeof(BOOL) * numOfRows * numOfCols);
		BOOL (*grid)[numOfRows][numOfCols] = dataMatrix;
		for (i = 0;i < numOfRows; ++i)
			for (j = 0;j < numOfCols; ++j)
				(*grid)[i][j] = NO;
	} else {
		// throw exception or something
		NSLog(@"ERROR: BCDataMatrix unknown encoding %s\n", anEncode);
		return nil;
	}
	
	return self;
}

- (BCDataMatrix *)initWithContentsOfFile: (NSString *)aFile andEncode: (char *)anEncode
{
	return [self initWithContentsOfFile: aFile andEncode: anEncode andFormat: nil];
}

- (BCDataMatrix *)initWithContentsOfFile: (NSString *)aFile andEncode: (char *)anEncode andFormat: (NSDictionary *)format
{
	BOOL parseRowNames, parseColNames, matrixLayout;
	NSCharacterSet *separator;

	// default format is no row/column names and matrix layout
	parseRowNames = NO;
	parseColNames = NO;
	matrixLayout = YES;
	separator = [NSCharacterSet whitespaceCharacterSet];

	if (format) {
		NSString *s = [format objectForKey: BCDataLayout];
		if ([s isEqualToString: BCListFormat]) matrixLayout = NO;
		if (matrixLayout) {
			// we can parse the row/columns in matrix layout
			s = [format objectForKey: BCParseColumnNames];
			if (s && [s boolValue]) parseColNames = YES;
			s = [format objectForKey: BCParseRowNames];
			if (s && [s boolValue]) parseRowNames = YES;
		} else {
			// row/columns names must be given to us for list layout
			colNames = (NSArray *)[format objectForKey: BCColumnNames];
			if (!colNames) {
				NSLog(@"ERROR: List of column names required for list layout");
				return nil;
			}
			colNames = [[NSArray alloc] initWithArray: colNames];
			numOfCols = [colNames count];
			rowNames = (NSArray *)[format objectForKey: BCRowNames];
			if (!rowNames) {
				NSLog(@"ERROR: List of row names required for list layout");
				return nil;
			}
			rowNames = [[NSArray alloc] initWithArray: rowNames];
			numOfRows = [rowNames count];
		}
		s = [format objectForKey: BCSeparatorCharacterSet];
		if (s) separator = (NSCharacterSet *)s;
	}

	NSString *contents = [NSString stringWithContentsOfFile: aFile];
	if (!contents) {
		NSLog(@"ERROR: Unable to read contents of file: %@", aFile);
		return nil;
	}

	if (matrixLayout) {
		NSUInteger start, end, next; 
		NSUInteger i, j;
		NSAutoreleasePool *pool = [NSAutoreleasePool new];
		NSRange range;
		unsigned stringLength = [contents length];
		BOOL parseFailure = NO;

		// scan through it once to determine number of rows and columns
		range.location = 0;
		range.length = 1;
		i = 0; j = 0;
		do {
			// one line at a time
			[contents getLineStart:&start end:&next contentsEnd:&end forRange:range]; 
			range.location = start; 
			range.length = end-start; 
			NSString *s = [contents substringWithRange: range];

			// skip empty lines
			if ([s length] == 0) {
				range.location = next; 
				range.length = 1; 
				continue;
			}

			NSArray *a = [s componentsSeparatedByCharactersInSet: separator];

			if (i == 0) {
				if (parseColNames) {
					colNames = [[NSArray alloc] initWithArray: a];
					numOfCols = [a count];
					if (parseRowNames) --numOfCols;
				} else {
					if (parseRowNames) {
						numOfCols = [a count] - 1;
					} else {
						numOfCols = [a count];
					}
				}
			}
			++i;

			range.location = next; 
			range.length = 1; 
		} while (next < stringLength);

		int expectCols = numOfCols;
		if (parseRowNames) {
			rowNames = [NSMutableArray new];
			expectCols = numOfCols + 1;
		}
		numOfRows = i;
		if (parseColNames) --numOfRows;

		[self initEmptyDataMatrixWithRows: numOfRows andColumns: numOfCols andEncode: anEncode];

		// scan through again to parse the data
		range.location = 0;
		range.length = 1;
		i = 0; j = 0;
		do {
			// one line at a time
			[contents getLineStart:&start end:&next contentsEnd:&end forRange:range]; 
			range.location = start; 
			range.length = end-start; 
			NSString *s = [contents substringWithRange: range];

			// skip empty lines
			if ([s length] == 0) {
				range.location = next; 
				range.length = 1; 
				continue;
			}

			if ((i == 0) && (parseColNames)) {
				// skip the column names
				parseColNames = NO;
			} else {
				NSArray *a = [s componentsSeparatedByCharactersInSet: separator];
				if ([a count] != expectCols) {
					NSLog(@"Invalid matrix format for data matrix, expected %d items, got %d", expectCols, [a count]);
					NSLog(@"Offending line:");
					NSLog(@"%@", s);
					parseFailure = YES;
					break;
				}

				if (parseRowNames) [(NSMutableArray *)rowNames addObject: [a objectAtIndex: 0]];

				if (!strcmp(encode, BCidEncode)) {
					id (*grid)[numOfRows][numOfCols] = dataMatrix;
					for (j = 0; j < numOfCols; ++j)
						if (parseRowNames) (*grid)[i][j] = [a objectAtIndex: (j + 1)];
						else (*grid)[i][j] = [a objectAtIndex: j];
				} else if (!strcmp(encode, BCintEncode)) {
					int (*grid)[numOfRows][numOfCols] = dataMatrix;
					for (j = 0; j < numOfCols; ++j)
						if (parseRowNames) (*grid)[i][j] = [[a objectAtIndex: (j + 1)] intValue];
						else (*grid)[i][j] = [[a objectAtIndex: j] intValue];
				} else if (!strcmp(encode, BClongEncode)) {
					long (*grid)[numOfRows][numOfCols] = dataMatrix;
					for (j = 0; j < numOfCols; ++j)
						if (parseRowNames) (*grid)[i][j] = [[a objectAtIndex: (j + 1)] longValue];
						else (*grid)[i][j] = [[a objectAtIndex: j] longValue];
				} else if (!strcmp(encode, BCdoubleEncode)) {
					double (*grid)[numOfRows][numOfCols] = dataMatrix;
					for (j = 0; j < numOfCols; ++j)
						if (parseRowNames) (*grid)[i][j] = [[a objectAtIndex: (j + 1)] doubleValue];
						else (*grid)[i][j] = [[a objectAtIndex: j] doubleValue];
				} else if (!strcmp(encode, BCboolEncode)) {
					BOOL (*grid)[numOfRows][numOfCols] = dataMatrix;
					for (j = 0; j < numOfCols; ++j)
						if (parseRowNames) (*grid)[i][j] = [[a objectAtIndex: (j + 1)] boolValue];
						else (*grid)[i][j] = [[a objectAtIndex: j] boolValue];
				}

				++i;
			}

			range.location = next; 
			range.length = 1; 
		} while (next < stringLength); 

		[pool release];
		if (parseFailure) {
			[self dealloc];
			return nil;
		}

	} else {
		// The list layout has one line for each entry in the format
		//		rowName colName value

		NSUInteger start, end, next; 
		NSUInteger i, j;
		NSAutoreleasePool *pool = [NSAutoreleasePool new];
		NSRange range;
		unsigned stringLength = [contents length];
		BOOL parseFailure = NO;

		[self initEmptyDataMatrixWithRows: numOfRows andColumns: numOfCols andEncode: anEncode];

		range.location = 0;
		range.length = 1;
		do {
			// one line at a time
			[contents getLineStart:&start end:&next contentsEnd:&end forRange:range]; 
			range.location = start; 
			range.length = end-start; 
			NSString *s = [contents substringWithRange: range];

			// skip empty lines
			if ([s length] == 0) {
				range.location = next; 
				range.length = 1; 
				continue;
			}

			NSArray *a = [s componentsSeparatedByCharactersInSet: separator];
			if ([a count] != 3) {
				NSLog(@"Invalid list format for data matrix, expected 3 items, got %d", [a count]);
				NSLog(@"Offending line:");
				NSLog(@"%@", s);
				parseFailure = YES;
				break;
			}

			i = [rowNames indexOfObject: [a objectAtIndex: 0]];
			if (i == NSNotFound) {
				NSLog(@"Found unknown row name in file: %@", [a objectAtIndex: 0]);
				NSLog(@"Offending line:");
				NSLog(@"%@", s);
				parseFailure = YES;
				break;
			}

			j = [rowNames indexOfObject: [a objectAtIndex: 1]];
			if (j == NSNotFound) {
				NSLog(@"Found unknown row name in file: %@", [a objectAtIndex: 1]);
				NSLog(@"Offending line:");
				NSLog(@"%@", s);
				parseFailure = YES;
				break;
			}

			if (!strcmp(encode, BCidEncode)) {
				id (*grid)[numOfRows][numOfCols] = dataMatrix;
				(*grid)[i][j] = [a objectAtIndex: 2];
			} else if (!strcmp(encode, BCintEncode)) {
				int (*grid)[numOfRows][numOfCols] = dataMatrix;
				(*grid)[i][j] = [[a objectAtIndex: 2] intValue];
			} else if (!strcmp(encode, BClongEncode)) {
				long (*grid)[numOfRows][numOfCols] = dataMatrix;
				(*grid)[i][j] = [[a objectAtIndex: 2] longValue];
			} else if (!strcmp(encode, BCdoubleEncode)) {
				double (*grid)[numOfRows][numOfCols] = dataMatrix;
				(*grid)[i][j] = [[a objectAtIndex: 2] doubleValue];
			} else if (!strcmp(encode, BCboolEncode)) {
				BOOL (*grid)[numOfRows][numOfCols] = dataMatrix;
				(*grid)[i][j] = [[a objectAtIndex: 2] boolValue];
			}
				
			range.location = next; 
			range.length = 1; 
		} while (next < stringLength); 

		[pool release];
		if (parseFailure) {
			[self dealloc];
			return nil;
		}
	}

	return self;
}

- (BCDataMatrix *)initWithRowMajorMatrix: (void *)aMatrix numberOfRows: (unsigned int)rows
	andColumns: (unsigned int)cols andEncode: (char *)anEncode
{
	return nil;
}

- (BCDataMatrix *)initWithColumnMajorMatrix: (void *)aMatrix numberOfRows: (unsigned int)rows
	andColumns: (unsigned int)cols andEncode: (char *)anEncode
{
	return nil;
}

- (void)dealloc
{
	if (rowNames) [rowNames release];
	if (colNames) [colNames release];
	if (dataMatrix) free(dataMatrix);
	[super dealloc];
}

- (unsigned int)numberOfRows { return numOfRows; }
- (unsigned int)numberOfColumns { return numOfCols; }
- (void *)dataMatrix { return dataMatrix; }
- (char *)matrixEncoding { return encode; }

- (BOOL)isColumnMajor { return isColumnMajor; }
- (void)setColumnMajor: (BOOL)aFlag
{
	if (aFlag == isColumnMajor) return;

	// TODO: convert matrix

	isColumnMajor = aFlag;
}

- (BCDataMatrix *)dataMatrixFromRowRange: (NSRange)rows andColumnRange: (NSRange)cols
{
	// check valid ranges
	if ((rows.length == 0) || (cols.length == 0)) return nil;
	if ((rows.location + rows.length) > numOfRows) return nil;
	if ((cols.location + cols.length) > numOfCols) return nil;

	BCDataMatrix *newMatrix = [BCDataMatrix emptyDataMatrixWithRows: rows.length
		andColumns: cols.length andEncode: encode];

	int i, j;
	if (!strcmp(encode, BCidEncode)) {
		id (*grid1)[numOfRows][numOfCols] = dataMatrix;
		id (*grid2)[rows.length][cols.length] = [newMatrix dataMatrix];
		for (i = 0;i < rows.length; ++i)
			for (j = 0;j < cols.length; ++j)
				(*grid2)[i][j] = (*grid1)[i + rows.location][j + cols.location];
	} else if (!strcmp(encode, BCintEncode)) {
		int (*grid1)[numOfRows][numOfCols] = dataMatrix;
		int (*grid2)[rows.length][cols.length] = [newMatrix dataMatrix];
		for (i = 0;i < rows.length; ++i)
			for (j = 0;j < cols.length; ++j)
				(*grid2)[i][j] = (*grid1)[i + rows.location][j + cols.location];
	} else if (!strcmp(encode, BClongEncode)) {
		long (*grid1)[numOfRows][numOfCols] = dataMatrix;
		long (*grid2)[rows.length][cols.length] = [newMatrix dataMatrix];
		for (i = 0;i < rows.length; ++i)
			for (j = 0;j < cols.length; ++j)
				(*grid2)[i][j] = (*grid1)[i + rows.location][j + cols.location];
	} else if (!strcmp(encode, BCdoubleEncode)) {
		double (*grid1)[numOfRows][numOfCols] = dataMatrix;
		double (*grid2)[rows.length][cols.length] = [newMatrix dataMatrix];
		for (i = 0;i < rows.length; ++i)
			for (j = 0;j < cols.length; ++j)
				(*grid2)[i][j] = (*grid1)[i + rows.location][j + cols.location];
	} else if (!strcmp(encode, BCboolEncode)) {
		BOOL (*grid1)[numOfRows][numOfCols] = dataMatrix;
		BOOL (*grid2)[rows.length][cols.length] = [newMatrix dataMatrix];
		for (i = 0;i < rows.length; ++i)
			for (j = 0;j < cols.length; ++j)
				(*grid2)[i][j] = (*grid1)[i + rows.location][j + cols.location];
	}

	return newMatrix;
}

@end
