//
//  MPExpression.m
//  MathPad
//
//  Created by Kim Wittenburg on 10.08.14.
//  Copyright (c) 2014 Kim Wittenburg. All rights reserved.
//

// TODO: Reorganise header/implementation order. Drop the primitive/not primitive Approach

#import "MPExpression.h"
#import "MPFunction.h"
#import "MPRangePath.h"

#import "NSIndexPath+MPAdditions.h"
#import "MPException.h"

#import "MPExpressionEvaluator.h"

@interface MPExpression () {
    NSMutableArray *__strong _elements;
}
@end

@interface MPExpression (MPExpressionPrivate)

- (NSUInteger)lengthOfElements:(NSArray *)elements;

- (void)validateElements:(NSArray *)elements;
- (BOOL)splitElementsAtLocation:(NSUInteger)location
                 insertionIndex:(out NSUInteger *)insertionIndex;

@end

@implementation MPExpression (MPExpressionPrivate)

- (NSUInteger)lengthOfElements:(NSArray *)elements
{
    NSUInteger length = 0;
    for (id<MPExpressionElement> element in elements) {
        length += element.length;
    }
    return length;
}

- (void)validateElements:(NSArray *)elements
{
    for (id element in elements) {
        if (![element conformsToProtocol:@protocol(MPExpressionElement)]) {
            @throw [NSException exceptionWithName:MPIllegalElementException
                                           reason:@"Elements must conform to the MPExpressionElement protocol."
                                         userInfo:@{MPIllegalElementExceptionElementKey: element}];
        }
    }
}

- (BOOL)splitElementsAtLocation:(NSUInteger)location
                 insertionIndex:(out NSUInteger *)insertionIndex
{
    if (location == 0) {
        *insertionIndex = 0;
        return NO;
    }
    
    NSUInteger splitOffset;
    NSUInteger splitElementIndex = [self indexOfElementAtSymbolLocation:location
                                                                 offset:&splitOffset];
    if (splitOffset != 0) {
        NSString *splitElement = (NSString *)self.elements[splitElementIndex];
        NSString *leftPart = [splitElement substringToIndex:splitOffset];
        NSString *rightPart = [splitElement substringFromIndex:splitOffset];
        [_elements replaceObjectsInRange:NSMakeRange(splitElementIndex, 1)
                    withObjectsFromArray:@[leftPart, rightPart]];
        ++splitElementIndex;
    }
    *insertionIndex = splitElementIndex;
    return splitOffset != 0;
}

@end

@implementation MPExpression {
    NSUInteger _cachedLength;
    NSRange _editedRange;
    BOOL _didSplitEndOnEditing;
    NSUInteger _replacementLength;
}

#pragma mark Creation Methods
- (instancetype)init
{
    return [self initWithElements:@[]];
}

- (instancetype)initWithElement:(id<MPExpressionElement>)element
{
    return [self initWithElements:@[element]];
}

- (instancetype)initWithElements:(NSArray *)elements
{
    self = [super init];
    if (self) {
        _cachedLength = 0;
        _elements = [[NSMutableArray alloc] initWithArray:elements
                                                copyItems:YES];
        [self fixElements];
    }
    return self;
}

#pragma mark Working With the Expression Tree
- (void)fixElements
{
    for (NSUInteger index = 0; index < self.elements.count; index++) {
        id<MPExpressionElement> next = index+1 < self.elements.count ? self.elements[index+1] : nil;
        id<MPExpressionElement> current = self.elements[index];
        if ([current isString]) {
            if (current.length == 0) {
                [_elements removeObjectAtIndex:index];
                if (index >= _editedRange.location && index < NSMaxRange(_editedRange)) {
                    --_replacementLength;
                }
                --index;
            } else if ([next isString]) {
                NSString *new = [NSString stringWithFormat:@"%@%@", current, next];
                [_elements replaceObjectsInRange:NSMakeRange(index, 2)
                                withObjectsFromArray:@[new]];
                NSUInteger maxReplacementIndex = _editedRange.location + _replacementLength;
                if (index == _editedRange.location - 1) {
                    --_editedRange.location;
                    ++_editedRange.length;
                } else if (index >= _editedRange.location && index < maxReplacementIndex - 1) {
                    --_replacementLength;
                } else if (index == maxReplacementIndex - 1) {
                    if (!_didSplitEndOnEditing) {
                        ++_editedRange.length;
                    }
                }
                --index;
            }
        } else {
            [(MPFunction *)current setParent:self];
        }
    }
}

#pragma mark Primitive Methods
- (NSUInteger)length
{
    if (_cachedLength == 0) {
        _cachedLength = [self lengthOfElements:self.elements];
    }
    return _cachedLength;
}

- (NSUInteger)numberOfElements
{
    return self.elements.count;
}

- (void)setObject:(id)obj atIndexedSubscript:(NSUInteger)idx
{
    [self replaceSymbolsInRange:NSMakeRange(idx, 1)
                    withElements:@[obj]];
}

