package storageadapter

import (
	"context"
	"sync"

	"github.com/filecoin-project/go-state-types/abi"

	actorsmarket "github.com/filecoin-project/lotus/chain/actors/builtin/market"
	"github.com/filecoin-project/lotus/chain/events"
	"github.com/filecoin-project/lotus/chain/events/state"
	"github.com/filecoin-project/lotus/chain/types"
)

// dealStateMatcher caches the DealStates for the most recent
// old/new tipset combination
type dealStateMatcher struct {
	preds *state.StatePredicates

	lk               sync.Mutex
	oldTsk           types.TipSetKey
	newTsk           types.TipSetKey
	oldDealStateRoot actorsmarket.DealStates
	newDealStateRoot actorsmarket.DealStates
}

func newDealStateMatcher(preds *state.StatePredicates) *dealStateMatcher {
	return &dealStateMatcher{preds: preds}
}

// matcher returns a function that checks if the state of the given dealID
// has changed.
// It caches the DealStates for the most recent old/new tipset combination.
func (mc *dealStateMatcher) matcher(ctx context.Context, dealID abi.DealID) events.StateMatchFunc {
	// The function that is called to check if the deal state has changed for
	// the target deal ID
	dealStateChangedForID := mc.preds.DealStateChangedForIDs([]abi.DealID{dealID})

	// The match function is called by the events API to check if there's
	// been a state change for the deal with the target deal ID
	match := func(oldTs, newTs *types.TipSet) (bool, events.StateChange, error) {
		mc.lk.Lock()
		defer mc.lk.Unlock()

		// Check if we've already fetched the DealStates for the given tipsets
		if mc.oldTsk == oldTs.Key() && mc.newTsk == newTs.Key() {
			// If we fetch the DealStates and there is no difference between
			// them, they are stored as nil. So we can just bail out.
			if mc.oldDealStateRoot == nil || mc.newDealStateRoot == nil {
				return false, nil, nil
			}

			// Check if the deal state has changed for the target ID
			return dealStateChangedForID(ctx, mc.oldDealStateRoot, mc.newDealStateRoot)
		}

		// We haven't already fetched the DealStates for the given tipsets, so
		// do so now

		// Replace dealStateChangedForID with a function that records the
		// DealStates so that we can cache them
		var oldDealStateRootSaved, newDealStateRootSaved actorsmarket.DealStates
		recorder := func(ctx context.Context, oldDealStateRoot, newDealStateRoot actorsmarket.DealStates) (changed bool, user state.UserData, err error) {
			// Record DealStates
			oldDealStateRootSaved = oldDealStateRoot
			newDealStateRootSaved = newDealStateRoot

			return dealStateChangedForID(ctx, oldDealStateRoot, newDealStateRoot)
		}

		// Call the match function
		dealDiff := mc.preds.OnStorageMarketActorChanged(
			mc.preds.OnDealStateChanged(recorder))
		matched, data, err := dealDiff(ctx, oldTs.Key(), newTs.Key())

		// Save the recorded DealStates for the tipsets
		mc.oldTsk = oldTs.Key()
		mc.newTsk = newTs.Key()
		mc.oldDealStateRoot = oldDealStateRootSaved
		mc.newDealStateRoot = newDealStateRootSaved

		return matched, data, err
	}
	return match
}