import os
import sys
import threading
import struct
import time

from parallels.core.utils.common.threading_utils import synchronized
from parallels.core.utils.entity import Entity


class RecordSeverity(object):
    INFO = 1
    ERROR = 2
    WARN = 3
    DEBUG = 4
    EXCEPTION = 5
    MIGRATOR_EXECUTION = 6


class RecordType(object):
    SIMPLE = 0
    COMPOUND_OPEN = 1
    COMPOUND_FINISHED = 2
    COMPOUND_HAS_CHILD_ERRORS = 4
    COMPOUND_HAS_CHILD_WARNINGS = 8


RECORD_NONE = 2147483647


class BinaryLogger(object):
    """Binary migrator log. Used for fast display of log messages in migrator GUI.

    Stores data in binary format to simplify and speed-up processing.

    :type _index_file: BinaryLogIndexFile
    :type _messages_file: BinaryLogMessagesFile
    """
    THREAD_LOCAL_LEVEL_STACK_VAR = 'binary_log_record_parent_ids'

    _instance = None

    def __init__(self):
        self._index_file = None
        self._messages_file = None

        self._write_lock = None
        self._initialized = False
        self._ignore_exceptions = True
        self._lock = threading.Lock()

        self._thread_local = threading.local()
        self._main_thread_level_info_stack = None

    @classmethod
    def get_instance(cls):
        """Obtain instance of binary log (singleton) and return it

        :rtype: parallels.core.logging.binary.BinaryLogger
        """
        if cls._instance is None:
            cls._instance = BinaryLogger()
        return cls._instance

    def initialize(self, index_file, messages_file):
        """Initialize binary log

        :type index_file: parallels.core.logging.binary.BinaryLogIndexFile
        :type messages_file: parallels.core.logging.binary.BinaryLogMessagesFile
        :rtype: None
        """
        self._index_file = index_file
        self._messages_file = messages_file

        last_record_id = self._index_file.get_last_top_level_record_id()
        self._get_current_level_info().last_record_id = last_record_id

        self._initialized = True

    def add_record(self, severity, message):
        """Write message to binary log

        :type severity: int
        :type message: str | unicode
        :rtype: None
        """
        # noinspection PyBroadException
        with self._lock:
            try:
                if not self._initialized:
                    return

                if sys.version_info.major >= 3:
                    if isinstance(message, str):
                        message = message.encode('utf-8')
                else:
                    if isinstance(message, unicode):
                        message = message.encode('utf-8')

                message_position = self._messages_file.add_message(message)
                message_length = len(message)
                date = int(time.time())

                current_level_info = self._get_current_level_info()

                new_record_id = self._index_file.add_record(
                    parent_record_id=current_level_info.parent_record_id, compound=False, severity=severity, date=date,
                    message_position=message_position, message_length=message_length
                )

                if current_level_info.last_record_id == RECORD_NONE:
                    if current_level_info.parent_record_id != RECORD_NONE:
                        self._index_file.set_first_child_record(current_level_info.parent_record_id, new_record_id)
                else:
                    self._index_file.set_next_record(current_level_info.last_record_id, new_record_id)

                level_info_stack = getattr(self._thread_local, self.THREAD_LOCAL_LEVEL_STACK_VAR, None)
                if level_info_stack is not None:
                    if severity == RecordSeverity.ERROR:
                        for item in level_info_stack:
                            if item.parent_record_id != RECORD_NONE:
                                self._index_file.set_has_child_errors(item.parent_record_id)
                    elif severity == RecordSeverity.WARN:
                        for item in level_info_stack:
                            if item.parent_record_id != RECORD_NONE:
                                self._index_file.set_has_child_warnings(item.parent_record_id)

                current_level_info.last_record_id = new_record_id
            except:
                if self._ignore_exceptions:
                    # In case of ANY failure, simply do not write binary log
                    pass
                else:
                    raise

    def start_level(self, severity, message):
        """Start new level in the log

        :type severity: int
        :type message: str | unicode
        :rtype: None
        """
        # noinspection PyBroadException
        with self._lock:
            try:
                if not self._initialized:
                    return

                if sys.version_info.major >= 3:
                    if isinstance(message, str):
                        message = message.encode('utf-8')
                else:
                    if isinstance(message, unicode):
                        message = message.encode('utf-8')

                message_position = self._messages_file.add_message(message)
                message_length = len(message)
                date = int(time.time())

                current_level_info = self._get_current_level_info()

                new_record_id = self._index_file.add_record(
                    parent_record_id=current_level_info.parent_record_id, compound=True, severity=severity, date=date,
                    message_position=message_position, message_length=message_length
                )

                if current_level_info.last_record_id == RECORD_NONE:
                    if current_level_info.parent_record_id != RECORD_NONE:
                        self._index_file.set_first_child_record(current_level_info.parent_record_id, new_record_id)
                else:
                    self._index_file.set_next_record(current_level_info.last_record_id, new_record_id)

                current_level_info.last_record_id = new_record_id

                self._create_level_info(new_record_id)
            except:
                if self._ignore_exceptions:
                    # In case of ANY failure, simply do not write binary log
                    pass
                else:
                    raise

    def end_level(self):
        """End log level (log level controls hierarchy of log records)

        :rtype: None
        """
        with self._lock:
            level_info = self._get_current_level_info()
            if level_info.parent_record_id != RECORD_NONE:
                self._index_file.set_finished(level_info.parent_record_id)
            self._pop_current_level_info()

    def _get_current_level_info(self):
        """Get current parent log record ID (for logging hierarchy)

        :rtype: parallels.core.logging.binary.LevelInfo
        """
        level_info_stack = getattr(self._thread_local, self.THREAD_LOCAL_LEVEL_STACK_VAR, None)
        if level_info_stack is None or len(level_info_stack) == 0:
            return self._create_level_info(RECORD_NONE)
        else:
            return level_info_stack[len(level_info_stack) - 1]

    def _create_level_info(self, record_id):
        """Set current parent log record ID (for logging hierarchy)

        :type record_id: int
        :rtype: parallels.core.logging.binary.LevelInfo
        """
        level_info_stack = getattr(self._thread_local, self.THREAD_LOCAL_LEVEL_STACK_VAR, None)

        if level_info_stack is None:
            if threading.current_thread().name != 'MainThread':
                level_info_stack = list(self._main_thread_level_info_stack)
            else:
                level_info_stack = []
                self._main_thread_level_info_stack = level_info_stack

            setattr(self._thread_local, self.THREAD_LOCAL_LEVEL_STACK_VAR, level_info_stack)

            if threading.current_thread().name != 'MainThread' and len(level_info_stack) > 0:
                return level_info_stack[len(level_info_stack) - 1]

        level_info = LevelInfo(parent_record_id=record_id)
        level_info_stack.append(level_info)
        return level_info

    def _pop_current_level_info(self):
        """Pop parent record ID from parent records stack (for logging hierarchy - end level).

        :rtype: None
        """
        level_info_stack = getattr(self._thread_local, self.THREAD_LOCAL_LEVEL_STACK_VAR, None)
        if level_info_stack is None:
            return
        level_info_stack.pop()


