// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package modindex contains code for building and searching an
// [Index] of the Go module cache.
package modindex

// The directory containing the index, returned by
// [IndexDir], contains a file index-name-<ver> that contains the name
// of the current index. We believe writing that short file is atomic.
// [Read] reads that file to get the file name of the index.
// WriteIndex writes an index with a unique name and then
// writes that name into a new version of index-name-<ver>.
// (<ver> stands for the CurrentVersion of the index format.)

import (
	"maps"
	"os"
	"path/filepath"
	"slices"
	"strings"
	"time"

	"golang.org/x/mod/semver"
)

// Update updates the index for the specified Go
// module cache directory, creating it as needed.
// On success it returns the current index.
func Update(gomodcache string) (*Index, error) {
	prev, err := Read(gomodcache)
	if err != nil {
		if !os.IsNotExist(err) {
			return nil, err
		}
		prev = nil
	}
	return update(gomodcache, prev)
}

// update builds, writes, and returns the current index.
//
// If old is nil, the new index is built from all of GOMODCACHE;
// otherwise it is built from the old index plus cache updates
// since the previous index's time.
func update(gomodcache string, old *Index) (*Index, error) {
	gomodcache, err := filepath.Abs(gomodcache)
	if err != nil {
		return nil, err
	}
	new, changed, err := build(gomodcache, old)
	if err != nil {
		return nil, err
	}
	if old == nil || changed {
		if err := write(gomodcache, new); err != nil {
			return nil, err
		}
	}
	return new, nil
}

// build returns a new index for the specified Go module cache (an
// absolute path).
//
// If an old index is provided, only directories more recent than it
// that it are scanned; older directories are provided by the old
// Index.
//
// The boolean result indicates whether new entries were found.
func build(gomodcache string, old *Index) (*Index, bool, error) {
	// Set the time window.
	var start time.Time // = dawn of time
	if old != nil {
		// -1s to accommodate skew between (fine-grained) time.Now
		// and (coarse) file system's mtime clock.
		start = old.ValidAt.Add(-1 * time.Second)
	}
	now := time.Now()
	end := now.Add(24 * time.Hour) // safely in the future

	// Enumerate GOMODCACHE package directories.
	// Choose the best (latest) package for each import path.
	pkgDirs := findDirs(gomodcache, start, end)
	dirByPath, err := bestDirByImportPath(pkgDirs)
	if err != nil {
		return nil, false, err
	}

	// For each import path it might occur only in
	// dirByPath, only in old, or in both.
	// If both, use the semantically later one.
	var entries []Entry
	if old != nil {
		for _, entry := range old.Entries {
			dir, ok := dirByPath[entry.ImportPath]
			if !ok || semver.Compare(dir.version, entry.Version) <= 0 {
				// New dir is missing or not more recent; use old entry.
				entries = append(entries, entry)
				delete(dirByPath, entry.ImportPath)
			}
		}
	}

	// Extract symbol information for all the new directories.
	newEntries := extractSymbols(gomodcache, maps.Values(dirByPath))
	entries = append(entries, newEntries...)
	slices.SortFunc(entries, func(x, y Entry) int {
		if n := strings.Compare(x.PkgName, y.PkgName); n != 0 {
			return n
		}
		return strings.Compare(x.ImportPath, y.ImportPath)
	})

	return &Index{
		GOMODCACHE: gomodcache,
		ValidAt:    now, // time before the directories were scanned
		Entries:    entries,
	}, len(newEntries) > 0, nil
}
