//
//  dice.m
//  longspear
//
//  Created by Niko Matsakis on Sat Jun 12 2004.
//  Copyright (c) 2004 __MyCompanyName__. All rights reserved.
//

#import "dice.h"


@implementation NSString (Dice)

/* Dice rolls basically look like the following
grammar:

number = [0-9]+             # some number of digits
dice   = number 'd' number  # 1d20
oper   = '+'
       | '-'
operr  = oper number
       | oper dice
roll   = oper number plusr*
       | dice plusr*

So basically a roll always starts with either '1d20' or 
'+3'.  It may then be continued by any number of 
additions of either constants of what have you.
*/

static BOOL _isdigit (unichar c)
{
    return (c >= '0' && c <= '9');
}

static BOOL _isoper (unichar c)
{
    return c == '+' || c == '-';
}

static int OperValue (unichar oper, int c)
{
    if (oper == '+') return c;
    return -c;
}

static int CheckForDieOrNum (NSString *self, int *idx, BOOL* wasroll);

static int CheckForOper (NSString *self, int numval, int *idx)
{
    // check for a + followed by a digit or a die roll spec
    // returns the value of what it finds, added to numval.
    // if numval is INT_MAX, then we'll clear it to zero before
    // adding the result.  The idea is that you can pass in
    // INT_MAX to tell if we found anything.
    
    int i = *idx, len = [self length];
    if (i < len) {
        unichar c = [self characterAtIndex:i];
        if (_isoper (c)) {
            int newidx = i+1;
            int tofoll = CheckForDieOrNum (self, &newidx, NULL);
            if (tofoll != INT_MAX) {
                *idx = newidx;
                if (numval == INT_MAX) numval = 0;
                numval += OperValue (c, tofoll);
            }
        }
    }
    
    return numval;
}    

int RollDie (int max)
{
    //return (rand () % max)+1;
    double r = rand ();
    double percentage = r / (double)RAND_MAX;
    double result = percentage * max;
    return floor(result) + 1;
}

static unichar ScanDigits (NSString *self, int startidx, NSRange *range)
{
    int len = [self length];
    range->location = startidx;
    
    unichar c = 0;
    while (startidx < len) {
        c = [self characterAtIndex:startidx];
        if (!_isdigit (c)) break;
        startidx++;
        c = 0;
    }
    
    range->length = startidx - range->location;
    return c; /* return non-digit char, or 0 if end of string */
}

static int CheckForDieOrNum (NSString *self, int *idx, BOOL* wasdie)
{
    int i = *idx;
    
    if (wasdie) *wasdie = NO;

    NSRange number, denom, type;
    
    unichar c = ScanDigits (self, i, &number);
    i += number.length;
    
    // if we found some digits we at least have a number,
    // possibly a dice roll
    if (number.length > 0) {
        // Get value of number as integer and advance '*idx' to skip
        // those digits.
        int numval = [[self substringWithRange:number] intValue];
        int denomval = 1;
        
        // check for fractions (sometimes people write '1/2d8', for example)
        if (c == '/') {
            // find number after the '/'
            i++; // consume '/'
            c = ScanDigits (self, i, &denom);
            i += denom.length;
            denomval = [[self substringWithRange:denom] intValue];
        }
        
        if (c == 'd' || c == 'D') {
            i++; // consume 'd'
            
            // Now scan for digits followed the 'd'
            c = ScanDigits (self, i, &type);
            
            if (type.length > 0) {
                int typeval = [[self substringWithRange:type] intValue];
                
                // roll the dice with typeval sides numval times:
                int result = 0;
                while (numval--) result += RollDie (typeval);
                
                // skip past all the characters involved
                *idx = i;
                
                // apply any denominator we may have found
                if (denomval != 1) result /= denomval;

                if (wasdie) *wasdie = YES; // note that we found a die
                                
                // check for any other things we need to add on
                return CheckForOper (self, result, idx);
            }            
        }
        
        // if we get here we either did not find a 'd' after
        // the digits or did not find another number after the
        // 'd'.  Either way, consume just the digits we found
        // and check for a '+' after them.  Note that we ignore
        // any denominators unless we completed a die roll.
        *idx = number.location + number.length;
        return CheckForOper (self, numval, idx);
    }
    
    // did not find any die roll.  return nil and do not change
    // *idx
    return INT_MAX;
}

static int 
CheckForRoll (NSString *self, int *idx, BOOL rollbonuses, BOOL *wasroll)
{
    unichar c = [self characterAtIndex:*idx];
    int roll = INT_MAX;

    if (wasroll) *wasroll = NO;

    if (_isoper (c)) {
        roll = CheckForOper (self, INT_MAX, idx);
        if (roll != INT_MAX && rollbonuses) {
            roll += RollDie (20);
            if (wasroll) *wasroll = YES;
        }
    }
    else if (_isdigit (c)) {
        roll = CheckForDieOrNum (self, idx, wasroll);
    }
    
    return roll;
}

