import os, time, sys
import subprocess
import threading
import json
import copy
TESTING = True

NUM_BOTS = 8
NUM_TICKS = 100
BOARD_SIZE = 10
TIME_LIMIT = 1
def f(t):
    return 1

def create_fifo(path):
    if not os.path.exists(path):
        os.mkfifo(path)
    return path

class Ai:

    def scan_input_thread(self, is_init = False):
        try:
            status = int(self.in_pipe.readline().decode("utf-8"))
        except:
            status = 2
            if self.status != 'RUNNING':
                # Obscure bug: the program can just BARELY tle.
                # Then the process will be killed in cleanup().
                # This causes this to error. 
                # Actually, this is just poor design. Multiple threads are hard.
                return
        if (status == 0):
            if is_init:
                pass
            else:
                for i in range(NUM_BOTS):
                    res = self.in_pipe.readline().decode("utf-8")
                    pos = [int(i) for i in res.split()]
                    self.bot_positions[i] = pos
        elif (status == 1):
            self.status = 'INVALID_CALL'
            self.error_message = self.in_pipe.readline().decode("utf-8")
        else:
            self.status = 'RUNTIME_ERROR'


    def do_tick(self, new_bot_positions, first_tick = False, last_tick = False):
        # Send the required information, and 
        # time how long it takes to receive the next bit of info
        if (first_tick):
            self.out_pipe.write(f"{self.remaining_time}\n".encode("utf-8"))
        else:
            for i in range(NUM_BOTS):
                self.out_pipe.write(f"{new_bot_positions[i][0]} {new_bot_positions[i][1]}\n".encode("utf-8"))
            self.out_pipe.write(f"{self.remaining_time}\n".encode("utf-8"))

        start_time = time.time()
        self.out_pipe.flush()
        t = threading.Thread(target=self.scan_input_thread,args= ())
        t.start()
        t.join(timeout = self.remaining_time)
        self.remaining_time -= time.time() - start_time
        if (self.remaining_time <= 0):            
            self.save_TLE_or_RE_verdict()
            self.cleanup()


    def cleanup(self):        
        self.in_pipe.close()
        self.out_pipe.close()
        self.ai_process.kill()

    def save_TLE_or_RE_verdict(self):
        if self.ai_process.poll() is None:
            self.status = 'TIME_LIMIT_EXCEEDED'
        else:
            # Actually I really don't think this could happened
            self.status = 'EXECUTION_KILLED'

    def __init__(self, id, path):
        self.id = id
        self.remaining_time = TIME_LIMIT        
        if (TESTING):
            self.input_path = create_fifo(f"tmp/{id}-in-{time.time()}")
            self.output_path = create_fifo(f"tmp/{id}-out-{time.time()}")
        else:
            self.input_path = f"{id}-in.pipe"
            self.output_path = f"{id}-out.pipe"

        self.bot_positions = [[0,0] for i in range(NUM_BOTS)]
        self.status = 'RUNNING'

        start_time = time.time()
        if TESTING:
            self.ai_process = subprocess.Popen([path,self.input_path,self.output_path],stdout=sys.stdout)
        else:
            self.ai_process = subprocess.Popen([path,self.input_path,self.output_path],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)

        self.in_pipe = open(self.input_path, "rb", buffering=0)
        self.out_pipe = open(self.output_path, "wb", buffering=0)

        t = threading.Thread(target=self.scan_input_thread,args= (True,))
        t.start()
        t.join(timeout = self.remaining_time)
        self.remaining_time -= time.time() - start_time
        if (self.remaining_time <= 0):            
            self.save_TLE_or_RE_verdict()
            self.cleanup()


if len(sys.argv)  != 4:
    print("Must have exactly 3 arguments: path to AI 1, path to AI 2, and output path to replay log!")
    exit(0)

replay_dict = {
    "board_size" : BOARD_SIZE,
    "num_bots" : 2,
    "ticks_ran" : 0
}

def check_for_RE_or_TLE(bot1,bot2,tick):
    global replay_dict
    if (bot1.status != 'RUNNING' or bot2.status != 'RUNNING'):
        status = []
        
        if (bot1.status != 'RUNNING' and bot2.status != 'RUNNING'):
            s = f"Tie. $1 and $2 both errored on tick {tick}."
            replay_dict['winner'] = 0

        elif (bot1.status != 'RUNNING'):
            s = f"$2 wins. $1 errored on tick {tick}."
            replay_dict['winner'] = 2
        else:
            s = f"$1 wins. $2 errored on tick {tick}."
            replay_dict['winner'] = 1
        if (tick == 0):
            s += " Note that errors on tick 0 means the error occured in init()."

        status.append(s)

        if (bot1.status == 'RUNTIME_ERROR'):
            status.append("$1 encountered a runtime error- your program crashed. Likely a segfault, or undefined behaviour.")
        if (bot1.status == 'INVALID_CALL'):
            status.append(f"$1 made an invalid call: '{bot1.error_message}")
        if (bot1.status == 'TIME_LIMIT_EXCEEDED'):
            status.append("$1 exceeded the time limit.")
        if (bot1.status == 'EXECUTION_KILLED'):
            status.append("$1 crashed the grader you compiled with. Likely a segfault, or undefined behaviour.")

        if (bot2.status == 'RUNTIME_ERROR'):
            status.append("$2 encountered a runtime error- your program crashed. Likely a segfault, or undefined behaviour.")
        if (bot2.status == 'INVALID_CALL'):
            status.append(f"$2 made an invalid call: '{bot2.error_message}")
        if (bot2.status == 'TIME_LIMIT_EXCEEDED'):
            status.append("$2 exceeded the time limit.")
        if (bot2.status == 'EXECUTION_KILLED'):
            status .append("$2 crashed the grader you compiled with. Likely a segfault, or undefined behaviour.")
        
        replay_dict['result_message'] = "\n".join(status)
        return True
    else:
        return False

