import json
import pygame
import sys
import shlex
import ast
import os
import threading
__Version__="1.8"
if sys.platform == "win32":
    from ctypes import windll
    try:
        windll.shcore.SetProcessDpiAwareness(1)
    except:
        pass
#errors
class RegisterError(Exception):
    def __init__(self, message):
        super().__init__(message)
        self.message = message
class AddressError(Exception):
    def __init__(self, message):
        super().__init__(message)
        self.message = message
class OpcodeError(Exception):
    def __init__(self, message):
        super().__init__(message)
        self.message = message
#hardware
class board:
    def __init__(self):
        if os.path.exists(self.file_path("Board")):
            self.data=self.load()
        else:
            self.data = {}
            self.dump(self.data)
    def dump(self,data):
        with open(self.file_path("Board"),"w") as f:
            json.dump(data,f,indent=1)
    def load(self):
        with open(self.file_path("Board"),"r") as f:
            return json.load(f)
    def file_path(self, relative_path):
        try:
            base_path = sys._MEIPASS
        except Exception:
            base_path = os.path.abspath(".")
        return os.path.join(base_path, relative_path)
class screen:
    def __init__(self, width, height):
        pygame.init()
        pygame.font.init()
        self.sd=board()
        self.font = pygame.font.SysFont("Consolas", 20)
        self.font_size=20
        self.screen = pygame.display.set_mode((width,height))
        self.clock = pygame.time.Clock()
        self.mouse_held = False
        self.held_keys={}
        self.shift_key={
            "a":"A","b":"B","c":"C","d":"D","e":"E","f":"F","g":"G","h":"H",
            "i":"I","j":"J","k":"K","l":"L","m":"M","n":"N","o":"O","p":"P",
            "q":"Q","r":"R","s":"S","t":"T","u":"U","v":"V","w":"W","x":"X",
            "y":"Y","z":"Z",
            "1":"!","2":"@","3":"#","4":"$","5":"%","6":"^","7":"&","8":"*",
            "9":"(","0":")",
            "[":"{","]":"}","\\":"|",";":":","'":'"',",":"<",".":">","/":"?",
            "-":"_","=":"+","`":"~"
        }
        self.sprites = {}
        self.surfaces = {}
        self.surface_log = []
        self.text_log = []
    def clear(self):
        self.screen.fill((0,0,0))
    def clear_logs(self):
        self.text_log = []
    def frame(self,tick=60):
        self.clock.tick(tick)
        pygame.display.flip()
    def events(self):
        e=(None,None)
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()
            elif event.type == pygame.KEYDOWN:
                e=("keydown",pygame.key.name(event.key))
                self.held_keys[pygame.key.name(event.key)]=True
            elif event.type == pygame.KEYUP:
                e=("keyup",pygame.key.name(event.key))
                self.held_keys[pygame.key.name(event.key)]=False
            elif event.type == pygame.MOUSEBUTTONDOWN:
                e=("mousedown",pygame.mouse.get_pos())
                self.mouse_held=True
            elif event.type == pygame.MOUSEBUTTONUP:
                e=("mouseup",pygame.mouse.get_pos())
                self.mouse_held=False
        return e
    def shift(self, key):
        if len(str(key))>1:
            raise TypeError("the input is not a single character.")
        return self.shift_key.get(key, "")

    def set_font(self,mode,font,size):
        if mode==1:
            self.font = pygame.font.Font(self.sd.file_path(font),size)
            self.font_size=size
        elif mode==2:
            self.font = pygame.font.SysFont(font,size)
            self.font_size=size
    def print(self,text,x=0,y=0,color=(255,255,255),add_to_text_log=True,blit_surface=None):
        if blit_surface is None:
            self.screen.blit(self.font.render(text,True,color),(x,y))
        else:
            self.surfaces[blit_surface]["data"].blit(self.font.render(text, True, color), (x, y))
        if add_to_text_log:
            self.text_log.append((text,x,y,color,blit_surface))
    def input(self,prompt,base_prompt="",x_pos=0,y_pos=0,color=(255,255,255),blit_surface=None):
        itext=base_prompt
        def update():
            self.clear()
            for text in self.text_log:
                txt=text[0]
                x=text[1]
                y=text[2]
                col=text[3]
                b_surf=text[4]
                self.print(txt,x,y,col,False,b_surf)
            self.render_surfaces()
            self.render_sprites()
        def insert_blink():
            t1 = ""
            t2 = []
            for char in itext:
                t2.append(" ")
            t2.insert(selection_index, blink)
            for char in t2:
                t1 += char
            return t1
        def blank_prompt():
            t=""
            for char in str(prompt):
                t=t+" "
            if len(itext)==0:
                return t[:-1]
            return t
        def insert_text(text):
            t1 = ""
            t2 = []
            for char in itext:
                t2.append(char)
            t2.insert(selection_index, text)
            for char in t2:
                t1 += char
            return t1
        blink_count=0
        blink=" "
        selection_index=0
        while True:
            update()
            if selection_index < 0:
                selection_index = 0
            elif selection_index >= len(itext) and len(itext)!=0:
                selection_index = len(itext)-1
            elif selection_index <= 0:
                selection_index = 0
            if blit_surface is None:
                self.print(prompt+itext,x_pos,y_pos,color,False)
                self.print(blank_prompt()+insert_blink(),x_pos+self.font_size/4, y_pos, color, False)
            else:
                self.print(prompt+itext,x_pos,y_pos,color,False,blit_surface)
                self.print(blank_prompt()+insert_blink(),x_pos+self.font_size/4, y_pos, color, False,blit_surface)
            e=self.events()
            if e==("keydown","return"):
                return itext
            elif e==("keydown","backspace"):
                t1=""
                t2=[]
                for char in itext:
                    t2.append(char)
                if len(t2)>0:
                    del t2[selection_index]
                    selection_index-=1
                for char in t2:
                    t1+=char
                itext=t1
            elif e==("keydown","left"):
                if selection_index<0:
                    selection_index=0
                elif selection_index==0:
                    selection_index=0
                else:
                    selection_index-=1
            elif e==("keydown","right"):
                if selection_index<0:
                    selection_index=0
                elif selection_index>=len(itext):
                    selection_index=len(itext)-1
                else:
                    selection_index+=1
            elif e==("keydown","space"):
                selection_index+=1
                itext=insert_text(" ")
            elif e[0]=="keydown":
                selection_index+=1
                if self.is_held("left shift") or self.is_held("right shift"):
                    if e!=("keydown","left shift") and e!=("keydown","right shift"):
                        itext=itext+self.shift(e[1])
                else:
                    itext=insert_text(e[1])
            if blink_count>15:
                blink_count=0
                if blink==" ":
                    blink="|"
                elif blink == "|":
                    blink=" "
            blink_count+=1
            self.frame(60)

    class sprite:
        def __init__(self,screen):
            self.sc=screen
            self.x=0
            self.y=0
            self.rect=None
            self.image=None
        def set_image(self,image):
            self.image=pygame.image.load(self.sc.sd.file_path(image))
            self.rect = self.image.get_rect(topleft=(self.x, self.y))
        def update(self):
            if self.image:
                self.sc.screen.blit(self.image, (self.x, self.y))
                self.rect.topleft = (self.x, self.y)
        def move(self,x,y):
            self.x+=x
            self.y+=y
        def set_pos(self,x,y):
            self.x=x
            self.y=y
        def scale(self,width,height):
            self.image=pygame.transform.scale(self.image,(width,height))
    def add_sprite(self,sprite):
        self.sprites[sprite]=self.sprite(self)
    def render_sprites(self):
        for sprite in self.sprites:
            self.sprites[sprite].update()
    def move_sprite(self,sprite,x,y):
        self.sprites[sprite].move(x,y)
    def set_sprite_pos(self,sprite,x,y):
        self.sprites[sprite].set_pos(x,y)
    def set_sprite_image(self,sprite,image):
        self.sprites[sprite].set_image(image)
    def kill_sprite(self,sprite):
        del(self.sprites[sprite])
    def sprite_touching_sprite(self, sprite_a, sprite_b):
        return self.sprites[sprite_a].rect.colliderect(
            self.sprites[sprite_b].rect
        )
    def sprite_touching_mouse(self, sprite):
        pos = pygame.mouse.get_pos()
        return self.sprites[sprite].rect.collidepoint(pos)
    def sprite_click(self,sprite,event):
        if event[0] == "mousedown":
            if self.sprites[sprite].rect.collidepoint(event[1][0], event[1][1]):
                return True
        return False
    def scale_sprite(self,sprite,width,height):
        self.sprites[sprite].scale(width,height)

    def surface(self, name, x=0, y=0, width=10, height=10, color=(255, 255, 255)):
        self.surfaces[name] = {}
        self.surfaces[name]["pos"] = (x, y)
        self.surfaces[name]["data"] = pygame.Surface((width, height))
        self.surfaces[name]["data"].fill(color)
        self.surfaces[name]["rect"] = self.surfaces[name]["data"].get_rect(topleft=(x,y))
    def render_surfaces(self):
        for surface in self.surfaces:
            self.screen.blit(self.surfaces[surface]["data"], self.surfaces[surface]["pos"])
    def surface_click(self,surface,event):
        if self.surfaces[surface]["rect"].collidepoint(pygame.mouse.get_pos()) and event[0]=="mousedown":
            return True
        return False
    def surface_touching_mouse(self,surface):
        return self.surfaces[surface]["rect"].collidepoint(pygame.mouse.get_pos())
    def kill_surface(self,name):
        del(self.surfaces[name])

    def quit(self):
        pygame.quit()
    def wait(self,t):
        pygame.time.wait(t)
    def set_window_name(self,name):
        pygame.display.set_caption(name)
    def set_window_icon(self,file):
        f=self.sd.file_path(file)
        pygame.display.set_icon(pygame.image.load(f))
    def mouse_down(self):
        return self.mouse_held
    def is_held(self, key):
        return self.held_keys.get(key, False)
    def mouse_pos(self):
        return pygame.mouse.get_pos()