class LevelInfo(object):
    def __init__(self, parent_record_id=RECORD_NONE, last_record_id=RECORD_NONE):
        self.parent_record_id = parent_record_id
        self.last_record_id = last_record_id


class BinaryLogIndexFile(object):
    # Record format:
    # - next record ID (long)
    # - first child record ID (long)
    # - record type (long)
    # - severity (long)
    # - date (long)
    # - message position in messages file (long)
    # - message length (long)
    RECORD_FORMAT = "IIIIIIII"
    RECORD_SIZE = struct.calcsize(RECORD_FORMAT)

    def __init__(self, fileobj):
        self._fileobj = fileobj

    def add_record(self, parent_record_id, compound, severity, date, message_position, message_length):
        """
        :type parent_record_id: int
        :type compound: bool
        :type severity: int
        :type date: int
        :type message_position: int
        :type message_length: int
        :rtype: int
        """
        self._fileobj.seek(0, os.SEEK_END)

        start_position = self._fileobj.tell()

        if compound:
            record_type = RecordType.COMPOUND_OPEN
        else:
            record_type = RecordType.SIMPLE

        self._fileobj.write(struct.pack(
            self.RECORD_FORMAT,
            parent_record_id, RECORD_NONE, RECORD_NONE, record_type, severity, date, message_position, message_length
        ))
        self._fileobj.flush()

        return start_position / self.RECORD_SIZE

    def set_next_record(self, record_id, next_record_id):
        self._fileobj.seek(record_id * self.RECORD_SIZE + struct.calcsize("I"))
        self._fileobj.write(struct.pack("I", next_record_id))
        self._fileobj.flush()

    def set_first_child_record(self, record_id, child_record_id):
        self._fileobj.seek(record_id * self.RECORD_SIZE + struct.calcsize("II"))
        self._fileobj.write(struct.pack("I", child_record_id))
        self._fileobj.flush()

    def set_finished(self, record_id):
        self._set_record_type_flag(record_id, RecordType.COMPOUND_FINISHED)

    def set_has_child_errors(self, record_id):
        self._set_record_type_flag(record_id, RecordType.COMPOUND_HAS_CHILD_ERRORS)

    def set_has_child_warnings(self, record_id):
        self._set_record_type_flag(record_id, RecordType.COMPOUND_HAS_CHILD_WARNINGS)

    @synchronized
    def _set_record_type_flag(self, record_id, flag):
        record_type_position = int(record_id * self.RECORD_SIZE + struct.calcsize("III"))

        self._fileobj.seek(record_type_position)
        record_type_size = struct.calcsize("I")
        record_type_data = self._fileobj.read(record_type_size)
        record_type, = struct.unpack("I", record_type_data)

        self._fileobj.seek(record_type_position)
        self._fileobj.write(struct.pack("I", record_type | flag))
        self._fileobj.flush()

    def get_last_top_level_record_id(self):
        last_top_level_record_id = RECORD_NONE

        while True:
            self._fileobj.seek(
                last_top_level_record_id * self.RECORD_SIZE if last_top_level_record_id != RECORD_NONE else 0
            )
            record_id_size = struct.calcsize("II")
            record_data = self._fileobj.read(record_id_size)

            if len(record_data) != record_id_size:
                break

            if last_top_level_record_id == RECORD_NONE:
                # we have at least one record in the file - it is the first top one
                last_top_level_record_id = 0

            _, next_last_top_level_record_id = struct.unpack("II", record_data)

            if next_last_top_level_record_id == RECORD_NONE:
                break

            last_top_level_record_id = next_last_top_level_record_id

        return last_top_level_record_id

    def iter_records(self):
        self._fileobj.seek(0, os.SEEK_SET)

        while True:
            data = self._fileobj.read(self.RECORD_SIZE)

            if len(data) == 0:
                return
            (
                parent_record_id, next_record_id, first_child_record_id, record_type,
                severity, date, message_position, message_length
            ) = struct.unpack(self.RECORD_FORMAT, data)

            yield RecordInfo(
                parent_record_id, next_record_id, first_child_record_id, record_type, severity, date,
                message_position, message_length
            )