def transform_coords(coords):
    return [[BOARD_SIZE-a[1]-1,BOARD_SIZE-a[0]-1] for a in coords]



board = [[0] * BOARD_SIZE for i in range(BOARD_SIZE)]
board[0][0] = 1
board[BOARD_SIZE-1][BOARD_SIZE-1] = 2
scores = [1,1]

def output_result():
    replay_dict['final_scores'] = scores
    with open(sys.argv[3], 'w') as f:
        if TESTING:
            f.write(json.dumps(replay_dict,indent=2))
        else:
            f.write(json.dumps(replay_dict, separators=(',',':')))
        # json.dump(replay_dict,f)
    bot1.cleanup()
    bot2.cleanup()
    print(replay_dict['result_message'])
    print(f"Final scores: {scores[0]}-{scores[1]}")




def update_board(pos1,pos2):
    global board
    cnts = [[0] * BOARD_SIZE for i in range(BOARD_SIZE)]
    
    for p in pos1:
        cnts[p[0]][p[1]] +=1
    for p in pos2:
        cnts[p[0]][p[1]] -= 1

    def cnt_to_val(x):
        if (x < 0):
            return 2
        elif x > 0:
            return 1
        else:
            return 0
        
    for p in pos1:
        board[p[0]][p[1]] = cnt_to_val(cnts[p[0]][p[1]])
    for p in pos2:
        board[p[0]][p[1]] = cnt_to_val(cnts[p[0]][p[1]])
    
def encode_board():
    s = []
    for y in range(BOARD_SIZE):
        for x in range(BOARD_SIZE):
            s += str(board[x][y])
    return ''.join(s)

def print_board():
    for y in range(BOARD_SIZE-1,-1,-1):
        s = []
        for x in range(BOARD_SIZE):
            s += str(board[x][y])
        print("".join(s))

def update_scores(tick):
    global scores
    p1_cnt = 0
    p2_cnt = 0
    for i in range(BOARD_SIZE):
        for j in range(BOARD_SIZE):
            if board[i][j] == 1:
                p1_cnt+=1
            if board[i][j] == 2:
                p2_cnt+=1
    scores[0] += p1_cnt * f(tick)
    scores[1] += p2_cnt * f(tick)    


replay_dict['p1_replay'] = [[[0,0] for i in range(NUM_BOTS)]]
replay_dict['p2_replay'] = [[[BOARD_SIZE-1,BOARD_SIZE-1] for i in range(NUM_BOTS)]]
replay_dict['boards'] = [encode_board()]
replay_dict['scores'] = [scores.copy()]
replay_dict['remaining_times'] = [TIME_LIMIT,TIME_LIMIT]

# Runs init().
bot1 = Ai(1, sys.argv[1])
bot2 = Ai(2, sys.argv[2])

if check_for_RE_or_TLE(bot1,bot2,0):
    output_result()
    exit(0)

for cur_tick in range(1,NUM_TICKS+1):
    temp_positions = copy.deepcopy(bot1.bot_positions)
    bot1.do_tick(transform_coords(bot2.bot_positions), first_tick = cur_tick == 1, last_tick = cur_tick == NUM_TICKS)
    bot2.do_tick(transform_coords(temp_positions    ), first_tick = cur_tick == 1, last_tick = cur_tick == NUM_TICKS)
    if check_for_RE_or_TLE(bot1,bot2,cur_tick):
        output_result()
        exit(0)
    replay_dict['p1_replay'].append(bot1.bot_positions.copy())
    replay_dict['p2_replay'].append(transform_coords(bot2.bot_positions.copy()))
    update_board(bot1.bot_positions,transform_coords(bot2.bot_positions.copy()))
    replay_dict['boards'].append(encode_board())
    replay_dict['ticks_ran'] = cur_tick
    update_scores(cur_tick)
    replay_dict['scores'].append(scores.copy())
    replay_dict['remaining_times'].append([bot1.remaining_time,bot2.remaining_time])
    
    if TESTING:
        # print_board()
        # print("\n\n")
        pass

if scores[0] > scores[1]:
    replay_dict['result_message'] = '$1 successfully beat $2 by scoring more points!'
    replay_dict['winner'] = 1
elif scores[0] < scores[1]:
    replay_dict['result_message'] = '$2 successfully beat $1 by scoring more points!'
    replay_dict['winner'] = 2
else:
    replay_dict['result_message'] = 'Tie! $1 and $2 scored exactly the same number of points.'
    replay_dict['winner'] = 0

output_result()
