"""The operating system file entry implementation."""

import os
import platform
import stat
import sys

import pysmdev

try:
    import xattr
except ImportError:
    xattr = None

from dfdatetime import posix_time as dfdatetime_posix_time

from dfvfs.lib import definitions
from dfvfs.path import os_path_spec
from dfvfs.vfs import attribute
from dfvfs.vfs import file_entry
from dfvfs.vfs import os_attribute
from dfvfs.vfs import os_directory


class OSFileEntry(file_entry.FileEntry):
    """File system file entry that uses os."""

    TYPE_INDICATOR = definitions.TYPE_INDICATOR_OS

    _OS_IS_WINDOWS = platform.system() == "Windows"
    _PY_3_11_AND_EARLIER = bool(tuple(sys.version_info[0:2]) <= (3, 11))

    def __init__(self, resolver_context, file_system, path_spec, is_root=False):
        """Initializes a file entry.

        Args:
          resolver_context (Context): resolver context.
          file_system (FileSystem): file system.
          path_spec (PathSpec): path specification.
          is_root (Optional[bool]): True if the file entry is the root file entry
              of the corresponding file system.
        """
        location = getattr(path_spec, "location", None)

        # Windows does not support running os.stat on device files so we use
        # libsmdev to do an initial check.
        is_windows_device = False
        if self._OS_IS_WINDOWS and location:
            try:
                # pylint: disable=no-member
                is_windows_device = pysmdev.check_device(location)
            except OSError:
                pass

        stat_info = None
        if not is_windows_device and location:
            # We are only catching OSError. However on the Windows platform
            # a WindowsError can be raised as well. We are not catching that since
            # that error does not exist on non-Windows platforms.
            try:
                stat_info = os.lstat(location)
            except OSError:
                stat_info = None

        super().__init__(
            resolver_context, file_system, path_spec, is_root=is_root, is_virtual=False
        )
        self._is_windows_device = is_windows_device
        self._location = location
        self._name = None
        self._stat_info = stat_info

        if is_windows_device:
            self.entry_type = definitions.FILE_ENTRY_TYPE_DEVICE

        elif stat_info:
            # If location contains a trailing segment separator and points to
            # a symbolic link to a directory stat info will not indicate
            # the file entry as a symbolic link. The following check ensures
            # that the LINK type is correctly detected.
            is_link = os.path.islink(location)

            # The stat info member st_mode can have multiple types e.g.
            # LINK and DIRECTORY in case of a symbolic link to a directory
            # dfVFS currently only supports one type so we need to check
            # for LINK first.
            if stat.S_ISLNK(stat_info.st_mode) or is_link:
                self.entry_type = definitions.FILE_ENTRY_TYPE_LINK
            elif stat.S_ISREG(stat_info.st_mode):
                self.entry_type = definitions.FILE_ENTRY_TYPE_FILE
            elif stat.S_ISDIR(stat_info.st_mode):
                self.entry_type = definitions.FILE_ENTRY_TYPE_DIRECTORY
            elif stat.S_ISCHR(stat_info.st_mode):
                self.entry_type = definitions.FILE_ENTRY_TYPE_CHARACTER_DEVICE
            elif stat.S_ISBLK(stat_info.st_mode):
                self.entry_type = definitions.FILE_ENTRY_TYPE_BLOCK_DEVICE
            elif stat.S_ISFIFO(stat_info.st_mode):
                self.entry_type = definitions.FILE_ENTRY_TYPE_PIPE
            elif stat.S_ISSOCK(stat_info.st_mode):
                self.entry_type = definitions.FILE_ENTRY_TYPE_SOCKET

    def _GetAttributes(self):
        """Retrieves the attributes.

        Returns:
          list[Attribute]: attributes.
        """
        if self._attributes is None:
            self._attributes = []

            if xattr:
                try:
                    attribute_names = list(xattr.listxattr(self._location))
                except OSError:
                    attribute_names = []

                for name in attribute_names:
                    if isinstance(name, bytes):
                        name = os.fsdecode(name)

                    try:
                        extended_attribute = os_attribute.OSExtendedAttribute(
                            self._location, name
                        )
                        self._attributes.append(extended_attribute)
                    except OSError:
                        pass

        return self._attributes

    def _GetDirectory(self):
        """Retrieves a directory.

        Returns:
          OSDirectory: a directory.
        """
        if self.entry_type != definitions.FILE_ENTRY_TYPE_DIRECTORY:
            return None

        return os_directory.OSDirectory(self._file_system, self.path_spec)

    def _GetLink(self):
        """Retrieves the link.

        Returns:
          str: full path of the linked file entry.
        """
        if self._link is None:
            self._link = ""

            if self._location is None:
                return self._link

            self._link = os.readlink(self._location)
            self._link = os.path.abspath(self._link)

        return self._link

    def _GetStatAttribute(self):
        """Retrieves a stat attribute.

        Returns:
          StatAttribute: a stat attribute or None if not available.
        """
        stat_attribute = attribute.StatAttribute()

        if not self._is_windows_device and self._stat_info:
            stat_attribute.group_identifier = self._stat_info.st_gid
            stat_attribute.inode_number = self._stat_info.st_ino
            stat_attribute.mode = stat.S_IMODE(self._stat_info.st_mode)
            stat_attribute.number_of_links = self._stat_info.st_nlink
            stat_attribute.owner_identifier = self._stat_info.st_uid
            stat_attribute.size = self._stat_info.st_size

        stat_attribute.type = self.entry_type

        return stat_attribute

    def _GetSubFileEntries(self):
        """Retrieves sub file entries.

        Yields:
          OSFileEntry: a sub file entry.
        """
        if self._directory is None:
            self._directory = self._GetDirectory()

        if self._directory:
            for path_spec in self._directory.entries:
                yield OSFileEntry(self._resolver_context, self._file_system, path_spec)

    @property
    def access_time(self):
        """dfdatetime.DateTimeValues: access time or None if not available."""
        if self._stat_info is None:
            return None

        timestamp = getattr(self._stat_info, "st_atime_ns", None)
        if timestamp is not None:
            return dfdatetime_posix_time.PosixTimeInNanoseconds(timestamp=timestamp)

        timestamp = int(self._stat_info.st_atime)
        return dfdatetime_posix_time.PosixTime(timestamp=timestamp)

    @property
    def change_time(self):
        """dfdatetime.DateTimeValues: change time or None if not available."""
        if self._stat_info is None:
            return None

        # Per Python os.stat() documentation the value of stat_results.st_ctime
        # contains the creation time on Windows.
        if self._OS_IS_WINDOWS:
            return None

        timestamp = getattr(self._stat_info, "st_ctime_ns", None)
        if timestamp is not None:
            return dfdatetime_posix_time.PosixTimeInNanoseconds(timestamp=timestamp)

        timestamp = int(self._stat_info.st_ctime)
        return dfdatetime_posix_time.PosixTime(timestamp=timestamp)

    @property
    def creation_time(self):
        """dfdatetime.DateTimeValues: creation time or None if not available."""
        if self._stat_info is None:
            return None

        # Per Python os.stat() documentation the value of stat_results.st_ctime
        # contains the creation time on Windows.
        if self._OS_IS_WINDOWS and self._PY_3_11_AND_EARLIER:
            timestamp = getattr(self._stat_info, "st_ctime_ns", None)
            if timestamp is not None:
                return dfdatetime_posix_time.PosixTimeInNanoseconds(timestamp=timestamp)

            timestamp = int(self._stat_info.st_ctime)

        else:
            timestamp = getattr(self._stat_info, "st_birthtime", None)
            if timestamp is None:
                return None

        return dfdatetime_posix_time.PosixTime(timestamp=timestamp)

    @property
    def modification_time(self):
        """dfdatetime.DateTimeValues: modification time or None if not available."""
        if self._stat_info is None:
            return None

        timestamp = getattr(self._stat_info, "st_mtime_ns", None)
        if timestamp is not None:
            return dfdatetime_posix_time.PosixTimeInNanoseconds(timestamp=timestamp)

        timestamp = int(self._stat_info.st_mtime)
        return dfdatetime_posix_time.PosixTime(timestamp=timestamp)

    @property
    def name(self):
        """str: name of the file entry, without the full path."""
        if self._name is None:
            if self._location is not None:
                self._name = self._file_system.BasenamePath(self._location)
        return self._name

    @property
    def size(self):
        """int: size of the file entry in bytes or None if not available."""
        if self._is_windows_device or not self._stat_info:
            return None

        return self._stat_info.st_size

    def GetLinkedFileEntry(self):
        """Retrieves the linked file entry, for example for a symbolic link.

        Returns:
          OSFileEntry: linked file entry or None if not available.
        """
        link = self._GetLink()
        if not link:
            return None

        path_spec = os_path_spec.OSPathSpec(location=link)
        return OSFileEntry(self._resolver_context, self._file_system, path_spec)

    def GetParentFileEntry(self):
        """Retrieves the parent file entry.

        Returns:
          OSFileEntry: parent file entry or None if not available.
        """
        if self._location is None:
            return None

        parent_location = self._file_system.DirnamePath(self._location)
        if parent_location is None:
            return None

        if parent_location == "":
            parent_location = self._file_system.PATH_SEPARATOR

        path_spec = os_path_spec.OSPathSpec(location=parent_location)
        return OSFileEntry(self._resolver_context, self._file_system, path_spec)
