Source: machine/innerComponents/memory.js

const assert = require('../../tools/assert');
Word = require("../word").Word;

const NB_CHRS_PER_WORD = require("../constants").NB_CHRS_PER_WORD;
const MEMORY_MODE = require("../constants").MEMORY_MODE;

/**
 * A Memory is Word directly connected to the Bullgamma (not through an Octad or a DrumBlock). It possess some
 * computation methods that makes it easier to manipulate for instructions.
 */
class Memory extends Word {

  /**
   * constructs a new instance of Memory
   * @param id the ID of this memory
   * @param bullGamma the machine attached to this memory
   * @param nb_blocks the number of blocks for this memory (same as Word)
   */
  constructor(id, bullGamma, nb_blocks = NB_CHRS_PER_WORD) {
    super(nb_blocks);
    this.id = id;
    this.bullGamma = bullGamma;
  }

  /**
   * @returns {MEMORY_MODE} the computation mode for this Memory
   */
  getMode() {
    return this.bullGamma.getMemoryMode();
  }

  /**
   * Set every memory block in range to 0
   * @param from start index of the selected memory blocks, should be positive or zero
   * @param to end index (excluded) of the selected memory blocks, should be inferior to NB_CHRS_PER_WORD
   */
  setToZero(from, to) {
    assert.equal(from >= 0, true, "from parameter should be superior to 0");
    assert.equal(to <= this.blocks.length, true, "to parameter should be inferior to " + this.blocks.length);

    for (let i = from; i < to; i++) {
      this.blocks[i] = 0;
    }
  }

  /**
   * Set the selected memory block to the given value.
   * If in decimal mode and value is > 9, the value's digits are split then the lower one goes to blocks[idx] while
   * blocks[idx + 1] gets the higher one.
   * @param idx the idx of the the block that should be set, must be positive or zero but inferior to
   * NB_CHRS_PER_WORD
   * @param value the value to which the block should be set, must be positive or zero and inferior to 16.
   */
  setBlockValue(idx, value) {
    assert.equal(idx < this.blocks.length, true, "idx should be inferior to " + this.blocks.length);
    assert.equal(idx >= 0, true, "idx should be not be negative");
    assert.equal(value >= 0, true, "value should not be negative");
    assert.equal(value < 16, true, "value should be inferior to 16");

    if (value >= this.getMode().base) {
      this.blocks[(idx + 1) % this.blocks.length] = 1; // FIXME : += 1 ?
    }
    this.blocks[idx] = value % this.getMode().base;
  }

  /**
   * Copy the selected values from an other memory
   * If the calculator is in decimal mode, only ten's complement values will be copied
   * @param other the other memory from which values will be copied
   * @param from which block index should the copy start from, should be positive and inferior to 12
   * @param to where should the copy end (excluded), should be inferior or equal to 12
   */
  copyBlockValues(other, from=0, to=this.blocks.length) {
    assert.equal(from >= 0, true, "from should be positive");
    assert.equal(to <= this.blocks.length, true, "to should be inferior or equal to " + this.blocks.length);

    for (let i = from; i < to; i++) {
      this.blocks[i] = other.blocks[i] % this.getMode().base;
    }
  }

  /**
   * Every block in the memory gets the value of its right neighbour (index 0 gets value of index 11)
   */
  shiftLeft() {
    let buff = this.blocks[this.blocks.length - 1];
    for (let i = this.blocks.length - 1; i > 0; --i) {
      this.blocks[i] = this.blocks[i - 1];
    }
    this.blocks[0] = buff;
  }

  /**
   * Every block in the memory gets the value of its left neighbour (index 11 gets value of index 0)
   */
  shiftRight() {
    let buff = this.blocks[0];
    for (let i = 0 ; i < this.blocks.length - 1; ++i) {
      this.blocks[i] = this.blocks[i + 1];
    }
    this.blocks[this.blocks.length- 1] = buff;
  }

