// Copyright 2013 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 godoc

import (
	"errors"
	pathpkg "path"
	"time"

	"golang.org/x/tools/godoc/analysis"
	"golang.org/x/tools/godoc/util"
	"golang.org/x/tools/godoc/vfs"
)

// A Corpus holds all the state related to serving and indexing a
// collection of Go code.
//
// Construct a new Corpus with NewCorpus, then modify options,
// then call its Init method.
type Corpus struct {
	fs vfs.FileSystem

	// Verbose logging.
	Verbose bool

	// IndexEnabled controls whether indexing is enabled.
	IndexEnabled bool

	// IndexFiles specifies a glob pattern specifying index files.
	// If not empty, the index is read from these files in sorted
	// order.
	IndexFiles string

	// IndexThrottle specifies the indexing throttle value
	// between 0.0 and 1.0. At 0.0, the indexer always sleeps.
	// At 1.0, the indexer never sleeps. Because 0.0 is useless
	// and redundant with setting IndexEnabled to false, the
	// zero value for IndexThrottle means 0.9.
	IndexThrottle float64

	// IndexInterval specifies the time to sleep between reindexing
	// all the sources.
	// If zero, a default is used. If negative, the index is only
	// built once.
	IndexInterval time.Duration

	// IndexDocs enables indexing of Go documentation.
	// This will produce search results for exported types, functions,
	// methods, variables, and constants, and will link to the godoc
	// documentation for those identifiers.
	IndexDocs bool

	// IndexGoCode enables indexing of Go source code.
	// This will produce search results for internal and external identifiers
	// and will link to both declarations and uses of those identifiers in
	// source code.
	IndexGoCode bool

	// IndexFullText enables full-text indexing.
	// This will provide search results for any matching text in any file that
	// is indexed, including non-Go files (see whitelisted in index.go).
	// Regexp searching is supported via full-text indexing.
	IndexFullText bool

	// MaxResults optionally specifies the maximum results for indexing.
	MaxResults int

	// SummarizePackage optionally specifies a function to
	// summarize a package. It exists as an optimization to
	// avoid reading files to parse package comments.
	//
	// If SummarizePackage returns false for ok, the caller
	// ignores all return values and parses the files in the package
	// as if SummarizePackage were nil.
	//
	// If showList is false, the package is hidden from the
	// package listing.
	SummarizePackage func(pkg string) (summary string, showList, ok bool)

	// IndexDirectory optionally specifies a function to determine
	// whether the provided directory should be indexed.  The dir
	// will be of the form "/src/cmd/6a", "/doc/play",
	// "/src/io", etc.
	// If nil, all directories are indexed if indexing is enabled.
	IndexDirectory func(dir string) bool

	testDir string // TODO(bradfitz,adg): migrate old godoc flag? looks unused.

	// Send a value on this channel to trigger a metadata refresh.
	// It is buffered so that if a signal is not lost if sent
	// during a refresh.
	refreshMetadataSignal chan bool

	// file system information
	fsTree      util.RWValue // *Directory tree of packages, updated with each sync (but sync code is removed now)
	fsModified  util.RWValue // timestamp of last call to invalidateIndex
	docMetadata util.RWValue // mapping from paths to *Metadata

	// SearchIndex is the search index in use.
	searchIndex util.RWValue

	// Analysis is the result of type and pointer analysis.
	Analysis analysis.Result
}

// NewCorpus returns a new Corpus from a filesystem.
// The returned corpus has all indexing enabled and MaxResults set to 1000.
// Change or set any options on Corpus before calling the Corpus.Init method.
func NewCorpus(fs vfs.FileSystem) *Corpus {
	c := &Corpus{
		fs: fs,
		refreshMetadataSignal: make(chan bool, 1),

		MaxResults:    1000,
		IndexEnabled:  true,
		IndexDocs:     true,
		IndexGoCode:   true,
		IndexFullText: true,
	}
	return c
}

func (c *Corpus) CurrentIndex() (*Index, time.Time) {
	v, t := c.searchIndex.Get()
	idx, _ := v.(*Index)
	return idx, t
}

func (c *Corpus) FSModifiedTime() time.Time {
	_, ts := c.fsModified.Get()
	return ts
}

// Init initializes Corpus, once options on Corpus are set.
// It must be called before any subsequent method calls.
func (c *Corpus) Init() error {
	// TODO(bradfitz): do this in a goroutine because newDirectory might block for a long time?
	// It used to be sometimes done in a goroutine before, at least in HTTP server mode.
	if err := c.initFSTree(); err != nil {
		return err
	}
	c.updateMetadata()
	go c.refreshMetadataLoop()
	return nil
}

func (c *Corpus) initFSTree() error {
	dir := c.newDirectory(pathpkg.Join("/", c.testDir), -1)
	if dir == nil {
		return errors.New("godoc: corpus fstree is nil")
	}
	c.fsTree.Set(dir)
	c.invalidateIndex()
	return nil
}
