/** Add bonus to all attack / damage field of given power matching given condition */
chargen.System.Commons.addBonusForEq = function(subject, field, bonus, source, bonustype, condition) {
//  if (power.accessory.indexOf('weapon') ||  >= 0) // Don't waste time checking eq when they are all going to pass...
  let rules = subject.rules;
  if (typeof(condition) == 'string') {
    for each (let part in chargen.MULTI_ATTACKS) {
      let f = field+part;
      if ( subject['equipment'+part] && rules[f] && rules[f].addBonus && subject['equipment'+part].is(condition) )
        rules[f].addBonus(bonus, source, bonustype);
    }

  } else if (typeof(condition) == 'object') {
    for each (let part in chargen.MULTI_ATTACKS) {
      let f = field+part;
      if ( (subject['equipment'+part] == condition) && rules[f] && rules[f].addBonus)
        rules[f].addBonus(bonus, source, bonustype);
    }

  } else if (typeof(condition) == 'function') {
    for each (let part in chargen.MULTI_ATTACKS) {
      let f = field+part;
      if (rules[f] && rules[f].addBonus && condition(subject['equipment'+part], subject))
        rules[f].addBonus(bonus, source, bonustype);
    }

  } else {
    throw "Unrecognised condition type";
  }
}

/** Add bonus to all applicable fields of given power */
chargen.System.Commons.addBonusToAll = function(subject, field, bonus, source, bonustype) {
  let rules = subject.rules;
  for each (let part in chargen.MULTI_ATTACKS) {
    let f = field+part;
    if (rules[f] && rules[f].addBonus) rules[f].addBonus(bonus, source, bonustype);
  }
}

/** Removes bonus from all applicable fields of given power */
chargen.System.Commons.removeBonusFromAll = function(subject, field, source, bonustype) {
  let rules = subject.rules;
  for each (let part in chargen.MULTI_ATTACKS) {
    let f = field+part;
    if (rules[f]) rules[f].removeBonus(source, bonustype);
  }
}

