package hclpack

import (
	"fmt"

	"github.com/hashicorp/hcl2/hcl"
)

// Body is an implementation of hcl.Body.
type Body struct {
	Attributes  map[string]Attribute
	ChildBlocks []Block

	MissingItemRange_ hcl.Range
}

var _ hcl.Body = (*Body)(nil)

// Content is an implementation of the method of the same name on hcl.Body.
//
// When Content is called directly on a hclpack.Body, all child block bodies
// are guaranteed to be of type *hclpack.Body, so callers can type-assert
// to obtain a child Body in order to serialize it separately if needed.
func (b *Body) Content(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostics) {
	return b.content(schema, nil)
}

// PartialContent is an implementation of the method of the same name on hcl.Body.
//
// The returned "remain" body may share some backing objects with the receiver,
// so neither the receiver nor the returned remain body, or any descendent
// objects within them, may be mutated after this method is used.
//
// When Content is called directly on a hclpack.Body, all child block bodies
// and the returned "remain" body are guaranteed to be of type *hclpack.Body,
// so callers can type-assert to obtain a child Body in order to serialize it
// separately if needed.
func (b *Body) PartialContent(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Body, hcl.Diagnostics) {
	remain := &Body{
		MissingItemRange_: b.MissingItemRange_,
	}
	content, diags := b.content(schema, remain)
	return content, remain, diags
}

func (b *Body) content(schema *hcl.BodySchema, remain *Body) (*hcl.BodyContent, hcl.Diagnostics) {
	if b == nil {
		b = &Body{} // We'll treat a nil body like an empty one, for convenience
	}
	var diags hcl.Diagnostics

	var attrs map[string]*hcl.Attribute
	var attrUsed map[string]struct{}
	if len(b.Attributes) > 0 {
		attrs = make(map[string]*hcl.Attribute, len(b.Attributes))
		attrUsed = make(map[string]struct{}, len(b.Attributes))
	}
	for _, attrS := range schema.Attributes {
		name := attrS.Name
		attr, exists := b.Attributes[name]
		if !exists {
			if attrS.Required {
				diags = append(diags, &hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  "Missing required argument",
					Detail:   fmt.Sprintf("The argument %q is required, but no definition was found.", attrS.Name),
					Subject:  &b.MissingItemRange_,
				})
			}
			continue
		}

		attrs[name] = attr.asHCLAttribute(name)
		attrUsed[name] = struct{}{}
	}

	for name, attr := range b.Attributes {
		if _, used := attrUsed[name]; used {
			continue
		}
		if remain != nil {
			remain.setAttribute(name, attr)
			continue
		}
		var suggestions []string
		for _, attrS := range schema.Attributes {
			if _, defined := attrs[name]; defined {
				continue
			}
			suggestions = append(suggestions, attrS.Name)
		}
		suggestion := nameSuggestion(name, suggestions)
		if suggestion != "" {
			suggestion = fmt.Sprintf(" Did you mean %q?", suggestion)
		} else {
			// Is there a block of the same name?
			for _, blockS := range schema.Blocks {
				if blockS.Type == name {
					suggestion = fmt.Sprintf(" Did you mean to define a block of type %q?", name)
					break
				}
			}
		}
		diags = append(diags, &hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Unsupported argument",
			Detail:   fmt.Sprintf("An argument named %q is not expected here.%s", name, suggestion),
			Subject:  &attr.NameRange,
		})
	}

	blocksWanted := make(map[string]hcl.BlockHeaderSchema)
	for _, blockS := range schema.Blocks {
		blocksWanted[blockS.Type] = blockS
	}

	var blocks []*hcl.Block
	for _, block := range b.ChildBlocks {
		// Redeclare block on stack so the pointer to the body is set on the
		// correct block. https://github.com/hashicorp/hcl2/issues/72
		block := block

		blockTy := block.Type
		blockS, wanted := blocksWanted[blockTy]
		if !wanted {
			if remain != nil {
				remain.appendBlock(block)
				continue
			}
			var suggestions []string
			for _, blockS := range schema.Blocks {
				suggestions = append(suggestions, blockS.Type)
			}
			suggestion := nameSuggestion(blockTy, suggestions)
			if suggestion != "" {
				suggestion = fmt.Sprintf(" Did you mean %q?", suggestion)
			} else {
				// Is there an attribute of the same name?
				for _, attrS := range schema.Attributes {
					if attrS.Name == blockTy {
						suggestion = fmt.Sprintf(" Did you mean to define argument %q?", blockTy)
						break
					}
				}
			}

			diags = append(diags, &hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Unsupported block type",
				Detail:   fmt.Sprintf("Blocks of type %q are not expected here.%s", blockTy, suggestion),
				Subject:  &block.TypeRange,
			})
			continue
		}

		if len(block.Labels) != len(blockS.LabelNames) {
			if len(blockS.LabelNames) == 0 {
				diags = append(diags, &hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  fmt.Sprintf("Extraneous label for %s", blockTy),
					Detail: fmt.Sprintf(
						"No labels are expected for %s blocks.", blockTy,
					),
					Subject: &block.DefRange,
					Context: &block.DefRange,
				})
			} else {
				diags = append(diags, &hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  fmt.Sprintf("Wrong label count for %s", blockTy),
					Detail: fmt.Sprintf(
						"%s blocks expect %d label(s), but got %d.",
						blockTy, len(blockS.LabelNames), len(block.Labels),
					),
					Subject: &block.DefRange,
					Context: &block.DefRange,
				})
			}
			continue
		}

		blocks = append(blocks, block.asHCLBlock())
	}

	return &hcl.BodyContent{
		Attributes:       attrs,
		Blocks:           blocks,
		MissingItemRange: b.MissingItemRange_,
	}, diags
}

