import numpy as np
import copy

room_label = [(0, 'LivingRoom', 1, "PublicArea",[220, 213, 205]),
              (1, 'MasterRoom', 0, "Bedroom",[138, 113, 91]),
              (2, 'Kitchen', 1, "FunctionArea",[244, 245, 247]),
              (3, 'Bathroom', 0, "FunctionArea",[224, 225, 227]),
              (4, 'DiningRoom', 1, "FunctionArea",[200, 193, 185]),
              (5, 'ChildRoom', 0, "Bedroom",[198, 173, 151]),
              (6, 'StudyRoom', 0, "Bedroom",[178, 153, 131]),
              (7, 'SecondRoom', 0, "Bedroom",[158, 133, 111]),
              (8, 'GuestRoom', 0, "Bedroom",[189, 172, 146]),
              (9, 'Balcony', 1, "PublicArea",[244, 237, 224]),
              (10, 'Entrance', 1, "PublicArea",[238, 235, 230]),
              (11, 'Storage', 0, "PublicArea",[226, 220, 206]),
              (12, 'Wall-in', 0, "PublicArea",[226, 220, 206]),
              (13, 'External', 0, "External",[255, 255, 255]),
              (14, 'ExteriorWall', 0, "ExteriorWall",[0, 0, 0]),
              (15, 'FrontDoor', 0, "FrontDoor",[255,255,0]),
              (16, 'InteriorWall', 0, "InteriorWall",[128,128,128]),
              (17, 'InteriorDoor', 0, "InteriorDoor",[255,255,255])]

class DirectedLine():
    def __init__(self,p1,p2):
        '''search direction : 0(horizontal) / 1(vertical)'''
        if np.abs(p1[0]-p2[0])<1e-6:
            self.dir = 1
            self.level = p1[0]
            self.minLevel = min(p1[1],p2[1])
            self.maxLevel = max(p1[1],p2[1])
        else:
            self.dir = 0
            self.level = p1[1]
            self.minLevel = min(p1[0],p2[0])
            self.maxLevel = max(p1[0],p2[0])
    
    def __repr__(self):
        if self.dir==0: return f'({self.level},[{self.minLevel},{self.maxLevel}])'
        else: return f'([{self.minLevel},{self.maxLevel}],{self.level})'
    
    @property
    def length(self):return self.maxLevel-self.minLevel
    
    def is_contact(self,line): 
        minl = min(self.minLevel,line.minLevel)
        maxl = max(self.maxLevel,line.maxLevel)
        length = maxl-minl
        return (
            self.dir==line.dir and 
            #self.level!=line.level and 
            abs(self.level-line.level)<6 and
            length < self.length+line.length
        )
    
    @staticmethod
    def lines_from_boundary(boundary):
        if len(boundary)==0:return []
        pts = boundary.tolist()+[boundary[0].tolist()]
        lines = [ DirectedLine(pts[i],pts[i+1]) for i in range(len(pts)-1)]
        return lines

class DirectedWall():
    def __init__(self):
        '''orientation : 0(right) / 1(down) / 2(left) / 3(up)'''
        self.dir = 0
        self.rect = np.array([0,0,0,0])
    
    @property
    def width(self):return self.rect[2]
    
    @property
    def height(self):return self.rect[3]
    
    @property
    def center(self):return self.rect[:2]+self.rect[2:]/2
    
    def setX(self,x):self.rect[0]=x
    def setY(self,y):self.rect[1]=y
    def setWidth(self,w):self.rect[2]=w
    def setHeight(self,h):self.rect[2]=h
    def setLeft(self,x):
        self.rect[2]=(self.rect[0]-x)+self.rect[2]
        self.rect[0]=x
    def setTop(self,y):
        self.rect[3]=(self.rect[1]-y)+self.rect[3]
        self.rect[1]=y
    
    def to_line(self):
        if self.dir in [0,2]:
            return DirectedLine([self.rect[0],self.rect[1]],[self.rect[0],self.rect[1]+self.rect[3]])
        else:
            return DirectedLine([self.rect[0],self.rect[1]],[self.rect[0]+self.rect[2],self.rect[1]])
    
    def __repr__(self):
        pos = ['right','down','left','up','None'][self.dir]
        return f'({pos},{self.rect})'

