/*
 * Emulate a basic 6502 (MOS) chip.
 */
const BITM = require('../utils/bitman.js');
var IMem = require('../memory').IMem;

// mode = 0 - Immediate
// mode = 1 - Zero Page
// mode = 2 - Zero Page, X
// mode = 3 - Zero Page, Y
// mode = 4 - Absolute
// mode = 5 - Absolute, X
// mode = 6 - Absolute, Y
// mode = 7 - Indirect, X
// mode = 8 - Indirect, Y
function ProcessOp(cpu, mode){
  if (mode < 0 || mode > 9){return false;}

  switch(cpu.__cycle){
    case 0:
      if (mode === 0){
        PCUp(cpu, 1);
        cpu.__mem.address = cpu.__PC;
        //PCUp(cpu, 1);
        cpu.__opv = cpu.__mem.byte;
      }
      return (mode === 0 || mode === 9);
    case 1:
      switch(mode){
        case 1: // Zero Page
          cpu.__mem.address = cpu.__opv;
          cpu.__opv = cpu.__mem.byte;
          return true;
        case 2: // Zero Page, X
        case 3: // Zero Page, Y
          cpu.__opv = (cpu.__opv + ((mode === 2) ? cpu.__XR : cpu.__YR)) & 0xFF; break;
        case 4: // Absolute
        case 5: // Absolute, X
        case 6: // Absolute, Y
          PCUp(cpu, 1);
          cpu.__mem.address = cpu.__PC;
          //PCUp(cpu, 1);
          cpu.__opv |= cpu.__mem.byte << 8;
          break;
        case 7: // Indirect, X
          cpu.__opv = (cpu.__opv + cpu.__XR) & 0xFF; break;
        case 8: // Indirect, Y
          cpu.__mem.address = cpu.__opv;
          cpu.__opv = cpu.__mem.byte;
          break;
      } break;
    case 2:
      switch(mode){
        case 2: // Zero Page, X
        case 3: // Zero Page, Y
        case 4: // Absolute
          cpu.__mem.address = cpu.__opv;
          cpu.__opv = cpu.__mem.byte;
          return true;
        case 5: // Absolute, X
        case 6: // Absolute, Y
          let s = (mode === 5) ? cpu.__XR : cpu.__YR;
          let l = (cpu.__opv & 0xFF) + s;
          cpu.__opv = (cpu.__opv & 0xFF00) | (l & 0xFF);
          if (l < 255){
            cpu.__mem.address = cpu.__opv;
            cpu.__opv = cpu.__mem.byte;
            return true;
          }
          break;
        case 7: // Indirect, X
          cpu.__mem.address = cpu.__opv;
          cpu.__opv = cpu.__mem.byte;
          break;
        case 8: // Indirect, Y
          cpu.__mem.address += 1;
          cpu.__opv |= cpu.__mem.byte << 8;
          break;
      } break;
    case 3:
      switch(mode){
        case 5: // Absolute, X
        case 6: // Absolute, Y
          let h = (cpu.__opv >> 8) + 1;
          cpu.__mem.address = (cpu.__opv & 0xFF) | (h << 8);
          cpu.__opv = cpu.__mem.byte;
          return true;
        case 7: // Indirect, X
          cpu.__mem.address += 1;
          cpu.__opv |= cpu.__mem.byte << 8;
          break;
        case 8: // Indirect, Y
          let l = (cpu.__opv & 0xFF) + cpu.__YR;
          cpu.__opv = (cpu.__opv & 0xFF00) | (l & 0xFF);
          if (l <= 255){
            cpu.__mem.address = cpu.__opv;
            cpu.__opv = cpu.__mem.byte;
            return true;
          }
          break;
      } break;
    case 4:
      if (mode === 8){
        let h = (cpu.__opv >> 8) + 1;
        cpu.__opv = (cpu.__opv & 0x00FF) | (h << 8);
      }
      cpu.__mem.address = cpu.__opv;
      cpu.__opv = cpu.__mem.byte;
      return true;
  }
  cpu.__cycle += 1;
  return false;
}


function ADC(cpu){
  let pmode = [0x69, 0x65, 0x75, null, 0x6D, 0x7D, 0x79, 0x61, 0x71, null].indexOf(cpu.__op);
  if (ProcessOp(cpu, pmode) === true){
    cpu.__op = -1;
    ALU(cpu, 0, cpu.__opv);
  }
}

function AND(cpu){
  let pmode = [0x29, 0x25, 0x35, null, 0x2D, 0x3D, 0x39, 0x21, 0x31, null].indexOf(cpu.__op);
  if (ProcessOp(cpu, pmode) === true){
    cpu.__op = -1;
    this.__AR &= this.__opv;
    cpu.Z = (this.__AR === 0);
    cpu.N = BITM.val(this.__AR, 7);
  }
}

