class LogParser(object):
    """
    This class load Isaac's log file, and incrementally modify a state representing this log
    """
    def __init__(self, prefix, tracker_version):
        self.state = TrackerState("", tracker_version)
        self.log = logging.getLogger("tracker")
        self.file_prefix = prefix

        self.game_version = ""
        self.__reset()

        # FIXME run summary.
        self.run_ended = 0

    def __reset(self):
        """Reset variable specific to the log file/run"""
        # Variables describing the parser state
        self.getting_start_items = False
        self.current_room = ""
        self.current_seed = ""
        # Cached contents of log
        self.content = ""
        # Log split into lines
        self.splitfile = []
        self.run_start_line = 0
        self.seek = 0
        self.spawned_coop_baby = 0



    def parse(self):
        """
        Parse the log file and return a TrackerState object,
        or None if the log file couldn't be found
        """
        # Attempt to load log_file
        if not self.__load_log_file():
            return None
        self.splitfile = self.content.splitlines()

        self.getting_start_items = False # This will become true if we are getting starting items

        # Process log's new output
        for current_line_number, line in enumerate(self.splitfile[self.seek:]):
            self.__parse_line(current_line_number, line)


        self.seek = len(self.splitfile)
        return self.state


    def __parse_line(self, line_number, line):
        """
        Parse a line using the (line_number, line) tuple
        """
        if line.startswith('Mom clear time:'):
            self.__parse_boss(line)

        # Check and handle the end of the run; the order is important
        # - we want it after boss kill but before "RNG Start Seed"
        self.__check_end_run(line_number + self.seek, line)

        if line.startswith('RNG Start Seed:'):
            self.__parse_seed(line)
        if line.startswith('Room'):
            self.__parse_room(line)
        if line.startswith('Level::Init'):
            self.__parse_floor(line_number, line)
        if line.startswith("Curse"):
            self.__parse_curse(line)
        if line.startswith("Spawn co-player!"):
            self.spawned_coop_baby = line_number + self.seek
        if re.search(r"Added \d+ Collectibles", line):
            self.log.debug("Reroll detected!")
            self.state.reroll()
        if line.startswith('Adding collectible'):
            self.__parse_item(line_number, line)


    def __parse_boss(self, line):
        """ Parse a boss line """
        # TODO "Boss %i added to SaveState", Mom:6, It lives:25,
        # Satan/The Lamb:24/54 Isaac/???:39/40, Mega Satan:55
        kill_time = int(line.split(" ")[-1])
        # If you re-enter a room you get a "mom clear time" again,
        # check for that (can you fight the same boss twice?)
        # FIXME right now we only have support for Mom (6)
        self.state.add_boss("6")

    def __parse_seed(self, line):
        """ Parse a seed line """
        # This assumes a fixed width, but from what I see it seems safe
        self.current_seed = line[16:25]

    def __parse_room(self, line):
        """ Parse a room line """
        self.current_room = re.search(r'\((.*)\)', line).group(1)
        if 'Start Room' not in line:
            self.getting_start_items = False
        self.log.debug("Entered room: %s", self.current_room)

    def __parse_floor(self, line_number, line):
        """ Parse the floor in line and push it to the state """
        # Create a floor tuple with the floor id and the alternate id
        if self.game_version == "Afterbirth":
            regexp_str = r"Level::Init m_Stage (\d+), m_StageType (\d+)"
        else:
            regexp_str = r"Level::Init m_Stage (\d+), m_AltStage (\d+)"
        floor_tuple = tuple([re.search(regexp_str, line).group(x) for x in [1, 2]])

        self.getting_start_items = True
        # Assume floors aren't cursed until we see they are
        floor = int(floor_tuple[0])
        alt = floor_tuple[1]

        # Special handling for cath and chest and Afterbirth
        if self.game_version == "Afterbirth":
            # In Afterbirth Cath is an alternate of Sheol (which is 10)
            # and Chest is an alternate of Dark room (which is 11)
            if floor == 10 and alt == '0':
                floor -= 1
            elif floor == 11 and alt == '1':
                floor += 1
        else:
            # In Rebirth floors have different numbers
            if alt == '1' and (floor == 9 or floor == 11):
                floor += 1
        floor_id = 'f' + str(floor)
        # Greed mode
        if alt == '3':
            floor_id += 'g'

        # when we see a new floor 1, that means a new run has started
        if floor == 1:
            self.log.debug("Starting new run, seed: %s", self.current_seed)
            self.run_start_line = line_number + self.seek
            self.state.reset(self.current_seed)
            self.run_ended = False


        self.state.add_floor(Floor(floor_id))
        return True

    def __parse_curse(self, line):
        """ Parse the curse and add it to the last floor """
        if line.startswith("Curse of the Labyrinth!"):
            self.state.add_curse(Curse.Labyrinth)
        if line.startswith("Curse of Blind"):
            self.state.add_curse(Curse.Blind)
        if line.startswith("Curse of the Lost!"):
            self.state.add_curse(Curse.Lost)

    def __parse_item(self, line_number, line):
        """ Parse an Item and push it to the state """
        if len(self.splitfile) > 1 and self.splitfile[line_number + self.seek - 1] == line:
            self.log.debug("Skipped duplicate item line from baby presence")
            return False
        space_split = line.split(" ") # Hacky string manipulation
        item_id = space_split[2] # A string has the form of "Adding collectible 105 (The D6)"

        # Check if the item ID exists
        if not Item.contains_info(item_id):
            item_id = "NEW"

        item_name = " ".join(space_split[3:])[1:-1]
        self.log.debug("Picked up item. id: %s, name: %s", item_id, item_name)
        if ((line_number + self.seek) - self.spawned_coop_baby) < (len(self.state.item_list) + 10) \
                and self.state.contains_item(item_id):
            self.log.debug("Skipped duplicate item line from baby entry")
            return False
        #it's a blind pickup if we're on a blind floor and they don't have the black candle
        blind_pickup = self.state.last_floor.floor_has_curse(Curse.Blind) and not self.state.contains_item('260')
        added = self.state.add_item(Item(item_id, self.state.last_floor, self.getting_start_items, blind=blind_pickup))
        if not added:
            self.log.debug("Skipped adding item %s to avoid space-bar duplicate", item_id)
        return True

    def __load_log_file(self):
        """
        Attempt to load log file from common location.
        Return true if successfully loaded, false otherwise
        """
        path = None
        logfile_location = ""
        game_names = ("Afterbirth", "Rebirth")
        if platform.system() == "Windows":
            logfile_location = os.environ['USERPROFILE'] + '/Documents/My Games/Binding of Isaac {}/'
        elif platform.system() == "Linux":
            logfile_location = os.getenv('XDG_DATA_HOME',
                os.path.expanduser('~') + '/.local/share') + '/binding of isaac {}/'
            game_names = ("afterbirth", "rebirth")
        elif platform.system() == "Darwin":
            logfile_location = os.path.expanduser('~') + '/Library/Application Support/Binding of Isaac {}/'
        if os.path.exists(logfile_location.format(game_names[0])):
            logfile_location = logfile_location.format(game_names[0])
            self.game_version = "Afterbirth"
        else:
            logfile_location = logfile_location.format(game_names[1])
            self.game_version = "Rebirth"

        for check in (self.file_prefix + '../log.txt', logfile_location + 'log.txt'):
            if os.path.isfile(check):
                path = check
                break
        if path is None:
            return False

        cached_length = len(self.content)
        file_size = os.path.getsize(path)
        if cached_length > file_size or cached_length == 0: # New log file or first time loading the log
            self.__reset()
            self.content = open(path, 'rb').read()
        elif cached_length < file_size:  # Append existing content
            f = open(path, 'rb')
            f.seek(cached_length + 1)
            self.content += f.read()
        return True


