import re
from tabulate import tabulate

def calc_edit_distance(s, t):
    m, n = len(s) + 1, len(t) + 1
    f = {}
    for i in range(m):
        f[i, 0] = i
    for j in range(n):
        f[0, j] = j
    for i in range(1, m):
        for j in range(1, n):
            cost = 0 if s[i - 1] == t[j - 1] else 1
            f[i, j] = min(f[i, j - 1] + 1, f[i - 1, j] + 1, f[i - 1, j - 1] + cost)
    return f[m - 1, n - 1]

def compare_words(s, t, max_distance=2):
    if s is None or t is None:
        return False
    s = s.lower()
    t = t.lower()
    exceptions = ['right', 'take', 'more']
    if s in exceptions or t in exceptions:
        return s == t
    dist = calc_edit_distance(s.lower(), t.lower())
    if len(s) <= 3:
        return dist == 0
    if len(s) <= 5:
        return dist <= 1
    return dist <= max_distance


class Action(object):
    def __init__(self, act_type, extra):
        self.act_type = act_type
        self.extra = extra

    @classmethod
    def from_line(cls, action):
        act_type = None
        extra = None
        if ('CmdBuild' in action) or ('CmdGather' in action) or ('CmdMove' in action) or ('CmdAttack' in action):
            unit_id = cls.get_id(action)
            # enemy's action
            if unit_id > 100000:
                return cls(act_type, extra)
            if 'CmdBuild' in action:
                act_type = 'build'
                extra = cls.get_extra(action)
            if 'CmdGather' in action:
                act_type = 'gather'
            if 'CmdMove' in action:
                act_type = 'move'
            if 'CmdAttack' in action:
                act_type = 'attack'
        return cls(act_type, extra)

    @classmethod
    def get_id(cls, action):
        try:
            pieces = action.strip().split('\"')
            id_pieces = pieces[2].strip().split(' ')
            idx = id_pieces[4]
            return int(idx)
        except:
            return 1000000000

    @classmethod
    def get_extra(cls, action):
        try:
            pieces = action.strip().split('\"')
            return pieces[3].strip().lower()
        except:
            return None

    def match(self, cmds, mask):
        act_idx = None
        for i, cmd in enumerate(cmds):
            if self == cmd:
                act_idx = i
                break
        if act_idx is None:
            return mask, True
        mask[act_idx] = True
        return mask, False

    def __str__(self):
        if self.extra is None:
            return self.act_type
        return '%s:%s' % (self.act_type, self.extra)

    def __eq__(self, other):
        return self.act_type == other.act_type and self.extra == other.extra


ACTIONS_NICKNAMES = {
    'build': ['create', 'build', 'construct', 'produce', 'train', 'make'],
    'gather': ['crystal', 'resource', 'rock', 'collect', 'gather', 'mine', 'bring', 'rock', 'mineral', 'boulder'],
    'move': ['explore', 'go', 'move', 'find', 'scout', 'look', 'send', 'drag'],
    'attack': ['attack', 'fight', 'defend', 'protect', 'enemy', 'target'],
}


UNITS_NICKNAMES = {
    'peasant': ['peasant', 'worker', 'farmer', 'man', 'guy', 'guys'],
    'spearman': ['spearman'],
    'swordman': ['swordman'],
    'cavalry': ['cavalry', 'knight', 'horse'],
    'dragon': ['dragon', 'bird', 'fly'],
    'archer': ['archer'],
    'catapult': ['catapult', 'tank', 'car'],
    'barrack': ['barrack'],
    'blacksmith': ['blacksmith'],
    'stable': ['stable'],
    'workshop': ['workshop'],
    'guard_tower': ['guard tower', 'guard_tower', 'tower', 'guard'],
    'town_hall': ['base', 'town_hall', 'town hall', 'town', 'hall'],
}