function ASL(cpu){
  let pmode = [null, 0x06, 0x16, null, 0x0E, 0x1E, null, null, null, 0x0A].indexOf(cpu.__op);
  if (cpu.__ophold === 1){pmode = -1;}
  if (cpu.__ophold === 1 || ProcessOp(cpu, pmode) === true){
    if (cpu.__ophold === 0){
      cpu.__opv = (mode === 9) ? cpu.__AR : cpu.__opv;
      cpu.C = BITM.val(cpu.__opv, 7);
      cpu.__opv = (cpu.__opv << 1) & 0xFF;
      cpu.__ophold = 1;
    } else {
      if (mode === 9){
        cpu.__AR = cpu.__opv;
      } else {
        cpu.__mem.byte = cpu.__opv;
      }
      cpu.Z = (this.__opv === 0);
      cpu.N = BITM.val(this.__opv, 7);
      cpu.__op = -1;
      cpu.__ophold = 0;
    }
  }
}

function BIT(cpu){
  let pmode = [null, 0x24, null, null, 0x2C, null, null, null, null, null].indexOf(cpu.__op);
  if (ProcessOp(cpu, pmode) === true){
    cpu.__op = -1;
    let v = this.__AR & this.__opv;
    cpu.Z = (v === 0);
    cpu.N = BITM.val(this.__opv, 7);
    cpu.V = BITM.val(this.__opv, 6);
  }
}

function BRANCH(cpu){
  switch(cpu.__cycle){
    case 0:
      let branch = false;
      switch(cpu.__op){
        case 0x10: // BPL
          branch = (cpu.N === 0); break;
        case 0x30: // BMI
          branch = (cpu.N === 1); break;
        case 0x50: // BVC
          branch = (cpu.V === 0); break;
        case 0x70: // BVS
          branch = (cpu.V === 1); break;
        case 0x90: // BCC
          branch = (cpu.C === 0); break;
        case 0xB0: // BCS
          branch = (cpu.C === 1); break;
        case 0xD0: // BNE
          branch = (cpu.Z === 0); break;
        case 0xF0: // BEQ
          branch = (cpu.Z === 1); break;
      }
      if (branch === false)
        PCUp(cpu, 1);
    case 1:
      if (cpu.__cycle === 1){ // TODO: Huh???
        cpu.__mem.address = this.__PC;
        let v = cpu.__mem.byte;
        if (v > 128){
          PCDown(cpu, 255 - v);
        } else {
          PCUp(cpu, v);
        }
      }
      cpu.__op = -1;
  }
  cpu.__cycle += 1;
}

function BRK(cpu, nmi){
  nmi = (nmi === true);
  switch(cpu.__cycle){
    case 0:
      PCUp(cpu, 1);
    case 1:
      StackPush(cpu, cpu.__PC >> 8); break;
    case 2:
      StackPush(cpu, cpu.__PC & 0xFF); break;
    case 3:
      StackPush(cpu, cpu.__SP); break;
    case 4:
      cpu.__mem.address = (nmi) ? 0xFFFC : 0xFFFE;
      cpu.__PC = cpu.__mem.byte;
      break;
    case 5:
      cpu.__mem.address = (nmi) ? 0xFFFD : 0xFFFF;
      cpu.__PC |= cpu.__mem.byte << 8;
      cpu.B = 1; 
      cpu.__op = -1;
      break;
  }
  cpu.__cycle += 1;
}

function CMP(cpu){
  let pmode = [0xC9, 0xC5, 0xD5, null, 0xCD, 0xDD, 0xD9, 0xC1, 0xD1, null].indexOf(cpu.__op);
  if (ProcessOp(cpu, pmode) === true){
    cpu.__op = -1;
    cpu.C = (cpu.__AR >= cpu.__opv);
    cpu.Z = (this.__AR === cpu.__opv);
    let v = this.__AR - cpu.__opv;
    if (v < 0){v += 256;}
    cpu.N = BITM.val(v, 7);
  }
}

function CPX(cpu){
  let pmode = [0xE0, 0xE4, null, null, 0xEC, null, null, null, null, null].indexOf(cpu.__op);
  if (ProcessOp(cpu, pmode) === true){
    cpu.__op = -1;
    cpu.C = (cpu.__XR >= cpu.__opv);
    cpu.Z = (this.__XR === cpu.__opv);
    let v = this.__XR - cpu.__opv;
    if (v < 0){v += 256;}
    cpu.N = BITM.val(v, 7);
  }
}

function CPY(cpu){
  let pmode = [0xC0, 0xC4, null, null, 0xCC, null, null, null, null, null].indexOf(cpu.__op);
  if (ProcessOp(cpu, pmode) === true){
    cpu.__op = -1;
    cpu.C = (cpu.__YR >= cpu.__opv);
    cpu.Z = (this.__YR === cpu.__opv);
    let v = this.__YR - cpu.__opv;
    if (v < 0){v += 256;}
    cpu.N = BITM.val(v, 7);
  }
}

