// Copyright (C) 2021  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.

// Package demo provides overlayed storage, similar to DemoStorage in ZODB/py.
//
// Storage combines base and δ storages as if δ transactional log is logically
// appended to base.
package demo

import (
	"context"
	"errors"
	"fmt"
	"io"
	"net/url"
	"regexp"
	"sync"

	"lab.nexedi.com/kirr/go123/mem"
	"lab.nexedi.com/kirr/go123/xerr"
	"lab.nexedi.com/kirr/go123/xsync"
	"lab.nexedi.com/kirr/neo/go/internal/task"
	"lab.nexedi.com/kirr/neo/go/zodb"
)

// Storage combines base and δ storages as if δ transactional log is logically
// appended to base.
//
// Base must remain unmodified, and all δ transactions must be with TIDs
// strictly after base.Head .
type Storage struct {
	base zodb.IStorageDriver
	δ    zodb.IStorageDriver

	baseAt0    zodb.Tid
	baseWatchq <-chan zodb.Event
	δWatchq    <-chan zodb.Event // nil if demo is opened with watchq=nil
	δWatchq0   <-chan zodb.Event // buffer for δ events queued while δ was initially verified
	watchq     chan<- zodb.Event // user requested to deliver events here

	watchWG     *xsync.WorkGroup
	watchCancel func()

	downOnce sync.Once
	down     chan struct{} // ready when storage is down
	downErr  error         // reason for shutdown

	closeOnce sync.Once
	closed    chan struct{} // ready when storage is Closed
}

// baseMutatedError is reported when Storage.base is detected to change.
type baseMutatedError struct {
	baseAt0  zodb.Tid
	baseHead zodb.Tid
}

func (e *baseMutatedError) Error() string {
	return fmt.Sprintf("base.head mutated from @%s to @%s", e.baseAt0, e.baseHead)
}

// baseErrorEvent is reported on error event detected on Storage.base .
type baseErrorEvent struct {
	err error
}

func (e *baseErrorEvent) Error() string {
	return fmt.Sprintf("base: error event: %s", e.err)
}

func (e *baseErrorEvent) Cause() error  { return e.err }
func (e *baseErrorEvent) Unwrap() error { return e.err }


// watcher task detects base mutation and proxies δ events to user watchq.
func (d *Storage) watcher(ctx context.Context) error {
	if d.watchq != nil {
		defer close(d.watchq)
	}

	for {
		var δWatchq <-chan zodb.Event
		if len(d.δWatchq0) != 0 {
			δWatchq = d.δWatchq0
		} else {
			δWatchq = d.δWatchq
		}

		select {
		// Close requests to stop watching
		case <-ctx.Done():
			return ctx.Err()

		// event on base -> base mutated/error + shutdown
		case event, ok := <-d.baseWatchq:
			if !ok {
				// base closed
				d.baseWatchq = nil
				continue
			}

			var edown error
			switch event := event.(type) {
			case *zodb.EventCommit:
				edown = &baseMutatedError{d.baseAt0, event.Tid}

			case *zodb.EventError:
				edown = &baseErrorEvent{event.Err}

			default:
				edown = fmt.Errorf("base: unexpected event %T", event)
			}

			d.shutdown(edown)
			ev := &zodb.EventError{&zodb.OpError{URL: d.URL(), Op: "watcher", Err: edown}}
			if d.watchq != nil {
				select {
				case <-d.closed:
					// wakeup to return edown

				case d.watchq <- ev:
					// ok
				}
			}
			return edown

		// event on δ -> proxy to user
		case event, ok := <-δWatchq:
			if !ok {
				// δ closed
				d.δWatchq = nil
				continue
			}

			select {
			case <-d.closed:
				return nil

			case d.watchq <- event: // !nil because d.δWatchq != nil
				// ok
			}
		}
	}
}

// shutdown marks Storage as no longer operational due to reason.
func (d *Storage) shutdown(reason error) {
	d.downOnce.Do(func() {
		d.downErr = reason
		close(d.down)
	})
}

var errClosed = errors.New("storage is closed")

