"""Parser for custom destinations jump list (.customDestinations-ms) files."""

import os

from dfvfs.lib import definitions
from dfvfs.lib import errors as dfvfs_errors
from dfvfs.path import factory as path_spec_factory
from dfvfs.resolver import resolver

from plaso.lib import dtfabric_helper
from plaso.lib import errors
from plaso.lib import specification
from plaso.parsers import interface
from plaso.parsers import manager
from plaso.parsers import winlnk


class CustomDestinationsParser(
    interface.FileObjectParser, dtfabric_helper.DtFabricHelper
):
    """Parses custom destinations jump list (.customDestinations-ms) files."""

    NAME = "custom_destinations"
    DATA_FORMAT = "Custom destinations jump list (.customDestinations-ms) file"

    _INITIAL_FILE_OFFSET = None

    _DEFINITION_FILE = os.path.join(
        os.path.dirname(__file__), "custom_destinations.yaml"
    )

    # We cannot use the parser registry here since winlnk could be disabled.
    # TODO: see if there is a more elegant solution for this.
    _WINLNK_PARSER = winlnk.WinLnkParser()

    _CATEGORY_FOOTER_SIGNATURE = b"\xab\xfb\xbf\xba"

    _LNK_GUID = b"\x01\x14\x02\x00\x00\x00\x00\x00\xc0\x00\x00\x00\x00\x00\x00\x46"

    def _ParseCategoryHeader(self, file_object, file_offset):
        """Parse a category header.

        Args:
          file_object (file): file-like object.
          file_offset (int): offset of the category header relative to the start of
              the file.

        Returns:
          tuple[custom_category_header, int]: category header and the number of
              bytes read.

        Raises:
          ParseError: if the category header cannot be read.
        """
        data_type_map = self._GetDataTypeMap("custom_category_header")

        category_header, bytes_read = self._ReadStructureFromFileObject(
            file_object, file_offset, data_type_map
        )
        if category_header.category_type > 2:
            raise errors.ParseError(
                f"Unsupported category type: {category_header.category_type:d}."
            )

        file_offset += bytes_read
        total_bytes_read = bytes_read

        data_type_map = self._GetDataTypeMap(
            f"custom_category_header_type_{category_header.category_type:d}"
        )
        category_header_value, bytes_read = self._ReadStructureFromFileObject(
            file_object, file_offset, data_type_map
        )
        if category_header.category_type in (0, 2):
            setattr(
                category_header,
                "number_of_entries",
                category_header_value.number_of_entries,
            )

        total_bytes_read += bytes_read

        return category_header, total_bytes_read

    def _ParseLNKFile(
        self, parser_mediator, file_entry, file_offset, remaining_file_size
    ):
        """Parses a LNK file stored within the .customDestinations-ms file.

        Args:
          parser_mediator (ParserMediator): mediates interactions between parsers
              and other components, such as storage and dfVFS.
          file_entry (dfvfs.FileEntry): a file entry.
          file_offset (int): offset to the LNK file, relative to the start of the
              .customDestinations-ms file.
          remaining_file_size (int): size of the data remaining in the
              .customDestinations-ms file.

        Returns:
          int: size of the LNK file data or 0 if the LNK file could not be read.
        """
        path_spec = path_spec_factory.Factory.NewPathSpec(
            definitions.TYPE_INDICATOR_DATA_RANGE,
            range_offset=file_offset,
            range_size=remaining_file_size,
            parent=file_entry.path_spec,
        )
        display_name = f"{file_entry.name:s} # 0x{file_offset:08x}"

        try:
            lnk_file_object = resolver.Resolver.OpenFileObject(
                path_spec, resolver_context=parser_mediator.resolver_context
            )
        except (dfvfs_errors.BackEndError, RuntimeError) as exception:
            parser_mediator.ProduceWarning(
                f"unable to open LNK file: {display_name:s} with error: {exception!s}"
            )
            return 0

        parser_mediator.AppendToParserChain(self._WINLNK_PARSER.NAME)
        try:
            lnk_file_object.seek(0, os.SEEK_SET)
            self._WINLNK_PARSER.ParseFileLNKFile(
                parser_mediator, lnk_file_object, display_name
            )
        finally:
            parser_mediator.PopFromParserChain()

        # We cannot rely on the file size in the LNK data so we get the last offset that
        # was read instead.
        return lnk_file_object.get_offset()

    @classmethod
    def GetFormatSpecification(cls):
        """Retrieves the format specification.

        Returns:
          FormatSpecification: format specification.
        """
        format_specification = specification.FormatSpecification(cls.NAME)
        format_specification.AddNewSignature(b"\xab\xfb\xbf\xba", offset=-4)
        return format_specification

    def ParseFileObject(self, parser_mediator, file_object):
        """Parses a .customDestinations-ms file-like object.

        Args:
          parser_mediator (ParserMediator): mediates interactions between parsers
              and other components, such as storage and dfVFS.
          file_object (dfvfs.FileIO): a file-like object.

        Raises:
          WrongParser: when the file cannot be parsed.
        """
        file_entry = parser_mediator.GetFileEntry()
        display_name = parser_mediator.GetDisplayName()
        file_size = file_object.get_size()

        file_header_map = self._GetDataTypeMap("custom_file_header")

        try:
            file_header, file_offset = self._ReadStructureFromFileObject(
                file_object, 0, file_header_map
            )
        except (ValueError, errors.ParseError) as exception:
            raise errors.WrongParser(
                f"Invalid Custom Destination: {display_name:s} - unable to parse "
                f"file header with error: {exception!s}"
            )

        entry_header_data_type_map = self._GetDataTypeMap("custom_entry_header")
        category_footer_data_type_map = self._GetDataTypeMap("custom_category_footer")

        for _ in range(file_header.number_of_categories):
            try:
                category_header, bytes_read = self._ParseCategoryHeader(
                    file_object, file_offset
                )
            except (ValueError, errors.ParseError) as exception:
                raise errors.WrongParser(
                    f"Invalid Custom Destination: {display_name:s} - unable to parse "
                    f"category header with error: {exception!s}"
                )

            file_offset += bytes_read

            number_of_entries = getattr(category_header, "number_of_entries", 0)
            for entry_index in range(number_of_entries):
                if file_size - file_offset < 16:
                    break

                try:
                    entry_header, _ = self._ReadStructureFromFileObject(
                        file_object, file_offset, entry_header_data_type_map
                    )
                except errors.ParseError as exception:
                    parser_mediator.ProduceWarning(
                        f"unable to parse entry header at offset: 0x{file_offset:08x} "
                        f"with error: {exception!s}"
                    )
                    break

                if entry_header.guid == self._LNK_GUID:
                    file_offset += 16

                    remaining_file_size = file_size - file_offset

                    lnk_file_size = self._ParseLNKFile(
                        parser_mediator, file_entry, file_offset, remaining_file_size
                    )
                    file_offset += lnk_file_size

                elif entry_header.guid[:4] != self._CATEGORY_FOOTER_SIGNATURE:
                    parser_mediator.ProduceWarning(
                        f"unsupported entry: {entry_index:d} at offset: "
                        f"0x{file_offset:08x}"
                    )
                    break

                file_object.seek(file_offset, os.SEEK_SET)

            try:
                _, bytes_read = self._ReadStructureFromFileObject(
                    file_object, file_offset, category_footer_data_type_map
                )
            except (ValueError, errors.ParseError) as exception:
                raise errors.WrongParser(
                    f"Invalid Custom Destination: {display_name:s} - unable to parse "
                    f"category footer with error: {exception!s}"
                )

            file_offset += 4


manager.ParsersManager.RegisterParser(CustomDestinationsParser)