function DEC(cpu){
  let pmode = [null, 0xC6, 0xD6, null, 0xCE, 0xDE, null, null, null, null].indexOf(cpu.__op);
  if (cpu.__ophold === 1){pmode = -1;}
  if (cpu.__ophold === 1 || ProcessOp(cpu, pmode) === true){
    if (cpu.__ophold === 0){
      cpu.__opv = ByteWrap(cpu.__opv, -1);
      cpu.__ophold = 1;
    } else {
      cpu.__mem.byte = cpu.__opv;
      cpu.Z = (this.__opv === 0);
      cpu.N = BITM.val(this.__opv, 7);
      cpu.__op = -1;
      cpu.__ophold = 0;
    }
  }
}

function EOR(cpu){
  let pmode = [0x49, 0x45, 0x55, null, 0x4D, 0x5D, 0x59, 0x41, 0x51, null].indexOf(cpu.__op);
  if (ProcessOp(cpu, pmode) === true){
    cpu.__op = -1;
    cpu.__AR ^= cpu.__opv;
    cpu.Z = (cpu.__AR === 0);
    cpu.N = BITM.val(cpu.__AR, 7);
  }
}

function FLAG(cpu){
  switch (cpu.__op){
    case 0x18: // CLC
      cpu.C = 0; break;
    case 0x38: // SEC
      cpu.C = 1; break;
    case 0x58: // CLI
      cpu.I = 0; break;
    case 0x78: // SEI
      cpu.I = 1; break;
    case 0xB8: // CLV
      cpu.V = 0; break;
    case 0xD8: // CLD
      cpu.D = 0; break;
    case 0xF8: // SED
      cpu.D = 1; break;
  }
  cpu.__op = -1;
}

function INC(cpu){
  let pmode = [null, 0xE6, 0xF6, null, 0xEE, 0xFE, null, null, null, null].indexOf(cpu.__op);
  if (cpu.__ophold === 1){pmode = -1;}
  if (cpu.__ophold === 1 || ProcessOp(cpu, pmode) === true){
    if (cpu.__ophold === 0){
      cpu.__opv = ByteWrap(cpu.__opv, 1);
      cpu.__ophold = 1;
    } else {
      cpu.__mem.byte = cpu.__opv;
      cpu.Z = (this.__opv === 0);
      cpu.N = BITM.val(this.__opv, 7);
      cpu.__op = -1;
      cpu.__ophold = 0;
    }
  }
}

function JMP(cpu){
  switch(cpu.__cycle){
    case 0:
      cpu.__mem.address = cpu.__PC;
      cpu.__opv = cpu.__mem.byte;
      PCUp(cpu, 1); break;
    case 1:
      cpu.__mem.address = cpu.__PC;
      cpu.__opv |= cpu.__mem.byte << 8;
      if (cpu.__op === 0x4C){
        cpu.__PC = cpu.__opv;
        cpu.__op = -1;
      }
      break;
    case 2:
      cpu.__mem.address = cpu.__opv;
      cpu.__PC = cpu.__mem.byte;
      break;
    case 3:
      cpu.__mem.address += 1;
      cpu.__PC |= cpu.__mem.byte << 8;
      cpu.__op = -1;
      break;
  }
  cpu.__cycle += 1;
}

function JRS(cpu){
  switch(cpu.__cycle){
    case 0:
      cpu.__mem.address = cpu.__PC;
      cpu.__opv = cpu.__mem.byte;
      PCUp(cpu, 1); break;
    case 1:
      // Discard stack data (or... No Op)
      break;
    case 2:
      StackPush(cpu, cpu.__PC >> 8); break;
    case 3:
      StackPush(cpu, cpu.__PC & 0xFF); break;
    case 4:
      cpu.__mem.address = cpu.__PC;
      cpu.__PC = cpu.__opv | (cpu.__mem.byte << 8)
      cpu.__op = -1;
      break;
  }
  cpu.__cycle += 1;
}

function LDA(cpu){
 let pmode = [0xA9, 0xA5, 0xB5, null, 0xAD, 0xBD, 0xB9, 0xA1, 0xB1, null].indexOf(cpu.__op);
  if (ProcessOp(cpu, pmode) === true){
    cpu.__op = -1;
    cpu.__AR = cpu.__opv;
    cpu.Z = (cpu.__AR === 0);
    cpu.N = (cpu.__AR >= 0x80);
  } 
}

function LDX(cpu){
  let pmode = [0xA2, 0xA6, null, 0xB6, 0xAE, null, 0xBE, null, null, null].indexOf(cpu.__op);
  if (ProcessOp(cpu, pmode) === true){
    cpu.__op = -1;
    cpu.__XR = cpu.__opv;
    cpu.Z = (cpu.__XR === 0);
    cpu.N = (cpu.__XR >= 0x80);
  } 
}

