import { includes, cloneDeep } from 'lodash';

// The computation memo is an in-memory data structure used only during the
// computation process
export default class ComputationMemo {
  constructor(props){
    this.statsByVariableName = {};
    this.extraStatsByVariableName = {};
    this.statsById = {};
    this.originalPropsById = {};
    this.propsById = {};
    this.skillsByAbility = {};
    this.unassignedEffects = [];
    this.classLevelsById = {};
    this.classes = {};
    this.togglesById = {};
    this.toggleIds = new Set();
    // First note all the ids of all the toggles
    props.forEach((prop) => {
      if (
        prop.type === 'toggle'
      ) {
        this.toggleIds.add(prop._id);
      }
    });
    props.filter((prop) => {
      if (
        prop.type === 'toggle'
      ) {
        this.addToggle(prop);
      } else {
        return true;
      }
    }).filter((prop) => {
      if (
        prop.type === 'attribute' ||
        prop.type === 'skill'
      ) {
        // Add all the stats
        this.addStat(prop);
      } else {
        return true;
      }
    }).forEach((prop) => {
      // Now add all effects and proficiencies
      if (prop.type === 'effect'){
        this.addEffect(prop);
      } else if (prop.type === 'proficiency') {
        this.addProficiency(prop);
      } else if (prop.type === 'classLevel'){
        this.addClassLevel(prop);
      }
    });
  }
  registerProperty(prop){
    this.originalPropsById[prop._id] = cloneDeep(prop);
    this.propsById[prop._id] = prop;
    prop.computationDetails = propDetails(prop);
    prop.ancestors.forEach(ancestor => {
      if (this.toggleIds.has(ancestor.id)){
        prop.computationDetails.toggleAncestors.push(ancestor.id);
      }
    });
    return prop;
  }
  addToggle(prop){
    prop = this.registerProperty(prop);
    this.togglesById[prop._id] = prop;
  }
  addClassLevel(prop){
    prop = this.registerProperty(prop);
    this.classLevelsById[prop._id] = prop;
  }
  addStat(prop){
    let variableName = prop.variableName;
    if (!variableName) return;
    let existingStat = this.statsByVariableName[variableName];
    if (existingStat){
      existingStat.computationDetails.idsOfSameName.push(prop._id);
      this.originalPropsById[prop._id] = cloneDeep(prop);
      if (prop.baseValueCalculation){
        existingStat.computationDetails.effects.push({
          operation: 'base',
          calculation: prop.baseValueCalculation,
          stats: [variableName],
          computationDetails: propDetailsByType.effect(),
          statBase: true,
        });
      }
      if (prop.baseProficiency){
        existingStat.computationDetails.proficiencies.push({
          value: prop.baseProficiency,
          stats: [variableName],
          computationDetails: propDetailsByType.proficiency(),
          type: 'proficiency',
          statBase: true,
        });
      }
    } else {
      prop = this.registerProperty(prop);
      this.statsById[prop._id] = prop;
      this.statsByVariableName[variableName] = prop;
      if (
        prop.type === 'skill' &&
        isSkillCheck(prop) &&
        prop.ability
      ){
        this.addSkillToAbility(prop, prop.ability)
      }
    }
  }
  addSkillToAbility(prop, ability){
    if (!this.skillsByAbility[ability]){
      this.skillsByAbility[ability] = [];
    }
    this.skillsByAbility[ability].push(prop);
  }
  addEffect(prop){
    prop = this.registerProperty(prop);
    let targets = this.getEffectTargets(prop);
    targets.forEach(target => {
      if (target.computationDetails.effects){
        target.computationDetails.effects.push(prop);
      }
    });
    if (!targets.size){
      this.unassignedEffects.push(prop);
    }
  }
  getEffectTargets(prop){
    let targets = new Set();
    if (!prop.stats) return targets;
    prop.stats.forEach((statName) => {
      let target = this.statsByVariableName[statName];
      if (!target) return;
      targets.add(target);
      if (isSkillOperation(prop) && isAbility(target)){
        let extras = this.skillsByAbility[statName] || [];
        extras.forEach(ex =>{
          // Only pass on ability effects to skills and checks
          if (ex.skillType === 'skill' || ex.skillType === 'check'){
            targets.add(ex)
          }
        });
      }
    });
    return targets;
  }
  addProficiency(prop){
    prop = this.registerProperty(prop);
    let targets = this.getProficiencyTargets(prop);
    targets.forEach(target => {
      target.computationDetails.proficiencies.push(prop);
    });
  }
  getProficiencyTargets(prop){
    let targets = new Set();
    if (!prop.stats) return targets;
    prop.stats.forEach(statName => {
      let target = this.statsByVariableName[statName];
      if (!target) return;
      targets.add(target);
      if (isAbility(target)) {
        let extras = this.skillsByAbility[statName] || [];
        extras.forEach(ex =>{
          // Only pass on ability proficiencies to skills and checks
          if (ex.skillType === 'skill' || ex.skillType === 'check'){
            targets.add(ex)
          }
        });
      }
    });
    return targets;
  }
}

function isAbility(prop){
  return prop.type === 'attribute' &&
  prop.attributeType === 'ability'
}

function isSkillCheck(prop){
  return includes(['skill', 'check', 'save', 'utility'], prop.skillType);
}

const skillOperations = [
  'advantage',
  'disadvantage',
  'passiveAdd',
  'fail',
  'conditional',
  'rollBonus',
];

function isSkillOperation(prop){
  return skillOperations.includes(prop.operation);
}

function propDetails(prop){
  return propDetailsByType[prop.type]() || {};
}

const propDetailsByType = {
  toggle(){
    return {
      computed: false,
      busyComputing: false,
      toggleAncestors: [],
      disabledByToggle: false,
    };
  },
  attribute(){
    return {
      computed: false,
      busyComputing: false,
      effects: [],
      toggleAncestors: [],
      disabledByToggle: false,
      idsOfSameName: [],
    };
  },
  skill(){
    return {
      computed: false,
      busyComputing: false,
      effects: [],
      proficiencies: [],
      toggleAncestors: [],
      disabledByToggle: false,
      idsOfSameName: [],
    };
  },
  effect(){
    return {
      computed: false,
      busyComputing: false,
      toggleAncestors: [],
      disabledByToggle: false,
    };
  },
  classLevel(){
    return {
      computed: true,
      toggleAncestors: [],
      disabledByToggle: false,
    };
  },
  proficiency(){
    return {
      toggleAncestors: [],
      disabledByToggle: false,
    };
  },
}
