// Copyright 2019 CUE 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 astutil

import (
	"path"
	"strconv"
	"strings"

	"cuelang.org/go/cue/ast"
	"cuelang.org/go/cue/token"
)

// ImportPathName derives the package name from the given import path.
//
// Examples:
//      string           string
//      foo.com/bar      bar
//      foo.com/bar:baz  baz
//
func ImportPathName(id string) string {
	name := path.Base(id)
	if p := strings.LastIndexByte(name, ':'); p > 0 {
		name = name[p+1:]
	}
	return name
}

// ImportInfo describes the information contained in an ImportSpec.
type ImportInfo struct {
	Ident   string // identifier used to refer to the import
	PkgName string // name of the package
	ID      string // full import path, including the name
	Dir     string // import path, excluding the name
}

// ParseImportSpec returns the name and full path of an ImportSpec.
func ParseImportSpec(spec *ast.ImportSpec) (info ImportInfo, err error) {
	str, err := strconv.Unquote(spec.Path.Value)
	if err != nil {
		return info, err
	}

	info.ID = str

	if p := strings.LastIndexByte(str, ':'); p > 0 {
		info.Dir = str[:p]
		info.PkgName = str[p+1:]
	} else {
		info.Dir = str
		info.PkgName = path.Base(str)
	}

	if spec.Name != nil {
		info.Ident = spec.Name.Name
	} else {
		info.Ident = info.PkgName
	}

	return info, nil
}

// CopyComments associates comments of one node with another.
// It may change the relative position of comments.
func CopyComments(to, from ast.Node) {
	if from == nil {
		return
	}
	ast.SetComments(to, from.Comments())
}

// CopyPosition sets the position of one node to another.
func CopyPosition(to, from ast.Node) {
	if from == nil {
		return
	}
	ast.SetPos(to, from.Pos())
}

// CopyMeta copies comments and position information from one node to another.
// It returns the destination node.
func CopyMeta(to, from ast.Node) ast.Node {
	if from == nil {
		return to
	}
	ast.SetComments(to, from.Comments())
	ast.SetPos(to, from.Pos())
	return to
}

// insertImport looks up an existing import with the given name and path or will
// add spec if it doesn't exist. It returns a spec in decls matching spec.
func insertImport(decls *[]ast.Decl, spec *ast.ImportSpec) *ast.ImportSpec {
	x, _ := ParseImportSpec(spec)

	a := *decls

	var imports *ast.ImportDecl
	var orig *ast.ImportSpec

	p := 0
outer:
	for i := 0; i < len(a); i++ {
		d := a[i]
		switch t := d.(type) {
		default:
			break outer

		case *ast.Package:
			p = i + 1
		case *ast.CommentGroup:
			p = i + 1
		case *ast.Attribute:
			continue
		case *ast.ImportDecl:
			p = i + 1
			imports = t
			for _, s := range t.Specs {
				y, _ := ParseImportSpec(s)
				if y.ID != x.ID {
					continue
				}
				orig = s
				if x.Ident == "" || y.Ident == x.Ident {
					return s
				}
			}
		}
	}

	// Import not found, add one.
	if imports == nil {
		imports = &ast.ImportDecl{}
		preamble := append(a[:p:p], imports)
		a = append(preamble, a[p:]...)
		*decls = a
	}

	if orig != nil {
		CopyComments(spec, orig)
	}
	imports.Specs = append(imports.Specs, spec)
	ast.SetRelPos(imports.Specs[0], token.NoRelPos)

	return spec
}