function LDY(cpu){
  let pmode = [0xA0, 0xA4, 0xB4, null, 0xAC, 0xBC, null, null, null, null].indexOf(cpu.__op);
  if (ProcessOp(cpu, pmode) === true){
    cpu.__op = -1;
    cpu.__YR = cpu.__opv;
    cpu.Z = (cpu.__YR === 0);
    cpu.N = (cpu.__YR >= 0x80);
  }
}

function LSR(cpu){
  let pmode = [null, 0x46, 0x56, null, 0x4E, 0x5E, null, null, null, 0x4A].indexOf(cpu.__op);
  if (cpu.__ophold === 1){pmode = -1;}
  if (cpu.__ophold === 1 || ProcessOp(cpu, pmode) === true){
    if (cpu.__ophold === 0){
      cpu.__opv = (mode === 9) ? cpu.__AR : cpu.__opv;
      cpu.C = BITM.val(cpu.__opv, 0);
      cpu.__opv = cpu.__opv >> 1;
      cpu.__ophold = 1;
    } else {
      if (mode === 9){
        cpu.__AR = cpu.__opv;
      } else {
        cpu.__mem.byte = cpu.__opv;
      }
      // TODO: Is Z and N effected by the A register, or by the resulting value? I think the latter.
      cpu.Z = (this.__opv === 0);
      cpu.N = BITM.val(this.__opv, 7);
      cpu.__op = -1;
      cpu.__ophold = 0;
    }
  }
}

function ORA(cpu){
  let pmode = [0x09, 0x05, 0x15, null, 0x0D, 0x1D, 0x19, 0x01, 0x11, null].indexOf(cpu.__op);
  if (ProcessOp(cpu, pmode) === true){
    cpu.__op = -1;
    cpu.__AR |= cpu.__opv;
    cpu.Z = (cpu.__AR === 0);
    cpu.N = BITM.val(cpu.__AR, 7);
  }
}

function REGISTER(cpu){
  let t = 0;
  switch(this.__op){
    case 0xAA: // TAX
      cpu.__XR = cpu.__AR;
      t = cpu.__XR;
      break;
    case 0x8A: // TXA
      cpu.__AR = cpu.__XR;
      t = cpu.__AR;
      break;
    case 0xCA: // DEX
      cpu.__XR = (cpu.__XR === 0) ? 255 : cpu.__XR - 1;
      t = cpu.__XR;
      break;
    case 0xE8: // INX
      cpu.__XR = (cpu.__XR === 255) ? 0 : cpu.__XR + 1;
      t = cpu.__XR;
      break;
    case 0xA8: // TAY
      cpu.__YR = cpu.__AR;
      t = cpu.__YR;
      break;
    case 0x98: // TYA
      cpu.__AR = cpu.__YR;
      t = cpu.__AR;
      break;
    case 0x88: // DEY
      cpu.__YR = (cpu.__YR === 0) ? 255 : cpu.__YR - 1;
      t = cpu.__YR;
      break;
    case 0xC8: // INY
      cpu.__YR = (cpu.__YR === 255) ? 0 : cpu.__YR + 1; 
      t = cpu.__YR;
      break;
  }

  cpu.N = (t >= 0x80);
  cpu.Z = (t === 0);
  cpu.__op = -1;
}

function ROL(cpu){
  let pmode = [null, 0x26, 0x36, null, 0x2E, 0x3E, null, null, null, 0x2A].indexOf(cpu.__op);
  if (cpu.__ophold === 1){pmode = -1;}
  if (cpu.__ophold === 1 || ProcessOp(cpu, pmode) === true){
    if (cpu.__ophold === 0){
      let v = cpu.C; 
      cpu.__opv = (mode === 9) ? cpu.__AR : cpu.__opv;
      cpu.C = BITM.val(cpu.__opv, 7);
      cpu.__opv = ((cpu.__opv << 1) + v) & 0xFF;
      cpu.__ophold = 1;
    } else {
      if (mode === 9){
        cpu.__AR = cpu.__opv;
      } else {
        cpu.__mem.byte = cpu.__opv;
      }
      cpu.Z = (this.__opv === 0);
      cpu.N = BITM.val(this.__opv, 7);
      cpu.__op = -1;
      cpu.__ophold = 0;
    }
  }
}

function ROR(cpu){
  let pmode = [null, 0x66, 0x76, null, 0x6E, 0x7E, null, null, null, 0x6A].indexOf(cpu.__op);
  if (cpu.__ophold === 1){pmode = -1;}
  if (cpu.__ophold === 1 || ProcessOp(cpu, pmode) === true){
    if (cpu.__ophold === 0){
      let v = cpu.C; 
      cpu.__opv = (mode === 9) ? cpu.__AR : cpu.__opv;
      cpu.C = BITM.val(cpu.__opv, 0);
      cpu.__opv = ((cpu.__opv >> 1) + (v * 0x80)) & 0xFF;
      cpu.__ophold = 1;
    } else {
      if (mode === 9){
        cpu.__AR = cpu.__opv;
      } else {
        cpu.__mem.byte = cpu.__opv;
      }
      cpu.Z = (this.__opv === 0);
      cpu.N = BITM.val(this.__opv, 7);
      cpu.__op = -1;
      cpu.__ophold = 0;
    }
  }
}

