import pandas as pd
import numpy as np
import os, sys, traci
import json
import sumolib
from tqdm import tqdm

class DailyPreprocessor():
    def __init__(self):
        self.path_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
        self.issues = []
    
    # 1. 데이터 불러오기
    def load_data(self):
        self.load_networks()
        self.load_tables()
        self.check_networks()
        self.check_tables()
        print('1. 모든 데이터가 로드되었습니다.')

    # 1-1. 네트워크 불러오기
    def load_networks(self):
        self.net = sumolib.net.readNet(os.path.join(self.path_root, 'Data', 'networks', 'sn.net.xml'))
        print("1-1. 네트워크가 로드되었습니다.")

    # 1-2. 테이블 불러오기
    def load_tables(self):
        # 모든 컬럼에 대하여 데이터타입 지정
        loading_dtype = {
            'inter_no':'int', 'start_hour':'int', 'start_minute':'int', 'cycle':'int','offset':'int',
            'node_id':'str', 'inter_type':'str', 'parent_id':'str','child_id':'str',
            'direction':'str', 'condition':'str', 'inc_edge':'str', 'out_edge':'str',
            'end_unix':'int', 'inter_name':'str', 'inter_lat':'float', 'inter_lon':'float',
            'group_no':'int', 'main_phase_no':'int', 'phase_no':'int','ring_type':'str'
            }
        for alph in ['A', 'B']:
            for j in range(1,9):
                loading_dtype[f'angle_{alph}{j}'] = 'str'
                loading_dtype[f'dura_{alph}{j}'] = 'int'

        self.path_table = os.path.join(self.path_root, 'Data', 'tables')

        self.inter_info = pd.read_csv(os.path.join(self.path_table, 'inter_info.csv'), dtype=loading_dtype)
        self.angle      = pd.read_csv(os.path.join(self.path_table, 'angle.csv'), dtype=loading_dtype)
        self.plan       = pd.read_csv(os.path.join(self.path_table, 'plan.csv'), dtype=loading_dtype)
        self.inter_node = pd.read_csv(os.path.join(self.path_table, 'inter_node.csv'), dtype=loading_dtype)
        self.uturn      = pd.read_csv(os.path.join(self.path_table, 'child_uturn.csv'), dtype=loading_dtype)
        self.coord      = pd.read_csv(os.path.join(self.path_table, 'child_coord.csv'), dtype=loading_dtype)
        self.nema       = pd.read_csv(os.path.join(self.path_table, 'nema.csv'), encoding='cp949', dtype=loading_dtype)
        print("1-2. 테이블들이 로드되었습니다.")

    # 1-3. 테이블 불러오기
    def check_networks(self):
        # https://sumo.dlr.de/docs/Netedit/neteditUsageExamples.html#simplify_tls_program_state_after_changing_connections
        if 'SUMO_HOME' in os.environ:
            tools = os.path.join(os.environ['SUMO_HOME'], 'tools')
            if tools not in sys.path:
                sys.path.append(tools)
        else:
            raise EnvironmentError("please declare environment variable 'SUMO_HOME'")
        traci.start([sumolib.checkBinary('sumo'), "-n", os.path.join(self.path_root, 'Data', 'networks', 'sn.net.xml')])
        nodes = [node for node in self.net.getNodes() if node.getType()=='traffic_light']
        for node in nodes:
            node_id = node.getID()
            from_xml   = len([c for c in node.getConnections() if c.getTLLinkIndex() >= 0])
            from_traci = len(traci.trafficlight.getRedYellowGreenState(node_id))
            if from_xml != from_traci:
                sub = {'id': node_id, 'type': 'node', 'note': '유효하지 않은 연결이있음. netedit에서 clean states 필요.'}
                self.issues.append(sub)
        traci.close()
        print("1-3. 네트워크의 모든 clean state requirement들을 체크했습니다.")
    
    # 1-4. 테이블의 무결성 검사
    def check_tables(self):
        self.check_inter_info()
        self.check_angle()
        self.check_plan()
        print("1-4. 모든 테이블들의 무결성을 검사했고 이상 없습니다.")
        pass

    # 1-4-1. 교차로정보(inter_info) 검사
    def check_inter_info(self):
        # 1-4-1-1. inter_lat, inter_lon 적절성 검사
        self.inter_info.loc[0, 'inter_lat'] = 38.0 # 에러 발생을 위한 코드
        self.max_lon, self.min_lon = 127.207888, 127.012492
        self.max_lat, self.min_lat = 37.480693, 37.337112
        for _, row in self.inter_info.iterrows():
            latbool = self.min_lat <= row['inter_lat'] <= self.max_lat
            lonbool = self.min_lon <= row['inter_lon'] <= self.max_lon
            if not(latbool and lonbool):
                msg = f"1-4-1-1. 위도 또는 경도가 범위를 벗어난 교차로가 있습니다: inter_no : {row['inter_no']}"
                self.issues.append(msg)
        # 교차로목록 정의
        self.inter_nos = sorted(self.inter_info.inter_no.unique())

    # 1-4-2. 방위각정보(inter_info) 검사
    def check_angle(self):
        # 1-4-2-1. inter_no 검사
        # self.angle.loc[0, 'inter_no'] = '4' # 에러 발생을 위한 코드
        missing_inter_nos = set(self.angle.inter_no) - set(self.inter_nos)
        if missing_inter_nos:
            msg = f"1-4-2-1. angle의 inter_no 중 교차로 목록(inter_nos)에 포함되지 않는 항목이 있습니다: {missing_inter_nos}"
            self.issues.append(msg)

    # 1-4-3. 신호계획(plan) 검사
    def check_plan(self):
        # 1-4-3-1. inter_no 검사
        # self.plan.loc[0, 'inter_no'] = '4' # 에러 발생을 위한 코드
        missing_inter_nos = set(self.plan.inter_no) - set(self.inter_nos)
        if missing_inter_nos:
            msg = f"1-4-3-1. plan의 inter_no 중 교차로 목록(inter_nos)에 포함되지 않는 항목이 있습니다: {missing_inter_nos}"
            self.issues.append(msg)

        # 1-4-3-2. 시작시각 검사
        # self.plan.loc[0, 'start_hour'] = 27 # 에러 발생을 위한 코드
        for _, row in self.plan.iterrows():
            start_hour = row.start_hour
            start_minute = row.start_minute
            if not (0 <= start_hour <= 23) or not (0 <= start_minute <= 59):
                msg = f"1-4-3-2. plan에 잘못된 형식의 start_time이 존재합니다: {start_hour, start_minute}"
                self.issues.append(msg)

        # 1-4-3-3. 현시시간 검사
        # self.plan.loc[0, 'dura_A1'] = -2 # 에러 발생을 위한 코드
        durations = self.plan[[f'dura_{alph}{j}' for alph in ['A','B'] for j in range(1, 9)]]
        valid_indices = ((durations >= 0) & (durations <= 200)).all(axis=1)
        invalid_inter_nos = sorted(self.plan[~ valid_indices].inter_no.unique())
        if invalid_inter_nos:
            msg = f"1-4-3-3. 음수이거나 200보다 큰 현시시간이 존재합니다. : {invalid_inter_nos}"
        
        # 1-4-3-4. 주기 일관성 검사
        # self.plan.loc[0, 'cycle'] = 50 # 에러 발생을 위한 코드
        inconsistent_cycle = self.plan.groupby(['inter_no', 'start_hour', 'start_minute'])['cycle'].nunique().gt(1)
        if inconsistent_cycle.any():
            inc_inter_no, start_hour, start_minute = inconsistent_cycle[inconsistent_cycle].index[0]
            msg = f"1-4-3-4. inter_no:{inc_inter_no}, start_hour:{start_minute}, start_hour:{start_minute}일 때, cycle이 유일하게 결정되지 않습니다."
            self.issues.append(msg)

        # 1-4-3-5. 현시시간 / 주기 검사
        # self.plan.loc[0, 'duration'] = 10 # 에러 발생을 위한 코드
        right_duration = True
        for (inter_no, start_hour, start_minute), group in self.plan.groupby(['inter_no', 'start_hour', 'start_minute']):
            A_sum = group[[f'dura_A{j}' for j in range(1, 9)]].iloc[0].sum()
            B_sum = group[[f'dura_B{j}' for j in range(1, 9)]].iloc[0].sum()
            # A_sum = group[group['ring_type']=='A']['duration'].sum()
            # B_sum = group[group['ring_type']=='B']['duration'].sum()
            cycle = group['cycle'].unique()[0]
            if not (A_sum == B_sum == cycle):
                right_duration = False
                inc_inter_no = inter_no
        if not right_duration:
            msg = f"1-4-4-5. inter_no:{inc_inter_no}, A링현시시간의 합과 B링현시시간의 합이 일치하지 않거나, 현시시간의 합과 주기가 일치하지 않습니다."
            self.issues.append(msg)

    # 2. 중간산출물 만들기
    def get_intermediates(self):
        self.get_matches()
        # self.get_movements()
        self.get_node2num_cycles()

    # 2-1 매칭테이블들 생성
    def get_matches(self):
        self.make_match1()
        self.make_match2()
        self.make_match3()
        self.make_match4()
        self.make_match5()
        self.make_match6()
        self.make_matching()
        
    # 2-1-1
    def make_match1(self):
        '''
        신호 DB에는 매 초마다 이동류정보가 업데이트 된다. 그리고 이 이동류정보를 매 5초마다 불러와서 사용하게 된다.
        '../Data/tables/move/'에는 5초마다의 이동류정보가 저장되어 있다.

        return : 통합된 이동류정보
        - 모든 inter_no(교차로번호)에 대한 A, B링 현시별 이동류정보

        match1을 만드는 데 시간이 소요되므로 한 번 만들어서 저장해두고 저장해둔 것을 쓴다.
        '''
        # [이동류번호] 불러오기 (약 1분의 소요시간)
        path_move = os.path.join(self.path_root, 'Data', 'tables', 'move')
        csv_moves = os.listdir(path_move)
        moves = [pd.read_csv(os.path.join(path_move, csv_move), index_col=0) for csv_move in tqdm(csv_moves, desc='이동류정보 불러오는 중 : match1')]
        self.match1 = pd.concat(moves).drop_duplicates().sort_values(by=['inter_no','phas_A','phas_B']).reset_index(drop=True)
        self.match1.to_csv(os.path.join(self.path_root, 'Intermediates', 'match1.csv'))

    # 2-1-2
    def make_match2(self):
        '''
        match1을 계층화함.
        - match1의 컬럼 : inter_no, phas_A, phas_B, move_A, move_B
        - match2의 컬럼 : inter_no, phase_no, ring_type, move_no
        '''
        # 계층화 (inter_no, phas_A, phas_B, move_A, move_B) -> ('inter_no', 'phase_no', 'ring_type', 'move_no')
        matchA = self.match1[['inter_no', 'phas_A', 'move_A']].copy()
        matchA.columns = ['inter_no', 'phase_no', 'move_no']
        matchA['ring_type'] = 'A'
        matchB = self.match1[['inter_no', 'phas_B', 'move_B']].copy()
        matchB.columns = ['inter_no', 'phase_no', 'move_no']
        matchB['ring_type'] = 'B'
        self.match2 = pd.concat([matchA, matchB]).drop_duplicates()
        self.match2 = self.match2[['inter_no', 'phase_no', 'ring_type', 'move_no']]
        self.match2 = self.match2.sort_values(by=list(self.match2.columns))

    # 2-1-3
    def make_match3(self):
        '''
        각 movement들에 방향(진입방향, 진출방향)을 매칭시켜 추가함.
        - match2의 컬럼 : inter_no, phase_no, ring_type, move_no
        - match3의 컬럼 : inter_no, phase_no, ring_type, move_no, inc_dir, out_dir

        nema : 
        - 컬럼 : move_no, inc_dir, out_dir
        - 모든 종류의 이동류번호에 대하여 진입방향과 진출방향을 매칭시키는 테이블
        - 이동류번호 : 1 ~ 16, 17, 18, 21
        - 진입, 진출방향(8방위) : 동, 서, 남, 북, 북동, 북서, 남동, 남서
        '''
        # nema 정보 불러오기 및 병합
        self.match3 = pd.merge(self.match2, self.nema, how='left', on='move_no').drop_duplicates()

    # 2-1-4
    def make_match4(self):
        '''
        방위각 정보를 매칭시켜 추가함.
        - match3의 컬럼 : inter_no, phase_no, ring_type, move_no, inc_dir, out_dir
        - match4의 컬럼 : inter_no, phase_no, ring_type, move_no, inc_dir, out_dir, inc_angle, out_angle

        angle_original : 
        - 컬럼 : inter_no, angle_Aj, angle_Bj (j : 1 ~ 8)
        - 모든 종류의 이동류번호에 대하여 진입방향과 진출방향을 매칭시키는 테이블
        - 이동류번호 : 1 ~ 16, 17, 18, 21
        - 진입, 진출방향(8방위) : 동, 서, 남, 북, 북동, 북서, 남동, 남서
        '''

        # 계층화
        angles = []
        for i, row in self.angle.iterrows():
            angle_codes = row[[f'angle_{alph}{j}' for alph in ['A', 'B'] for j in range(1,9)]]
            new = pd.DataFrame({'inter_no':[row.inter_no] * 16, 'phase_no':list(range(1, 9))*2, 'ring_type':['A'] * 8 + ['B'] * 8, 'angle_code':angle_codes.to_list()})
            angles.append(new)
        angles = pd.concat(angles)
        angles = angles.dropna().reset_index(drop=True)

        # 병합
        six_chars = angles.angle_code.apply(lambda x:len(x)==6)
        angles.loc[six_chars,'inc_angle'] = angles.angle_code.apply(lambda x:x[:3])
        angles.loc[six_chars,'out_angle'] = angles.angle_code.apply(lambda x:x[3:])
        angles = angles.drop('angle_code', axis=1)
        self.match4 = pd.merge(self.match3, angles, how='left', left_on=['inter_no', 'phase_no', 'ring_type'],
                        right_on=['inter_no', 'phase_no', 'ring_type']).drop_duplicates()

    # 2-1-5
    def make_match5(self):
        '''
        진입엣지id, 진출엣지id, 노드id를 추가함 (주교차로).
        - match4의 컬럼 : inter_no, phase_no, ring_type, move_no, inc_dir, out_dir, inc_angle, out_angle
        - match5의 컬럼 : inter_no, phase_no, ring_type, move_no, inc_dir, out_dir, inc_angle, out_angle, inc_edge, out_edge, node_id
        
        사용된 데이터 : 
        (1) net
        - 성남시 정자동 부근의 샘플 네트워크
        (2) inter_node
        - 교차로번호와 노드id를 매칭시키는 테이블.
        - parent/child 정보도 포함되어 있음
        - 컬럼 : inter_no, node_id, inter_type
        (3) inter_info
        - 교차로 정보. 여기에서는 위도와 경도가 쓰임.
        - 컬럼 : inter_no, inter_name, inter_lat, inter_lon, group_no, main_phase_no

        진입엣지id, 진출엣지id를 얻는 과정 :
        - match5 = match4.copy()의 각 열을 순회하면서 아래 과정을 반복함.
        * 진입에 대해서만 서술하겠지만 진출도 마찬가지로 설명될 수 있음
        - 해당 행의 교차로정보로부터 노드ID를 얻어내고, 해당 노드에 대한 모든 진출엣지id를 inc_edges에 저장.
        * inc_edge(진입엣지) : incoming edge, out_edge(진출엣지) : outgoing_edge
        - inc_edges의 모든 진입엣지에 대하여 진입방향(inc_dires, 2차원 단위벡터)을 얻어냄.
        - 해당 행의 진입각으로부터 그에 대응되는 진입각방향(단위벡터)를 얻어냄.
        - 주어진 진입각방향에 대하여 내적이 가장 작은 진입방향에 대한 진입엣지를 inc_edge_id로 지정함.
        '''

        # parent node만 가져옴.
        inter_node1 = self.inter_node[self.inter_node.inter_type == 'parent'].drop('inter_type', axis=1)
        inter_info1 = self.inter_info[['inter_no', 'inter_lat', 'inter_lon']]
        inter = pd.merge(inter_node1, inter_info1, how='left', left_on=['inter_no'],
                        right_on=['inter_no']).drop_duplicates()

        self.inter2node = dict(zip(inter['inter_no'], inter['node_id']))

        self.match5 = self.match4.copy()
        # 진입진출ID 매칭
        for index, row in self.match5.iterrows():
            node_id = self.inter2node[row.inter_no]
            node = self.net.getNode(node_id)
            # 교차로의 모든 (from / to) edges
            inc_edges = [edge for edge in node.getIncoming() if edge.getFunction() == ''] # incoming edges
            out_edges = [edge for edge in node.getOutgoing() if edge.getFunction() == ''] # outgoing edges
            # 교차로의 모든 (from / to) directions
            inc_dirs = []
            for inc_edge in inc_edges:
                start = inc_edge.getShape()[-2]
                end = inc_edge.getShape()[-1]
                inc_dir = np.array(end) - np.array(start)
                inc_dir = inc_dir / (inc_dir ** 2).sum() ** 0.5
                inc_dirs.append(inc_dir)
            out_dirs = []
            for out_edge in out_edges:
                start = out_edge.getShape()[0]
                end = out_edge.getShape()[1]
                out_dir = np.array(end) - np.array(start)
                out_dir = out_dir / (out_dir ** 2).sum() ** 0.5
                out_dirs.append(out_dir)
            # 진입각, 진출각 불러오기
            if not pd.isna(row.inc_angle):
                inc_angle = int(row.inc_angle)
                out_angle = int(row.out_angle)
                # 방위각을 일반각으로 가공, 라디안 변환, 단위벡터로 변환
                inc_angle = (-90 - inc_angle) % 360
                inc_angle = inc_angle * np.pi / 180.
                inc_dir_true = np.array([np.cos(inc_angle), np.sin(inc_angle)])
                out_angle = (90 - out_angle) % 360
                out_angle = out_angle * np.pi / 180.
                out_dir_true = np.array([np.cos(out_angle), np.sin(out_angle)])
                # 매칭 엣지 반환
                inc_index = np.array([np.dot(inc_dir, inc_dir_true) for inc_dir in inc_dirs]).argmax()
                out_index = np.array([np.dot(out_dir, out_dir_true) for out_dir in out_dirs]).argmax()
                inc_edge_id = inc_edges[inc_index].getID()
                out_edge_id = out_edges[out_index].getID()
                self.match5.at[index, 'inc_edge'] = inc_edge_id
                self.match5.at[index, 'out_edge'] = out_edge_id
        self.match5['node_id'] = self.match5['inter_no'].map(self.inter2node)
        self.match5 = self.match5.sort_values(by=['inter_no','phase_no','ring_type']).reset_index(drop=True)

    # 2-1-6
    def make_match6(self):
        '''
        진입엣지id, 진출엣지id, 노드id를 추가함 (부교차로).
        - match6의 컬럼 : inter_no, phase_no, ring_type, move_no, inc_dir, out_dir, inc_angle, out_angle, inc_edge, out_edge, node_id
        
        사용된 데이터 : 
        (1) inter_node
        - 교차로번호와 노드id를 매칭시키는 테이블.
        - parent/child 정보도 포함되어 있음
        - 컬럼 : inter_no, node_id, inter_type
        (2) uturn (유턴정보)
        - 컬럼 : parent_id, child_id, direction, condition, inc_edge, out_edge
        - parent_id, child_id : 주교차로id, 유턴교차로id
        - direction : 주교차로에 대한 유턴노드의 상대적인 위치(방향)
        - condition : 좌회전시, 직진시, 직좌시, 보행신호시 중 하나
        - inc_edge, out_edge : 유턴에 대한 진입진출엣지
        (3) coord (연동교차로정보)
        - 컬럼 : parent_id, child_id, phase_no, ring_type, inc_edge, out_edge
        - parent_id, child_id : 주교차로id, 연동교차로id
        - 나머지 컬럼 : 각 (현시, 링)별 진입진출엣지

        설명 :
        - match5는 주교차로에 대해서만 진입엣지id, 진출엣지id, 노드id를 추가했었음.
        여기에서 uturn, coord를 사용해서 부교차로들(유턴교차로, 연동교차로)에 대해서도 해당 값들을 부여함.
        유턴교차로 :
        - directions를 정북기준 시계방향의 8방위로 정함.
            - 이를 통해 진입방향이 주어진 경우에 좌회전, 직진, 보행 등에 대한 (진입방향, 진출방향)을 얻어낼 수 있음.
            - 예) 진입방향(direction)이 '북'일 때, 
            - 직진 : (북, 남)
                * 남 : directions[(ind + 4) % len(directions)]
            - 좌회전 : (북, 동)
                * 동 : directions[(ind + 2) % len(directions)]
            - 보행 : (서, 동)
                * 서 : directions[(ind - 2) % len(directions)]
        - uturn의 각 행을 순회하면서 아래 과정을 반복함
            - match5에서 parent_id에 해당하는 행들을 가져옴(cmatch).
            - condition 별로 진입방향, 진출방향A, 진출방향B 정함.
                - 상술한 directions를 활용하여 정함.
            - (진입방향, 진출방향A, 진출방향B)을 고려하여 (현시, 링) 별로 진입엣지id, 진출엣지id를 정함.
                - ex) cmatch.loc[(cmatch.inc_dir==inc_dire) & (cmatch.out_dir==out_dire_A), ['inc_edge', 'out_edge']] = [inc_edge_id, out_edge_id]
        - 순회하면서 만든 cmatch를 cmatchs라는 리스트에 저장함.

        연동교차로 :
        - 연동교차로의 경우 coord에 (현시, 링)별 진입엣지ID, 진출엣지ID가 명시되어 있음.
        - 'inc_dir', 'out_dir', 'inc_angle','out_angle'와 같은 열들은 np.nan을 지정해놓음.
        - 이 열들은, 사실상 다음 스텝부터는 사용되지 않는 열들이기 때문에 np.nan으로 지정해놓아도 문제없음.

        match6 :
        - 이렇게 얻은 match5, cmatchs, coord를 모두 pd.concat하여 match6을 얻어냄.
        '''

        self.node2inter = dict(zip(self.inter_node['node_id'], self.inter_node['inter_no']))

        child_ids = self.inter_node[self.inter_node.inter_type=='child'].node_id.unique()
        ch2pa = {} # child to parent
        for child_id in child_ids:
            parent_no = self.inter_node[self.inter_node.node_id==child_id].inter_no.iloc[0]
            sub_inter_node = self.inter_node[self.inter_node.inter_no==parent_no]
            ch2pa[child_id] = sub_inter_node[sub_inter_node.inter_type=='parent'].iloc[0].node_id
        directions = ['북', '북동', '동', '남동', '남', '남서', '서', '북서'] # 정북기준 시계방향으로 8방향

        # 각 uturn node에 대하여 (inc_edge_id, out_edge_id) 부여
        cmatches = []
        for _, row in self.uturn.iterrows():
            child_id = row.child_id
            parent_id = row.parent_id
            direction = row.direction
            condition = row.condition
            inc_edge_id = row.inc_edge
            out_edge_id = row.out_edge
            # match5에서 parent_id에 해당하는 행들을 가져옴
            cmatch = self.match5.copy()[self.match5.node_id==parent_id] # match dataframe for a child node
            cmatch = cmatch.sort_values(by=['phase_no', 'ring_type']).reset_index(drop=True)
            cmatch['node_id'] = child_id
            cmatch[['inc_edge', 'out_edge']] = np.nan

            # condition 별로 inc_dire, out_dire_A, out_dire_B를 정함
            ind = directions.index(direction)
            if condition == "좌회전시":
                inc_dire = direction
                out_dire_A = out_dire_B = directions[(ind + 2) % len(directions)]
            elif condition == "직진시":
                inc_dire = direction
                out_dire_A = out_dire_B = directions[(ind + 4) % len(directions)]
            elif condition == "보행신호시":
                inc_dire = directions[(ind + 2) % len(directions)]
                out_dire_A = directions[(ind - 2) % len(directions)]
                out_dire_B = directions[(ind - 2) % len(directions)]

            # (inc_dire, out_dire_A, out_dire_B) 별로 inc_edge_id, out_edge_id를 정함
            if condition == '보행신호시':
                cmatch.loc[(cmatch.inc_dir==inc_dire) & (cmatch.out_dir==out_dire_A), ['inc_edge', 'out_edge']] = [inc_edge_id, out_edge_id]
                cmatch.loc[(cmatch.inc_dir==inc_dire) & (cmatch.out_dir==out_dire_B), ['inc_edge', 'out_edge']] = [inc_edge_id, out_edge_id]
                # 이동류번호가 17(보행신호)이면서 유턴노드방향으로 가는 신호가 없으면 (inc_edge_id, out_edge_id)를 부여한다.
                cmatch.loc[(cmatch.move_no==17) & (cmatch.out_dir!=direction), ['inc_edge', 'out_edge']] = [inc_edge_id, out_edge_id]
            else: # '직진시', '좌회전시'
                cmatch.loc[(cmatch.inc_dir==inc_dire) & (cmatch.out_dir==out_dire_A), ['inc_edge', 'out_edge']] = [inc_edge_id, out_edge_id]
                cmatch.loc[(cmatch.inc_dir==inc_dire) & (cmatch.out_dir==out_dire_B), ['inc_edge', 'out_edge']] = [inc_edge_id, out_edge_id]
            # 유턴신호의 이동류번호를 19로 부여한다.
            cmatch.loc[(cmatch.inc_dir==inc_dire) & (cmatch.out_dir==out_dire_A), 'move_no'] = 19
            cmatch.loc[(cmatch.inc_dir==inc_dire) & (cmatch.out_dir==out_dire_B), 'move_no'] = 19
            cmatches.append(cmatch)

        # 각 coordination node에 대하여 (inc_edge_id, out_edge_id) 부여
        self.coord['inter_no'] = self.coord['parent_id'].map(self.node2inter)
        self.coord = self.coord.rename(columns={'child_id':'node_id'})
        self.coord[['inc_dir', 'out_dir', 'inc_angle','out_angle']] = np.nan
        self.coord['move_no'] = 20
        self.coord = self.coord[['inter_no', 'phase_no', 'ring_type', 'move_no', 'inc_dir', 'out_dir', 'inc_angle','out_angle', 'inc_edge', 'out_edge', 'node_id']]
        
        # display(coord)
        cmatches = pd.concat(cmatches)
        self.match6 = pd.concat([self.match5, cmatches, self.coord]).drop_duplicates().sort_values(by=['inter_no', 'node_id', 'phase_no', 'ring_type'])
        self.match6.to_csv(os.path.join(self.path_root, 'Intermediates', 'match6.csv'))

    # 2-1-7
    def make_matching(self):
        '''
        이동류 매칭 : 각 교차로에 대하여, 가능한 모든 이동류 (1~18, 21)에 대한 진입·진출엣지ID를 지정한다.
        모든 이동류에 대해 지정하므로, 시차제시 이전과 다른 이동류가 등장하더라도 항상 진입·진출 엣지 ID를 지정할 수 있다.    
        - matching의 컬럼 : inter_no, move_no, inc_dir, out_dir, inc_edge, out_edge, node_id
        
        설명 : 
        - 필요한 리스트, 딕셔너리 등을 정의
            (1) 가능한 (진입방향, 진출방향) 목록 [리스트]
            (2) 각 교차로별 방향 목록 : pdires (possible directions) [딕셔너리]
            (3) 각 (교차로, 진입방향) 별 진입id 목록 : inc2id (incoming direction to incoming edge_id) [딕셔너리]
            (4) 각 (교차로, 진출방향) 별 진출id 목록 : out2id (outgoing direction to outgoing edge_id) [딕셔너리]
            (5) 각 교차로별 가능한 (진입방향, 진출방향) 목록 : pflow (possible flows) [딕셔너리]
        - matching은 빈 리스트로 지정.
        - 모든 노드id에 대하여 다음 과정을 반복
        - 해당 노드id에 대한 모든 가능한 (진입방향, 진출방향)에 대하여 다음 과정을 반복
        - (노드id, 진입방향)으로부터 진입엣지id를 얻어냄. 마찬가지로 진출엣지id도 얻어냄
        - 얻어낸 정보를 바탕으로 한 행(new_row)을 만들고 이것을 matching에 append
        '''

        self.match7 = self.match6.copy()
        self.match7 = self.match7[['inter_no', 'move_no', 'inc_dir', 'out_dir', 'inc_edge', 'out_edge', 'node_id']]

        parent_ids = sorted(self.inter_node[self.inter_node.inter_type=='parent'].node_id.unique())
        child_ids = sorted(self.inter_node[self.inter_node.inter_type=='child'].node_id.unique())

        # (1) 가능한 (진입방향, 진출방향) 목록 
        flows = self.nema.dropna().apply(lambda row: (row['inc_dir'], row['out_dir']), axis=1).tolist()
        # (2) 각 교차로별 방향 목록 : pdires (possible directions)
        pdires = {}
        for node_id in parent_ids:
            dires = self.match7[self.match7.node_id == node_id][['inc_dir','out_dir']].values.flatten()
            dires = {dire for dire in dires if type(dire)==str}
            pdires[node_id] = dires
        # (3) 각 (교차로, 진입방향) 별 진입id 목록 : inc2id (incoming direction to incoming edge_id)
        inc2id = {}
        for node_id in parent_ids:
            for inc_dir in pdires[node_id]:
                df = self.match7[(self.match7.node_id==node_id) & (self.match7.inc_dir==inc_dir)]
                inc2id[(node_id, inc_dir)] = df.inc_edge.iloc[0]
        # (4) 각 (교차로, 진출방향) 별 진출id 목록 : out2id (outgoing direction to outgoing edge_id)
        out2id = {}
        for node_id in parent_ids:
            for out_dir in pdires[node_id]:
                df = self.match7[(self.match7.node_id==node_id) & (self.match7.out_dir==out_dir)]
                out2id[(node_id, out_dir)] = df.out_edge.iloc[0]
        # (5) 각 교차로별 가능한 (진입방향, 진출방향) 목록 : pflow (possible flows)
        pflow = {}
        for node_id in parent_ids:
            pflow[node_id] = [flow for flow in flows if set(flow).issubset(pdires[node_id])]
        # (6) 가능한 이동류에 대하여 진입id, 진출id 배정 : matching
        # node2inter = dict(zip(self.match7['node_id'], self.match7['inter_no']))
        dires_right = ['북', '서', '남', '동', '북'] # ex (북, 서), (서, 남) 등은 우회전 flow
        self.matching = []
        for node_id in parent_ids:
            inter_no = self.node2inter[node_id]
            # 좌회전과 직진(1 ~ 16)
            for (inc_dir, out_dir) in pflow[node_id]:
                move_no = self.nema[(self.nema.inc_dir==inc_dir) & (self.nema.out_dir==out_dir)].move_no.iloc[0]
                inc_edge = inc2id[(node_id, inc_dir)]
                out_edge = out2id[(node_id, out_dir)]
                new_row = pd.DataFrame({'inter_no':[inter_no], 'move_no':[move_no],
                                        'inc_dir':[inc_dir], 'out_dir':[out_dir],
                                        'inc_edge':[inc_edge], 'out_edge':[out_edge], 'node_id':[node_id]})
                self.matching.append(new_row)
            # 보행신호(17), 전적색(18)
            new_row = pd.DataFrame({'inter_no':[inter_no] * 2, 'move_no':[17, 18],
                                    'inc_dir':[None]*2, 'out_dir':[None]*2,
                                    'inc_edge':[None]*2, 'out_edge':[None]*2, 'node_id':[node_id]*2})
            self.matching.append(new_row)
            # 신호우회전(21)
            for d in range(len(dires_right)-1):
                inc_dir = dires_right[d]
                out_dir = dires_right[d+1]
                if {inc_dir, out_dir}.issubset(pdires[node_id]):
                    inc_edge = inc2id[(node_id, inc_dir)]
                    out_edge = out2id[(node_id, out_dir)]
                    new_row = pd.DataFrame({'inter_no':[inter_no], 'move_no':[21],
                                            'inc_dir':[inc_dir], 'out_dir':[out_dir],
                                            'inc_edge':[inc_edge], 'out_edge':[out_edge], 'node_id':[node_id]})
                    self.matching.append(new_row)
        self.matching.append(self.match7[self.match7.node_id.isin(child_ids)])
        self.matching = pd.concat(self.matching)
        self.matching = self.matching.dropna().sort_values(by=['inter_no', 'node_id', 'move_no']).reset_index(drop=True)
        self.matching['move_no'] = self.matching['move_no'].astype(int)
        self.matching.to_csv(os.path.join(self.path_root, 'Intermediates', 'matching.csv'))

    # 2-2
    def get_movements(self):
        movements_path = os.path.join(self.path_root, 'Intermediates', 'movement')
        movements_list = [pd.read_csv(os.path.join(movements_path, file), index_col=0) for file in tqdm(os.listdir(movements_path), desc='이동류정보 불러오는 중 : movements')]
        movements = pd.concat(movements_list)
        movements = movements.drop(columns=['start_unix'])
        movements = movements.drop_duplicates()
        movements = movements.sort_values(by=['inter_no', 'phas_A', 'phas_B'])
        movements = movements.reset_index(drop=True)
        movements.to_csv(os.path.join(self.path_root, 'Intermediates', 'movements.csv'))
        return movements

    # 2-3 node2num_cycles : A dictionary that maps a node_id to the number of cycles
    def get_node2num_cycles(self):
        # node2inter = dict(zip(inter_node['node_id'], inter_node['inter_no']))
        self.node_ids = sorted(self.inter_node.node_id.unique())

        Aplan = self.plan.copy()[['inter_no'] + [f'dura_A{j}' for j in range(1,9)] + ['cycle']]
        grouped = Aplan.groupby('inter_no')
        df = grouped.agg({'cycle': 'min'}).reset_index()
        df = df.rename(columns={'cycle': 'min_cycle'})
        df['num_cycle'] = 300 // df['min_cycle'] + 2
        inter2num_cycles = dict(zip(df['inter_no'], df['num_cycle']))
        node2numcycles = {node_id : inter2num_cycles[self.node2inter[node_id]] for node_id in self.node_ids}
        with open(os.path.join('Intermediates','node2numcycles.json'), 'w') as file:
            json.dump(node2numcycles, file, indent=4)
        return node2numcycles
    
    # 3. 이슈사항 저장
    def write_issues(self):
        path_issues = os.path.join(self.path_root, "Results", "issues_intermediates.txt")
        with open(path_issues, "w", encoding="utf-8") as file:
            for item in self.issues:
                file.write(item + "\n")
        if self.issues:
            print("데이터 처리 중 발생한 특이사항은 다음과 같습니다. :")
            for review in self.issues:
                print(review)

    def main(self):
        # 1. 데이터 불러오기
        self.load_data()
        # 2. 중간산출물 만들기
        self.get_intermediates()
        # 3. 이슈사항 저장
        self.write_issues()

if __name__ == '__main__':
    self = DailyPreprocessor()
    self.main()