'''
This Code converts the BookShelf format to Proto buf format
'''
import re
import os
from datetime import date

def find_pin_act_loc(cx, cy, x_offset, y_offset, orient):
    if orient in ["E", "FE", "W", "FW"]:
        x_offset, y_offset = y_offset, x_offset

    if orient in ["FN", "S", "FE", "W"]:
        x_offset *= -1

    if orient in ["S", "FS", "E", "FE"]:
        y_offset *= -1

    pin_x = cx + x_offset
    pin_y = cy + y_offset
    return pin_x, pin_y

def print_placeholder(fp, key, value):
    fp.write("  attr {\n")
    fp.write(f"    key: \"{key}\"\n")
    fp.write("    value {\n")
    fp.write(f"      placeholder: \"{value}\"\n")
    fp.write("    }\n")
    fp.write("  }\n")
    return

def print_float(fp, key, value):
    fp.write("  attr {\n")
    fp.write(f"    key: \"{key}\"\n")
    fp.write("    value {\n")
    fp.write(f"      f: {value}\n")
    fp.write("    }\n")
    fp.write("  }\n")
    return

class instance:
    def __init__(self, idx, name):
        self.id = idx
        self.master_id = None
        self.name = name
        self.width = None
        self.height = None
        self.llx = None
        self.lly = None
        self.cx = None
        self.cy = None
        self.orient = None
        self.isMacro = None   # Considers macro if heigh is more than row height
        self.ipins = []
        self.opins = []
        self.ipin_x = []
        self.ipin_y = []
        self.opin_x = []
        self.opin_y = []
        self.ipin_net_id = []
        self.opin_net_id = []
        self.pstatus = None
        self.ipin_act_x_offset = []
        self.ipin_act_y_offset = []
        self.opin_act_y_offset = []
        self.opin_act_x_offset = []
        return

    def update_size(self, height, width):
        self.height = height
        self.width = width
        return
    
    def update_location(self, x, y, orient, pstatus = "UNPLACED"):
        self.llx = float(x)
        self.lly = float(y)
        self.orient = orient.upper()
        self.pstatus = pstatus.upper()
        return
    
    def update_center(self):
        self.cx = self.llx + self.width/2
        self.cy = self.lly + self.height/2
        return

    def connect_net(self, net_name, net_id, isInput, x_offset = 0.0, \
                    y_offset = 0.0 ):
        # If isInput is 1 means the connected pin is input pin
        # Check if the net exists:
        if isInput:
            if net_id in self.ipin_net_id:
                print(f"[INFO] [ERROR-1] Inst: {self.name} Net: {net_name}"\
                        " is alerady connected")
                return
        else:
            if net_id in self.opin_net_id:
                print(f"[INFO] [ERROR-2] Inst: {self.name} Net: {net_name}"\
                        " is alerady connected")
                return

        if isInput:
            pin_id = len(self.ipins) + 1
            self.ipins.append(f'IP{pin_id}')
            self.ipin_act_x_offset.append(x_offset)
            self.ipin_act_y_offset.append(y_offset)
            self.ipin_net_id.append(net_id)
        else:
            pin_id = len(self.opins) + 1
            self.opins.append(f'OP{pin_id}')
            self.opin_act_x_offset.append(x_offset)
            self.opin_act_y_offset.append(y_offset)
            self.opin_net_id.append(net_id)

    def update_pin_loc(self):
        for i in range(len(self.ipins)):
            pin_x, pin_y = find_pin_act_loc(self.cx, self.cy, \
                                            self.ipin_act_x_offset[i], \
                                            self.ipin_act_y_offset[i], \
                                            self.orient)
            self.ipin_x.append(pin_x)
            self.ipin_y.append(pin_y)

        for i in range(len(self.opins)):
            pin_x, pin_y = find_pin_act_loc(self.cx, self.cy, \
                                            self.opin_act_x_offset[i], \
                                            self.opin_act_y_offset[i], \
                                            self.orient)
            self.opin_x.append(pin_x)
            self.opin_y.append(pin_y)
        return


    def check_pins(self):
        for x_offset in self.ipin_act_x_offset + self.opin_act_x_offset:
            if round(x_offset,6) < round(-1*self.width/2.0,6) \
                or round(x_offset,6) > round(self.width/2,6):
                print(f"[INFO][ERROR] Cell:{self.name} Orient:{self.orient} has pin out of bbox")
                return

        for y_offset in self.ipin_act_y_offset + self.opin_act_y_offset:
            if round(y_offset,6) < round(-1*self.height/2.0,6) \
                or round(y_offset,6) > round(self.height/2,6):
                print(f"[INFO][ERROR] Cell:{self.name} Orient:{self.orient} has pin out of bbox")
                return
        return

    def update_isMacro(self, site_height):
        if self.height != site_height:
            self.isMacro = True
        else:
            self.isMacro = False
        return

    def update_master_id(self, idx):
        self.master_id = idx
        return