class Entry():
    def __init__(self):
        '''door type : 0(door) / 1(open wall)'''
        self.type = -1
        self.entry = None
        
    def __repr__(self):
        if self.type==0: return f'(door,{self.entry})'
        else: return f'(open wall,{self.entry})'   
        
class Room():
    def __init__(self):
        self.box = None
        self.category = None
        self.boundary = None
        
        self.map = None
        self.entry = None
        self.windows = []
    
    @property
    def label(self):return room_label[self.category][1]
    
    @property
    def type(self): return room_label[self.category][3]
    
    @property
    def center(self): return self.box.reshape(-1,2).mean(0)

    @staticmethod
    def rooms_from_data(data):
        rooms = []
        for i in range(len(data.rType)):
            room = Room()
            room.box = data.newBox[i]
            room.category = data.rType[i]
            room.boundary = data.rBoundary[i]
            room.lines = DirectedLine.lines_from_boundary(room.boundary)
            rooms.append(room)
        return rooms

    @staticmethod
    def from_node_box(node,box):
        x0,y0,x1,y1 = box
        room = Room()
        room.box = box
        room.category = node[-1]
        room.boundary = np.array([
                [x0,y0],
                [x0,y1],
                [x1,y1],
                [x1,y0]
            ])
        room.lines = DirectedLine.lines_from_boundary(room.boundary)
        # room.boundary = [
        #     DirectedLine((x0,y0),(x1,y0)), # top
        #     DirectedLine((x1,y0),(x1,y1)), # right
        #     DirectedLine((x0,y1),(x1,y1)), # bottom
        #     DirectedLine((x0,y0),(x0,y1)), # left
        #     ]
        return room
        
    @staticmethod
    def from_boundary(boundary):
        # pts = boundary[:,:2].tolist()
        # pts = pts+[pts[0]]
        room = Room()
        
        room.category = 0
        room.box = np.array([np.min(boundary[:,0]),np.min(boundary[:,1]),np.max(boundary[:,0]),np.max(boundary[:,1])])
        room.boundary = boundary[:,:2]
        room.lines = DirectedLine.lines_from_boundary(room.boundary)
        # room.boundary = [
        #     DirectedLine(pts[i],pts[i+1])
        #     for i in range(len(pts)-1)
        #     ]
        return room
        
    def __repr__(self):
        return f'({self.label},{self.type},{self.box},{self.entry},{self.windows})'
        
def find_contact_walls(room1,room2,reverse=False):
    contactWalls = []
    lines1 = copy.deepcopy(room1.lines) #DirectedLine.from_boundary(room1.boundary)#room1.lines
    center1 = room1.center
    lines2 = copy.deepcopy(room2.lines) #DirectedLine.from_boundary(room2.boundary)# room2.lines
    center2 = room2.center
    temp = []
    
    for i in range(len(lines1)):
        line1 = lines1[i]
        
        for j in range(len(lines2)):
            line2 = lines2[j]
            
            if line1.is_contact(line2):
                contactWall = DirectedWall()
                if line1.dir==0:
                    minh = line1.level if not reverse else line2.level #min(line1.level,line2.level)
                    maxh = line1.level if not reverse else line2.level #max(line1.level,line2.level)
                    minw = max(line1.minLevel,line2.minLevel)
                    maxw = min(line1.maxLevel,line2.maxLevel)
                    # @todo:boudanry not work!
                    if center1[1] > line1.level: contactWall.dir=1
                    else: contactWall.dir=3
                    contactWall.rect = np.array([minw,minh,maxw-minw,maxh-minh])
                else:
                    minw = line1.level if not reverse else line2.level#min(line1.level,line2.level)
                    maxw = line1.level if not reverse else line2.level#max(line1.level,line2.level)
                    minh = max(line1.minLevel,line2.minLevel)
                    maxh = min(line1.maxLevel,line2.maxLevel)
                    if center1[0] > line1.level: contactWall.dir=0
                    else: contactWall.dir=2
                    contactWall.rect = np.array([minw,minh,maxw-minw,maxh-minh])
                contactWalls.append(contactWall)
    return contactWalls

