// Copyright (C) The Arvados Authors. All rights reserved.
//
// SPDX-License-Identifier: Apache-2.0

package arvados

import (
	"os"
	"time"
)

// lookupnode is a caching tree node that is initially empty and calls
// loadOne and loadAll to load/update child nodes as needed.
//
// See (*customFileSystem)MountUsers for example usage.
type lookupnode struct {
	treenode
	loadOne func(parent inode, name string) (inode, error)
	loadAll func(parent inode) ([]inode, error)
	stale   func(time.Time) bool

	// internal fields
	staleAll time.Time
	staleOne map[string]time.Time
}

// Sync flushes pending writes for loaded children and, if successful,
// triggers a reload on next lookup.
func (ln *lookupnode) Sync() error {
	err := ln.treenode.Sync()
	if err != nil {
		return err
	}
	ln.Lock()
	ln.staleAll = time.Time{}
	ln.staleOne = nil
	ln.Unlock()
	return nil
}

func (ln *lookupnode) Readdir() ([]os.FileInfo, error) {
	ln.Lock()
	checkTime := time.Now()
	if ln.stale(ln.staleAll) {
		all, err := ln.loadAll(ln)
		if err != nil {
			ln.Unlock()
			return nil, err
		}
		for _, child := range all {
			var name string
			if hl, ok := child.(*hardlink); ok && hl.inode == ln {
				// If child is a hardlink to its
				// parent, FileInfo()->RLock() will
				// deadlock, because we already have
				// the write lock. In this situation
				// we can safely access the hardlink's
				// name directly.
				name = hl.name
			} else {
				name = child.FileInfo().Name()
			}
			_, err = ln.treenode.Child(name, func(inode) (inode, error) {
				return child, nil
			})
			if err != nil {
				ln.Unlock()
				return nil, err
			}
		}
		ln.staleAll = checkTime
		// No value in ln.staleOne can make a difference to an
		// "entry is stale?" test now, because no value is
		// newer than ln.staleAll. Reclaim memory.
		ln.staleOne = nil
	}
	ln.Unlock()
	return ln.treenode.Readdir()
}

// Child rejects (with ErrInvalidOperation) calls to add/replace
// children, instead calling loadOne when a non-existing child is
// looked up.
func (ln *lookupnode) Child(name string, replace func(inode) (inode, error)) (inode, error) {
	checkTime := time.Now()
	var existing inode
	var err error
	if ln.stale(ln.staleAll) && ln.stale(ln.staleOne[name]) {
		existing, err = ln.treenode.Child(name, func(inode) (inode, error) {
			return ln.loadOne(ln, name)
		})
		if err == nil && existing != nil {
			if ln.staleOne == nil {
				ln.staleOne = map[string]time.Time{name: checkTime}
			} else {
				ln.staleOne[name] = checkTime
			}
		}
	} else {
		existing, err = ln.treenode.Child(name, nil)
		if err != nil && !os.IsNotExist(err) {
			return existing, err
		}
	}
	if replace != nil {
		// Let the callback try to delete or replace the
		// existing node; if it does, return
		// ErrInvalidOperation.
		if tryRepl, err := replace(existing); err != nil {
			// Propagate error from callback
			return existing, err
		} else if tryRepl != existing {
			return existing, ErrInvalidOperation
		}
	}
	// Return original error from ln.treenode.Child() (it might be
	// ErrNotExist).
	return existing, err
}