class master:
    def __init__(self, idx, name):
        self.id = idx
        self.name = name
        self.width = None
        self.height = None
        self.isMacro = None   # Considers macro if heigh is more than row height
        self.ipins = []
        self.opins = []
        self.ipin_act_x_offset = []
        self.ipin_act_y_offset = []
        self.opin_act_y_offset = []
        self.opin_act_x_offset = []
        return

    def update_master(self, inst):
        self.width = inst.width
        self.height = inst.height
        self.isMacro = inst.isMacro
        self.ipins = [x for x in inst.ipins]
        self.opins = [x for x in inst.opins]
        self.ipin_act_x_offset = [x for x in inst.ipin_act_x_offset]
        self.ipin_act_y_offset = [y for y in inst.ipin_act_y_offset]
        self.opin_act_x_offset = [x for x in inst.opin_act_x_offset]
        self.opin_act_y_offset = [y for y in inst.opin_act_y_offset]
        return

class port:
    def __init__(self, name):
        self.id = None
        self.name = name
        self.x = None
        self.y = None
        self.isInput = None
        self.net_id = None
        self.side = None
        self.px = None # Protobuf location
        self.py = None # Protobuf location
    
    def connect_net(self, net_id, net_name, isInput):
        if self.net_id != None:
            print(f"[INFO] [ERROR-3] {self.name} is connected. Check Net: {net_name}")
        
        self.net_id = net_id
        self.isInput = isInput
        return
    
    def update_id(self, idx):
        self.id = idx
        return

    def update_location(self, x, y):
        self.x = x
        self.y = y
        return
    
    def update_side(self, pt_x, pt_y, dx, dy):
        cond1 = pt_x - pt_y
        cond2 = pt_x/dx + pt_y/dy - 1
        if cond1 > 0:
            if cond2 > 0:
                self.side = "right"
                return
            else:
                self.side = "bottom"
                return
        else:
            if cond2 > 0:
                self.side = "top"
                return
            else:
                self.side = "left"
                return

    def update_protobuf_location(self, c2dx, c2dy, cox, coy, dx, dy, dd = 0):
        '''
        c2dx = core to die spacing along x axix
        c2dy = core to die spacing along y axix
        cox = core origin x
        coy = core origin y
        dx = core widht
        dy = core height
        '''
        die_llx = cox - c2dx[0]
        die_lly = coy - c2dy[0]
        die_dx = dx + sum(c2dx)
        die_dy = dy + sum(c2dy)
        
        X = self.x - die_llx
        Y = self.y - die_lly
        
        if self.side == None:
            self.update_side(X, Y, die_dx, die_dy)
        
        co_urx = cox + dx
        co_ury = coy + dy
        
        if self.side == "left":
            self.px = dd
        elif self.side == "right":
            self.px = dx - dd
        
        if self.side in ["left", "right"]:
            if self.y <= coy:
                ## Directly print py Considering Core origin (0, 0)
                self.py = dd
            elif self.y >= co_ury:
                self.py = dy - dd
            else:
                self.py = self.y - coy
        
        if self.side == "top":
            self.py = dy - dd
        elif self.side == "bottom":
            self.py = dd
        
        if self.side in ["top", "bottom"]:
            if self.x <= cox:
                self.px = dd
            elif self.x >= co_urx:
                self.px = dx - dd
            else:
                self.px = self.x - cox
        return

