// Copyright 2019 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 cache

import (
	"fmt"
	"go/ast"
	"go/scanner"
	"go/token"
	"go/types"
	"slices"
	"sync"

	"golang.org/x/tools/go/types/objectpath"
	"golang.org/x/tools/gopls/internal/cache/metadata"
	"golang.org/x/tools/gopls/internal/cache/methodsets"
	"golang.org/x/tools/gopls/internal/cache/parsego"
	"golang.org/x/tools/gopls/internal/cache/testfuncs"
	"golang.org/x/tools/gopls/internal/cache/xrefs"
	"golang.org/x/tools/gopls/internal/protocol"
	"golang.org/x/tools/gopls/internal/util/asm"
	"golang.org/x/tools/gopls/internal/util/safetoken"
)

// Convenient aliases for very heavily used types.
type (
	PackageID   = metadata.PackageID
	PackagePath = metadata.PackagePath
	PackageName = metadata.PackageName
	ImportPath  = metadata.ImportPath
)

// A Package is the union of package metadata and type checking results.
//
// TODO(rfindley): for now, we do not persist the post-processing of
// loadDiagnostics, because the value of the snapshot.packages map is just the
// package handle. Fix this.
type Package struct {
	metadata        *metadata.Package
	loadDiagnostics []*Diagnostic
	pkg             *syntaxPackage
}

// syntaxPackage contains parse trees and type information for a package.
type syntaxPackage struct {
	// -- identifiers --
	id PackageID

	// -- outputs --
	fset            *token.FileSet // for now, same as the snapshot's FileSet
	goFiles         []*parsego.File
	compiledGoFiles []*parsego.File
	asmFiles        []*asm.File
	diagnostics     []*Diagnostic
	parseErrors     []scanner.ErrorList
	typeErrors      []types.Error
	types           *types.Package
	typesInfo       *types.Info
	typesSizes      types.Sizes
	importMap       map[PackagePath]*types.Package

	xrefsOnce sync.Once
	_xrefs    *xrefs.Index // only used by the xrefs method

	methodsetsOnce sync.Once
	_methodsets    *methodsets.Index // only used by the methodsets method

	testsOnce sync.Once
	_tests    *testfuncs.Index // only used by the tests method
}

func (p *syntaxPackage) xrefs(enc *objectpath.Encoder) *xrefs.Index {
	p.xrefsOnce.Do(func() {
		p._xrefs = xrefs.NewIndex(enc, p.types, p.typesInfo, p.compiledGoFiles, p.asmFiles)
	})
	return p._xrefs
}

func (p *syntaxPackage) methodsets(enc *objectpath.Encoder) *methodsets.Index {
	p.methodsetsOnce.Do(func() {
		p._methodsets = methodsets.NewIndex(enc, p.fset, p.types)
	})
	return p._methodsets
}

func (p *syntaxPackage) tests() *testfuncs.Index {
	p.testsOnce.Do(func() {
		p._tests = testfuncs.NewIndex(p.compiledGoFiles, p.typesInfo)
	})
	return p._tests
}

// hasFixedFiles reports whether there are any 'fixed' compiled go files in the
// package.
//
// Intended to be used to refine bug reports.
func (p *syntaxPackage) hasFixedFiles() bool {
	return slices.ContainsFunc(p.compiledGoFiles, (*parsego.File).Fixed)
}

func (p *Package) String() string { return string(p.metadata.ID) }

func (p *Package) Metadata() *metadata.Package { return p.metadata }

// A loadScope defines a package loading scope for use with go/packages.
//
// TODO(rfindley): move this to load.go.
type loadScope interface {
	aScope()
}

// TODO(rfindley): move to load.go
type (
	fileLoadScope    protocol.DocumentURI // load packages containing a file (including command-line-arguments)
	packageLoadScope string               // load a specific package (the value is its PackageID)
	moduleLoadScope  struct {
		dir        string // dir containing the go.mod file
		modulePath string // parsed module path
	}
	viewLoadScope struct{} // load the workspace
)

