#!/usr/bin/env python3
"""Assemble a test program, run it on an independent 6502 emulator,
run a2c on it, compile the C, and compare final memory states."""
import subprocess, sys, os

# ---- tiny assembler (labels + subset) ----
M = [
 ("LDX","#",0xA0 if False else 0xA2),("LDY","#",0xA0),("LDA","#",0xA9),
 ("LDA","abs",0xAD),("LDA","absx",0xBD),("LDA","zp",0xA5),
 ("STA","abs",0x8D),("STA","absx",0x9D),("STA","zp",0x85),
 ("ADC","absx",0x7D),("ADC","#",0x69),("SBC","#",0xE9),
 ("CPX","#",0xE0),("CPY","#",0xC0),("CMP","#",0xC9),
 ("BEQ","rel",0xF0),("BNE","rel",0xD0),("BCC","rel",0x90),("BCS","rel",0xB0),
 ("JMP","abs",0x4C),("JSR","abs",0x20),("RTS","",0x60),
 ("INX","",0xE8),("INY","",0xC8),("DEX","",0xCA),("DEY","",0x88),
 ("CLC","",0x18),("SEC","",0x38),("TAX","",0xAA),("TXA","",0x8A),
 ("LSR","a",0x4A),("ASL","a",0x0A),("ROL","a",0x2A),
 ("NOP","",0xEA),
]
def opcodes():
    d={}
    for mn,mode,op in M: d[(mn,mode)]=op
    return d
OPC=opcodes()

def parsev(s):
    s=s.strip()
    if s.startswith('$'): return int(s[1:],16)
    if s.startswith('%'): return int(s[1:],2)
    return int(s,0)
def asm(src, org=0x0600):
    lines=[l.split(';')[0].strip() for l in src.splitlines()]
    lines=[l for l in lines if l]
    labels={}; pc=org
    def size(l):
        parts=l.split(None,1)
        mn=parts[0]; rest=parts[1].strip() if len(parts)>1 else ''
        if rest=='': return 1
        if rest=='A': return 1
        if mn in ('BEQ','BNE','BCC','BCS'): return 2
        if rest.startswith('#'): return 2
        if rest.endswith(',X') or rest.endswith(',Y'):
            try: int(rest[:-2],0); return 3
            except: return 3
        try:
            v=parsev(rest); return 2 if v<0x100 else 3
        except: return 3
    # label pass
    pc=org; ordered=[]
    for l in lines:
        if l.endswith(':'):
            labels[l[:-1]]=pc
        else:
            ordered.append((pc,l)); pc+=size(l)
    out=bytearray()
    for addr,l in ordered:
        parts=l.split(None,1)
        mn=parts[0]; rest=parts[1].strip() if len(parts)>1 else ''
        if rest=='':
            out.append(OPC[(mn,'')])
        elif rest=='A':
            out.append(OPC[(mn,'a')])
        elif rest.startswith('#'):
            out.append(OPC[(mn,'#')]); out.append(parsev(rest[1:])&0xFF)
        elif mn=='JMP' or mn=='JSR':
            t=labels[rest] if rest in labels else parsev(rest)
            out.append(OPC[(mn,'abs')]); out+=bytes([t&0xFF,(t>>8)&0xFF])
        elif rest.endswith(',X') or rest.endswith(',Y'):
            base=rest[:-2]
            t=labels[base] if base in labels else parsev(base)
            suffix='absx' if rest.endswith(',X') else 'abs'
            if mn=='LDA': out.append(OPC[(mn,'absx')])
            elif mn=='STA': out.append(OPC[('STA','absx')])
            elif mn=='ADC': out.append(OPC[('ADC','absx')])
            else: raise Exception('bad '+l)
            out+=bytes([t&0xFF,(t>>8)&0xFF])
        elif mn in ('BEQ','BNE','BCC','BCS'):
            t=labels[rest] if rest in labels else parsev(rest)
            off=t-(addr+2)
            assert -128<=off<127, (l,off)
            out.append(OPC[(mn,'rel')]); out.append(off&0xFF)
        else:
            t=labels[rest] if rest in labels else parsev(rest)
            if t<0x100:
                out.append(OPC[(mn,'zp')]); out.append(t&0xFF)
            else:
                out.append(OPC[(mn,'abs')]); out+=bytes([t&0xFF,(t>>8)&0xFF])
    return bytes(out), labels