def find_longest_wall(contactWalls,dtype=1):
    contactLength = 0
    openWall = None
    for i in range(len(contactWalls)):
        maxLength = max(contactWalls[i].width,contactWalls[i].height)
        if maxLength>contactLength:
            contactLength = maxLength
            openWall = contactWalls[i]
    if contactLength!=0:
        # @todo: adjust door
        openWall = adjust_door(openWall,dtype)
        entry = Entry()
        entry.type = dtype
        entry.entry = openWall
        assert entry.entry.dir!=-1, "find longest wall with dir -1!"
        return entry
    return None

def find_closest_wall(candidateDoors,frontDoorCenter,dtype=1,boundary_lines=[]):
    dis = 1e8
    door = None
    for i in range(len(candidateDoors)):
        maxLength = max(candidateDoors[i].width,candidateDoors[i].height)
        if maxLength<12:continue

        valid = True
        line = candidateDoors[i].to_line()
        for b_line in boundary_lines:
            if line.is_contact(b_line):
                valid = False
                break
        if not valid: continue

        center = candidateDoors[i].center
        candidateDis = np.sum(np.power((center-frontDoorCenter),2))
        if dis>candidateDis:
            dis = candidateDis
            door = candidateDoors[i]
    if door is not None:
        door = adjust_door(door,dtype)
        entry = Entry()
        entry.type = dtype
        entry.entry = door
        assert entry.entry.dir!=-1, "find closest wall with dir -1!"
        return entry
    return None

def adjust_door(door,dtype=1):
    if door.dir in [1,3]:
        if dtype==1: 
            door.rect[0] = door.rect[0]+door.rect[2]/8
            door.rect[2] = door.rect[2]*3/4
        else:
            # door.rect[0] = door.center[0]-6
            door.rect[2] = min(2*6,door.rect[2])
    else:
        if dtype==1:
            door.rect[1] = door.rect[1]+door.rect[3]/8
            door.rect[3] = door.rect[3]*3/4
        else:
            # door.rect[1] = door.center[1]-6
            door.rect[3] = min(2*6,door.rect[3])
    return door

def add_interior_door(rooms,living_idx,house):
    frontDoorCenter = house.boundary[:2].mean(0)
    for i in range(len(rooms)):
        if i==living_idx:continue
        # 1. Balcony: find the longest door
        # 2. Public Area: find the longest door
        # 3. Others:
        #    3.1 Contact with living room: find the cloest door with the front door
        #    3.2 Others: find the longest wall
        if rooms[i].label == 'Balcony':
            contactWalls = []
            for j in range(len(rooms)):
                if i!=j:
                    contactWalls.extend(find_contact_walls(rooms[i],rooms[j]))
            
            rooms[i].entry = find_longest_wall(contactWalls,dtype=1)
        else:
            contactWalls = find_contact_walls(rooms[i],rooms[living_idx])
            if len(contactWalls)>0:
                if rooms[i].type == 'PublicArea':
                    rooms[i].entry = find_longest_wall(contactWalls,dtype=1)
                else:
                    candidateDoors = [ 
                        wall for wall in contactWalls 
                        if (wall.width>wall.height and wall.width>2*6) or 
                        (wall.height>wall.width and wall.height>2*6)
                    ]
                    if len(candidateDoors)==0:
                        rooms[i].entry = find_longest_wall(contactWalls,dtype=0)
                    else:
                        rooms[i].entry = find_closest_wall(contactWalls,frontDoorCenter,dtype=0,boundary_lines=house.lines)
            else:
                contactWalls = []
                for j in range(len(rooms)):
                    if i!=j:
                        contactWalls.extend(find_contact_walls(rooms[i],rooms[j]))
                if len(contactWalls)>0:
                    rooms[i].entry = find_closest_wall(contactWalls,frontDoorCenter,dtype=1,boundary_lines=house.lines)
            
    return rooms

