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

#import "MPExpression.h"

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

#import "MPRangePath.h"

#import "MPExpressionTokenizer.h"
#import "MPToken.h"
#import "MPExpressionParser.h"
#import "MPParsedExpression.h"

#import "NSIndexPath+MPAdditions.h"


NSString *const MPIllegalElementException = @"Illegal Element Exception";
NSString *const MPIllegalElementExceptionElementKey = @"MPIllegalElementExceptionElementKey";



@interface MPExpression () {
    NSMutableArray * _elements;
}

/*!
    @method     tokens
    @abstract   Private method. Returns an array containing all tokens from the
                receiver.
 
    @return     All items in the <code>MPTokenReferenceFrame</code>.
 */
- (NSArray *)tokens;


/*!
    @method     validateElements:
    @abstract   Private method. Checks whether all objects in the specified array
                are valid expression elements.

    @discussion If an object is not valid a
                <code>MPIllegalElementException</code> is raised.
 
    @param      elements
                The array of objects to be validated.
 */
- (void)validateElements:(NSArray *)elements;


/*!
    @method     fixElements
    @abstract   Private method. Restores consistency in the receiver after a
                change was made.
 */
- (void)fixElements;


/*!
    @method     _replaceSymbolsInRange:withElements:
    @abstract   Private method. Replaces the symbols in the specified range with
                the elements from the <code>elements</code> array.

    @discussion This is the most primitive mutation method of the @c MPExpression
                class.
 
    @param      range
                The range of symbols to be replaced. The range is specified in 
                the symbol reference frame. If the range exceeds the receiver's
                bounds a <code>NSRangeException</code> is raised.
 
    @param      elements
                The elements that should replace the symbols in the specified
                range.
 */
- (void)_replaceSymbolsInRange:(NSRange)range
                  withElements:(NSArray *)elements;


/*!
    @method     _splitElementsAtLocation:insertionIndex:
    @abstract   Splits the receiver's elements at the specified
                <code>location</code>.

    @discussion The split location can be inside a string element. In that case
                the string is replaced with two other smaller strings.
 
                Splitting the elements destroys the receiver's integrity. The
                receiver of this message must also receive a
                <code>-fixElements</code> message soon afterwards.
 
    @param      location
                The index where to split the elements. Specified in the symbol
                reference frame.
 
    @param      insertionIndex
                Splitting elements may shift the indexes of the following
                elements. This parameter is set to the index (specified in the
                element reference frame) that should be used to insert a new 
                element at the location where the elements were previously
                splitted.
 
    @return     @c YES if a string element was split into two smaller strings,
                <code>NO</code> if the split <code>location</code> corresponds to
                a location between two elements.
 */
- (BOOL)_splitElementsAtLocation:(NSUInteger)location
                  insertionIndex:(NSUInteger *)insertionIndex;

@end



@implementation MPExpression {
    NSArray *_tokenCache;
    
    NSRange _editedRange;
    BOOL _didSplitStartOnEditing;
    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) {
        [self validateElements:elements];
        _elements = [[NSMutableArray alloc] initWithArray:elements
                                                copyItems:YES];
        [self fixElements];
    }
    return self;
}


#pragma mark Private Methods


- (NSArray *)tokens
{
    if (!_tokenCache) {
        _tokenCache = [MPExpressionTokenizer tokenizeExpression:self];
    }
    return _tokenCache;
}


- (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}];
        }
    }
}


- (void)fixElements
{
    for (NSUInteger index = 0; index < _elements.count; index++) {
        id<MPExpressionElement> next = index+1 < _elements.count ? _elements[index+1] : nil;
        id<MPExpressionElement> current = _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 && !_didSplitStartOnEditing) {
                    --_editedRange.location;
                    ++_editedRange.length;
                    if (index == maxReplacementIndex-1 && !_didSplitEndOnEditing) {
                        ++_editedRange.length;
                        ++_replacementLength;
                    }
                } else if (index >= _editedRange.location && index < maxReplacementIndex - 1) {
                    --_replacementLength;
                } else if (index == maxReplacementIndex-1 && !_didSplitEndOnEditing) {
                    ++_editedRange.length;
                }
                --index;
            }
        } else {
            [(MPFunction *)current setParent:self];
        }
    }
}


#pragma mark Querying Expressions


- (MPExpression *)rootExpression
{
    if (self.parent == nil) {
        return self;
    }
    return [self.parent rootExpression];
}


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


