# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4; coding:utf-8 -*-
#
# Copyright 2022 Kenneth Loafman
#
# This file is part of duplicity.
#
# Duplicity is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or (at your
# option) any later version.
#
# Duplicity is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with duplicity; if not, write to the Free Software Foundation,
# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

"""
ctypes-based interface to librsync.
"""

import ctypes
import ctypes.util
import os

# Load librsync
_librsync_path = ctypes.util.find_library("rsync")
if not _librsync_path:
    # Try common locations if find_library fails
    for path in ["librsync.so", "librsync.dylib", "librsync.so.2", "librsync.so.1"]:
        try:
            ctypes.CDLL(path)
            _librsync_path = path
            break
        except OSError:
            continue

if not _librsync_path:
    raise ImportError("librsync library not found")

_librsync = ctypes.CDLL(_librsync_path)

# Constants
RS_JOB_BLOCKSIZE = 65536
RS_DEFAULT_BLOCK_LEN = 2048

# Result codes
RS_DONE = 0
RS_BLOCKED = 1

# Magic numbers
RS_MD4_SIG_MAGIC = 0x72730136
RS_BLAKE2_SIG_MAGIC = 0x72730137


class librsyncError(Exception):
    """
    Signifies an error in internal librsync processing.
    """

    pass


# Structures
class rs_buffers_t(ctypes.Structure):
    """
    ctypes structure for rs_buffers_t.
    """

    _fields_ = [
        ("next_in", ctypes.c_char_p),
        ("avail_in", ctypes.c_size_t),
        ("eof_in", ctypes.c_int),
        ("next_out", ctypes.c_char_p),
        ("avail_out", ctypes.c_size_t),
    ]


# Function prototypes
_librsync.rs_sig_begin.restype = ctypes.c_void_p
_librsync.rs_sig_begin.argtypes = [ctypes.c_size_t, ctypes.c_size_t, ctypes.c_int]

_librsync.rs_loadsig_begin.restype = ctypes.c_void_p
_librsync.rs_loadsig_begin.argtypes = [ctypes.POINTER(ctypes.c_void_p)]

_librsync.rs_delta_begin.restype = ctypes.c_void_p
_librsync.rs_delta_begin.argtypes = [ctypes.c_void_p]

_librsync.rs_patch_begin.restype = ctypes.c_void_p
# rs_copy_cb(void *opaque, rs_long_t pos, size_t *len, void **buf)
RS_COPY_CB = ctypes.CFUNCTYPE(
    ctypes.c_int, ctypes.c_void_p, ctypes.c_longlong, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(ctypes.c_void_p)
)
_librsync.rs_patch_begin.argtypes = [RS_COPY_CB, ctypes.c_void_p]

_librsync.rs_job_iter.restype = ctypes.c_int
_librsync.rs_job_iter.argtypes = [ctypes.c_void_p, ctypes.POINTER(rs_buffers_t)]

_librsync.rs_job_free.restype = ctypes.c_int
_librsync.rs_job_free.argtypes = [ctypes.c_void_p]

_librsync.rs_build_hash_table.restype = ctypes.c_int
_librsync.rs_build_hash_table.argtypes = [ctypes.c_void_p]

_librsync.rs_free_sumset.restype = None
_librsync.rs_free_sumset.argtypes = [ctypes.c_void_p]

_librsync.rs_strerror.restype = ctypes.c_char_p
_librsync.rs_strerror.argtypes = [ctypes.c_int]


def _set_error(result, location):
    err_msg = _librsync.rs_strerror(result).decode("utf-8")
    raise librsyncError(f"librsync error {result} ({err_msg}) while in {location}")


class SigMaker:
    """
    Class to generate signatures using librsync.
    """

    def __init__(self, blocklen):
        """
        Initialize the SigMaker.

        :param int blocklen: The block length for the signature.
        """
        # Using 8 for strong_len and RS_MD4_SIG_MAGIC as in _librsyncmodule.c
        self.job = _librsync.rs_sig_begin(blocklen, 8, RS_MD4_SIG_MAGIC)
        if not self.job:
            raise librsyncError("Failed to create sig_job")

    def cycle(self, inbuf):
        """
        Process a chunk of input data for signature generation.

        :param bytes inbuf: The input data to process.
        :returns: A tuple of (done, bytes_read, cycle_out).
        :rtype: tuple
        """
        buf = rs_buffers_t()
        buf.next_in = ctypes.c_char_p(inbuf)
        buf.avail_in = len(inbuf)
        buf.eof_in = len(inbuf) == 0

        outbuf = ctypes.create_string_buffer(RS_JOB_BLOCKSIZE)
        buf.next_out = ctypes.cast(outbuf, ctypes.c_char_p)
        buf.avail_out = RS_JOB_BLOCKSIZE

        result = _librsync.rs_job_iter(self.job, ctypes.byref(buf))

        if result not in [RS_DONE, RS_BLOCKED]:
            _set_error(result, "sig cycle")

        return (result == RS_DONE), len(inbuf) - buf.avail_in, outbuf.raw[: RS_JOB_BLOCKSIZE - buf.avail_out]

    def __del__(self):
        if hasattr(self, "job") and self.job:
            _librsync.rs_job_free(self.job)