- (id)objectAtIndexedSubscript:(NSUInteger)idx
{
    return [self elementAtIndex:idx];
}

- (id<MPExpressionElement>)elementAtIndex:(NSUInteger)anIndex
{
    return self.elements[anIndex];
}

- (NSArray *)elementsInRange:(NSRange)range
{
    return [self.elements subarrayWithRange:range];
}

#warning If multiple equal expressions exist errors may occur...
- (NSUInteger)indexOfElement:(id<MPExpressionElement>)element
{
    return [self.elements indexOfObject:element];
}

- (NSUInteger)indexOfElementAtSymbolLocation:(NSUInteger)location
                                      offset:(out NSUInteger *)offset
{
    if (location == 0) {
        if (offset != NULL) {
            *offset = 0;
        }
        return 0;
    }
    
    // Calculating elementIndex and splitOffset
    NSUInteger totalLength = 0;
    NSUInteger elementIndex = 0;
    NSUInteger elementLength = 0;
    for (id<MPExpressionElement> element in self.elements) {
        elementLength = element.length;
        totalLength += elementLength;
        if (totalLength >= location) {
            break;
        }
        ++elementIndex;
    }
    NSUInteger splitOffset = elementLength - (totalLength - location);

    id<MPExpressionElement> element = self.elements[elementIndex];
    if (splitOffset == element.length) {
        splitOffset = 0;
        elementIndex++;
    }
    if (offset != NULL) {
        *offset = splitOffset;
    }
    return elementIndex;
}

- (NSUInteger)locationOfElementAtIndex:(NSUInteger)index
{
    NSUInteger location = 0;
    for (NSUInteger i = 0; i < index; i++) {
        location += [self elementAtIndex:i].length;
    }
    return location;
}

- (void)replaceSymbolsInRange:(NSRange)range
                  withElements:(NSArray *)elements
{
    if (NSMaxRange(range) > self.length) {
        @throw [NSException exceptionWithName:NSRangeException
                                       reason:@"Range out of bounds of expression"
                                     userInfo:nil];
    }
    [self validateElements:elements];
    
    // Locate the position, split the elements
    NSUInteger startIndex; // startIndex is inclusive
    BOOL didSplitStart = NO;
    if ([self numberOfElements] == 0) {
        startIndex = 0;
    } else {
        didSplitStart = [self splitElementsAtLocation:range.location
                                       insertionIndex:&startIndex];
    }
    NSUInteger endIndex; // endIndex is exclusive
    BOOL didSplitEnd = [self splitElementsAtLocation:NSMaxRange(range)
                                      insertionIndex:&endIndex];
    
    // Perform the replacement
    NSMutableArray *newElements = [[NSMutableArray alloc] initWithArray:elements
                                                              copyItems:YES];
    [_elements replaceObjectsInRange:NSMakeRange(startIndex, endIndex-startIndex)
                    withObjectsFromArray:newElements];
    
    _cachedLength = 0;
    
    NSUInteger editLocation = startIndex - (didSplitStart ? 1 : 0);
    NSUInteger editLength = range.length > 0 ? (endIndex - startIndex) : 0;
    _editedRange = NSMakeRange(editLocation, editLength);
    _didSplitEndOnEditing = didSplitEnd;
    _replacementLength = elements.count + (didSplitStart ? 1 : 0) + (didSplitEnd ? 1 : 0);
    
    [self fixElements];
    [self didChangeElementsInRangePath:[[MPRangePath alloc] initWithRange:_editedRange]
                     replacementLength:_replacementLength];
}

- (NSArray *)elements
{
    return _elements;
}

- (NSDecimalNumber *)evaluateWithError:(MPParseError *__autoreleasing *)error
{
    return [self.evaluator evaluateWithError:error];
}

@synthesize evaluator = _evaluator;
- (MPExpressionEvaluator *)evaluator
{
    if (!_evaluator) {
        _evaluator = [[MPExpressionEvaluator alloc] initWithExpression:self];
    }
    return _evaluator;
}

#pragma mark Notifications
- (void)didChangeElementsInRangePath:(MPRangePath *)rangePath
                   replacementLength:(NSUInteger)replacementLength
{
    NSUInteger selfIndex = [self.parent indexOfChild:self];
    MPRangePath *newPath = MPMakeRangePath([rangePath.location indexPathByPreceedingIndex:selfIndex], rangePath.length);
    [self.parent didChangeElementsInRangePath:newPath
                            replacementLength:replacementLength];
}