- (NSUInteger)countItemsInReferenceFrame:(MPReferenceFrame)referenceFrame
{
    switch (referenceFrame) {
        case MPElementReferenceFrame:
            return _elements.count;
            
        case MPSymbolReferenceFrame:
        {
            NSUInteger count = 0;
            for (id<MPExpressionElement> element in _elements) {
                count += element.length;
            }
            return count;
        }
            
        case MPTokenReferenceFrame:
            return self.tokens.count;
    }
}


- (id)itemAtIndex:(NSUInteger)anIndex
   referenceFrame:(MPReferenceFrame)referenceFrame
{
    switch (referenceFrame) {
        case MPElementReferenceFrame:
            return _elements[anIndex];
            
        case MPSymbolReferenceFrame:
        {
            NSUInteger offsetInElement;
            NSUInteger elementIndex = [self convertIndex:anIndex
                                      fromReferenceFrame:MPSymbolReferenceFrame
                                        toReferenceFrame:MPElementReferenceFrame
                                                  offset:&offsetInElement];
            id<MPExpressionElement> element = [self elementAtIndex:elementIndex];
            if ([element isFunction]) {
                return element;
            } else {
                return [((NSString *)element) substringWithRange:NSMakeRange(offsetInElement, 1)];
            }
        }
            
        case MPTokenReferenceFrame:
            return self.tokens[anIndex];
    }
}


- (id<MPExpressionElement>)elementAtIndex:(NSUInteger)anIndex
                           referenceFrame:(MPReferenceFrame)referenceFrame
{
    NSUInteger elementIndex = [self convertIndex:anIndex
                              fromReferenceFrame:referenceFrame
                                toReferenceFrame:MPElementReferenceFrame];
    return _elements[elementIndex];
}


- (NSUInteger)indexOfElement:(id<MPExpressionElement>)element
{
    return [_elements indexOfObject:element];
}


- (NSArray *)itemsInRange:(NSRange)aRange
           referenceFrame:(MPReferenceFrame)referenceFrame
{
    MPExpression *subexpression = [self subexpressionWithRange:aRange
                                                referenceFrame:referenceFrame];
    return [subexpression allItemsInReferenceFrame:referenceFrame];
}


- (NSArray *)allItemsInReferenceFrame:(MPReferenceFrame)referenceFrame
{
    switch (referenceFrame) {
        case MPElementReferenceFrame:
            return _elements;
            
        case MPSymbolReferenceFrame:
        {
            NSMutableArray *symbols = [[NSMutableArray alloc] init];
            for (id<MPExpressionElement> element in _elements) {
                if ([element isString]) {
                    for (NSUInteger i = 0; i < [element length]; i++) {
                        NSString *ichar  = [NSString stringWithFormat:@"%c", [((NSString *)element) characterAtIndex:i]];
                        [symbols addObject:ichar];
                    }
                } else {
                    [symbols addObject:element];
                }
            }
        }
            
        case MPTokenReferenceFrame:
            return self.tokens;
    }
}


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


- (NSUInteger)convertIndex:(NSUInteger)anIndex
        fromReferenceFrame:(MPReferenceFrame)fromReferenceFrame
          toReferenceFrame:(MPReferenceFrame)toReferenceFrame
{
    return [self convertIndex:anIndex
           fromReferenceFrame:fromReferenceFrame
             toReferenceFrame:toReferenceFrame
                       offset:NULL];
}