- (NSAttributedString*) interpretDiceValuesWithRepeatPattern:(NSArray*)inpattern 
                                             savingPatternTo:(NSArray**)_outpattern
{
    int i, len = [self length];
    
    NSMutableAttributedString *result = 
        [[[NSMutableAttributedString alloc] init] autorelease];
        
    NSMutableArray *outpattern = [[[NSMutableArray alloc] initWithCapacity:32] autorelease];
    if (_outpattern) *_outpattern = outpattern;
    
    if (len == 0) {
        /* empty string? */
        return result;
    }
        
    /* first check whether the string is actually just a number */
    for (i = 0; i < len; i++) {
        unichar c = [self characterAtIndex:i];
        if (!isdigit (c) && !isspace (c))
            break;
    }
    if (i == len) {
        /* if the string is just a number, interpret it as 1d20 + the number */
        [result appendAttributedString:
            [[[NSAttributedString alloc] initWithString:
                [NSString stringWithFormat:@"%s ", self]] autorelease]];
        [result appendAttributedString:
            [[[NSAttributedString alloc] initWithString:
                [NSString stringWithFormat:@"(%d)", (RollDie (20)+[self intValue])]
                                             attributes:
                [NSDictionary dictionaryWithObjectsAndKeys:[NSColor redColor], NSForegroundColorAttributeName, nil]]
                autorelease]];
        [outpattern addObject:[NSNumber numberWithInt:1]];
        return result;
    }

    // initialize the result with the contents of our string with no attributes
    [result appendAttributedString:
        [[[NSAttributedString alloc] initWithString:self] autorelease]];

    // now iterate through looking for dicerolls or bonuses preceded by a plus or minus
    int indexadjust = 0;
    int multiple = -1;
    int rollcnt = 0;
    for (i = 0; i < len; i++) {
        // Check for the beginnings of the various
        // parts of the grammar above:
        int nextidx = i;
        BOOL wasroll;
        int roll = CheckForRoll (self, &nextidx, YES, &wasroll);
        
        // At this point roll contains INT_MAX if we didn't find
        // a roll or number; if roll is not intmax, then wasroll is
        // YES if we found a roll or bonus (like 1d20 or +3), and 
        // wasroll is NO if it was a plain number.
        
        if (roll == INT_MAX) {
            i = nextidx;
            continue;
        }
        
        // no rolls can be less than zero...
        if (roll < 0) {
            roll = 1;
        }
        
        // If this was just a plain number, we remember it as a "multiple"
        // This means the next roll we find we will roll n times.
        // This handles things like: "4 Claw +1d4".
        if (!wasroll) {
            multiple = roll;
            i = nextidx;
            continue;
        }
        
        // 'i' is not positioned after the dice or bonus spec
        // now we must insert " (%d)" with the roll, but do it in
        // red
        NSMutableString *rollstr = [[[NSMutableString alloc] initWithCapacity:32] autorelease];

        // if we didn't find an explicit multipler, search the pattern
        if (multiple == -1) {
            if (inpattern && rollcnt < [inpattern count]) {
                multiple = [[inpattern objectAtIndex:rollcnt] intValue];
            }
            else {
                multiple = 1;
            }
        }
        
        // add the multiple to the out pattern
        [outpattern addObject:[NSNumber numberWithInt:multiple]];
        rollcnt++;
        
        [rollstr appendString:[NSString stringWithFormat:@" (%d", roll]];
        while (--multiple) {
            nextidx = i;
            roll = CheckForRoll (self, &nextidx, YES, NULL); // nextidx will always turn out the same
            [rollstr appendString:[NSString stringWithFormat:@",%d", roll]];
        }
        [rollstr appendString:@")"];

        i = nextidx;
        
        NSDictionary *attrs = 
            [NSDictionary dictionaryWithObjectsAndKeys:[NSColor redColor], NSForegroundColorAttributeName, nil];
        NSAttributedString *attrstr = 
            [[[NSAttributedString alloc] initWithString:rollstr attributes:attrs] autorelease];
        [result insertAttributedString:attrstr atIndex:i+indexadjust];
        indexadjust += [rollstr length];
        multiple = -1;
    }
    
    return result;
}

- (int) rolledValue
{
    // search for the first roll we can find
    int i, len = [self length];
    for (i = 0; i < len; i++) {
        int roll = CheckForRoll (self, &i, NO, NULL);
        if (roll != INT_MAX)
            return (roll < 0 ? 1 : roll);
    }
    
    // can't find a roll?
    return 0;
}

@end