#pragma mark Basic NSObject Methods
/*
- (BOOL)isEqual:(id)object
{
    if (self == object) {
        return YES;
    }
    if (object == nil) {
        return NO;
    }
    if (![object isKindOfClass:[MPExpression class]]) {
        return NO;
    }
    return [self isEqualToExpression:(MPExpression *)object];
}

- (BOOL)isEqualToExpression:(MPExpression *)anExpression
{
    return [self.elements isEqualToArray:anExpression.elements];
}
*/
- (NSString *)description
{
#warning Bad Implementation
    NSMutableString *description = [[NSMutableString alloc] init];
    NSUInteger index = 0;
    for (id element in self.elements) {
        if ([element isString]) {
            NSMutableString *correctedSymbol = [[element stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] mutableCopy];
            // Prefix operator
            if (element != self.elements[0]) {
                unichar prefix = [correctedSymbol characterAtIndex:0];
                if ([[NSCharacterSet decimalDigitCharacterSet] characterIsMember:prefix]) {
                    [correctedSymbol insertString:@"*"
                                          atIndex:0];
                }
            }
            // Suffix operator
            if (element != [self.elements lastObject]) {
                unichar suffix = [correctedSymbol characterAtIndex:correctedSymbol.length-1];
                if ([[NSCharacterSet decimalDigitCharacterSet] characterIsMember:suffix]) {
                    [correctedSymbol appendString:@"*"];
                }
            }
            [description appendString:correctedSymbol];
        } else if (index > 0 && [self.elements[index-1] isKindOfClass:[MPFunction class]]) {
            [description appendFormat:@"*%@", [element description]];
        } else {
            [description appendString:[element description]];
        }
        index++;
    }
    return description;
}

- (NSUInteger)hash
{
    return [self.elements hash];
}

#pragma mark - NSCopying
- (id)copyWithZone:(NSZone *)zone
{
    MPExpression *copy = [[MPExpression allocWithZone:zone] initWithElements:self.elements];
    return copy;
}

#pragma mark - NSCoding
- (id)initWithCoder:(NSCoder *)aDecoder
{
    // TODO: Test Coding
    return [self initWithElements:[aDecoder decodeObject]];
}

- (void)encodeWithCoder:(NSCoder *)aCoder
{
    [aCoder encodeObject:self.elements];
}

@end

@implementation MPExpression (MPExpressionExtension)

#pragma mark Working With the Expression Tree
- (MPExpression *)rootExpression
{
    if (self.parent == nil) {
        return self;
    }
    return [self.parent rootExpression];
}

- (id)elementAtIndexPath:(NSIndexPath *)indexPath
{
    if (indexPath.length == 0) {
        return self;
    }
    id<MPExpressionElement> element = [self elementAtIndex:[indexPath indexAtPosition:0]];
    if (indexPath.length == 1) {
        return element;
    }
    if ([element isFunction]) {
        return [(MPFunction *)element elementAtIndexPath:[indexPath indexPathByRemovingFirstIndex]];
    }
    return nil;
}

- (NSArray *)elementsInRangePath:(MPRangePath *)rangePath
{
    MPExpression *targetExpression = [self elementAtIndexPath:[rangePath.location indexPathByRemovingLastIndex]];
    return [targetExpression elementsInRange:rangePath.rangeAtLastIndex];
}

- (NSIndexPath *)indexPath
{
    if (self.parent) {
        NSUInteger selfIndex = [self.parent indexOfChild:self];
        return [[self.parent indexPath] indexPathByAddingIndex:selfIndex];
    } else {
        return [[NSIndexPath alloc] init];
    }
}

#pragma mark Working With Expressions
- (MPExpression *)subexpressionFromLocation:(NSUInteger)from
{
    return [self subexpressionWithRange:NSMakeRange(from, self.length - from)];
}

- (MPExpression *)subexpressionToLocation:(NSUInteger)to
{
    return [self subexpressionWithRange:NSMakeRange(0, to)];
}

- (MPExpression *)subexpressionWithRange:(NSRange)range
{
    MPExpression *subexpression = [self copy];
    NSRange preceedingRange = NSMakeRange(0, range.location);
    NSUInteger firstOut = NSMaxRange(range);
    NSRange exceedingRange = NSMakeRange(firstOut, self.length-firstOut);
    [subexpression deleteElementsInRange:exceedingRange];
    [subexpression deleteElementsInRange:preceedingRange];
    return subexpression;
}

#pragma mark Mutating Expressions
- (void)appendElement:(id<MPExpressionElement>)anElement
{
    [self appendElements:@[anElement]];
}

- (void)appendElements:(NSArray *)elements
{
    [self replaceSymbolsInRange:NSMakeRange(self.length, 0) withElements:elements];
}

- (void)insertElement:(id<MPExpressionElement>)anElement
           atLocation:(NSUInteger)location
{
    [self insertElements:@[anElement] atLocation:location];
}

- (void)insertElements:(NSArray *)elements
            atLocation:(NSUInteger)location
{
    [self replaceSymbolsInRange:NSMakeRange(location, 0) withElements:elements];
}

- (void)deleteElementsInRange:(NSRange)range
{
    [self replaceSymbolsInRange:range withElements:@[]];
}

@end