class net:
    def __init__(self, idx, name) -> None:
        self.id = idx
        self.name = name
        self.dtype = None
        self.did = None
        self.stypes = []
        self.sids = []
        return
    def add_driver(self, dtype, did):
        if self.did == None:
            self.dtype = dtype
            self.did = did
        else:
            print(f"[INFO][ERROR-4] Net has the driver id:{did} type:{dtype}")
        return
    
    def add_sink(self, stype, sid):
        if sid not in self.sids:
            self.sids.append(sid)
            self.stypes.append(stype)
        else:
            print(f"[INFO][ERROR-5] Net:{self.name} has the sink id:{sid} type:{stype}")      
        return

class row:
    def __init__(self, idx):
        self.id = idx
        self.llx = None
        self.lly = None
        self.urx = None
        self.ury = None
        self.orient = None
        self.site_count = None
        self.inst_ids = []
        return
    def update_llx(self, llx):
        self.llx = llx
        return
    def update_lly(self, lly):
        self.lly = lly
        return
    def update_site_count(self, count):
        if self.site_count == None:
            self.site_count = int(count)
            self.inst_ids = [None]*int(count)
        elif self.site_count != int(count):
            print(f"[INFO][ERROR-6] Row id: {self.id} check site count")
        return
    def update_orient(self, orient):
        if self.orient == None:
            self.orient = orient
        elif self.orient != orient:
            print(f"[INFO][ERROR-7] Row id: {self.id} multiple orient")
        return
    def update_size(self, swidth, sheight):
        self.urx = self.llx + self.site_count*swidth
        self.ury = self.lly + sheight
        return