// Close implements zodb.IStorageDriver .
func (d *Storage) Close() (err error) {
	defer func() {
		if err != nil {
			err = &zodb.OpError{URL: d.URL(), Op: "close", Err: err}
		}
	}()

	d.shutdown(errClosed)
	d.closeOnce.Do(func() {
		close(d.closed)
	})
	errδ    := d.δ.Close()
	errBase := d.base.Close()

	// cancel watcher; don't propagate its error to Close - watcher error
	// goes to watchq and op errors.
	d.watchCancel()
	_ = d.watchWG.Wait()

	return xerr.Merge(errδ, errBase)
}

// Sync implements zodb.IStorageDriver .
func (d *Storage) Sync(ctx context.Context) (_ zodb.Tid, err error) {
	defer func() {
		if err != nil {
			err = &zodb.OpError{URL: d.URL(), Op: "sync", Err: err}
		}
	}()
	if ready(d.down) {
		return zodb.InvalidTid, d.downErr
	}

	var head zodb.Tid
	wg := xsync.NewWorkGroup(ctx)
	wg.Go(func(ctx context.Context) error {
		h, err := d.δ.Sync(ctx)
		head = h
		return err
	})
	wg.Go(func(ctx context.Context) error {
		baseHead, err := d.base.Sync(ctx)
		if err != nil {
			return err
		}
		if baseHead != d.baseAt0 {
			return &baseMutatedError{d.baseAt0, baseHead}
		}
		return nil
	})

	err = wg.Wait()
	if err != nil {
		return zodb.InvalidTid, err
	}

	// δ is just created database
	if head == 0 {
		head = d.baseAt0
	}

	return head, nil
}

// Load implements zodb.IStorageDriver .
func (d *Storage) Load(ctx context.Context, xid zodb.Xid) (_ *mem.Buf, _ zodb.Tid, err error) {
	defer func() {
		if err != nil {
			err = &zodb.OpError{URL: d.URL(), Op: "load", Args: xid, Err: err}
		}
	}()
	if ready(d.down) {
		return nil, zodb.InvalidTid, d.downErr
	}

	var eNoData   *zodb.NoDataError
	var eNoObject *zodb.NoObjectError

	inδ := false
	if xid.At > d.baseAt0 {
		data, serial, err := d.δ.Load(ctx, xid)
		if err == nil {
			// object data is present in δ
			return data, serial, nil
		}

		useBase := false
		switch {
		case errors.As(err, &eNoData):
			if eNoData.DeletedAt != 0 {
				// object deleted in δ -> whiteout
				return data, serial, eNoData
			} else {
				// object present in δ but not yet created as of xid.at
				useBase = true
				inδ = true
			}

		case errors.As(err, &eNoObject):
			// object not created in δ
			useBase = true
		}

		if !useBase {
			return data, serial, err
		}
	}

	// cap xid.At to .baseAt0 (we convert it back on error return, and
	// it makes Load more robust wrt simultaneous base mutation).
	xidBase := xid
	if xid.At > d.baseAt0 {
		xidBase.At = d.baseAt0
	}

	data, serial, err := d.base.Load(ctx, xidBase)
	if err == nil {
		return data, serial, nil
	}

	switch {
	case errors.As(err, &eNoData):
		err = eNoData
	case errors.As(err, &eNoObject):
		if !inδ {
			err = eNoObject
		} else {
			// object is present in δ
			err = &zodb.NoDataError{Oid: xid.Oid, DeletedAt: 0}
		}
	}

	return data, serial, err
}

// Iterator implements zodb.IStorageDriver .
func (d *Storage) Iterate(ctx context.Context, tidMin, tidMax zodb.Tid) zodb.ITxnIterator {
	panic("TODO")
}

// URL implements zodb.IStorageDriver .
func (d *Storage) URL() string {
	return "demo:(" + d.base.URL() + ")/(" + d.δ.URL() + ")"
}

var demoRe = regexp.MustCompile(`^\((.*)\)/\((.*)\)$`)