class DeltaMaker:
    """
    Class to generate deltas using librsync.
    """

    def __init__(self, sig_string):
        """
        Initialize the DeltaMaker.

        :param bytes sig_string: The signature to use for delta generation.
        """
        self.sig_ptr = ctypes.c_void_p()
        sig_loader = _librsync.rs_loadsig_begin(ctypes.byref(self.sig_ptr))

        buf = rs_buffers_t()
        buf.next_in = ctypes.c_char_p(sig_string)
        buf.avail_in = len(sig_string)
        buf.eof_in = 1

        outbuf = ctypes.create_string_buffer(RS_JOB_BLOCKSIZE)
        buf.next_out = ctypes.cast(outbuf, ctypes.c_char_p)
        buf.avail_out = RS_JOB_BLOCKSIZE

        result = _librsync.rs_job_iter(sig_loader, ctypes.byref(buf))
        _librsync.rs_job_free(sig_loader)

        if result != RS_DONE:
            _set_error(result, "delta rs_signature_t builder")

        result = _librsync.rs_build_hash_table(self.sig_ptr)
        if result != RS_DONE:
            _set_error(result, "delta rs_build_hash_table")

        self.job = _librsync.rs_delta_begin(self.sig_ptr)
        if not self.job:
            raise librsyncError("Failed to create delta_job")

    def cycle(self, inbuf):
        """
        Process a chunk of input data for delta generation.

        :param bytes inbuf: The input data to process.
        :returns: A tuple of (done, bytes_read, cycle_out).
        :rtype: tuple
        """
        buf = rs_buffers_t()
        buf.next_in = ctypes.c_char_p(inbuf)
        buf.avail_in = len(inbuf)
        buf.eof_in = len(inbuf) == 0

        outbuf = ctypes.create_string_buffer(RS_JOB_BLOCKSIZE)
        buf.next_out = ctypes.cast(outbuf, ctypes.c_char_p)
        buf.avail_out = RS_JOB_BLOCKSIZE

        result = _librsync.rs_job_iter(self.job, ctypes.byref(buf))

        if result not in [RS_DONE, RS_BLOCKED]:
            _set_error(result, "delta cycle")

        return (result == RS_DONE), len(inbuf) - buf.avail_in, outbuf.raw[: RS_JOB_BLOCKSIZE - buf.avail_out]

    def __del__(self):
        if hasattr(self, "job") and self.job:
            _librsync.rs_job_free(self.job)
        if hasattr(self, "sig_ptr") and self.sig_ptr:
            _librsync.rs_free_sumset(self.sig_ptr)


class PatchMaker:
    """
    Class to apply patches using librsync.
    """

    def __init__(self, basis_file):
        """
        Initialize the PatchMaker.

        :param basis_file: The original file to patch.
        """
        self.basis_file = basis_file
        # We need to keep a reference to the callback to prevent it from being GC'd
        self._callback = RS_COPY_CB(self._copy_cb)
        self.job = _librsync.rs_patch_begin(self._callback, None)
        self._internal_buf = None

    def _copy_cb(self, opaque, pos, length_ptr, buf_ptr):
        try:
            self.basis_file.seek(pos)
            data = self.basis_file.read(length_ptr[0])
            # We must provide a buffer that stays alive until librsync is done with it.
            # rs_job_iter for patch will call this and then consume the buffer.
            self._internal_buf = ctypes.create_string_buffer(data)
            buf_ptr[0] = ctypes.cast(self._internal_buf, ctypes.c_void_p)
            length_ptr[0] = len(data)
            return RS_DONE
        except Exception:
            return -1  # RS_IO_ERROR or similar

    def cycle(self, inbuf):
        """
        Process a chunk of input data for patching.

        :param bytes inbuf: The input data to process.
        :returns: A tuple of (done, bytes_read, cycle_out).
        :rtype: tuple
        """
        buf = rs_buffers_t()
        buf.next_in = ctypes.c_char_p(inbuf)
        buf.avail_in = len(inbuf)
        buf.eof_in = len(inbuf) == 0

        outbuf = ctypes.create_string_buffer(RS_JOB_BLOCKSIZE)
        buf.next_out = ctypes.cast(outbuf, ctypes.c_char_p)
        buf.avail_out = RS_JOB_BLOCKSIZE

        result = _librsync.rs_job_iter(self.job, ctypes.byref(buf))

        if result not in [RS_DONE, RS_BLOCKED]:
            _set_error(result, "patch cycle")

        return (result == RS_DONE), len(inbuf) - buf.avail_in, outbuf.raw[: RS_JOB_BLOCKSIZE - buf.avail_out]

    def __del__(self):
        if hasattr(self, "job") and self.job:
            _librsync.rs_job_free(self.job)


def new_sigmaker(blocklen):
    """
    Create a new SigMaker instance.

    :param int blocklen: The block length.
    :returns: A new SigMaker instance.
    :rtype: SigMaker
    """
    return SigMaker(blocklen)


def new_deltamaker(sig_string):
    """
    Create a new DeltaMaker instance.

    :param bytes sig_string: The signature string.
    :returns: A new DeltaMaker instance.
    :rtype: DeltaMaker
    """
    return DeltaMaker(sig_string)


def new_patchmaker(basis_file):
    """
    Create a new PatchMaker instance.

    :param basis_file: The original file to patch.
    :returns: A new PatchMaker instance.
    :rtype: PatchMaker
    """
    return PatchMaker(basis_file)


def get_version():
    """
    Return the version string of the loaded librsync library.

    :returns: The version string.
    :rtype: str
    """
    return ctypes.c_char_p(ctypes.addressof(ctypes.c_char.in_dll(_librsync, "rs_librsync_version"))).value.decode(
        "utf-8"
    )