class canvas_object:
    def __init__(self, name, unit = 100):
        self.design = name
        self.unit = float(unit)
        self.site_width = None
        self.site_height = None
        self.site_spacing = None
        self.core_llx = None
        self.core_lly = None
        self.core_urx = None
        self.core_ury = None
        self.core_dx = None
        self.core_dy = None
        self.die_llx = None
        self.die_lly = None
        self.die_urx = None
        self.die_ury = None
        self.die_dx = None
        self.die_dy = None
        self.row_count = None
        self.port_count = None
        self.inst_count = None
        self.pin_count = None
        self.net_count = None
        self.BookShelf_dir = None
        self.c2dx = []
        self.c2dy = []
        self.nets = []
        self.insts = []
        self.masters = []
        self.ports = []
        self.netMap = {}
        self.instMap = {}
        self.masterMap = {}
        self.portMap = {}
        self.pb_id = {}
        self.pb_type = {}
        self.rows = []
        return
    def init_core(self):
        self.core_llx = self.rows[0].llx
        self.core_lly = self.rows[0].lly
        self.core_urx = self.rows[0].urx
        self.core_ury = self.rows[0].ury
        return
    def init_die(self):
        self.die_llx = self.core_llx
        self.die_lly = self.core_lly
        self.die_urx = self.core_urx
        self.die_ury = self.core_ury
        return
    def update_c2d_spacing(self):
        self.c2dx.append(self.core_llx - self.die_llx)
        self.c2dx.append(self.die_urx - self.core_urx)
        self.c2dy.append(self.core_lly - self.die_lly)
        self.c2dy.append(self.die_ury - self.core_ury)
        return
    def print_core_details(self):
        print(f"Core llx:{self.core_llx} Core lly:{self.core_lly}")
        print(f"Core urx:{self.core_urx} Core ury:{self.core_ury}")
        print(f"Core dx:{self.core_dx} Core dy:{self.core_dy}")
        return
    def print_die_details(self):
        print(f"Die llx:{self.die_llx} Die lly:{self.die_lly}")
        print(f"Die urx:{self.die_urx} Die ury:{self.die_ury}")
        print(f"Die dx:{self.die_dx} Die dy:{self.die_dy}")
        return

    def check_inst_op(self, count, isPrint = False):
        _count = 0
        for _inst in self.insts:
            if len(_inst.opins) == count:
                _count += 1
                if isPrint:
                    print(f"Inst Name:{_inst.name} id:{_inst.id} Macro:{_inst.isMacro}")

        print(f"Total number of instances with {count} output pins is {_count}")
        return

    def report_macros(self, isPrint = False):
        _count = 0
        for _inst in self.insts:
            if _inst.isMacro:
                if isPrint:
                    print(f"Inst id:{_inst.id} name:{_inst.name}")
                _count += 1
        print(f"Total number of macro instances is {_count}")
        return

    def update_port_side(self):
        for _port in self.ports:
            _port.update_protobuf_location(self.c2dx, self.c2dy, self.core_llx,\
                self.core_lly, self.core_dx, self.core_dy, 0.5/self.unit)
        return
    
    def update_inst_type(self):
        for _inst in self.insts:
            _inst.update_isMacro(self.site_height)
        return

    def update_inst_pin_loc(self):
        for _inst in self.insts:
            _inst.update_pin_loc()
        return

    def check_inst_pins(self):
        for _inst in self.insts:
            _inst.check_pins()

    def read_scl(self, scl_file):
        '''
        This reads the design.scl file and updates the core information.

        This function updates the row information.
        '''
        scl_fp = open(scl_file, 'r')
        row_id = 0
    
        crow = None
        for line in scl_fp.readlines():
            if re.match(r"(^\s*#)|(^\s*UCLA\s*scl\s*1.0$)|(^\s*$)|(^\sEnd\s*$)", line):
                continue
            
            if re.match("^\s*CoreRow\s*Horizontal\s*$", line):
                self.rows.append(row(row_id))
                crow = self.rows[row_id]
                row_id += 1
                continue
            
            lly = re.findall(r"^\s*Coordinate\s*:\s*([0-9,-,\.]*)", line)
            sheight = re.findall(r"\s*Height\s*:\s*([0-9,-,\.]*)", line)
            swidth = re.findall(r"^\s*Sitewidth\s*:\s*([0-9,-,\.]*)", line)
            sorient = re.findall(r"^\s*Siteorient\s*:\s*([F, S, E, W, N]*)", line)
            llx = re.findall(r"^\s*SubrowOrigin\s*:\s*([0-9,-,\.]*)", line)
            sspacing = re.findall(r"^\s*Sitespacing\s*:\s*([0-9,-,\.]*)", line)
            numsites = re.findall(r"\s\s*Num[S,s]ites\s*:\s*([0-9]*)", line)

            if lly and lly[0] != "" :
                _lly = float(lly[0])/self.unit
                crow.update_lly(_lly)
            
            if sheight and sheight[0] != "":
                if self.site_height == None:
                    self.site_height = float(sheight[0])/self.unit
                elif self.site_height != float(sheight[0])/self.unit:
                    print(f"[INFO][ERROR-8] Row id:{row_id-1} site height is different")
            
            if swidth and swidth[0] != "":
                if self.site_width == None:
                    self.site_width = float(swidth[0])/self.unit
                elif self.site_width != float(swidth[0])/self.unit:
                    print(f"[INFO][ERROR-9] Row id:{row_id-1} site width is different")
            
            if sorient and sorient[0] != "":
                crow.update_orient(sorient[0])
            
            if llx and llx[0] != "":
                _llx = float(llx[0])/self.unit
                crow.update_llx(_llx)
            
            if sspacing and sspacing[0] != "":
                if self.site_spacing == None:
                    self.site_spacing = float(sspacing[0])/self.unit
                elif self.site_spacing != float(sspacing[0])/self.unit:
                    print(f"[INFO][ERROR-10] Row id:{row_id-1} site spacing is different")
            
            if numsites and numsites[0] != "":
                crow.update_site_count(numsites[0])
        
        self.rows[0].update_size(self.site_width, self.site_height)
        self.init_core()
        for _row in self.rows:
            _row.update_size(self.site_width, self.site_height)
            self.core_llx = min(self.core_llx, _row.llx)
            self.core_lly = min(self.core_lly, _row.lly)
            self.core_urx = max(self.core_urx, _row.urx)
            self.core_ury = max(self.core_ury, _row.ury)
        self.core_dx = self.core_urx - self.core_llx
        self.core_dy = self.core_ury - self.core_lly

        scl_fp.close()
        return

    def read_nodes(self, nodes_file):
        '''
        Read design.nodes file and updated the terminals and instances
        '''
        nodes_fp = open(nodes_file, 'r')
        node_count = None
        port_id = 0
        inst_id = 0
        terminal_count = None
        fixed_inst_count = 0
        for line in nodes_fp.readlines():
            if re.match(r"(^\s*#)|(^\s*UCLA\s*nodes\s*1.0$)|(^\s*$)", line):
                continue

            num_nodes = re.findall(r"^\s*NumNodes\s*:\s*([0-9]*)", line)
            if num_nodes and num_nodes[0] != "":
                node_count = int(num_nodes[0])
                if self.port_count != None:
                    self.inst_count = node_count - self.port_count
                continue

            num_terminal = re.findall(r"^\s*NumTerminals\s*:\s*([0-9]*)", line)
            if num_terminal and num_terminal[0] != "":
                terminal_count = int(num_terminal[0])
                continue

            items = line.split()
            # By default ports has 1 width and 1 height
            if len(items) == 4 and items[1] == "1" and items[2] == "1" and \
                items[3] in ['terminal', 'terminal_NI']:
                self.ports.append(port(items[0]))
                self.ports[-1].update_id(port_id)
                self.portMap[items[0]] = port_id
                port_id += 1
            elif (len(items) == 4 and items[3] == 'terminal') or \
                len(items) == 3:
                self.insts.append(instance(inst_id, items[0]))
                _height = float(items[2])/self.unit
                _width = float(items[1])/self.unit
                self.insts[-1].update_size(_height, _width)
                self.instMap[items[0]] = inst_id
                inst_id += 1
                if len(items) == 4 and items[3] == 'terminal':
                    fixed_inst_count += 1
        
        self.inst_count = inst_id
        self.port_count = port_id
        if port_id + inst_id != node_count:
            print(f"[INFO][ERROR-11] Mismatch in total number of nodes and read nodes")

        if terminal_count - fixed_inst_count != port_id:
            print(f"[INFO][ERROR-12] Mismatch in the port count and fixed instance count")
        
        nodes_fp.close()
        return

    def read_pl(self, pl_file):
        '''
        Read design.pl file and update the terminal and isntance locations
        '''
        pl_fp = open(pl_file, 'r')

        for line in pl_fp.readlines():
            if re.match(r"(^\s*#)|(^\s*UCLA\s*pl\s*1.0$)|(^\s*$)", line):
                continue
            items = line.split()

            _x = float(items[1])/self.unit
            _y = float(items[2])/self.unit
            _orient = items[4]

            if items[0] in self.portMap:
                port_id = self.portMap[items[0]]
                _port = self.ports[port_id]
                _port.update_location(_x, _y)
            elif items[0] in self.instMap:
                inst_id = self.instMap[items[0]]
                _inst = self.insts[inst_id]
                _pstatus = "UNPLACED"
                if len(items) == 6:
                    _pstatus = items[5]

                _inst.update_location(_x, _y, _orient, _pstatus)
                _inst.update_center()

        self.init_die()
        for _port in self.ports:
            self.die_llx = min(self.die_llx, _port.x)
            self.die_lly = min(self.die_lly, _port.y)
            self.die_urx = max(self.die_urx, _port.x)
            self.die_ury = max(self.die_ury, _port.y)

        self.die_dx = self.die_urx - self.die_llx
        self.die_dy = self.die_ury - self.die_lly
        self.update_c2d_spacing()
        self.update_port_side()
        self.update_inst_type()
        pl_fp.close()
        return

    def read_net_healper(self, line, net_name, net_id, isDriver):
        items = line.split()
        pname = items[0]
        ptype = None
        idx = None
        if items[0] in self.portMap:
            ptype = "port"
            idx = self.portMap[pname]
            self.ports[idx].connect_net(net_id, net_name, isDriver)
        else:
            ptype = "inst"
            idx = self.instMap[pname]
            _x_offset = float(items[3])/self.unit
            _y_offset = float(items[4])/self.unit
            self.insts[idx].connect_net(net_name, net_id, not isDriver, \
                                        _x_offset, _y_offset)
        
        if isDriver:
            self.nets[net_id].add_driver(ptype, idx)
        else:
            self.nets[net_id].add_sink(ptype, idx)
        return

    def read_nets(self, nets_file):
        '''
        Read design.nets file and add nets, pins and pin locations and pin typs.

        For each net we consider the first pin is the driver.
        '''
        nets_fp = open(nets_file, 'r')
        net_id = 0
        lines = nets_fp.readlines()
        no_lines = len(lines)
        i = 0
        while i < no_lines:
            if re.match(r"(^\s*#)|(^\s*UCLA\s*nets\s*1.0$)|(^\s*$)", lines[i]):
                i += 1
                continue

            num_nets = re.findall(r"^\s*NumNets\s*:\s*([0-9]*)", lines[i])
            num_pins = re.findall(r"^\s*NumPins\s*:\s*([0-9]*)", lines[i])

            if num_nets and num_nets[0] != "":
                self.net_count = int(num_nets[0])
                i += 1
                continue

            if num_pins and num_pins[0] != "":
                self.pin_count = int(num_pins[0])
                i += 1
                continue

            items = lines[i].split()
            net_degree = 0
            if items[0] == "NetDegree":
                ## Add net details ##
                net_degree = int(items[2])
                net_name = items[3]
                self.nets.append(net(net_id, net_name))
                self.netMap[net_name] = net_id
                i += 1

                ## SPins = Sinks DPins = Drivers BiPins = Bidrections ##
                SPins = []
                DPins = []
                BiPins = []
                for _ in range(net_degree):
                    items = lines[i].split()
                    if items[1] == 'I':
                        SPins.append(i)
                    elif items[1] == 'O':
                        DPins.append(i)
                    elif items[1] == 'B':
                        BiPins.append(i)
                    else:
                        print(f"[INFO][ERROR] Check net:{net_name} sink/driver not defined")
                    i += 1
                
                AckDriver = None
                if len(DPins) > 0:
                    AckDriver = DPins[0]
                    del DPins[0]
                elif len(BiPins) > 0:
                    AckDriver = BiPins[0]
                    del BiPins[0]
                elif len(SPins) > 0:
                    AckDriver = SPins[0]
                    del SPins[0]
                else:
                    print(f"[INFO][ERROR] Check net:{net_name} does not have driver and sink")
                
                self.read_net_healper(lines[AckDriver], net_name, net_id, True)
                for j in DPins + SPins + BiPins:
                    self.read_net_healper(lines[j], net_name, net_id, False)
                
                net_id += 1
        del lines
        nets_fp.close()
        self.update_inst_pin_loc()
        return

    def write_header(self, fp):
        user = os.environ["USER"]
        if self.BookShelf_dir == None:
            run_dir = os.environ["PWD"]
        else:
            run_dir = self.BookShelf_dir
        today = date.today()
        fp.write(f"# User: {user}\n")
        fp.write(f"# Date: {today}\n")
        fp.write(f"# Run area: {run_dir}\n")
        fp.write(f"# Block: {self.design}\n")
        fp.write("# FP bbox: {0.0 0.0} {" f"{self.core_dx} {self.core_dy}" "}\n")
        fp.write("# Columns : 10  Rows : 10\n")
        return

    def print_net(self, net_id, fp):
        no_sink = len(self.nets[net_id].sids)
        for i in range(no_sink):
            stype = self.nets[net_id].stypes[i]
            sid = self.nets[net_id].sids[i]
            if stype == "inst":
                inst_name = self.insts[sid].name
                if self.insts[sid].isMacro:
                    # print(f"inst id:{sid} net id:{net_id}")
                    idx = self.insts[sid].ipin_net_id.index(net_id)
                    pin_name = self.insts[sid].ipins[idx]
                    fp.write(f"  input: \"{inst_name}/{pin_name}\"\n")
                else:
                    fp.write(f"  input: \"{inst_name}\"\n")
            else:
                port_name = self.ports[sid].name
                fp.write(f"  input: \"{port_name}\"\n")
        return

    def write_node_port(self, port_id, fp):
        fp.write("node {\n")
        _port = self.ports[port_id]
        port_name = _port.name
        fp.write(f"  name: \"{port_name}\"\n")
        
        if _port.isInput:
            self.print_net(_port.net_id, fp)
        
        print_placeholder(fp, "type", "port")
        print_placeholder(fp, "side", _port.side)
        print_float(fp, "x", _port.px)
        print_float(fp, "y", _port.py)
        fp.write("}\n")
        return

    def write_node_macro(self, macro_id, fp):
        fp.write("node {\n")
        _macro = self.insts[macro_id]
        macro_name = _macro.name
        fp.write(f"  name: \"{macro_name}\"\n")

        print_float(fp, "width", _macro.width)
        print_float(fp, "height", _macro.height)
        print_placeholder(fp, "type", "macro")
        print_float(fp, "x", _macro.cx)
        print_float(fp, "y", _macro.cy)
        fp.write("}\n")
        return

    def write_node_macro_ipin(self, macro_id, pin_id, fp):
        fp.write("node {\n")
        _macro = self.insts[macro_id]
        macro_name = _macro.name
        pin_name = _macro.ipins[pin_id]
        fp.write(f"  name: \"{macro_name}/{pin_name}\"\n")
        print_placeholder(fp, "macro_name", macro_name)
        print_placeholder(fp, "type", "macro_pin")
        print_float(fp, "x_offset", _macro.ipin_act_x_offset[pin_id])
        print_float(fp, "y_offset", _macro.ipin_act_y_offset[pin_id])
        if len(_macro.ipin_x) > pin_id:
            print_float(fp, "x", _macro.ipin_x[pin_id])
            print_float(fp, "y", _macro.ipin_y[pin_id])
        fp.write("}\n")
        return

    def write_node_macro_opin(self, macro_id, pin_id, fp):
        fp.write("node {\n")
        _macro = self.insts[macro_id]
        macro_name = _macro.name
        pin_name = _macro.opins[pin_id]
        fp.write(f"  name: \"{macro_name}/{pin_name}\"\n")
        net_id = _macro.opin_net_id[pin_id]
        self.print_net(net_id, fp)
        print_placeholder(fp, "macro_name", macro_name)
        print_placeholder(fp, "type", "macro_pin")
        
        print_float(fp, "x_offset", _macro.opin_act_x_offset[pin_id])
        print_float(fp, "y_offset", _macro.opin_act_y_offset[pin_id])
        if len(_macro.opin_x) > pin_id:
            print_float(fp, "x", _macro.opin_x[pin_id])
            print_float(fp, "y", _macro.opin_y[pin_id])
        fp.write("}\n")
        return

    def write_node_stdcell(self, inst_id, fp):
        fp.write("node {\n")
        _inst = self.insts[inst_id]
        inst_name = _inst.name
        fp.write(f"  name: \"{inst_name}\"\n")

        for net_id in _inst.opin_net_id:
            self.print_net(net_id, fp)
        print_placeholder(fp, "type", "stdcell")
        print_float(fp, "width", _inst.width)
        print_float(fp, "height", _inst.height)
        print_float(fp, "x", _inst.cx)
        print_float(fp, "y", _inst.cy)
        fp.write("}\n")
        return

    def gen_pb_netlist(self, file_name=None):
        if file_name == None:
            if self.BookShelf_dir == None:
                file_name = f"{self.design}.pb.txt"
            else:
                file_name = f"{self.BookShelf_dir}/{self.design}.pb.txt"

        fp = open(file_name, "w")
        self.write_header(fp)
        for _port in self.ports:
            self.write_node_port(_port.id, fp)

        for _inst in self.insts:
            if _inst.isMacro:
                self.write_node_macro(_inst.id, fp)
                for i in range(len(_inst.ipins)):
                    self.write_node_macro_ipin(_inst.id, i, fp)

                for i in range(len(_inst.opins)):
                    self.write_node_macro_opin(_inst.id, i, fp)
            else:
                self.write_node_stdcell(_inst.id, fp)

        fp.close()
        print(f"Output protobuf netlist: {file_name}")
        return

    def read_BookShelf(self, dir, design = None):
        if design == None:
            design = self.design

        if os.path.exists(dir):
            self.BookShelf_dir = dir
        else:
            print(f"[INFO][ERROR-13] BookShelf dir does not exists.\nDIR:{dir}")
            return

        scl_file = f"{dir}/{design}.scl"
        if os.path.isfile(scl_file):
            print(f"Parsing: {scl_file}")
            self.read_scl(scl_file)
        else:
            print(f"[INFO][ERROR-14] Check file: \n{scl_file}")

        nodes_file = f"{dir}/{design}.nodes"
        if os.path.isfile(nodes_file):
            print(f"Parsing: {nodes_file}")
            self.read_nodes(nodes_file)
        else:
            print(f"[INFO][ERROR-15] Check file: \n{nodes_file}")

        pl_file = f"{dir}/{design}.pl"
        if os.path.isfile(pl_file):
            print(f"Parsing: {pl_file}")
            self.read_pl(pl_file)
        else:
            print(f"[INFO][ERROR-16] Check file: \n{pl_file}")

        nets_file = f"{dir}/{design}.nets"
        if os.path.isfile(nets_file):
            print(f"Parsing: {nets_file}")
            self.read_nets(nets_file)
        else:
            print(f"[INFO][ERROR-17] Check file: \n{nets_file}")
        
        for inst in self.insts:
            inst.name = inst.name.replace("\\","")
        
        for port in self.ports:
            port.name = port.name.replace("\\","")
        return

    def read_pb(self, pb_netlist):
        fp = open(pb_netlist, 'r')
        lines = fp.readlines()
        node_id = 0
        for line in lines:
            words = line.split()
            if words[0] == 'name:':
                node_name = words[1].replace('"','')
                if node_name == "__metadata__":
                    continue

                if node_name in self.portMap:
                    self.pb_id[node_id] = self.portMap[node_name]
                    self.pb_type[node_id] = "port"
                elif node_name in self.instMap:
                    self.pb_id[node_id] = self.instMap[node_name]
                    self.pb_type[node_id] = "inst"
                else:
                    self.pb_type[node_id] = "other"
                node_id += 1
        return
    
    def read_plc(self, plc_file):
        fp = open(plc_file, 'r')
        lines = fp.readlines()
        id_pattern = re.compile("[0-9]+")
        for line in lines:
            words = line.split()
            if id_pattern.match(words[0]) and (len(words) == 5):
                idx = int(words[0])
                if self.pb_type[idx] == "inst":
                    if self.insts[self.pb_id[idx]].isMacro:
                        self.insts[self.pb_id[idx]].cx = float(words[1])
                        self.insts[self.pb_id[idx]].cy = float(words[2])
                        self.insts[self.pb_id[idx]].pstatus = "FIXED"
        return
    
    def write_node(self, node_file = None):
        if node_file == None:
            node_file = f"{self.BookShelf_dir}/{self.design}.updated.nodes"
        
        fp = open(node_file, "w")
        today = date.today()
        user = user = os.environ["USER"]
        fp.write("UCLA nodes 1.0\n")
        fp.write(f"# Created : {today}\n")
        fp.write(f"# User: {user}\n\n")
        total_nodes = len(self.insts) + len(self.ports)
        total_fixed_instance = 0
        for inst in self.insts:
            if inst.pstatus == "FIXED":
                total_fixed_instance += 1
        
        total_terminals = len(self.ports) + total_fixed_instance

        fp.write(f"NumNodes : {total_nodes}\nNumTerminals : {total_terminals}\n")
        for port in self.ports:
            fp.write(f"  {port.name}  1  1 terminal\n")
        for inst in self.insts:
            height = round(inst.height * self.unit, 6)
            width = round(inst.width * self.unit, 6)
            eol = ""
            if inst.pstatus == "FIXED":
                eol = "terminal"
            fp.write(f"  {inst.name}  {width}  {height}  {eol}\n")
        fp.close()
        return

    def write_pl(self, pl_file = None):
        if pl_file == None:
            pl_file = f"{self.BookShelf_dir}/{self.design}.updated.pl"
        
        fp = open(pl_file, "w")
        today = date.today()
        user = user = os.environ["USER"]
        fp.write("UCLA pl 1.0\n")
        fp.write(f"# Created : {today}\n")
        fp.write(f"# User: {user}\n\n")

        for port in self.ports:
            px = port.x * self.unit
            py = port.y * self.unit
            fp.write(f"  {port.name}  {px}  {py} : N /FIXED\n")
        
        for inst in self.insts:
            px = (inst.cx - (inst.width * 0.5)) * self.unit
            py = (inst.cy - (inst.height * 0.5)) * self.unit
            eol = ""
            if inst.pstatus == "FIXED":
                eol = "/FIXED"
            else:
                px = (self.core_dx*0.5 - (inst.width * 0.5)) * self.unit
                px = (self.core_dy*0.5 - (inst.width * 0.5)) * self.unit

            px = round(px, 6)
            py = round(py, 6)
            fp.write(f"  {inst.name}  {px}  {py} : N {eol}\n")
        
        fp.close()