chargen.Rule = {

  // Get first Number for a given rule.  Recursive.
  getFirstNumber : function(op) {
    if (!op) return null;
    if (op.parts) {
      for each (let part in op.parts) { let result = this.getFirstNumber(part); if (result) return result; }
      return null;
    } else if (op.modifiers !== undefined && op.properties !== undefined)
      return op;
    else
      return null;
  },

  /* Parse an associated array of rule string and return a new array in original structure */
  parseAll : function (from, context) {
    var result = {}, regx = null;
    var full = null;

    for each (let [name, text] in Iterator(from)) {
      if (startsWith(name, 'hit_damage'))
        result[name] = this.parseDamage(text, context);
      else if (typeof text == 'object')
        { if (text.getDesc) result[name] = cloneObject(text); } // Clone if seems to be a rule. Ignore otherwise.
//        result[name] = this.parseAll(text, context); // an assoc array, I hope
      else if (typeof text == 'string') {
        result[name] = this.parse(text, context);
      } else
        result[name] = text;
    }
    return result;
  },

  regx_damage: /^(\d*[wW]|\d*[dD]\d+)?(\+[\+\w]+)?$/,
  regx_plus: /\+/g,

  /* Parse power damage into Rule.  Got special handling. */
  parseDamage : function (text, context) {
    if (typeof(text) == 'object') return text;
    let full = null, base, stat;
    let damage = text.match(chargen.Rule.regx_damage);
    if (damage && damage[0]) {
      [full, base, stat] = damage;
      if (base && isNaN(+base[0])) base = '1'+base; // d8 -> 1d8
//      else base = base.toUpperCase();

      // Convert 2W+strM into {{2W||strM||damage,roll}} then parse.
      text = '{{' + (base || '') + (stat ? stat.replace(this.regx_plus, '||') : '' ) + '||damage,roll}}';
      let number = this.parse(text, context);

      // Parsed. Add properties.
      if (context.energy)
        for each (let i in context.energy)
          number.properties.push(i);
      if (context.accessory)
        for each (let i in context.accessory)
          number.properties.push(i);

      return number;
    } else if (text[0] != '{') {
      cerror("Cannot parse damage: " + text);
    }
    return this.parse(text, context);
  },

  /* Parse a long mixed string into Rule, e.g. power hit effect or fancy feat description */
  parse: function(text, context) {
    let rule = chargen.Rule;
    if (!text) return new rule.Constant('');
    let pos = text.indexOf('{{');
    if (pos < 0) return new rule.Constant(text);

    let left = [], right = text;
    while (pos >= 0) {
      if (pos > 0)
        left.push(new rule.Constant(right.slice(0, pos)));
      right = right.slice(pos); // Keep {{ if end tag not found

      pos = right.indexOf('}}');
      if (pos < 0) break;
      if (pos > 0)
        left.push(new rule.Number(right.slice(2, pos), context));
      right = right.slice(pos+2);

      pos = right.indexOf('{{');
    }

    if (right)
      left.push(new rule.Constant(right));
    return left.length <= 1 ? left[0] : new rule.Composite(left);
  },

  /** Turns 'wisM' into target['wisM'] for all stats */
  translate: function(input, target) {
    let result = input;
    if (!result) return result;
    let base = target;
    if (target.abilities) base = target.abilities;
    for each (let stat in chargen.STATS) {
      result = result.replace(new RegExp('\\b'+stat+'Atk\\b', 'g'), base[stat+'Atk'] );
      result = result.replace(new RegExp('\\b'+stat+'Mod\\b', 'g'), target[stat+'Mod'] );
      result = result.replace(new RegExp('\\b'+stat+'M\\b', 'g'), target[stat+'M'] );
      result = result.replace(new RegExp('\\b'+stat+'\\b', 'g'), base[stat] );
    }
    for each (let stat in ['ac','fort','ref','will']) // There is no case where we need them in numbers
      result = result.replace(new RegExp('\\b'+stat+'\\b', 'g'), text[stat]);
/*    // Convert stats into numbers
    if (target.stats) base = target.stats;
    for each (let stat in ['ac','fort','ref','will'])
      if (base[stat].getValue)
        result = result.replace(new RegExp('\\b'+stat+'\\b', 'g'), base[stat].getValue());
      else
        result = result.replace(new RegExp('\\b'+stat+'\\b', 'g'), base[stat]);
*/
    return result;
  }

};


chargen.Rule.Eval = function(expression) {
//  this.expression = expression;
  if (expression.indexOf('|') < 0) expression = expression + '|' + expression;
  [this.calc,this.desc] = expression.split('|', 2);
  this._eval_calc = this.calc.charAt(this.calc.length-1) == ';';
  this._eval_desc = this.desc.charAt(this.desc.length-1) == ';';
};
let (p = chargen.Rule.Eval.prototype) {
  p.Op = function(op, param1, param2, param3) {
    if (op == 'replace') {
/*      if (!this._eval_desc && typeof this.desc == 'string') this.desc = this.desc.replace(param1, param2);
      if (!this._eval_calc && typeof this.calc == 'string') this.calc = this.calc.replace(param1, param2);*/
      this.desc = this.desc.replace(param1, param2);
      this.calc = this.calc.replace(param1, param2);
    }
  };
  p.getDesc  = function(context){ return this._eval_desc ? eval(this.desc, context) : chargen.Rule.translate(this.desc, text); }
  p.getValue = function(context){ return this._eval_calc ? eval(this.calc, context) : chargen.Rule.translate(this.calc, chr ); }
}

chargen.Rule.Constant = function(value) { this.value = value; };
chargen.Rule.Constant.prototype = {
  Op : function(op, param1, param2, param3) {
    if (op == 'replace') {
      if (typeof this.value == 'string') this.value = this.value.replace(param1, param2);
      else if (this.value === param1) this.value = param2;
    };
  },
  getDesc  : function(){ return this.value; },
  getValue : function(){ return this.value; }
}

