import threading
import os
import logging
import stat

from .ReadLineHandler import ReadLineHandler

log = logging.getLogger(__name__)

'''Spawn a thread to "tail" a log file. For each line read, provided
callbacks do something with the output. Callbacks must be a subclass
of ReadLineHandler.

'''

class TailFile(threading.Thread):
    def __init__(self, log_file, store=None, stop_at_eof=False):
        ''' log_file - the log file to monitor
            store - a ReadPositionStore instance
        '''
        self.log_file = log_file
        self.store = store
        self.stop_at_eof = stop_at_eof

        self.fp = None
        self.inode = None
        self.callbacks = []
        self.interrupt = threading.Event()

        name=f'{__name__}-{os.path.basename(log_file)}'
        log.debug('init thread: %s', name)
        super(TailFile, self).__init__(name=name, daemon=True)
        
    def stop(self, do_join=True):
        self.interrupt.set()
        # close must be called to unblock the thread fp.readline() call
        self._close()
        if do_join:
            self.join()

    def __del__(self):
        self.stop(do_join=False)

    def add_handler(self, fn):
        assert self.is_alive() == False
        self.callbacks.append(fn)

    def clear_callbacks(self):
        assert self.is_alive() == False
        self.callbacks = []

    def _open(self):
        self._close()
        self.inode = os.stat(self.log_file)[stat.ST_INO]
        self.fp = open(
            self.log_file,
            "r",
            encoding="utf-8",
            errors="backslashreplace"
        )

    def _close(self):        
        if self.fp is not None:
            self.fp.close()
        self.fp = None

    def _is_rotated(self):
        try:
            return os.stat(self.log_file)[stat.ST_INO] != self.inode
        except FileNotFoundError:
            return False

    def _issue_callbacks(self, line):
        for cb in self.callbacks:
            cb.handle(line)

    def _notify_end_of_callbacks(self):
        for cb in self.callbacks:
            cb.end_of_callbacks(self)

    def _restore_read_position(self):
        if self.fp is None:
            return
        
        if self.store is None:
            self.fp.seek(
                0,
                os.SEEK_END
            )
        else:
            pos = self.store.get(self.log_file, self.inode)
            size = os.stat(self.log_file)[stat.ST_SIZE]
            if size < pos:
                log.debug("truncated: %s" % self.log_file)
                self.fp.seek(0, os.SEEK_SET)
            else:
                # if pos>size here, the seek call succeeds and returns
                # 'pos', but future reads will fail
                self.fp.seek(pos, os.SEEK_SET)
        
    def run(self):
        self.interrupt.clear()

        # initial open - wait until file exists
        while not self.interrupt.is_set() and self.fp is None:
            try:
                self._open()
            except FileNotFoundError:
                log.debug('log file "%s" not found, waiting...', self.log_file)
                self.interrupt.wait(2)
                continue

        # restore reading position
        self._restore_read_position()
        
        while not self.interrupt.is_set():                
            try:
                line = self.fp.readline() # blocking
                if line=='':
                    log.debug('got EOF')
                    if self.stop_at_eof:
                        self.interrupt.set()
                    
                    # EOF - check if file was rotated
                    if self._is_rotated():
                        log.debug('rotated')
                        self._open()
                        if self.store is not None:
                            self.store.clear(self.log_file)
                            
                    # if not rotated, sleep
                    else:
                        self.interrupt.wait(1)
                        
                else:
                    # save position and call all callbacks
                    if self.store is not None:
                        self.store.save(
                            self.log_file,
                            self.inode,
                            self.fp.tell()
                        )
                    self._issue_callbacks(line)

            except Exception as e:
                log.error('exception processing line: %s', line)
                log.exception(e)
                if self.interrupt.wait(1) is not True:
                    if self._is_rotated():
                        self._open()


        self._close()

        try:
            self._notify_end_of_callbacks()
        except Exception as e:
            log.exception(e)