// Copyright 2020 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 export

import (
	"cuelang.org/go/cue/ast"
	"cuelang.org/go/cue/token"
	"cuelang.org/go/internal"
	"cuelang.org/go/internal/core/adt"
)

// ExtractDoc collects documentation strings for a field.
//
// Comments are attached to a field with a field shorthand belong to the
// child node. So in the following the comment is attached to field bar.
//
//     // comment
//     foo: bar: 2
//
func ExtractDoc(v *adt.Vertex) (docs []*ast.CommentGroup) {
	return extractDocs(v, v.Conjuncts)
}

func extractDocs(v *adt.Vertex, a []adt.Conjunct) (docs []*ast.CommentGroup) {
	fields := []*ast.Field{}

	// Collect docs directly related to this Vertex.
	for _, x := range a {
		if v, ok := x.Elem().(*adt.Vertex); ok {
			docs = append(docs, extractDocs(v, v.Conjuncts)...)
			continue
		}

		switch f := x.Source().(type) {
		case *ast.Field:
			if hasShorthandValue(f) {
				continue
			}
			fields = append(fields, f)
			for _, cg := range f.Comments() {
				if !containsDoc(docs, cg) && cg.Doc {
					docs = append(docs, cg)
				}
			}

		case *ast.File:
			if c := internal.FileComment(f); c != nil {
				docs = append(docs, c)
			}
		}
	}

	if v == nil {
		return docs
	}

	// Collect docs from parent scopes in collapsed fields.
	for p := v.Parent; p != nil; p = p.Parent {

		newFields := []*ast.Field{}

		for _, x := range p.Conjuncts {
			f, ok := x.Source().(*ast.Field)
			if !ok || !hasShorthandValue(f) {
				continue
			}

			nested := nestedField(f)
			for _, child := range fields {
				if nested == child {
					newFields = append(newFields, f)
					for _, cg := range f.Comments() {
						if !containsDoc(docs, cg) && cg.Doc {
							docs = append(docs, cg)
						}
					}
				}
			}
		}

		fields = newFields
	}
	return docs
}

// hasShorthandValue reports whether this field has a struct value that will
// be rendered as a shorthand, for instance:
//
//     f: g: 2
//
func hasShorthandValue(f *ast.Field) bool {
	if f = nestedField(f); f == nil {
		return false
	}

	// Not a regular field, but shorthand field.
	// TODO: Should we return here? For now mimic old implementation.
	if _, _, err := ast.LabelName(f.Label); err != nil {
		return false
	}

	return true
}

// nestedField returns the child field of a field shorthand.
func nestedField(f *ast.Field) *ast.Field {
	s, _ := f.Value.(*ast.StructLit)
	if s == nil ||
		len(s.Elts) != 1 ||
		s.Lbrace != token.NoPos ||
		s.Rbrace != token.NoPos {
		return nil
	}

	f, _ = s.Elts[0].(*ast.Field)
	return f
}

func containsDoc(a []*ast.CommentGroup, cg *ast.CommentGroup) bool {
	for _, c := range a {
		if c == cg {
			return true
		}
	}

	for _, c := range a {
		if c.Text() == cg.Text() {
			return true
		}
	}

	return false
}

func ExtractFieldAttrs(v *adt.Vertex) (attrs []*ast.Attribute) {
	for _, x := range v.Conjuncts {
		attrs = extractFieldAttrs(attrs, x)
	}
	return attrs
}

func extractFieldAttrs(attrs []*ast.Attribute, c adt.Conjunct) []*ast.Attribute {
	if f, ok := c.Source().(*ast.Field); ok {
		for _, a := range f.Attrs {
			if !containsAttr(attrs, a) {
				attrs = append(attrs, a)
			}
		}
	}
	return attrs
}

func ExtractDeclAttrs(v *adt.Vertex) (attrs []*ast.Attribute) {
	for _, st := range v.Structs {
		if src := st.StructLit; src != nil {
			attrs = extractDeclAttrs(attrs, src.Src)
		}
	}
	return attrs
}

func extractDeclAttrs(attrs []*ast.Attribute, n ast.Node) []*ast.Attribute {
	switch x := n.(type) {
	case nil:
	case *ast.File:
		info := internal.GetPackageInfo(x)
		attrs = appendDeclAttrs(attrs, x.Decls[info.Index:])
	case *ast.StructLit:
		attrs = appendDeclAttrs(attrs, x.Elts)
	}
	return attrs
}

func appendDeclAttrs(a []*ast.Attribute, decls []ast.Decl) []*ast.Attribute {
	for _, d := range decls {
		if attr, ok := d.(*ast.Attribute); ok && !containsAttr(a, attr) {
			a = append(a, attr)
		}
	}
	return a
}

func containsAttr(a []*ast.Attribute, x *ast.Attribute) bool {
	for _, e := range a {
		if e.Text == x.Text {
			return true
		}
	}
	return false
}