def find_windows(contactWalls,wtypes=['mid'],keep_longest=False):
    windows = []
    contactLength = 1e8
    for i in range(len(contactWalls)):
        contactWall = contactWalls[i]
        maxLength = max(contactWall.width,contactWall.height)
        if ('large' in wtypes and maxLength>3*12):
            contactWalls[i] = adjust_window(contactWalls[i],'large')
            windows.append(contactWall)
        elif 'mid' in wtypes and maxLength>3*9:
            contactWalls[i] = adjust_window(contactWalls[i],'mid')
            windows.append(contactWall)
        elif 'small' in wtypes and maxLength>2*5:
            contactWalls[i] = adjust_window(contactWalls[i],'small')
            windows.append(contactWall)
        elif 'balcony' in wtypes and maxLength>2*5:
            contactWalls[i] = adjust_window(contactWalls[i],'balcony')
            windows.append(contactWall)
    return windows

def find_window_by_length(contactWalls,wtypes=['mid'],ltype='max'):
    window = None
    contactLength = 0 if ltype=='max' else 1e8
    ufunc = np.greater if ltype=='max' else np.less
    for i in range(len(contactWalls)):
        contactWall = contactWalls[i]
        maxLength = max(contactWall.width,contactWall.height)
        if ufunc(maxLength,contactLength):
            if 'large' in wtypes and maxLength>3*12:
                contactWalls[i] = adjust_window(contactWalls[i],'large')
                window = contactWalls[i]
                contactLength = maxLength
            elif 'mid' in wtypes and maxLength>3*9:
                contactWalls[i] = adjust_window(contactWalls[i],'mid')
                window = contactWalls[i]
                contactLength = maxLength
            elif 'small' in wtypes and maxLength>2*5:
                contactWalls[i] = adjust_window(contactWalls[i],'small')
                window = contactWalls[i]
                contactLength = maxLength
    return [window] if window is not None else []

def adjust_window(window,wtype='mid'):
    hl = {'small':5,'mid':9,'large':12}
    if window.dir in [1,3]:
        if wtype=='balcony':
            window.rect[0] = window.rect[0]+window.rect[2]/10
            window.rect[2] = window.rect[2]*4/5
        else:
            length = hl[wtype]
            window.rect[0] = window.center[0]-length
            window.rect[2] = 2*length
    else:
        if wtype=='balcony':
            window.rect[1] = window.rect[1]+window.rect[3]/10
            window.rect[3] = window.rect[3]*4/5
        else:
            length = hl[wtype]
            window.rect[1] = window.center[1]-length
            window.rect[3] = 2*length
    return window

def add_window(rooms,house):
    for i in range(len(rooms)):
        # 1. Balcony: small(half=5)
        # 2. Living Room: mid(half=9),large(half=12)
        # 3. Bathroom: shortest wall, small
        # 4. Others: longest wall, mid
        contactWalls = find_contact_walls(rooms[i],house,reverse=True)
        if rooms[i].label == 'Balcony':
            rooms[i].windows.extend(find_windows(contactWalls,['balcony']))
        elif rooms[i].label == 'LivingRoom':
            rooms[i].windows.extend(find_windows(contactWalls,['mid','large']))
        elif rooms[i].label == 'Bathroom':
            rooms[i].windows.extend(find_window_by_length(contactWalls,['small'],'min'))
        else:
            rooms[i].windows.extend(find_window_by_length(contactWalls,['mid'],'max'))
    return rooms

def rooms_to_numpy(rooms):
    doors = []
    windows = []
    for i in range(len(rooms)):
        if rooms[i].entry is not None:
            door = rooms[i].entry.entry
            doors.append([i,door.rect[0],door.rect[1],door.rect[2],door.rect[3],door.dir])
        if len(rooms[i].windows) > 0:
            ws = [[i,w.rect[0],w.rect[1],w.rect[2],w.rect[3],w.dir] for w in rooms[i].windows]
            windows.extend(ws)
    return np.array(doors),np.array(windows)

def add_door_window(data):
    boundary = data.boundary
    living_idx = np.where(data.rType==0)[0][0]
    rooms = Room.rooms_from_data(data)
    house = Room.from_boundary(boundary[:,:2])
    house.lines = house.lines[1:]    

    rooms = add_interior_door(rooms,living_idx,house)
    rooms = add_window(rooms,house)
    
    return rooms_to_numpy(rooms)

def add_dw_fp(data):
    # data要有 boundary(网络输出经过matlab新返回的),newBox(网络输出经过matlab新返回的),rType,rBoundary(网络输出经过matlab新返回的)
    doors,windows = add_door_window(data)
    data.doors = doors
    data.windows = windows
    return data