// Implement the loadScope interface.
func (fileLoadScope) aScope()    {}
func (packageLoadScope) aScope() {}
func (moduleLoadScope) aScope()  {}
func (viewLoadScope) aScope()    {}

func (p *Package) CompiledGoFiles() []*parsego.File {
	return p.pkg.compiledGoFiles
}

func (p *Package) File(uri protocol.DocumentURI) (*parsego.File, error) {
	return p.pkg.File(uri)
}

// FileEnclosing returns the file of pkg that encloses the specified position,
// which must be mapped by p.FileSet().
func (p *Package) FileEnclosing(pos token.Pos) (*parsego.File, error) {
	for _, files := range [...][]*parsego.File{p.pkg.compiledGoFiles, p.pkg.goFiles} {
		for _, pgf := range files {
			if pgf.File.FileStart <= pos && pos <= pgf.File.FileEnd {
				return pgf, nil
			}
		}
	}
	return nil, fmt.Errorf("no parsed file for position %d (%s) in %v",
		pos, safetoken.StartPosition(p.FileSet(), pos), p.pkg.id)
}

func (pkg *syntaxPackage) File(uri protocol.DocumentURI) (*parsego.File, error) {
	for _, files := range [...][]*parsego.File{pkg.compiledGoFiles, pkg.goFiles} {
		for _, pgf := range files {
			if pgf.URI == uri {
				return pgf, nil
			}
		}
	}
	return nil, fmt.Errorf("no parsed file for %s in %v", uri, pkg.id)
}

// Syntax returns parsed compiled Go files contained in this package.
func (p *Package) Syntax() []*ast.File {
	var syntax []*ast.File
	for _, pgf := range p.pkg.compiledGoFiles {
		syntax = append(syntax, pgf.File)
	}
	return syntax
}

// FileSet returns the FileSet describing this package's positions.
//
// The returned FileSet is guaranteed to describe all Syntax, but may also
// describe additional files.
func (p *Package) FileSet() *token.FileSet {
	return p.pkg.fset
}

// Types returns the type checked go/types.Package.
func (p *Package) Types() *types.Package {
	return p.pkg.types
}

// TypesInfo returns the go/types.Info annotating the Syntax of this package
// with type information.
//
// All fields in the resulting Info are populated.
func (p *Package) TypesInfo() *types.Info {
	return p.pkg.typesInfo
}

// TypesSizes returns the sizing function used for types in this package.
func (p *Package) TypesSizes() types.Sizes {
	return p.pkg.typesSizes
}

// DependencyTypes returns the type checker's symbol for the specified
// package. It returns nil if path is not among the transitive
// dependencies of p, or if no symbols from that package were
// referenced during the type-checking of p.
func (p *Package) DependencyTypes(path PackagePath) *types.Package {
	return p.pkg.importMap[path]
}

// ParseErrors returns a slice containing all non-empty parse errors produces
// while parsing p.Syntax, or nil if the package contains no parse errors.
func (p *Package) ParseErrors() []scanner.ErrorList {
	return p.pkg.parseErrors
}

// TypeErrors returns the go/types.Errors produced during type checking this
// package, if any.
func (p *Package) TypeErrors() []types.Error {
	return p.pkg.typeErrors
}

func (p *Package) AsmFiles() []*asm.File {
	return p.pkg.asmFiles
}

func (p *Package) AsmFile(uri protocol.DocumentURI) (*asm.File, error) {
	return p.pkg.AsmFile(uri)
}

func (pkg *syntaxPackage) AsmFile(uri protocol.DocumentURI) (*asm.File, error) {
	for _, af := range pkg.asmFiles {
		if af.Mapper.URI == uri {
			return af, nil
		}
	}
	return nil, fmt.Errorf("no parsed file for %s in %v", uri, pkg.id)
}