- (NSUInteger)convertIndex:(NSUInteger)anIndex
        fromReferenceFrame:(MPReferenceFrame)fromReferenceFrame
          toReferenceFrame:(MPReferenceFrame)toReferenceFrame
                    offset:(NSUInteger *)offset
{
    if (fromReferenceFrame == toReferenceFrame || anIndex == 0) {
        if (offset) {
            *offset = 0;
        }
        return anIndex;
    }
    
    NSUInteger symbolIndex __block = 0;
    switch (fromReferenceFrame) {
        case MPElementReferenceFrame:
            [_elements enumerateObjectsUsingBlock:^(id<MPExpressionElement> obj, NSUInteger idx, BOOL *stop) {
                symbolIndex += obj.length;
                *stop = idx >= anIndex - 1;
            }];
            break;
            
        case MPSymbolReferenceFrame:
            symbolIndex = anIndex;
            break;
            
        case MPTokenReferenceFrame:
            [self.tokens enumerateObjectsUsingBlock:^(id<MPToken> obj, NSUInteger idx, BOOL *stop) {
                symbolIndex += obj.range.length;
                *stop = idx >= anIndex - 1;
            }];
            break;
    }
    
    switch (toReferenceFrame) {
        case MPElementReferenceFrame:
        {
            NSUInteger totalLength = 0;
            NSUInteger elementIndex = 0;
            id<MPExpressionElement> element;
            while (totalLength < symbolIndex) {
                element = _elements[elementIndex++];
                totalLength += element.length;
            }
            --elementIndex;
            NSUInteger offsetInElement = element.length - totalLength + symbolIndex;
            if (offsetInElement == element.length) {
                offsetInElement = 0;
                elementIndex++;
            }
            if (offset) {
                *offset = offsetInElement;
            }
            return elementIndex;
        }
            
        case MPSymbolReferenceFrame:
            if (offset) {
                *offset = 0;
            }
            return symbolIndex;
            
        case MPTokenReferenceFrame:
        {
            NSUInteger totalLength = 0;
            NSUInteger tokenIndex = 0;
            id<MPToken> token;
            while (totalLength < symbolIndex) {
                token = self.tokens[tokenIndex++];
                totalLength += token.range.length;
            }
            --tokenIndex;
            NSUInteger offsetInToken = token.range.length - totalLength + symbolIndex;
            if (offsetInToken == token.range.length) {
                offsetInToken = 0;
                tokenIndex++;
            }
            if (offset) {
                *offset = offsetInToken;
            }
            return tokenIndex;
        }
    }
}


- (NSRange)convertRange:(NSRange)aRange
     fromReferenceFrame:(MPReferenceFrame)fromReferenceFrame
       toReferenceFrame:(MPReferenceFrame)toReferenceFrame
{
    return [self convertRange:aRange
           fromReferenceFrame:fromReferenceFrame
             toReferenceFrame:toReferenceFrame
                leadingOffset:NULL
               trailingOffset:NULL];
}


- (NSRange)convertRange:(NSRange)aRange
     fromReferenceFrame:(MPReferenceFrame)fromReferenceFrame
       toReferenceFrame:(MPReferenceFrame)toReferenceFrame
          leadingOffset:(NSUInteger *)leadingOffset
         trailingOffset:(NSUInteger *)trailingOffset
{
    NSUInteger start = [self convertIndex:aRange.location
                       fromReferenceFrame:fromReferenceFrame
                         toReferenceFrame:toReferenceFrame
                                   offset:leadingOffset];
    NSUInteger end = [self convertIndex:NSMaxRange(aRange)
                     fromReferenceFrame:fromReferenceFrame
                       toReferenceFrame:toReferenceFrame
                                 offset:trailingOffset];
    return NSMakeRange(start, end - start);
}


#pragma mark Mutating Expressions


- (void)replaceItemsInRange:(NSRange)aRange
             referenceFrame:(MPReferenceFrame)referenceFrame
               withElements:(NSArray *)elements
{
    NSUInteger start = [self convertIndex:aRange.location
                       fromReferenceFrame:referenceFrame
                         toReferenceFrame:MPSymbolReferenceFrame];
    NSUInteger end = [self convertIndex:NSMaxRange(aRange)
                     fromReferenceFrame:referenceFrame
                       toReferenceFrame:MPSymbolReferenceFrame];
    [self _replaceSymbolsInRange:NSMakeRange(start, end - start)
                    withElements:elements];
}