class Replay(object):
    def __init__(self, replay_path, verbose=False):
        self.verbose = verbose
        try:
            self._parse_replay(replay_path)
        except:
            print('[error] unable to parse %s' % replay_path)
        self._eval_replay()

    def _cmd_starts(self, line):
        if not ('CmdIssueInstruction' in line):
            return False
        pieces = [x for x in line.strip().split(' ') if x != '']
        # player id
        return pieces[5] == '0'

    def _parse_cmd(self, line):
        def find_in_nicknames(word, nicknames):
            for name in nicknames:
                nicks = nicknames[name]
                for nick in nicks:
                    if compare_words(word, nick):
                        return name
            return None

        def match_act(word):
            return find_in_nicknames(word, ACTIONS_NICKNAMES)

        def match_unit(word):
            return find_in_nicknames(word, UNITS_NICKNAMES)

        try:
            line = re.sub(r'[,:.!?]' ,' ' , line)
            str_cmd = line.strip().split('\"')[3].strip().lower()
            words = str_cmd.split(' ')
            cmds = []
            for i in range(len(words)):
                act = match_act(words[i])
                if act is not None:
                    if act == 'build':
                        for j in range(i + 1, len(words)):
                            unit = match_unit(words[j])
                            if unit is not None:
                                new_act = Action(act, unit)
                                if new_act not in cmds:
                                    cmds.append(new_act)
                                break
                    else:
                        new_act = Action(act, None)
                        if new_act not in cmds:
                            cmds.append(new_act)
            return cmds, str_cmd
        except Exception as e:
            return [], str_cmd

    def _parse_opponent(self, replay_path):
        try:
            tokens = replay_path.split('/')[-1].split('_')[9:]
            name_tokens = []
            for token in tokens:
                if token == 'fs':
                    break
                name_tokens.append(token)
            return '_'.join(name_tokens)
        except:
            print(replay_path)
            return None

    def _parse_coach_id(self, replay_path):
        return replay_path.split('_')[2]

    def _parse_player_id(self, replay_path):
        return replay_path.split('_')[1]

    def _parse_replay(self, replay_path):
        self.cmd2act = []
        self.win = False
        self.lose = False
        self.opponent = self._parse_opponent(replay_path)
        self.coach_id = self._parse_coach_id(replay_path)
        self.player_id = self._parse_player_id(replay_path)
        self.warnings = 0
        cur_cmd = None
        str_cmd = None
        cur_acts = []
        num_lines = 0
        with open(replay_path, 'r') as f:
            for line in f:
                num_lines += 1
                if self._cmd_starts(line):
                    if cur_cmd is not None:
                        self.cmd2act.append((cur_cmd, cur_acts, str_cmd))
                    cur_cmd, str_cmd = self._parse_cmd(line)
                    cur_acts = []
                act = Action.from_line(line)
                if act.act_type != None:
                    if act not in cur_acts:
                        cur_acts.append(act)
                if 'CmdWarnInstruction' in line:
                    self.warnings += 1
                if 'WON tcpplayer' in line:
                    self.win = True
                elif 'WON' in line:
                    self.lose = True

        self.empty = num_lines <= 5


        if cur_cmd is not None:
            self.cmd2act.append((cur_cmd, cur_acts, str_cmd))

    def _eval_replay(self):
        self.num_correct = 0
        self.num_incorrect = 0
        self.num_covered = 0
        self.num_issued = 0
        self.num_actions = 0

        for (cmds, acts, str_cmd) in self.cmd2act:
            if self.verbose:
                print('-' * 40)
                print('message: \"%s\"' % str_cmd)
                print('commands: [%s]' % (', '.join(['%s' % c for c in cmds])))
                print('actions: [%s]' % (', '.join(['%s' % a for a in acts])))

            mask = [False] * len(cmds)
            num_correct, num_incorrect = 0, 0
            for act in acts:
                mask, incorrect = act.match(cmds, mask)
                if incorrect:
                    self.num_incorrect += 1
                    num_incorrect += 1
                else:
                    self.num_correct += 1
                    num_correct += 1
            self.num_covered += sum(1 if x else 0 for x in mask)
            self.num_issued += len(mask)
            self.num_actions += len(acts)
            if self.verbose:
                print('covered: %d/%d' % (sum(1 if x else 0 for x in mask), len(mask)))
                print('correct: %d/%d' % (num_correct, len(acts)))
                print('incorrect: %d/%d' % (num_incorrect, len(acts)))
                print('')

    def is_win(self):
        return self.win

    def is_empty(self):
        return self.empty

    def is_lose(self):
        return self.lose

    def is_unfinished(self):
        return (not self.win) and (not self.lose) and (not self.empty)

    def is_finished(self):
        return self.win or self.lose

    def num_warnings(self):
        return self.warnings

    def num_instructions(self):
        return max(len(self.cmd2act), 1)

    def num_commands(self):
        return max(sum(len(cmd) for (cmd, act, _) in self.cmd2act), 1)

    def num_actions(self):
        return max(sum(len(act) for (cmd, act, _) in self.cmd2act), 1)

    def num_covered_commands(self):
        return self.num_covered

    def num_correct_actions(self):
        return self.num_correct

    def num_incorrect_actions(self):
        return self.num_incorrect

    def evaluate(self):
        stats = {
            'win': self.is_win(),
            'messages': self.num_instructions(),
            'warnings': self.num_warnings(),
            'commands': '%d/%d (%.2f%%)' % (self.num_covered_commands(), self.num_commands(), 100. * self.num_covered_commands() / self.num_commands()),
            'correct actions': '%d/%d (%.2f%%)' % (self.num_correct_actions(), self.num_actions(), 100. * self.num_correct_actions() / self.num_actions()),
            'incorrect actions': '%d/%d (%.2f%%)' % (self.num_incorrect_actions(), self.num_actions(), 100. * self.num_incorrect_actions() / self.num_actions()),
        }
        return stats

    def __str__(self):
        stats = self.evaluate()
        tab_stats = [[k, v] for k, v in stats.items()]
        return tabulate(tab_stats)