  /**
   * compare the whole memory to another one between selected blocks
   * @param other the other memory to which it should be compared
   * @param from the starting block from which the comparison should start
   * @param to the end block of the comparison (excluded)
   * @return an array of two booleans, index 0 is true if this is greater than other, index 1 is true if they are equal
   */
  compareTo(other, from, to) {
    assert.equal(from >= 0, true, "from should not be negative");
    assert.equal(from < to, true, "from should be inferior to to");
    assert.equal(to <= this.blocks.length, true, "to should be inferior to the number of blocks per memory");
    let nbDigitsThis = this.blocks.length;
    while (this.blocks[nbDigitsThis - 1] === 0 && nbDigitsThis > 0) {
      --nbDigitsThis;
    }
    let nbDigitsOther = to - from;
    while (this.blocks[from + nbDigitsOther - 1] === 0 && nbDigitsOther > 0) {
      --nbDigitsOther;
    }
    if (nbDigitsThis > nbDigitsOther) {
      return [true, false];
    }
    if (nbDigitsThis < nbDigitsOther) {
      return [false, false];
    }
    for (let i = 0; i < nbDigitsThis; ++i) {
      if (this.blocks[nbDigitsThis - i - 1] > other.blocks[to - i - 1]) {
        return [true, false];
      }
      if (this.blocks[nbDigitsThis - i - 1] < other.blocks[to - i - 1]) {
        return [false, false];
      }
    }
    return [false, true];
  }

  /**
   * add the given memory to this one
   * @param other the memory that should be added
   * @param from index of the block from which the addition should start
   * @param to index of the block to which the addition should end (excluded)
   * @param overriding_carry if true, at the end of the addition, the resulting carry out will override the next
   * memory block if it is not null. Otherwise it will be added to the next memory block.
   */
  add(other, from, to, overriding_carry = true) {
    assert.equal(from >= 0, true, "from should not be negative");
    assert.equal(from < to, true, "from should be inferior to to");
    assert.equal(to <= this.blocks.length, true, "to should be inferior to the number of blocks per memory");
    let carry = 0;
    for (let i = from; i < to || carry === 1 && !overriding_carry; i++) {
      let other_val = i < to ? other.blocks[i] : 0;
      let res = this.blocks[i%this.blocks.length + (this.blocks.length - NB_CHRS_PER_WORD)] + other_val + carry;
      if (res >= this.getMode().base) {
        carry = 1;
        res -= this.getMode().base;
      } else {
        carry = 0;
      }
      this.blocks[i%this.blocks.length + (this.blocks.length - NB_CHRS_PER_WORD)] = res;
    }
    if (overriding_carry && carry) {
      this.blocks[to%this.blocks.length] += carry;
    }
  }

  /**
   * Add a value to any block of the memory, carrying out the result if in decimal mode.
   * @param value the value to add, must be between -16 and 16
   * @param at the block index to which the value should be added
   */
  addValue(value, at) {
    assert.equal(value > -16, true, "value should be superior to -16");
    assert.equal(value < 16, true, "value should be inferior to 16");
    assert.equal(at < this.blocks.length, true, "at should be inferior to the number of blocks per memory");
    this.blocks[at] = Math.abs(this.blocks[at] + value);
    for (let i = at; this.blocks[i] >= this.getMode().base; i = (i + 1)%this.blocks.length) {
      this.blocks[(i + 1) % this.blocks.length]++;
      this.blocks[i] -= this.getMode().base;
    }
  }


  /**
   * return the unsigned decimal value of this memory between the selected blocks (decimal mode only)
   * @param from the starting block from which the value should be computed
   * @param to the ending block (excluded)
   * @return {number} the computed value
   */
  getDecimalValue(from, to) {
    assert.equal(from >= 0, true, "from should not be negative");
    assert.equal(from < to, true, "from should be smaller than to");
    assert.equal(to <= this.blocks.length, true, "to should not be greater than the number of blocks per memory");
    let val = 0;
    let mult = 1;
    for (let i = from; i < to; ++i) {
      val += this.blocks[i]*mult;
      mult *= this.getMode().base;
    }
    return val;
  }

