import os
import sys
from settings import *
from itertools import permutations

NOMEKOP_ATTACK_MAP = {
    "Atrox": "bite",
    "Cleaf": "horn toss",
    "Gulfin": "water blast",
    "Jacana": "ember",
    "Plumette": "scratch"
}

NOMEKOP_DEFENSE_MAP = {
    "Atrox": "slither",
    "Cleaf": "spikey shell",
    "Gulfin": "water bubble",
    "Jacana": "fly",
    "Plumette" : "charm"
}

class Nomekop:
    def __init__(self, name, hitpoints, attack_damage, type):
        # attributes for battle logic
        self.name = name
        self.hitpoints = hitpoints
        self.attack_damage = attack_damage
        self.type = type
        self.defense_mode = False

        # attributes for drawing
        self.x = 0
        self.y = 0
        self.all_images = []

    def perform_move(self, move, target_nomekop):

        # informative debugging text, can ignore
        if self.name == target_nomekop.name:
            print(self.name, "use", move, "on yourself!")
        else:
            print(self.name, "use", move, "on", target_nomekop.name + "!")
    
        # 0b part that is already implemented
        if not (self.hitpoints > 0 and target_nomekop.hitpoints > 0):
            print("One of the nomekop have fainted, skipping move")
            return (None, None, None)

        # 0b rest of the implementation do rest of the implementation down here
        # if the nomekop is performing a defensive move, the function should
        # * print "Name will take half damage on the next turn", where Name is the actual name of the nomekop
        # * and set its defense_mode to True
        # otherwise the nomekop is performing an attacking move, in which case the function should
        # * determine the damage multiplier based on type matchups
        # * further adjust the multiplier if the target nomekop is in defense mode
        # * print the final damage number after applying the multiplier as -- "It does " + str(damage_num) + " damage!"
        # * apply the damage to the target nomekop and set its defense_mode attribute to False
        if move == NOMEKOP_DEFENSE_MAP[self.name]:
            self.defense_mode = True
            print(self.name, "will take half damage on the next turn")
        
        else:
            mult = 0
            if self.type == target_nomekop.type:
                mult = 1
            elif (self.type == "water" and target_nomekop.type == "fire") or (self.type == "plant" and target_nomekop.type == "water") or (self.type == "fire" and target_nomekop.type == "plant"):
                mult = 2
            else:
                mult = 0.5

            if target_nomekop.defense_mode:
                mult = mult * 0.5
                target_nomekop.defense_mode = False

            damage = self.attack_damage * mult
            print("It does " + str(damage) + " damage!")
            target_nomekop.hitpoints -= damage
            if target_nomekop.hitpoints < 0:
                target_nomekop.hitpoints = 0
        
        # return the attacking_nomekop, move, target_nomekop tuple, no need to change this
        return (self, move, target_nomekop)

    def __str__(self):
        return self.name
    
    def __repr__(self):
        return self.name


def compute_total_nomekop_hitpoints(nomekop_list):
    # implement 0a here
    total_hitpoints = 0
    for nomekop in nomekop_list:
        total_hitpoints += nomekop.hitpoints
    return total_hitpoints

def reset_hitpoints(nomekop_list, starting_hps):
    # implement 0a here
    for i in range(len(nomekop_list)):
        nomekop_list[i].hitpoints = starting_hps[i]
        nomekop_list[i].defense_mode = False
    return nomekop_list

class Node:
    def __init__(self, data, next):
        self.data = data
        self.next = next

class MoveQueue:
    def __init__(self):
        self.start = None
        self.end = None
        self.length = 0

    def insert_at_end(self, nomekop1, nomekop2):
        # inserts a new node object corresponding to a move to be executed
        if nomekop1 == nomekop2:
            new_node = Node((nomekop1, NOMEKOP_DEFENSE_MAP[nomekop1.name], nomekop2), None)
        else:
            new_node = Node((nomekop1, NOMEKOP_ATTACK_MAP[nomekop1.name], nomekop2), None)
        if self.length == 0:
            self.start = new_node
            self.end = new_node
        else:
            self.end.next = new_node
            self.end = new_node
        self.length += 1

    def remove_from_start(self):
        # same as typical queue remove_from_start
        if self.length == 0:
            return "no items found"
        else:
            data = self.start.data
            self.start = self.start.next
            self.length -= 1
            return data

    def process_next_move(self):
        # when queue is not empty this function should
        # * return the tuple (None, None, None) if the queue is empty
        # * otherwise, it should remove from move at the start of the queue
        # * call the perform_move method appropriately and return its output
        if self.length == 0:
            return (None, None, None)
        else:
            a, m, p = self.remove_from_start()
            return a.perform_move(m, p)

    def __str__(self):
        # should iterate through the queue and return a string of the form
        # "X1 use M1 on Y1! -> X2 use M2 on Y2! -> X3 use M3 on Y3! -> None"
        # anytime Xi and Yi are the same, the result should include "Xi use Mi on yourself"!
        result = ""
        curr_node = self.start
        while curr_node != None:
            a, m, t = curr_node.data
            if a.name == t.name:
                result += a.name + " use " + m + " on yourself! -> "
            else:
                result += a.name + " use " + m + " on " + t.name + "! -> "
            curr_node = curr_node.next
        result += "None"
        return result