class sound:
    def __init__(self):
        self.sounds={}
        self.sd=board()
        pygame.mixer.init()
    def add_sound(self,sound):
        self.sounds[sound]=pygame.mixer.Sound(self.sd.file_path(sound))
    def play_sound(self,sound):
        if sound in self.sounds:
            self.sounds[sound].play()
    def stop(self,sound):
        self.sounds[sound].stop()
class Disk:
    def __init__(self, sector_size, sectors):
        self.sector_size = sector_size
        self.sectors = sectors
        self.data = [[0] * sector_size for _ in range(sectors)]
    def write(self,addr,data):
        if 0 <= addr[0] < self.sectors:
            if 0 <= addr[1] < self.sector_size:
                self.data[addr[0]][addr[1]] = data
            else:
                raise Exception("[DISK PANIC]: Address out of bounds")
        else:
            raise Exception("[DISK PANIC]: Address out of bounds")
    def read(self,addr):
        if 0 <= addr[0] < self.sectors:
            if 0 <= addr[1] < self.sector_size:
                return self.data[addr[0]][addr[1]]
            else:
                raise Exception("[DISK PANIC]: Address out of bounds")
        else:
            raise Exception("[DISK PANIC]: Address out of bounds")
    def format(self):
        self.data = [[0] * self.sector_size for _ in range(self.sectors)]
    def dump(self, name):
        with open(name, "w") as f:
            json.dump(self.data,f)
    def load(self, name):
        with open(name, "r") as f:
            self.data = json.load(f)