function RTI(cpu){
  switch(cpu.__cycle){
    case 0: break; // Discard PC
    case 1: // Discard Stack Pointer Data
      StackPop(cpu); break;
    case 2:
      cpu.__PR = StackPop(cpu); break;
    case 3:
      cpu.__PC = StackPop(cpu); break;
    case 4:
      cpu.__PC |= StackPop(cpu) << 8;
      cpu.__op = -1;
      break;
  }
  cpu.__cycle += 1;
}

function RTS(cpu){
  switch(cpu.__cycle){
    case 0: break; // Discard PC
    case 1:
      StackPop(cpu); break;
    case 2:
      cpu.__PC = StackPop(cpu); break;
    case 3:
      cpu.__PC |= StackPop(cpu) << 8; break;
    case 4:
      PCUp(cpu, 1);
      cpu.__op = -1;
      break;
  }
  cpu.__cycle += 1;
}

function SBC(cpu){
  let pmode = [0xE9, 0xE5, 0xF5, null, 0xED, 0xFD, 0xF9, 0xE1, 0xF1].indexOf(cpu.__op);
  if (ProcessOp(cpu, pmode) === true){
    cpu.__op = -1;
    ALU(cpu, 1, cpu.__opv);
  }
}

function STA(cpu){
  let pmode = [null, 0x85, 0x95, null, 0x8D, 0x9D, 0x99, 0x81, 0x91, null].indexOf(cpu.__op);
  if (ProcessOp(cpu, pmode) === true){
    cpu.__op = -1;
    cpu.__mem.address = cpu.__opv;
    cpu.__mem.byte = cpu.__AR;
  }
}

function STACK(cpu){
  switch(cpu.__op){
    case 0x9A: // TXS
      cpu.__SP = cpu.__XR;
      cpu.__op = -1;
      break;
    case 0xBA: // TSX
      cpu.__XR = cpu.__SP;
      cpu.__op = -1;
      break;
    case 0x48: // PHA
      if (cpu.__cycle === 0){
        cpu.__mem.address = 0x0100 | cpu.__SP;
      } else if (cpu.__cycle === 1){
        cpu.__mem.byte = cpu.__AR;
        cpu.__SP = (cpu.__SP === 0) ? 255 : cpu.__SP - 1;
        cpu.__op = -1;
      }
      break;
    case 0x68: // PLA
      if (cpu.__cycle === 0){
        cpu.__mem.address = 0x0100 | cpu.__SP;
      } else if (cpu.__cycle === 1){
        cpu.__AR = cpu.__mem.byte;
        cpu.Z = (cpu.__AR === 0);
        cpu.N = BITM.isOn(cpu.__AR, 7); 
      } else if (cpu.__step === 2){ 
        cpu.__SP = (cpu.__SP === 255) ? 0 : cpu.__SP + 1;
        cpu.__op = -1;
      }
      break;
    case 0x08: // PHP
      if (cpu.__cycle === 0){
        cpu.__mem.address = 0x0100 | cpu.__SP;
      } else if (cpu.__cycle === 1){
        cpu.__mem.byte = cpu.__PR;
        cpu.__SP = (cpu.__SP === 0) ? 255 : cpu.__SP - 1;
        cpu.__op = -1;
      }
      break;
    case 0x28: // PLP
      if (cpu.__cycle === 0){
        cpu.__mem.address = 0x0100 | cpu.__SP;
      } else if (cpu.__cycle === 1){
        cpu.__SP = cpu.__mem.byte;
      } else if (cpu.__cycle === 2){
        cpu.__SP = (cpu.__SP === 255) ? 0 : cpu.__SP + 1;
        cpu.__op = -1;
      }
      break;
  }
  cpu.__cycle += 1;
}

function STX(cpu){
  let pmode = [null, 0x86, null, 0x96, 0x8E, null, null, null, null, null].indexOf(cpu.__op);
  if (ProcessOp(cpu, pmode) === true){
    cpu.__op = -1;
    cpu.__mem.address = cpu.__opv;
    cpu.__mem.byte = cpu.__XR;
  }
}

function STY(cpu){
  let pmode = [null, 0x84, 0x94, null, 0x8C, null, null, null, null, null].indexOf(cpu.__op);
  if (ProcessOp(cpu, pmode) === true){
    cpu.__op = -1;
    cpu.__mem.address = cpu.__opv;
    cpu.__mem.byte = cpu.__YR;
  }
}