  /**
   * set the value of the memory between the selected blocks
   * @param value
   * @param from
   * @param to
   */
  setDecimalValue(value, from, to) {
    assert.equal(from >= 0, true, "from should not be negative");
    assert.equal(from < to, true, "from should be smaller than to");
    assert.equal(value >= 0, true, "value should be an absolute value");
    assert.equal(to <= this.blocks.length, true, "to should not be greater than the number of blocks per memory");
    let digits = (value).toString(this.getMode().base).split("").map(
      (chr) => parseInt(chr, this.getMode().base)
    ).reverse();
    let i = 0;
    for (; i + from < to && i < digits.length; ++i) {
      this.blocks[from + i] = digits[i];
    }
    for (; i + from < to ; ++i) {
      this.blocks[from + i] = 0;
    }
  }

  /**
   * subtract the given memory to this one
   * @param other the memory that should be subtracted
   * @param from index of the block from which the subtraction should start
   * @param to index of the block to which the subtraction should end (excluded)
   */
  subtract(other, from = 0, to = this.blocks.length, this_from = from, this_to = to) {
    assert.equal(from >= 0, true, "from should not be negative");
    assert.equal(from < to, true, "from should be inferior to to");
    assert.equal(to <= this.blocks.length, true, "to should be inferior to the number of blocks per memory");
    let valM1 = this.getDecimalValue(this_from, this_to) - other.getDecimalValue(from, to);
    this.setDecimalValue(Math.abs(valM1), this_from, this_to);
    if (valM1 < 0 && this.getMode() === MEMORY_MODE.DECIMAL) {
      this.bullGamma.ms1 = 10;
    }
  }

  /**
   * multiply the given memory to this one
   * @param other the memory that should be multiplied
   * @param from index of the block from which the multiplication should start
   * @param to index of the block to which the multiplication should end (excluded)
   */
  multiply(other, from, to) {
    while (this.bullGamma.md !== 0) {
      if (this.blocks[0] === 0) {
        this.shiftRight();
        this.bullGamma.md--;
      } else {
        this.blocks[0]--;
        this.add(other, from, to, false);
      }
    }
  }

  /**
   * multiply the memory with the given value, equivalent to this = this * value * (10 or 2)^at
   * @param value
   * @param at
   */
  multiplyValue(value, at) {
    while (this.bullGamma.md !== 0) {
      if (this.blocks[0] === 0) {
        this.shiftRight();
        this.bullGamma.md--;
      } else {
        this.blocks[0]--;
        this.addValue(value, at);
      }
    }
  }

  /**
   * divide the given memory to this one
   * @param other the memory that should be divided
   * @param from index of the block from which the division should start
   * @param to index of the block to which the division should end (excluded)
   */
  divide(other, from, to) {
    let vmb = other.getDecimalValue(from, to);
    if (vmb === 0) {
      throw Error("Division by 0.");
    }
    while (this.bullGamma.md > 0) {
      while (this.getDecimalValue(from + this.blocks.length - NB_CHRS_PER_WORD, this.blocks.length) < vmb
        && this.bullGamma.md > 0) {
        this.shiftLeft();
        this.bullGamma.md--;
      }
      while (this.getDecimalValue(from + this.blocks.length - NB_CHRS_PER_WORD, this.blocks.length) >= vmb) {
        this.blocks[0]++;
        this.subtract(other, from, to, from + this.blocks.length - NB_CHRS_PER_WORD, this.blocks.length);
      }
    }
  }


  /**
   * divide the memory with the given value, equivalent to this = this / (value * (10 or 2)^at)
   * @param value
   * @param at
   */
  divideValue(value, at) {
    let mb = new Memory(0, this.bullGamma);
    mb.blocks[at] = value;
    this.divide(mb, 0, at + 1);
  }

}

module.exports.Memory = Memory;