- (void)_replaceSymbolsInRange:(NSRange)range
                  withElements:(NSArray *)elements
{
    if (NSMaxRange(range) > [self countItemsInReferenceFrame:MPSymbolReferenceFrame]) {
        @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 (_elements.count == 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];
    NSArray *removedElements = [_elements subarrayWithRange:NSMakeRange(startIndex, endIndex-startIndex)];
    [_elements replaceObjectsInRange:NSMakeRange(startIndex, endIndex-startIndex)
                    withObjectsFromArray:newElements];
    for (id<MPExpressionElement> element in removedElements) {
        if ([element isFunction]) {
            ((MPFunction *)element).parent = nil;
        }
    }
    
    _tokenCache = nil;
    
    NSUInteger editLocation = startIndex - (didSplitStart ? 1 : 0);
    NSUInteger editLength = endIndex - startIndex;
    if (range.length == 0 && didSplitStart) {
        editLength = 1;
    }

    _editedRange = NSMakeRange(editLocation, editLength);
    _didSplitStartOnEditing = didSplitStart;
    _didSplitEndOnEditing = didSplitEnd;
    _replacementLength = elements.count;
    if (didSplitStart) {
        _replacementLength++;
        if (range.length == 0) {
            _replacementLength++;
        }
    }
    if (didSplitEnd) {
        _replacementLength++;
    }

    [self fixElements];
    [self changedElementsInRangePath:[[MPRangePath alloc] initWithRange:_editedRange]
                     replacementLength:_replacementLength];
}


- (BOOL)_splitElementsAtLocation:(NSUInteger)location
                  insertionIndex:(NSUInteger *)insertionIndex
{
    if (location == 0) {
        *insertionIndex = 0;
        return NO;
    }
    
    NSUInteger splitOffset;
    NSUInteger splitElementIndex = [self convertIndex:location
                                   fromReferenceFrame:MPSymbolReferenceFrame
                                     toReferenceFrame:MPElementReferenceFrame
                                               offset:&splitOffset];
    if (splitOffset != 0) {
        NSString *splitElement = (NSString *)_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;
}


- (void)changedElementsInRangePath:(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];
}


- (MPExpression *)subexpressionFromIndex:(NSUInteger)from
                          referenceFrame:(MPReferenceFrame)referenceFrame
{
    return [self subexpressionWithRange:NSMakeRange(from, [self countItemsInReferenceFrame:referenceFrame] - from)
                         referenceFrame:referenceFrame];
}


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


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


#pragma mark Evaluating Expressions


- (MPParsedExpression *)parse:(NSArray *__autoreleasing *)errors
{
    return [self parseExpectingVariable:NO
                                 errors:errors];
}


- (MPParsedExpression *)parseExpectingVariable:(BOOL)flag
                                        errors:(NSArray *__autoreleasing *)errors
{
    return [[[MPExpressionParser alloc] initWithExpression:self] parseExpectingVariableDefinition:flag
                                                                                           errors:errors];
}


#pragma mark Basic NSObject Methods


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


- (NSUInteger)hash
{
    return [_elements hash];
}


#pragma mark - NSCopying


- (id)copyWithZone:(NSZone *)zone
{
    MPExpression *copy = [[MPExpression allocWithZone:zone] initWithElements:_elements];
    return copy;
}


#pragma mark - NSCoding


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


- (void)encodeWithCoder:(NSCoder *)aCoder
{
    [aCoder encodeObject:_elements];
}

@end



@implementation MPExpression (MPExpressionConvenience)

#pragma mark Querying Expressions


- (NSUInteger)countElements
{
    return [self countItemsInReferenceFrame:MPElementReferenceFrame];
}


- (NSUInteger)countSymbols
{
    return [self countItemsInReferenceFrame:MPSymbolReferenceFrame];
}


- (NSUInteger)countTokens
{
    return [self countItemsInReferenceFrame:MPTokenReferenceFrame];
}


- (id<MPExpressionElement>)elementAtIndex:(NSUInteger)anIndex
{
    return [self itemAtIndex:anIndex
              referenceFrame:MPElementReferenceFrame];
}


- (id<MPExpressionElement>)symbolAtIndex:(NSUInteger)anIndex
{
    return [self itemAtIndex:anIndex
              referenceFrame:MPSymbolReferenceFrame];
}


- (id<MPToken>)tokenAtIndex:(NSUInteger)anIndex
{
    return [self itemAtIndex:anIndex
              referenceFrame:MPTokenReferenceFrame];
}


#pragma mark Mutating Expressions


- (void)appendElement:(id<MPExpressionElement>)anElement
{
    [self appendElements:@[anElement]];
}


- (void)appendElements:(NSArray *)elements
{
    [self replaceItemsInRange:NSMakeRange([self countItemsInReferenceFrame:MPSymbolReferenceFrame], 0)
               referenceFrame:MPSymbolReferenceFrame
                 withElements:elements];
}


- (void)insertElement:(id<MPExpressionElement>)anElement
              atIndex:(NSUInteger)index
       referenceFrame:(MPReferenceFrame)referenceFrame
{
    [self insertElements:@[anElement]
                 atIndex:index
          referenceFrame:referenceFrame];
}


- (void)insertElements:(NSArray *)elements
               atIndex:(NSUInteger)index
        referenceFrame:(MPReferenceFrame)referenceFrame
{
    [self replaceItemsInRange:NSMakeRange(index, 0)
               referenceFrame:referenceFrame
                 withElements:elements];
}


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

@end