// --------------------------------------------------------------------------------------------

// Test to see if both a and b's high byte is the same... same page.
function SamePage(a, b){
  return ((a >> 8) == (b >> 8));
}

function ByteWrap(v, a){
  v += a;
  if (v < 0){v += 256;}
  else if (v > 255){v -= 256;}
  return v;
}

function StackPush(cpu, v){
  cpu.__mem.address = 0x0100 | cpu.__SP;
  cpu.__mem.byte = v & 0xFF;
  cpu.__SP = ByteWrap(cpu.__SP, -1);
}

function StackPop(cpu){
  cpu.__mem.address = 0x0100 | cpu.__SP;
  cpu.__SP = ByteWrap(cpu.__SP, 1);
  return cpu.__mem.byte;
}

function PCHI(cpu, b){
  cpu.__PC = (cpu.__PC & 0x00FF) | (b << 8);
}

function PCLOW(cpu, b){
  cpu.__PC = (cpu.__PC & 0xFF00) | b;
}

function PCUp(cpu, amount){
  if (cpu.__pcc === 1){
    PCHI(cpu, ((cpu.__PC & 0xFF00) >> 8) + 1);
    cpu.__pcc = 0;
  } else if ((cpu.__PC & 0x00FF) + amount > 255){
    cpu.__pcc = 1;
    PCLOW(cpu, ((cpu.__PC & 0x00FF) + amount) - 256);
  } else {
    PCLOW(cpu, (cpu.__PC & 0x00FF) + amount);
  }
}

function PCDown(cpu, amount){
  if (cpu.__pcc === -1){
    PCHI(cpu, ((cpu.__PC & 0xFF00) >> 8) - 1);
    cpu.__pcc = 0;
  } else if ((cpu.__PC & 0x00FF) - amount < 0){
    cpu.__pcc = -1;
    PCLOW(cpu, ((cpu.__PC & 0x00FF) - amount) + 256);
  } else {
    PCLOW(cpu, (cpu.__PC & 0x00FF) - amount);
  }
}

function MemAddrFrom(cpu, addr){
  cpu.__mem.address = addr;
  let v = cpu.__mem.byte;
  cpu.__mem.address = (addr + 1) & 0xFF;
  v |= cpu.__mem.byte << 8;
  return v;
}

function ALU(cpu, m, b){
  let A = 0;
  let IAR = cpu.__AR;
  if (cpu.D === 0){ // Binary Mode
    switch(m){
      case 0: // Addition
        A = (cpu.__AR + b) + ((cpu.C === 1) ? 1 : 0);
        break;
      case 1: // Subtraction
        A = (cpu.__AR - b) - ((cpu.C === 0) ? 1 : 0); 
        A += (A < 0) ? 256 : 0;
        break;
    } 
 
    cpu.__AR = A & 0xFF;

    // Both inputs have the same sign, but the output doesn't match the sign of the input.
    // (Twos Compliment)
    cpu.V = ((IAR & 0x80) === (b & 0x80)) && ((IAR & 0x80) !== (cpu.__AR & 0x80));
    cpu.C = (m === 0) ? (A >= 256) : (cpu.V === 0);
    cpu.N = BITM.isOn(cpu.__AR, 7);
    
    cpu.Z = (cpu.__AR === 0);
  } else { // Decimal (BCD) Mode
    // Borrowed from information located on...
    // http://www.6502.org/tutorials/decimal_mode.html
    let AL = 0;
    switch(m){ 
      case 0: // Addition 
        AL = (cpu.__AR & 0x0F) + (b & 0x0F) + ((cpu.C === 1) ? 1 : 0);
        AL = (AL < 0x0A) ? AL : ((AL + 0x06) & 0x0F) + 0x10;
        A = (cpu.__AR & 0xF0) + (b & 0xF0) + AL;
        A += (A >= 0xA0) ? 0x60 : 0;
        cpu.__AR = A & 0xFF;
        cpu.Z = (IAR + b + ((cpu.C === 1) ? 1 : 0) === 256);
        cpu.C = (A >= 0x100);
        cpu.N = (BITM.isOn(A, 7)) ? 1 : 0;
        cpu.V = (A >= -128 && A <= 127) ? 0 : 1; 
        break;
      case 1: // Subtraction
        AL = (cpu.__AR & 0x0F) - (b & 0x0F) + (cpu.C - 1);
        AL = (AL >= 0) ? AL : ((AL - 0x06) & 0x0F) - 0x10;
        A = (A & 0xF0) - (b & 0xF0) + AL;
        A -= (A >= 0) ? 0 : 0x60;
        cpu.__AR = A & 0xFF;

        cpu.V = ((IAR & 0x80) === (b & 0x80)) && ((IAR & 0x80) !== (cpu.__AR & 0x80));
        cpu.Z = ((cpu.__AR - b) - ((cpu.C === 0) ? 1 : 0) === 0);
        cpu.C = (cpu.V === 0);
        cpu.N = BITM.isOn(cpu.__AR, 7);
        break;
    }
  }
  return cpu.__AR;
}

