# -*- coding: utf-8 -*-
# Wendelin.bigfile | WCFS part of BigFile ZODB backend
# Copyright (C) 2014-2020  Nexedi SA and Contributors.
#                          Kirill Smelkov <kirr@nexedi.com>
#
# This program is free software: you can Use, Study, Modify and Redistribute
# it under the terms of the GNU General Public License version 3, or (at your
# option) any later version, as published by the Free Software Foundation.
#
# You can also Link and Combine this program with other software covered by
# the terms of any of the Free Software licenses or any of the Open Source
# Initiative approved licenses and Convey the resulting work. Corresponding
# source of such a combination shall include the source code for all other
# software used.
#
# This program is distributed WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# See COPYING file for full licensing terms.
# See https://www.nexedi.com/licensing for rationale and options.

# cython: language_level=2
# distutils: language=c++

"""Modules _file_zodb.pyx complements file_zodb.py with things that cannot be
implemented in Python.

It provides wcfs integration for ZBigFile handles opened with _use_wcfs=True.
"""

from __future__ import print_function, absolute_import

cdef extern from "wcfs/client/wcfs.h":
    pass

cdef extern from "bigfile/_bigfile.h":
    struct PyBigFile:
        pass
    ctypedef extern class wendelin.bigfile._bigfile.BigFile[object PyBigFile]:
        pass

# ZBigFile_mmap_ops is virtmem mmap  functions for _ZBigFile.
cdef extern from "<wendelin/bigfile/file.h>" nogil:
    struct bigfile_ops:
        pass
cdef extern from * nogil:
    """
    extern const bigfile_ops ZBigFile_mmap_ops;
    """
    const bigfile_ops ZBigFile_mmap_ops

import wcfs as pywcfs
from wendelin.lib import zodb as pyzodb
from wcfs.client cimport _wcfs as wcfs
from golang cimport error, nil, pyerror
from cpython cimport PyCapsule_New

from ZODB.Connection import Connection as ZConnection
from ZODB.utils import u64
from wendelin.lib.zodb import zconn_at
import weakref


# _ZBigFile is base class for ZBigFile that provides BigFile-line base.
#
# The other base line is from Persistent. It is not possible to inherit from
# both Persistent and BigFile at the same time since both are C types and their
# layouts conflict.
#
# _ZBigFile:
#
# - redirects loadblk/storeblk calls to ZBigFile.
# - provides blkmmapper with WCFS integration.
cdef public class _ZBigFile(BigFile) [object _ZBigFile, type _ZBigFile_Type]:
    cdef object     zself   # reference to ZBigFile
    cdef wcfs.FileH wfileh  # WCFS file handle. Initially nil, opened by blkmmapper

    # _new creates new _ZBigFile associated with ZBigFile zself.
    # XXX Cython does not allow __new__ nor to change arguments passed to __cinit__ / __init__
    @staticmethod
    def _new(zself, blksize):
        cdef _ZBigFile obj = _ZBigFile.__new__(_ZBigFile, blksize)
        obj.zself  = zself
        obj.wfileh = nil
        return obj

    def __dealloc__(_ZBigFile zf):
        cdef error err = nil
        if zf.wfileh != nil:
            err = zf.wfileh.close()
        zf.wfileh = nil
        if err != nil:
            raise pyerror.from_error(err)


    # redirect load/store to main class
    def loadblk(self, blk, buf):    return self.zself.loadblk(blk, buf)
    def storeblk(self, blk, buf):   return self.zself.storeblk(blk, buf)


    # blkmmapper complemnts loadblk/storeblk and is pycapsule with virtmem mmap
    # functions for _ZBigFile. MMap functions rely on .wfileh being initialized
    # by .fileh_open()
    blkmmapper = PyCapsule_New(<void*>&ZBigFile_mmap_ops, "wendelin.bigfile.IBlkMMapper", NULL)

    # fileh_open wraps BigFile.fileh_open and makes sure that WCFS file handle
    # corresponding to ZBigFile is opened if use_wcfs=True.
    def fileh_open(_ZBigFile zf, bint use_wcfs):
        mmap_overlay = False
        cdef wcfs.PyFileH pywfileh

        if use_wcfs:
            mmap_overlay = True
            if zf.wfileh == nil:
                zconn = zf.zself._p_jar
                assert zconn is not None

                # join zconn to wconn; link to wconn from _ZBigFile
                pywconn   = pywconnOf(zconn)
                pywfileh  = pywconn.open(zf.zself._p_oid)
                zf.wfileh = pywfileh.wfileh

        return super(_ZBigFile, zf).fileh_open(mmap_overlay)


# pywconnOf establishes and returns (py) wcfs.Conn associated with zconn.
#
# returned wcfs.Conn will be maintained to keep in sync with zconn, and will be
# closed when zconn is destroyed.
#
# It is invalid to make multiple simultaneous calls to pywconnOf with the same zconn.
# (in ZODB/py objects for zconn must be used from under 1 thread only).
cdef wcfs.PyConn pywconnOf(zconn):
    assert isinstance(zconn, ZConnection)
    assert zconn.opened

    wconn = getattr(zconn, '_wcfs_wconn', None)
    if wconn is not None:
        return wconn

    # zconn is not yet associated with wconn
    zstor = zconn.db().storage
    zurl  = pyzodb.zstor_2zurl(zstor)
    wc    = pywcfs.join(zurl)
    wconn = wc.connect(zconn_at(zconn))

    zconn._wcfs_wconn = wconn

    # keep wconn view of the database in sync with zconn
    zconn._wcfs_wconn_zsync = ZSync(zconn, wconn)

    return wconn


# ZSync keeps wconn in sync with zconn.
#
# wconn will be closed once zconn is destroyed (not closed, which returns it
# back into DB pool).
class ZSync:
    # .zconn_ref    weakref[zodb.Connection]
    # .wconn        (py) wcfs.Connection

    def __init__(zsync, zconn, wconn):
        assert zconn.opened
        zsync.wconn     = wconn
        zsync.zconn_ref = weakref.ref(zconn, zsync.on_zconn_dealloc)

        # NOTE zconn.onOpenCallback is not enough: zconn.at can change even
        # without zconn.close/zconn.open, e.g.:
        # zconn = DB.open(transaction_manager=tm)
        # tm.commit()   # zconn.at updated          (zconn.afterCompletion -> zconn.newTransaction)
        # tm.commit()   # zconn.at updated again
        zconn.onResyncCallback(zsync)

    # .zconn dealloc -> wconn.close
    def on_zconn_dealloc(zsync, _):
        zsync.wconn.close()

    # DB resyncs .zconn onto new database view.
    # -> resync .wconn to updated database view of ZODB connection.
    def on_connection_resync(zsync):
        zconn = zsync.zconn_ref()
        zsync.wconn.resync(zconn_at(zconn))