def find_best_timeline(player_nomekop_list, opp_nomekop_list, num_moves):
    # you can assume that player_nomekop_list = [CleafObject, PlumetteObject]
    # and opp_nomekop_list = [JacanaObject, GulfinObject, AtroxObject]

    # first form a list of tuples of the form
    # e.g., [(CleafObject, CleafObject), (CleafObject, JacanaObject), ..., (PlumetteObject, PlumetteObject), (PlumetteObject, JacanaObject), ... ]
    # the size of this list should be 8
    possible_moves = []
    for p in player_nomekop_list:
        possible_moves += [(p, p)]
        for o in opp_nomekop_list:
            possible_moves += [(p, o)]

    # then calculate all possible permutations of this list of size 6, corresponding to the max moves allowed
    # the size of this list should be 20,160
    # once you compute this, you might want to print out the 5000th and 20000th elements of the list below to see
    # what they look like
    possible_permutations = list(permutations(possible_moves, num_moves))

    # then calculate the starting hps of the player nomekop and opponent nomekop
    p_starting_hps = [p.hitpoints for p in player_nomekop_list]
    o_starting_hps = [o.hitpoints for o in opp_nomekop_list]

    # finally write a loop to find the best possible permutation (sequence of moves). for each sequence in the possible permutations
    # * create a player queue with all the moves in the sequence
    # * create a opponent queue with the Jacana, Gulfin, Atrox move sequence on Cleaf then Plumette
    # * simulate the battle by processing both queues, alternating between player and opponent, starting with the player
    # * calculate the score as Plumette's final hitpoints
    # * update the best score and best sequence as appropriate
    # * reset the hitpoints so that we are ready for the next iteration of the loop
    # NOTE: if in doubt, refer back to the top of this function to remind yourself what player_nomekop_list and opp_nomekop_list look like
    best_score = -1000
    best_sequence = [(player_nomekop_list[1], player_nomekop_list[1])] * num_moves # naive 6 move sequence where Plumette just uses charm 6 times

    for sequence in possible_permutations:
        player_q = MoveQueue()
        for move in sequence:
            player_q.insert_at_end(move[0], move[1])
        opp_q = MoveQueue()
        for n1 in player_nomekop_list:
            for n2 in opp_nomekop_list:
                opp_q.insert_at_end(n2, n1)

        for i in range(player_q.length):
            player_q.process_next_move()
            opp_q.process_next_move()

        score = player_nomekop_list[1].hitpoints
        if score > best_score:
            best_score = score
            best_sequence = sequence

        reset_hitpoints(player_nomekop_list, p_starting_hps)
        reset_hitpoints(opp_nomekop_list, o_starting_hps)
        
    return best_sequence