// --------------------------------------------------------------------------------------------
// --------------------------------------------------------------------------------------------
// --------------------------------------------------------------------------------------------


class CPU{
  constructor(){
    // Registers
    this.__PC = 0;   // Program Counter (16 bit)
    this.__SP = 255; // Stack Pointer (8 bit)
    this.__PR = 32;   // Status Register (8 bit, bit 5 is always 1)
    this.__XR = 0;   // X Register (8 bit)
    this.__YR = 0;   // Y Register (8 bit)
    this.__AR = 0;   // Accumulator Register (8 bit)

    // Variables to watch for Hardware Interrupts.
    this.__nmi = false;
    this.__irq = false;
    this.__rst = true;
    this.__iinit = false;

    // Variable for tracking tick operations.
    this.__op = -1;
    this.__ophold = 0;
    this.__opv = 0;
    this.__cycle = 0;
    this.__pcc = 0; // Program Counter Carry.

    // Memory module or controller.
    this.__mem = null; // Must be explicitly attached.

    // Hold any created CLK instances.
    this.__clkfn = null;
  }

  // ----------------------------------------
  // CPU Registers. Here for debug purposes.
  get PC(){return this.__PC;}
  get SP(){return this.__SP;}
  get P(){return this.__PR;}
  get X(){return this.__XR;}
  get Y(){return this.__YR;}
  get A(){return this.__AR;}

  // ----------------------------------------
  // Quick Flag Access
  get N(){return (BITM.isOn(this.__PR, 7)) ? 1 : 0;}
  set N(n){this.__PR = (n === true || n === 1) ? BITM.set(this.__PR, 7) : BITM.clear(this.__PR, 7);}

  get V(){return (BITM.isOn(this.__PR, 6)) ? 1 : 0;}
  set V(v){this.__PR = (v === true || v === 1) ? BITM.set(this.__PR, 6) : BITM.clear(this.__PR, 6);}

  get B(){return (BITM.isOn(this.__PR, 4)) ? 1 : 0;}
  set B(b){this.__PR = (b === true || b === 1) ? BITM.set(this.__PR, 4) : BITM.clear(this.__PR, 4);}

  get D(){return (BITM.isOn(this.__PR, 3)) ? 1 : 0;}
  set D(d){this.__PR = (d === true || d === 1) ? BITM.set(this.__PR, 3) : BITM.clear(this.__PR, 3);}
  
  get I(){return (BITM.isOn(this.__PR, 2)) ? 1 : 0;}
  set I(i){this.__PR = (i === true || i === 1) ? BITM.set(this.__PR, 2) : BITM.clear(this.__PR, 2);}

  get Z(){return (BITM.isOn(this.__PR, 1)) ? 1 : 0;}
  set Z(z){this.__PR = (z === true || z === 1) ? BITM.set(this.__PR, 1) : BITM.clear(this.__PR, 1);}

  get C(){return (BITM.isOn(this.__PR, 0)) ? 1 : 0;}
  set C(c){this.__PR = (c === true || c === 1) ? BITM.set(this.__PR, 0) : BITM.clear(this.__PR, 0);}

  // ----------------------------------------
  // Hardware interrupt triggers. Settable only.
  set NMI(n){
    this.__nmi = (n === true);
    this.__iinit = true;
  }

  set IRQ(q){
    if (BITM.val(this.__PR, 2) === 0 && this.__nmi === false){
      this.__irq = (q === true);
      this.__iinit = true;
    }
  }

  set reset(r){
    this.__rst = (r === true);
  }

  // -----------------------------------------
  // Set and Get Memory property.
  get memory(){return this.__mem;}
  set memory(m){
    if (!(m instanceof IMem))
      throw new ValueError("Expected Memory instance object.");
    this.__mem = m;
  }


  // -----------------------------------------
  
  hardReset(){
    this.__pcc = 0;
    this.__cycle = 0;
    this.__op = -1;
    this.__ophold = 0;
    this.__opv = 0;
    this.__iinit = false;
    this.__irq = false;
    this.__nmi = false;
    this.__rst = false;
    this.__SP = 255;
    this.__PR = 32;

    this.__mem.address = 0xFFFC;
    this.__PC = this.__mem.byte;
    this.__mem.address = 0xFFFD;
    this.__PC |= this.__mem.byte << 8;
  }