class cpu:
    def __init__(self,Arch_name="Undefined ARCH",screen_width=480,screen_height=480):
        self.registers={}
        self.commented=False
        self.ram=[]
        self.opcodes={}
        self.pc=0
        self.Heat=0
        self.MaxValue=0xffffffff
        self.program=[]
        self.sc=screen(screen_width, screen_height)
        self.comment_symbol=";"
        self.multiline_comment_symbol_start="<:"
        self.multiline_comment_symbol_end=":>"
        self.Arch_name=Arch_name
        self.sc.set_window_name(Arch_name)
        self.BuildCommands={}
        self.devices={}
        self.disk=Disk
        self.board=board()
        self.sound=sound()
    #-/resolves/-#
    def register_resolve(self,reg,val):
        tag = self.registers[reg]["tag"]
        if type(val) is not tag and tag is not None:
            raise TypeError(f"Register type mismatch, register {reg} is of type {self.registers[reg]['tag']}")
        elif tag is not None and isinstance(val, tag):
            if not isinstance(val, (int, float)):
                if len(val)<=self.MaxValue:
                    self.registers[reg]["val"]=val
                else:
                    self.registers[reg]["val"]=type(val)
            else:
                if val<=self.MaxValue:
                    self.registers[reg]["val"]=val
                else:
                    if isinstance(val, int):
                        self.registers[reg]["val"]=0
                    if isinstance(val, float):
                        self.registers[reg]["val"]=0.0
        elif self.registers[reg]["tag"] is None:
            if not isinstance(val, (int, float)):
                if len(val)<=self.MaxValue:
                    self.registers[reg]["val"]=val
                else:
                    self.registers[reg]["val"]=type(val)
            else:
                if val<=self.MaxValue:
                    self.registers[reg]["val"]=val
                else:
                    if isinstance(val, int):
                        self.registers[reg]["val"] = 0
                    if isinstance(val, float):
                        self.registers[reg]["val"] = 0.0
    def ram_resolve(self,addr,val):
        if isinstance(val, (int, float)):
            if val<=self.MaxValue:
                self.ram[addr]=val
            else:
                if isinstance(val, int):
                    self.ram[addr]=0
                elif isinstance(val, float):
                    self.ram[addr]=0.0
        else:
            if len(val)<=self.MaxValue:
                self.ram[addr]=val
            else:
                self.ram[addr]=type(val)
    #-/main/-#
    def main(self):
        self.print("Angel-CPU",0,0,(255,255,255))
        self.frame()
        self.wait(1000)
        self.clear()
        self.clear_logs()
        self.print("Type CMD for commands",0,0,(255,255,255))
        while True:
            self.clear()
            self.clear_logs()
            e=self.events()
            i=self.input(">","",0,0,(255,255,255))
            if i=="CMD":
                self.clear()
                self.clear_logs()
                self.print("CMD: lists commands",0,0,(255,255,255))
                self.print("Version: prints current version",0,20,(255,255,255))
                self.input("press enter to continue...","",0,80,(255,255,255))
            elif i=="Version":
                self.clear()
                self.clear_logs()
                self.print(f"Angel-CPU ver:{__Version__}",0,20,(255,255,255))
                self.input("press enter to continue...","",0,40,(255,255,255))
    #-/add's/-#
    def add_register(self, name, tag=None):
        default_values = {
            int: 0,
            str: "",
            bool: False,
            float: 0.0,
            bytes: b"",
            list: [],
            dict: {},
            set: set(),
            tuple: ()
        }
        self.registers[name] = {
            "val": default_values.get(tag, None),
            "tag": tag
        }
    def add_ram(self,size):
        self.ram=[0]*size
    def add_opcode(self,opcode,command):
        self.opcodes[opcode]=command
    def add_instruction(self,instruction):
        self.program.append(instruction)
    #-/set's/-#
    def set_max(self,max):
        self.MaxValue=max
    def set_pc(self,pc):
        self.pc=pc
    def set_register(self,register,value):
        if register in self.registers:
            self.register_resolve(register,value)
        else:
            raise RegisterError(f"Register {register} doesn't exist")
    def set_ram(self, addr, val):
        if 0 <= addr < len(self.ram):
            self.ram_resolve(addr, val)
        else:
            raise AddressError(f"Address {addr} is out of range")
    def reset(self):
        self.pc=0
        self.program=[]
    def set_comment_symbol(self,symbol):
        self.comment_symbol=symbol
    def set_multiline_comment_symbol_start(self,symbol):
        self.multiline_comment_symbol_start=symbol
    def set_multiline_comment_symbol_end(self,symbol):
        self.multiline_comment_symbol_end=symbol
    #-/checks/-#
    def register_exists(self,reg):
        if reg in self.registers:
            return True
        return False
    def register_type(self,reg):
        if self.register_exists(reg):
            return self.registers[reg]["tag"]
        else:
            raise RegisterError(f"Register {reg} doesn't exist")
    def ram_size(self):
        return len(self.ram)
    #-/Get's/-#
    def get_register(self,reg):
        if reg in self.registers:
            return self.registers[reg]["val"]
        raise RegisterError(f"Register {reg} doesn't exist")
    def get_ram(self,addr):
        if 0 <= addr < len(self.ram):
            return self.ram[addr]
        else:
            raise AddressError(f"Address {addr} is out of range")
    def get_pc(self):
        return self.pc
    #-/screen/-#
    def clear(self):
        self.sc.clear()
    def clear_logs(self):
        self.sc.clear_logs()
    def frame(self,tick=60):
        self.sc.frame(tick)
    def events(self):
        return self.sc.events()
    def shift_key(self,key):
        return self.sc.shift(key)

    def set_font(self,mode,font,size):
        self.sc.set_font(mode,font,size)
    def print(self,text,x,y,color=(255,255,255),add_to_text_log=True,blit_surface=None):
        self.sc.print(text,x,y,color,add_to_text_log,blit_surface)
    def input(self,prompt,base_prompt="",x_pos=0,y_pos=0,color=(255,255,255),blit_surface=None):
        return self.sc.input(prompt,base_prompt,x_pos,y_pos,color,blit_surface)

    def add_sprite(self,name=""):
        self.sc.add_sprite(name)
    def render_sprites(self):
        self.sc.render_sprites()
    def move_sprite(self,name,x,y):
        self.sc.move_sprite(name,x,y)
    def set_sprite_pos(self,sprite,x,y):
        self.sc.set_sprite_pos(sprite,x,y)
    def set_sprite_image(self,sprite,image):
        self.sc.set_sprite_image(sprite,image)
    def kill_sprite(self,sprite):
        self.sc.kill_sprite(sprite)
    def sprite_touching_sprite(self,sprite_a,sprite_b):
        return self.sc.sprite_touching_sprite(sprite_a,sprite_b)
    def sprite_touching_mouse(self,sprite):
        return self.sc.sprite_touching_mouse(sprite)
    def sprite_click(self,sprite,event):
        return self.sc.sprite_click(sprite,event)
    def scale_sprite(self,sprite,width,height):
        self.sc.scale_sprite(sprite,width,height)

    def surface(self, name, x=0, y=0, width=10, height=10, color=(255,255,255)):
        self.sc.surface(name, x, y, width, height, color)
    def render_surfaces(self):
        self.sc.render_surfaces()
    def surface_click(self,name,event):
        return self.sc.surface_click(name, event)
    def surface_touching_mouse(self,name):
        return self.sc.surface_touching_mouse(name)
    def kill_surface(self,name):
        self.sc.kill_surface(name)

    def quit(self):
        self.sc.quit()
    def wait(self,t):
        self.sc.wait(t)
    def set_window_name(self,name):
        self.sc.set_window_name(name)
    def set_window_icon(self,file_name):
        self.sc.set_window_icon(file_name)
    def mouse_down(self):
        return self.sc.mouse_down()
    def is_held(self,key):
        return self.sc.is_held(key)
    def mouse_pos(self):
        return self.sc.mouse_pos()
    #-/sound/-#
    def add_sound(self,file_name):
        self.sound.add_sound(file_name)
    def play_sound(self,file_name):
        self.sound.play_sound(file_name)
    def stop_sound(self,file_name):
        self.sound.stop(file_name)
    #-/assembler stuff/-#
    def _strip_comments_safe(self, line):
        in_string = False
        quote_char = None
        depth = 0
        i = 0

        while i < len(line):
            char = line[i]

            # Handle string start/end
            if char in ('"', "'"):
                if not in_string:
                    in_string = True
                    quote_char = char
                elif quote_char == char:
                    in_string = False

            # Track bracket depth (only if NOT in string)
            elif not in_string:
                if char in "([{":
                    depth += 1
                elif char in ")]}":
                    depth -= 1

                # Only treat as comment if:
                # - not inside string
                # - and depth == 0
                # - and comment symbol found
                if depth == 0 and line.startswith(self.comment_symbol, i):
                    return line[:i].strip()

            i += 1

        return line.strip()
    def _split_instruction(self, line):
        parts = line.strip().split(maxsplit=1)

        if not parts:
            return None, []

        name = parts[0].upper()

        args = ""
        if len(parts) > 1:
            args = parts[1]

        return name, args
    def _parse_arguments(self, arg_text):
        if not arg_text:
            return []

        args = []
        current = ""
        depth = 0
        in_string = False
        quote_char = None

        for char in arg_text:
            if char in ('"', "'"):
                if not in_string:
                    in_string = True
                    quote_char = char
                elif quote_char == char:
                    in_string = False

            if not in_string:
                if char in "([{":
                    depth += 1
                elif char in ")]}":
                    depth -= 1
                elif char == "," and depth == 0:
                    args.append(current.strip())
                    current = ""
                    continue

            current += char

        if current.strip():
            args.append(current.strip())

        parsed_args = []

        for arg in args:
            try:
                parsed_args.append(ast.literal_eval(arg))
            except:
                # If not a literal, treat as raw identifier
                parsed_args.append(arg)

        return parsed_args
    def _strip_multiline_comments(self, code):
        result = ""
        i = 0
        in_string = False
        quote_char = None
        in_comment = False

        while i < len(code):
            char = code[i]

            # Handle string start/end
            if char in ('"', "'") and not in_comment:
                if not in_string:
                    in_string = True
                    quote_char = char
                elif quote_char == char:
                    in_string = False

            # Detect comment start
            if (
                    not in_string
                    and not in_comment
                    and code.startswith(self.multiline_comment_symbol_start, i)
            ):
                in_comment = True
                i += len(self.multiline_comment_symbol_start)
                continue

            # Detect comment end
            if (
                    in_comment
                    and code.startswith(self.multiline_comment_symbol_end, i)
            ):
                in_comment = False
                i += len(self.multiline_comment_symbol_end)
                continue

            # Only append if NOT inside comment
            if not in_comment:
                result += char

            i += 1

        return result
    def assemble(self, code):
        code = self._strip_multiline_comments(code)
        lines = code.strip().split("\n")
        name_to_opcode = {
            func.__name__.upper(): opcode
            for opcode, func in self.opcodes.items()
        }
        for raw_line in lines:
            line = raw_line.strip()
            if not line:
                continue
            line = self._strip_comments_safe(line)
            if not line:
                continue
            inst_name, arg_text = self._split_instruction(line)
            if inst_name not in name_to_opcode:
                raise OpcodeError(f"Unknown instruction: {inst_name}")
            args = self._parse_arguments(arg_text)
            self.add_instruction(
                (name_to_opcode[inst_name], tuple(args))
            )
    #-/run/-#
    def run(self):
        while self.pc<len(self.program):
            command=self.program[self.pc][0]
            args=self.program[self.pc][1]
            self.pc+=1
            if command in self.opcodes:
                self.opcodes[command](*args)
    #-/Disks and devices/-#
    def add_disk(self,sector_size,sectors):
        return self.disk(sector_size,sectors)
    def attach_device(self,name,device):
        self.devices[name]=device
    def get_device(self,name):
        return self.devices[name]
def main():
    Angel = cpu("<Angel>")
    Angel.main()
'''
ARCH=cpu()
ARCH.set_max(0xff)
ARCH.add_register("ixa",None)
def mov(reg,val):
    ARCH.set_register(reg,val)
def log(txt):
    if txt in ARCH.registers:
        print(ARCH.get_register(txt))
    else:
        print(txt)
ARCH.add_opcode(0x0,mov)
ARCH.add_opcode(0x1,log)
c="""<: multiline! :>
mov ixa, {} ; this is a comment
log ixa <: multiline :>
"""
ARCH.assemble(c)
ARCH.run()
'''
if __name__ == "__main__":
    main()