if __name__ == "__main__":
    if len(sys.argv) > 1 and sys.argv[1] == "health":
        cleaf = Nomekop("Cleaf", 6, 4, "plant")
        plumette = Nomekop("Plumette", 2, 4, "plant")
        nomekop_list = [cleaf, plumette]

        print()
        print("Cleaf starting hitpoints", cleaf.hitpoints)
        print("Plumette starting hitpoints", plumette.hitpoints)
        print()
        
        print("Total hitpoints", compute_total_nomekop_hitpoints(nomekop_list))
        print()

        print("Manually changing hitpoints")
        print()
        cleaf.hitpoints = 2
        plumette.hitpoints = 1
        print("Cleaf new hitpoints", cleaf.hitpoints)
        print("Plumette new hitpoints", plumette.hitpoints)
        print()

        print("Resetting hitpoints")
        print()
        reset_hitpoints(nomekop_list, [6, 2])
        print("Cleaf reset hitpoints", cleaf.hitpoints)
        print("Plumette reset hitpoints", plumette.hitpoints)
        print()

    if len(sys.argv) > 1 and sys.argv[1] == "spar1":
        cleaf = Nomekop("Cleaf", 6, 4, "plant")
        plumette = Nomekop("Plumette", 2, 4, "plant")
        jacana = Nomekop("Jacana", 4, 3, "fire")
        nomekop_list = [cleaf, plumette, jacana]
        starting_hps = [6, 2, 4]

        print()
        jacana.perform_move(NOMEKOP_ATTACK_MAP[jacana.name], cleaf)
        print("Cleaf hitpoints", cleaf.hitpoints)
        print()

        print("Resetting hit points")
        reset_hitpoints(nomekop_list, [6, 2, 4])
        print()
        cleaf.perform_move(NOMEKOP_DEFENSE_MAP[cleaf.name], cleaf)
        jacana.perform_move(NOMEKOP_ATTACK_MAP[jacana.name], cleaf)
        plumette.perform_move(NOMEKOP_ATTACK_MAP[plumette.name], jacana)
        cleaf.perform_move(NOMEKOP_ATTACK_MAP[cleaf.name], plumette)
        print()
        print("Cleaf final hitpoints", cleaf.hitpoints)
        print("Plumette final hitpoints", plumette.hitpoints)
        print("Jacana final hitpoints", jacana.hitpoints)


    if len(sys.argv) > 1 and sys.argv[1] == "spar2":
        cleaf = Nomekop("Cleaf", 6, 4, "plant")
        plumette = Nomekop("Plumette", 2, 4, "plant")
        jacana = Nomekop("Jacana", 4, 3, "fire")

        player_q = MoveQueue()
        opp_q = MoveQueue()
        player_q.insert_at_end(cleaf, cleaf)
        opp_q.insert_at_end(jacana, cleaf)
        player_q.insert_at_end(cleaf, jacana)
        opp_q.insert_at_end(jacana, plumette)

        print("Player queue")
        print(player_q)
        print("Opponent queue")
        print(opp_q)
        print()

        print("Cleaf starting hitpoints", cleaf.hitpoints)
        print("Plumette starting hitpoints", plumette.hitpoints)
        print("Jacana starting hitpoints", jacana.hitpoints)
        print()

        print("Excuting moves in player and opponent queues")
        print()

        for i in range(player_q.length):
            player_q.process_next_move()
            opp_q.process_next_move()

        print()
        print("Cleaf final hitpoints", cleaf.hitpoints)
        print("Plumette final hitpoints", plumette.hitpoints)
        print("Jacana final hitpoints", jacana.hitpoints)
        print()


if len(sys.argv) > 1 and sys.argv[1] == "timelines":
    cleaf = Nomekop("Cleaf", 6, 4, "plant")
    plumette = Nomekop("Plumette", 2, 4, "plant")
    jacana = Nomekop("Jacana", 4, 3, "fire")
    gulfin = Nomekop("Gulfin", 8, 2, "water")
    atrox = Nomekop("Atrox", 3, 3, "plant")
    player_nomekop_list = [cleaf, plumette]
    opp_nomekop_list = [jacana, gulfin, atrox]

    print()
    print("Finding best sequence")
    best_sequence = find_best_timeline(player_nomekop_list, opp_nomekop_list, 6)
    print()

    player_q = MoveQueue()
    for move in best_sequence:
        player_q.insert_at_end(move[0], move[1])

    print()
    print("Queue from best sequence")
    print(player_q)
    print()

    opp_q = MoveQueue()
    # triple attack on cleaf
    opp_q.insert_at_end(jacana, cleaf)
    opp_q.insert_at_end(gulfin, cleaf)
    opp_q.insert_at_end(atrox, cleaf)

    # triple attack on plumette
    opp_q.insert_at_end(jacana, plumette)
    opp_q.insert_at_end(gulfin, plumette)
    opp_q.insert_at_end(atrox, plumette)

    print("Simulating battle")
    player_nomekop_list = reset_hitpoints(player_nomekop_list, [6, 2])
    opp_nomekop_list = reset_hitpoints(opp_nomekop_list, [4, 8, 3])
    print()
    for i in range(player_q.length):
        player_q.process_next_move()
        opp_q.process_next_move()    

    print()
    print("Final hitpoints after executing best solution")
    print()
    print("Cleaf final hitpoints", cleaf.hitpoints)
    print("Plumette final hitpoints", plumette.hitpoints)
    print("Jacana final hitpoints", jacana.hitpoints)
    print("Gulfin final hitpoints", gulfin.hitpoints)
    print("Atrox final hitpoints", atrox.hitpoints)