  clk(){
    if (this.__clkfn === null){
      this.__clkfn = (function(){
        if (this.__pcc !== 0){
          if (this.__pcc > 0) {
            PCUp(this, 1);
          } else {
            PCDown(this, 1);
          }
        } else if (this.__op < 0){
          if (this.__rst){
            this.__rst = false;
            this.__mem.address = 0xFFFC;
            this.__PC = this.__mem.byte;
            this.__mem.address = 0xFFFD;
            this.__PC |= this.__mem.byte << 8;

            // Disabling the IRQ interrupts is the ONLY flag that must be set
            // during reset. The others are random.
            this.I = 1;         
          } else if (this.__nmi) {
            if (this.__iinit){
              this.__cycle = 0;
              this.__iinit = false;
            }
            BRK(this, true); // BRK does all of the interrupt work. The 'true' tells BRK to use the nmi vectors.
            if (this.__cycle === 6)
              this.__nmi = false;
          } else if (this.__irq) {
            if (this.I === 0){
              if (this.__iinit){
                this.__cycle = 0;
                this.__iinit = false;
              }
              BRK(this); // Because both the BRK opcode and an IRQ interrupt do the same things!
              if (this.__cycle === 6)
                this.__irq = false;
            } else {
              // NOPE... try again!
              this.__irq = false;
              this.__clkfn();
            }
          } else {
            this.__cycle = 0;
            this.__mem.address = this.__PC;
            this.__op = this.__mem.byte;
            //PCUp(this, 1);
          }
        } else {
          switch(this.__op){
            case 0x69: case 0x65: case 0x75: case 0x6D: case 0x7D: case 0x79: case 0x61: case 0x71:
              ADC(this); break;
            case 0x29: case 0x25: case 0x35: case 0x2D: case 0x3D: case 0x39: case 0x21: case 0x31:
              AND(this); break;
            case 0x0A: case 0x06: case 0x16: case 0x0E: case 0x1E:
              ASL(this); break;
            case 0x24: case 0x2C:
              BIT(this); break;
            case 0x10: case 0x30: case 0x50: case 0x70: case 0x90: case 0xB0: case 0xD0: case 0xF0:
              BRANCH(this); break;
            case 0x00:
              BRK(this); break;
            case 0xC9: case 0xC5: case 0xD5: case 0xCD: case 0xDD: case 0xD9: case 0xC1: case 0xD1:
              CMP(this); break;
            case 0xE0: case 0xE4: case 0xEC:
              CPX(this); break;
            case 0xC0: case 0xC4: case 0xCC:
              CPY(this); break;
            case 0xC6: case 0xD6: case 0xCE: case 0xDE:
              DEC(this); break;
            case 0x49: case 0x45: case 0x55: case 0x4D: case 0x5D: case 0x59: case 0x41: case 0x51:
              EOR(this); break;
            case 0x18: case 0x38: case 0x58: case 0x78: case 0xB8: case 0xD8: case 0xF8:
              FLAG(this); break;
            case 0xE6: case 0xF6: case 0xEE: case 0xFE:
              INC(this); break;
            case 0x4C: case 0x6C:
              JMP(this); break;
            case 0x20:
              JSR(this); break;
            case 0xA9: case 0xA5: case 0xB5: case 0xAD: case 0xBD: case 0xB9: case 0xA1: case 0xB1:
              LDA(this); break;
            case 0xA2: case 0xA6: case 0xB6: case 0xAE: case 0xBE:
              LDX(this); break;
            case 0xA0: case 0xA4: case 0xB4: case 0xAC: case 0xBC:
              LDY(this); break;
            case 0x4A: case 0x46: case 0x56: case 0x4E: case 0x5E:
              LSR(this); break;
            case 0xEA:
              // NOP
              if (this.__step == 1){
                this.__op = -1;
              } else {this.__step += 1;}
              break;
            case 0x09: case 0x05: case 0x15: case 0x0D: case 0x1D: case 0x19: case 0x01: case 0x11:
              ORA(this); break;
            case 0xAA: case 0x8A: case 0xCA: case 0xE8: case 0xA8: case 0x98: case 0x88: case 0xC8:
              REGISTER(this); break;
            case 0x2A: case 0x26: case 0x36: case 0x2E: case 0x3E:
              ROL(this); break;
            case 0x6A: case 0x66: case 0x76: case 0x6E: case 0x7E:
              ROR(this); break;
            case 0x40:
              RTI(this); break;
            case 0x60:
              RTS(this); break;
            case 0xE9: case 0xE5: case 0xF5: case 0xED: case 0xFD: case 0xF9: case 0xE1: case 0xF1:
              SBC(this); break;
            case 0x85: case 0x95: case 0x8D: case 0x9D: case 0x99: case 0x81: case 0x91:
              STA(this); break;
            case 0x9A: case 0xBA: case 0x48: case 0x68: case 0x08: case 0x28:
              STACK(this); break;
            case 0x86: case 0x96: case 0x8E:
              STX(this); break;
            case 0x84: case 0x94: case 0x8C:
              STY(this); break;
          }
          if (this.__op < 0)
            PCUp(this, 1);
        }
      }).bind(this);
    }
    return this.__clkfn;
  }
}

module.exports = CPU;