class BinaryLogMessagesFile(object):
    def __init__(self, fileobj):
        self._fileobj = fileobj
        self._fileobj.seek(0, os.SEEK_END)

    def add_message(self, message):
        position = self._fileobj.tell()
        self._fileobj.write(message)
        self._fileobj.flush()
        return position

    def read_message(self, record_info):
        """
        :type record_info: parallels.core.logging.binary.RecordInfo
        :rtype: str | unicode 
        """
        self._fileobj.seek(record_info.message_position, os.SEEK_SET)
        message = self._fileobj.read(record_info.message_length)
        self._fileobj.seek(0, os.SEEK_END)
        return message


class RecordInfo(Entity):
    def __init__(
        self, parent_record_id, next_record_id, first_child_record_id, record_type, severity, date,
        message_position, message_length
    ):
        self._parent_id = parent_record_id
        self._next_record_id = next_record_id
        self._first_child_record_id = first_child_record_id
        self._record_type = record_type
        self._severity = severity
        self._date = date
        self._message_position = message_position
        self._message_length = message_length

    @property
    def parent_record_id(self):
        return self._parent_id

    @property
    def next_record_id(self):
        return self._next_record_id

    @property
    def first_child_record_id(self):
        return self._first_child_record_id

    @property
    def record_type(self):
        return self._record_type

    @property
    def severity(self):
        return self._severity

    @property
    def date(self):
        return self._date

    @property
    def message_position(self):
        return self._message_position

    @property
    def message_length(self):
        return self._message_length