# Above are legacy method for handling end of run
    def __check_end_run(self, line_number, line):
        if not self.run_ended:
            died_to = ""
            end_type = ""
            # FIXME right now I don't think boss detection in the log is working properly
            if self.state.last_boss and self.state.last_boss[0] in ['???', 'The Lamb', 'Mega Satan']:
                end_type = "Won"
            elif (self.state.seed != '') and line.startswith('RNG Start Seed:'):
                end_type = "Reset"
            elif line.startswith('Game Over.'):
                end_type = "Death"
                died_to = re.search(r'(?i)Killed by \((.*)\) spawned', line).group(1)
            if end_type:
                last_run = {
                    "bosses":   self.state.bosses,
                    "items":    self.state.item_list,
                    "seed":     self.state.seed,
                    "died_to":  died_to,
                    "end_type": end_type
                }
                self.run_ended = True
                self.log.debug("End of Run! %s", last_run)
                if end_type != "Reset":
                    self.__save_file(self.run_start_line, line_number, last_run)

    def __save_file(self, start, end, last_run):
        dir_name = self.file_prefix + "run_logs"
        if not os.path.isdir(dir_name):
            os.mkdir(dir_name)
        timestamp = int(time.time())
        seed = self.state.seed.replace(" ", "")
        data = "\n".join(self.splitfile[start:end + 1])
        # FIXME improve
        data = "%s\nRUN_OVER_LINE\n%s" % (data, last_run)
        run_name = "%s%s.log" % (seed, timestamp)
        in_memory_file = StringIO.StringIO()
        with zipfile.ZipFile(in_memory_file, mode='w', compression=zipfile.ZIP_DEFLATED) as zf:
            zf.writestr(run_name, data)
        with open(self.file_prefix + "run_logs/" + run_name + ".zip", "wb") as f:
            f.write(in_memory_file.getvalue())