func openByURL(ctx context.Context, u *url.URL, opt *zodb.DriverOptions) (_ zodb.IStorageDriver, _ zodb.Tid, err error) {
	// demo:(base_zurl)/(δ_zurl)
	defer task.Runningf(&ctx, "demo: open %s", u)(&err)

	if !(u.Opaque != "" && u.RawQuery == "" && u.Fragment == "") {
		// opaque != "" makes sure user,host,path are empty
		return nil, zodb.InvalidTid, fmt.Errorf("invalid url")
	}

	argv := demoRe.FindStringSubmatch(u.Opaque)
	if argv == nil {
		return nil, zodb.InvalidTid, fmt.Errorf("invalid url")
	}

	baseZURL := argv[1]
	δZURL    := argv[2]

	// open base - always readonly
	baseWatchq := make(chan zodb.Event)
	base, baseAt0, err := zodb.OpenDriver(ctx, baseZURL, &zodb.DriverOptions{
		ReadOnly: true,
		Watchq:   baseWatchq,
	})
	if err != nil {
		return nil, zodb.InvalidTid, err
	}
	defer func() {
		if err != nil {
			__ := base.Close()
			err = xerr.Merge(err, __)
		}
	}()

	// open δ    - as requested  but with events going through us
	δopt := *opt
	var δWatchq chan zodb.Event
	if δopt.Watchq != nil {
		δWatchq = make(chan zodb.Event)
		δopt.Watchq = δWatchq
	}
	δ, δAt0, err := zodb.OpenDriver(ctx, δZURL, &δopt)
	if err != nil {
		return nil, zodb.InvalidTid, err
	}
	defer func() {
		if err != nil {
			__ := δ.Close()
			err = xerr.Merge(err, __)
		}
	}()


	// verify that either
	// - δ is all empty (just created), or
	// - all δ transactions come strictly after base.Head .
	at0 := baseAt0
	var δEventq0 []zodb.Event
	if δAt0 != 0 {
		if δAt0 < baseAt0 {
			return nil, zodb.InvalidTid, fmt.Errorf("base is ahead of δ: base.head=%s  δ.head=%s", baseAt0, δAt0)
		}

		// read and queue data from δWatchq while we verify δ (not to deadlock δ driver)
		δq0Stop := make(chan struct{}) // reader <- main  "stop"
		δq0Done := make(chan struct{}) // reader -> main  "done"
		go func() {
			defer close(δq0Done)

			for {
				select {
				case <-δq0Stop:
					return

				case δevent, ok := <-δWatchq:
					if !ok {
						return
					}
					δEventq0 = append(δEventq0, δevent)
				}
			}
		}()

		// verify δ
		iδ := δ.Iterate(ctx, 0, baseAt0)
		δtxni, _, err := iδ.NextTxn(ctx)
		switch {
		case err == io.EOF:
			err = nil // ok - nothing in δ

		case err == nil:
			// there is a δ transaction ∈ [δAt0, baseAt0)
			err = fmt.Errorf("base overlaps with δ: base.head=%s  δ.tail=%s", baseAt0, δtxni.Tid)
		}
		// TODO iδ.Close()

		close(δq0Stop)
		<-δq0Done
		if err != nil {
			return nil, zodb.InvalidTid, err
		}

		at0 = δAt0
	}

	// requeue δWatchq0 <- δEventq0
	var δWatchq0 chan zodb.Event
	if l := len(δEventq0); l != 0 {
		δWatchq0 = make(chan zodb.Event, l)
		for _, ev := range δEventq0 {
			δWatchq0 <- ev
		}
	}

	d := &Storage{
		base: base,
		δ   : δ,

		baseAt0:    baseAt0,
		baseWatchq: baseWatchq,
		δWatchq:    δWatchq,
		δWatchq0:   δWatchq0,
		watchq:     opt.Watchq,

		down:   make(chan struct{}),
		closed: make(chan struct{}),
	}

	// spawn watcher to listen on baseWatchq and shutdown storage if base changes.
	ctx, cancel := context.WithCancel(context.Background())
	d.watchWG = xsync.NewWorkGroup(ctx)
	d.watchCancel = cancel
	d.watchWG.Go(d.watcher)

	return d, at0, nil
}

func init() {
	zodb.RegisterDriver("demo", openByURL)
}


// ---- misc ----

// ready returns whether c is ready.
func ready(c <-chan struct{}) bool {
	select {
	case <-c:
		return true
	default:
		return false
	}
}