# ---- independent emulator ----
def emulate(blob, org, entry, steps=200000):
    m=[0]*65536
    for i,b in enumerate(blob): m[org+i]=b
    a=x=y=0; sp=0xFD; c=z=n=v=0
    pc=entry; st=[]
    def setzn(r):
        return (0 if r==0 else 1), ((r>>7)&1)
    for _ in range(steps):
        op=m[pc]
        def rd(k=1):
            return [m[(pc+i)&0xFFFF] for i in range(0,k+1)]
        if op==0xA9: a=rd()[1]; z,n=setzn(a); pc+=2
        elif op==0xA2: x=rd()[1]; z,n=setzn(x); pc+=2
        elif op==0xA0: y=rd()[1]; z,n=setzn(y); pc+=2
        elif op==0xAD: ad=rd(3)[1]|(rd(3)[2]<<8); a=m[ad]; z,n=setzn(a); pc+=3
        elif op==0xBD:
            b2=rd(3); ad=(b2[1]|(b2[2]<<8)); ad=(ad+x)&0xFFFF; a=m[ad]; z,n=setzn(a); pc+=3
        elif op==0x8D:
            b2=rd(3); ad=b2[1]|(b2[2]<<8); m[ad]=a; pc+=3
        elif op==0x9D:
            b2=rd(3); ad=((b2[1]|(b2[2]<<8))+x)&0xFFFF; m[ad]=a; pc+=3
        elif op==0x85: m[rd()[1]]=a; pc+=2
        elif op==0x7D:
            b2=rd(3); ad=(b2[1]|(b2[2]<<8)+x)&0xFFFF; r=m[ad]
            t=a+r+c; c=1 if t>0xFF else 0
            nv=1 if ((a^t)&(r^t)&0x80) else 0; a=t&0xFF; z,n=setzn(a); v=nv; pc+=3
        elif op==0x69:
            r=rd()[1]; t=a+r+c; c=1 if t>0xFF else 0
            nv=1 if ((a^t)&(r^t)&0x80) else 0; a=t&0xFF; z,n=setzn(a); v=nv; pc+=2
        elif op==0xE9:
            r=rd()[1]; t=a-(r+(1-c)); c=1 if t<0x100 else 0
            nv=1 if ((a^r)&(a^t)&0x80) else 0; a=t&0xFF; z,n=setzn(a); v=nv; pc+=2
        elif op==0xE0:
            r=rd()[1]; t=x-r; c=1 if t<0x100 else 0; z,n=setzn(t&0xFF); pc+=2
        elif op==0xC9:
            r=rd()[1]; t=a-r; c=1 if t<0x100 else 0; z,n=setzn(t&0xFF); pc+=2
        elif op==0xF0:
            off=rd()[1]; off=off-256 if off>127 else off
            pc = pc+2+off if z==1 else pc+2
        elif op==0xD0:
            off=rd()[1]; off=off-256 if off>127 else off
            pc = pc+2+off if z==0 else pc+2
        elif op==0x90:
            off=rd()[1]; off=off-256 if off>127 else off
            pc = pc+2+off if c==0 else pc+2
        elif op==0xB0:
            off=rd()[1]; off=off-256 if off>127 else off
            pc = pc+2+off if c==1 else pc+2
        elif op==0x4C: pc = rd(3)[1]|(rd(3)[2]<<8)
        elif op==0x20:
            b2=rd(3); st.append(pc+2); pc=b2[1]|(b2[2]<<8)
        elif op==0x60:
            if not st: break
            pc=(st.pop()+1)&0xFFFF
        elif op==0xE8: x=(x+1)&0xFF; z,n=setzn(x); pc+=1
        elif op==0xC8: y=(y+1)&0xFF; z,n=setzn(y); pc+=1
        elif op==0xCA: x=(x-1)&0xFF; z,n=setzn(x); pc+=1
        elif op==0x18: c=0; pc+=1
        elif op==0x38: c=1; pc+=1
        elif op==0xAA: x=a; z,n=setzn(x); pc+=1
        elif op==0x8A: a=x; z,n=setzn(a); pc+=1
        elif op==0x4A:
            c=a&1; a=a>>1; z,n=setzn(a); pc+=1
        elif op==0x0A:
            c=1 if a&0x80 else 0; a=(a<<1)&0xFF; z,n=setzn(a); pc+=1
        elif op==0xEA: pc+=1
        else: raise Exception("emu: unknown opcode %02X at %04X"%(op,pc))
        print("TRACE op=%02X -> pc=%04X a=%02X x=%02X z=%d c=%d"%(op,pc,a,x,z,c))
    return m

SRC = """
start:
 LDX #$00
 LDY #$00
 LDA #$00
 STA $0200
 LDA #$01
 STA $0201
loop:
 CPX #$0A
 BEQ done
 CLC
 LDA $0200,X
 ADC $0201,X
 STA $0202,X
 INX
 JMP loop
done:
 JSR checksum
 RTS
checksum:
 LDA #$00
 LDX #$00
loop2:
 CPX #$0C
 BEQ end2
 CLC
 ADC $0200,X
 INX
 JMP loop2
end2:
 STA $00
 RTS
"""

def main():
    blob,labels=asm(SRC,0x0600)
    open("test.bin","wb").write(blob)
    print("assembled %d bytes, labels=%s"%(len(blob),{k:hex(v) for k,v in labels.items()}))
    m=emulate(blob,0x0600,labels['start'])
    ref={i:m[i] for i in range(65536) if m[i] and not (0x0600<=i<0x0600+len(blob))}
    open("ref.txt","w").write("".join("%04X %02X\n"%(i,ref[i]) for i in sorted(ref)))
    print("ref nonzero bytes: %d, checksum cell $00 = %d"%(len(ref),ref.get(0,0)))
    r=subprocess.run([sys.executable,"a2c.py","test.bin","0600","0600","gen.c"],capture_output=True,text=True)
    print(r.stdout,r.stderr)
    r=subprocess.run(["gcc","-O2","-o","gen","gen.c"],capture_output=True,text=True)
    if r.returncode: print("GCC FAIL:",r.stderr[:4000]); sys.exit(1)
    got={}
    for line in subprocess.run(["./gen"],capture_output=True,text=True).stdout.splitlines():
        p=line.split(); got[int(p[0],16)]=int(p[1],16)
    if got==ref:
        print("MATCH: transpiled C is memory-state equivalent (%d nonzero bytes)"%len(ref))
    else:
        only_ref={k:ref[k] for k in ref if k not in got or got[k]!=ref[k]}
        only_got={k:got[k] for k in got if k not in ref or ref[k]!=got[k]}
        print("MISMATCH\n ref-only/diff:",{hex(k):v for k,v in list(only_ref.items())[:20]},
              "\n gen-only/diff:",{hex(k):v for k,v in list(only_got.items())[:20]})
        sys.exit(2)


blob,labels=asm(SRC,0x0600)
emulate(blob,0x600,labels["start"],steps=14)
