/*
Copyright 2015 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package generator

import (
	"bytes"
	"io"

	"k8s.io/gengo/namer"
	"k8s.io/gengo/parser"
	"k8s.io/gengo/types"
)

// Package contains the contract for generating a package.
type Package interface {
	// Name returns the package short name.
	Name() string
	// Path returns the package import path.
	Path() string
	// SourcePath returns the location of the package on disk.
	SourcePath() string

	// Filter should return true if this package cares about this type.
	// Otherwise, this type will be omitted from the type ordering for
	// this package.
	Filter(*Context, *types.Type) bool

	// Header should return a header for the file, including comment markers.
	// Useful for copyright notices and doc strings. Include an
	// autogeneration notice! Do not include the "package x" line.
	Header(filename string) []byte

	// Generators returns the list of generators for this package. It is
	// allowed for more than one generator to write to the same file.
	// A Context is passed in case the list of generators depends on the
	// input types.
	Generators(*Context) []Generator
}

type File struct {
	Name              string
	FileType          string
	PackageName       string
	Header            []byte
	PackagePath       string
	PackageSourcePath string
	Imports           map[string]struct{}
	Vars              bytes.Buffer
	Consts            bytes.Buffer
	Body              bytes.Buffer
}

type FileType interface {
	AssembleFile(f *File, path string) error
	VerifyFile(f *File, path string) error
}

// Packages is a list of packages to generate.
type Packages []Package

// Generator is the contract for anything that wants to do auto-generation.
// It's expected that the io.Writers passed to the below functions will be
// ErrorTrackers; this allows implementations to not check for io errors,
// making more readable code.
//
// The call order for the functions that take a Context is:
// 1. Filter()        // Subsequent calls see only types that pass this.
// 2. Namers()        // Subsequent calls see the namers provided by this.
// 3. PackageVars()
// 4. PackageConsts()
// 5. Init()
// 6. GenerateType()  // Called N times, once per type in the context's Order.
// 7. Imports()
//
// You may have multiple generators for the same file.
type Generator interface {
	// The name of this generator. Will be included in generated comments.
	Name() string

	// Filter should return true if this generator cares about this type.
	// (otherwise, GenerateType will not be called.)
	//
	// Filter is called before any of the generator's other functions;
	// subsequent calls will get a context with only the types that passed
	// this filter.
	Filter(*Context, *types.Type) bool

	// If this generator needs special namers, return them here. These will
	// override the original namers in the context if there is a collision.
	// You may return nil if you don't need special names. These names will
	// be available in the context passed to the rest of the generator's
	// functions.
	//
	// A use case for this is to return a namer that tracks imports.
	Namers(*Context) namer.NameSystems

	// Init should write an init function, and any other content that's not
	// generated per-type. (It's not intended for generator specific
	// initialization! Do that when your Package constructs the
	// Generators.)
	Init(*Context, io.Writer) error

	// Finalize should write finish up functions, and any other content that's not
	// generated per-type.
	Finalize(*Context, io.Writer) error

	// PackageVars should emit an array of variable lines. They will be
	// placed in a var ( ... ) block. There's no need to include a leading
	// \t or trailing \n.
	PackageVars(*Context) []string

	// PackageConsts should emit an array of constant lines. They will be
	// placed in a const ( ... ) block. There's no need to include a leading
	// \t or trailing \n.
	PackageConsts(*Context) []string

	// GenerateType should emit the code for a particular type.
	GenerateType(*Context, *types.Type, io.Writer) error

	// Imports should return a list of necessary imports. They will be
	// formatted correctly. You do not need to include quotation marks,
	// return only the package name; alternatively, you can also return
	// imports in the format `name "path/to/pkg"`. Imports will be called
	// after Init, PackageVars, PackageConsts, and GenerateType, to allow
	// you to keep track of what imports you actually need.
	Imports(*Context) []string

	// Preferred file name of this generator, not including a path. It is
	// allowed for multiple generators to use the same filename, but it's
	// up to you to make sure they don't have colliding import names.
	// TODO: provide per-file import tracking, removing the requirement
	// that generators coordinate..
	Filename() string

	// A registered file type in the context to generate this file with. If
	// the FileType is not found in the context, execution will stop.
	FileType() string
}

// Context is global context for individual generators to consume.
type Context struct {
	// A map from the naming system to the names for that system. E.g., you
	// might have public names and several private naming systems.
	Namers namer.NameSystems

	// All the types, in case you want to look up something.
	Universe types.Universe

	// Incoming imports, i.e. packages importing the given package.
	incomingImports map[string][]string

	// Incoming transitive imports, i.e. the transitive closure of IncomingImports
	incomingTransitiveImports map[string][]string

	// All the user-specified packages.  This is after recursive expansion.
	Inputs []string

	// The canonical ordering of the types (will be filtered by both the
	// Package's and Generator's Filter methods).
	Order []*types.Type

	// A set of types this context can process. If this is empty or nil,
	// the default "golang" filetype will be provided.
	FileTypes map[string]FileType

	// If true, Execute* calls will just verify that the existing output is
	// correct. (You may set this after calling NewContext.)
	Verify bool

	// Allows generators to add packages at runtime.
	builder *parser.Builder

	// If specified, trim the prefix from a package's path before writing files.
	TrimPathPrefix string
}

// NewContext generates a context from the given builder, naming systems, and
// the naming system you wish to construct the canonical ordering from.
func NewContext(b *parser.Builder, nameSystems namer.NameSystems, canonicalOrderName string) (*Context, error) {
	universe, err := b.FindTypes()
	if err != nil {
		return nil, err
	}

	c := &Context{
		Namers:   namer.NameSystems{},
		Universe: universe,
		Inputs:   b.FindPackages(),
		FileTypes: map[string]FileType{
			GolangFileType: NewGolangFile(),
		},
		builder: b,
	}

	for name, systemNamer := range nameSystems {
		c.Namers[name] = systemNamer
		if name == canonicalOrderName {
			orderer := namer.Orderer{Namer: systemNamer}
			c.Order = orderer.OrderUniverse(universe)
		}
	}
	return c, nil
}

// IncomingImports returns the incoming imports for each package. The map is lazily computed.
func (ctxt *Context) IncomingImports() map[string][]string {
	if ctxt.incomingImports == nil {
		incoming := map[string][]string{}
		for _, pkg := range ctxt.Universe {
			for imp := range pkg.Imports {
				incoming[imp] = append(incoming[imp], pkg.Path)
			}
		}
		ctxt.incomingImports = incoming
	}
	return ctxt.incomingImports
}

// TransitiveIncomingImports returns the transitive closure of the incoming imports for each package.
// The map is lazily computed.
func (ctxt *Context) TransitiveIncomingImports() map[string][]string {
	if ctxt.incomingTransitiveImports == nil {
		ctxt.incomingTransitiveImports = transitiveClosure(ctxt.IncomingImports())
	}
	return ctxt.incomingTransitiveImports
}

// AddDir adds a Go package to the context. The specified path must be a single
// go package import path.  GOPATH, GOROOT, and the location of your go binary
// (`which go`) will all be searched, in the normal Go fashion.
// Deprecated. Please use AddDirectory.
func (ctxt *Context) AddDir(path string) error {
	ctxt.incomingImports = nil
	ctxt.incomingTransitiveImports = nil
	return ctxt.builder.AddDirTo(path, &ctxt.Universe)
}

// AddDirectory adds a Go package to the context. The specified path must be a
// single go package import path.  GOPATH, GOROOT, and the location of your go
// binary (`which go`) will all be searched, in the normal Go fashion.
func (ctxt *Context) AddDirectory(path string) (*types.Package, error) {
	ctxt.incomingImports = nil
	ctxt.incomingTransitiveImports = nil
	return ctxt.builder.AddDirectoryTo(path, &ctxt.Universe)
}