// JustAttributes is an implementation of the method of the same name on hcl.Body.
func (b *Body) JustAttributes() (hcl.Attributes, hcl.Diagnostics) {
	var diags hcl.Diagnostics
	if len(b.ChildBlocks) > 0 {
		for _, block := range b.ChildBlocks {
			diags = append(diags, &hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  fmt.Sprintf("Unexpected %s block", block.Type),
				Detail:   "Blocks are not allowed here.",
				Context:  &block.TypeRange,
			})
		}
		// We'll continue processing anyway, and return any attributes we find
		// so that the caller can do careful partial analysis.
	}

	if len(b.Attributes) == 0 {
		return nil, diags
	}

	ret := make(hcl.Attributes, len(b.Attributes))
	for n, a := range b.Attributes {
		ret[n] = a.asHCLAttribute(n)
	}
	return ret, diags
}

// MissingItemRange is an implementation of the method of the same name on hcl.Body.
func (b *Body) MissingItemRange() hcl.Range {
	return b.MissingItemRange_
}

func (b *Body) setAttribute(name string, attr Attribute) {
	if b.Attributes == nil {
		b.Attributes = make(map[string]Attribute)
	}
	b.Attributes[name] = attr
}

func (b *Body) appendBlock(block Block) {
	b.ChildBlocks = append(b.ChildBlocks, block)
}

func (b *Body) addRanges(rngs map[hcl.Range]struct{}) {
	rngs[b.MissingItemRange_] = struct{}{}
	for _, attr := range b.Attributes {
		attr.addRanges(rngs)
	}
	for _, block := range b.ChildBlocks {
		block.addRanges(rngs)
	}
}

// Block represents a nested block within a body.
type Block struct {
	Type   string
	Labels []string
	Body   Body

	DefRange, TypeRange hcl.Range
	LabelRanges         []hcl.Range
}

func (b *Block) asHCLBlock() *hcl.Block {
	return &hcl.Block{
		Type:   b.Type,
		Labels: b.Labels,
		Body:   &b.Body,

		TypeRange:   b.TypeRange,
		DefRange:    b.DefRange,
		LabelRanges: b.LabelRanges,
	}
}

func (b *Block) addRanges(rngs map[hcl.Range]struct{}) {
	rngs[b.DefRange] = struct{}{}
	rngs[b.TypeRange] = struct{}{}
	for _, rng := range b.LabelRanges {
		rngs[rng] = struct{}{}
	}
	b.Body.addRanges(rngs)
}

// Attribute represents an attribute definition within a body.
type Attribute struct {
	Expr Expression

	Range, NameRange hcl.Range
}

func (a *Attribute) asHCLAttribute(name string) *hcl.Attribute {
	return &hcl.Attribute{
		Name:      name,
		Expr:      &a.Expr,
		Range:     a.Range,
		NameRange: a.NameRange,
	}
}

func (a *Attribute) addRanges(rngs map[hcl.Range]struct{}) {
	rngs[a.Range] = struct{}{}
	rngs[a.NameRange] = struct{}{}
	a.Expr.addRanges(rngs)
}