chargen.Rule.Composite = function(arg) {
  this.parts = [];
  let l = arguments.length;
  for (let i = 0; i < l; i++) {
    let p = arguments[i];
    if (p instanceof chargen.Rule.Composite) // Another composite?
      for each (let j in p.parts)
        this.parts.push(j);
    else if (p.getDesc) // Rule?
      this.parts.push(p);
    else
      for each (let j in p)
        this.parts.push(j);
  }
}
chargen.Rule.Composite.prototype = {
  Op : function(op, param1, param2, param3) { for each (let part in this.parts) part.Op(op, param1, param2, param3); },
  getDesc  : function(context){ var result = ''; for each (let part in this.parts) result += part.getDesc (context); return result;},
  getValue : function(context){ var result = ''; for each (let part in this.parts) result += part.getValue(context); return result;},
  addBonus : function(value, source, type) { for each (let part in this.parts) if (part.addBonus) { part.addBonus(value, source, type); break; } },
  removeBonus : function(source, type) { for each (let part in this.parts) if (part.removeBonus) { part.removeBonus(source, type); break; } }
}



chargen.Rule.Number = function(base, context){
/*
  +{{5}} feat bonus...
  {{2W+Str}}, and you...
  {{5||fire}} + {{5||thunder}}, and you...
  push {{ Math.max(wisM,1); | wisdom modifier (min 1) || push }}
*/
  this.modifiers = {};
  this.properties = [];
  if (base) {
    let property = undefined;
    if (typeof base == 'string') {
      base = trim(base).split(/\s*\|\|\s*/);
      property = base.length > 1 ? base.pop() : undefined;
    } else {
      base = [base];
    }

    let type = '';

    // Property parsing
    if (property) {
      if (property.indexOf('dr') >= 0) // idr >> implement damage roll, wdr >> weapon damage roll, etc.
        property = ('^'+property).replace('^idr', '^dr,implement').replace('^wdr', '^dr,weapon').replace('^dr', '^damage,roll').slice(1);
      if (property.charAt(0) == '"') {
        this.properties = trim(property.slice(1)).split(/\s*,\s*/g);
        property = undefined;
      }
    }
    if ( context && property === undefined && context._type) {
      // Deduce from current context
      if (context._type == 'feat')
        type = 'feat';
      else if (context._type == 'power') {
        // First number in a power, if matches weapon/dice damage, will get processed before and after the parse.
        type = 'power';
      } else if (context._type == 'equipment') {
        type = 'item';
      };
    }
    if (property) {
      this.properties = this.properties.concat(trim(property).split(/\s*,\s*/g));
    }
    if (type) this.properties.unshift(type);

    let source = 'rule_part';
    let i = 1;
    for each (let part in base) {
      if (+part == part) // Number only?
        this.addBonus(+part, source+i);
      else
        this.addBonus(part, source+i);
      type = '';
      ++i;
    }
  }
};
chargen.Rule.Number.prototype = {
  modifiers : null, // Modifier by source. One effect from each source please, this is only a generator.
  stackCache : undefined, // Parsed stack cache
  properties : null,

  // Set a bonus. Type is optional, source can be object or string, must be unique
  addBonus : function(value, source, type) {
    // TODO: Multi-type source
    if (!type) type = '';
    if (typeof source == 'object') source = source.id;
    if (typeof value == 'number')
      value = new chargen.Rule.Constant(value);
    else if (typeof value == 'string')
      value = new chargen.Rule.Eval(value);
    if (!value.type) value.type = type;
    this.modifiers[source] = value;
    this.stackCache = undefined;
    return this;
  },
  removeBonus : function(source, type) {
    if (typeof source == 'object') source = source.id;
    if (this.modifiers[source]) {
      delete this.modifiers[source];
      this.stackCache = undefined;
    }
    return this;
  },

  isStackable : function(type) { return chargen.NO_STACKINGS.indexOf(type) < 0 },

  Op : function(op, param1, param2, param3) {
    if (op == 'set weapon')
      if (this.properties.indexOf('weapon') < 0)
        return;
      else
        this.weapon_damage = param1;
    else
      for each (let [source, mod] in Iterator(this.modifiers))
        mod.Op(op, param1, param2, param3);
  },

  getDesc : function(context) {
    var result = '';
    let mods = this.resolveModifierStack(context);
    if (mods) {
      let mod = chargen.System.modifier;
      for each (let m in mods) {
        let desc = m.getDesc(context);
        if (desc != "") result += mod(desc);
      }
      if (result.charAt(0) == '+') result = result.slice(1);
    }
    result = this._postGet(result, context);
    if (result && this.properties.indexOf('damage') >= 0) {
//      if (!context.energy) result += text.weapon.toLowerCase(); // Default to weapon damage... okey, may be not.
      result += ' ' + text.damage; // Only added in desc mode
    }
    return result;
  },

  getValue : function(context) {
    var result = '';
    let mods = this.resolveModifierStack(context);
    if (mods) {
      let mod = chargen.System.modifier;
      for each (let m in mods) {
        let val = m.getValue(context);
        if (val !== "") result += mod(val);
      }

      if (this.weapon_damage && result.match(/^\+?\d+W\b/)) {
        result = result.replace('w', this.weapon_damage).replace('W', this.weapon_damage);
      }

      // resolves +1+2+3
      let regx = /([+-]\d+)([+-]\d+)/;
      let match = result.match(regx);
      while (match) {
        result = result.replace( match[0], mod( +match[1]+(+match[2]) ) ); // Do not do global replace. May corrupt later modifiers
        match = result.match(regx);
      }

      // resolves 2*2 as in 2*2d6
      regx = /(\d+)\*(\d+)/;
      match = result.match(regx);
      while (match) {
        result = result.replace( match[0], +match[1]*+match[2] ); // Do not do global replace. May corrupt later modifiers
        match = result.match(regx);
      }

    }
    result = this._postGet(result, context);
    if (result && this.properties.indexOf('damage') >= 0 && trim(result) == '') return '0';
    if (result && this.properties.indexOf('damage') >= 0 && this.properties.indexOf('roll') >= 0) {
      // Calculate crit
      let [stat,] = result.split(' ', 2);
      let expr = stat.replace('d', '*'); //.replace('D', '*');
      try {
        let crit = eval(expr);
        if (context.rules && context.rules.critical)
          crit += context.rules.critical.getValue(context);
        result = result + ' (' + crit + ')';
      } catch (err) {
        //cwarn('Problem evaluating critical damage for "' + expr + '": ' + err);
      }
    }
    return result;
  },

  // Do special processing for damage, bonus & non-bonus.
  _postGet : function(result, context) {
    if (result && result[0] == '_') return result.slice(1); // Blank, leave me alone

    // If damage, add elements
    if (this.properties.indexOf('damage') >= 0) {
      result += ' ' + [text[energy].toLowerCase() for each (energy in context.energy)].join(' + ');
    }

    // Eliminates +0
    result = result.replace(/\+0(?=\D|$)/g, '');

    // Normalise modifiers
    if (this.properties.indexOf('bonus') < 0 && this.properties.indexOf('modifier') < 0) {
      // Not a bonus or modifier. Removes leading plus.
      if (result.charAt(0) == '+') result = result.slice(1);
    } else {
      // Add '+' if not set
      if (result == '') result = '+0';
      else if (result.match(/^\d+$/)) result = '+' + result;
      // Bonus? Add bonus type if set.
      if (this.properties.indexOf('bonus') >= 0) {
        for each (let prop in this.properties)
          if (chargen.BONUS_TYPE.indexOf(prop) >= 0) {
            result += ' ' + prop;
            break;
          }
        result += ' ' + text.bonus;
      }
    }

    return result;
  },

  resolveModifierStack : function(context) {
    if (this.stackCache) return this.stackCache;
    var stackingBonus = []; // Bonus that stack, no questions asked
    var bonusByType = {}; // *Constant* bonus by type
    var dices = []; // I cannot handle dice stacking in a chargen
    var penalties = []; // Penalties stacks, one effect from each unique source
    for each (let [source, mod] in Iterator(this.modifiers)) {
      let value = mod.getValue(context), type = mod.type;
      if (isNaN(value) || typeof value != 'number')
        dices.push(mod);
      else if (value > 0)
        if (type && !this.isStackable(type)) {
          if (bonusByType[type] && value <= bonusByType[type].getValue(context)) continue;
          bonusByType[type] = mod;
        } else
          stackingBonus.push(mod);
        else if (value < 0)
          penalties.push(mod);
    }
    this.stackCache = dices.concat([mod for each ([type,mod] in Iterator(bonusByType))]).concat(stackingBonus).concat(penalties);
    return this.stackCache;
  }

};