package config

import (
	"bufio"
	"fmt"
	"io"
	"os"

	"github.com/hashicorp/errwrap"
)

// configurable is an interface that must be implemented by any configuration
// formats of Terraform in order to return a *Config.
type configurable interface {
	Config() (*Config, error)
}

// importTree is the result of the first-pass load of the configuration
// files. It is a tree of raw configurables and then any children (their
// imports).
//
// An importTree can be turned into a configTree.
type importTree struct {
	Path     string
	Raw      configurable
	Children []*importTree
}

// This is the function type that must be implemented by the configuration
// file loader to turn a single file into a configurable and any additional
// imports.
type fileLoaderFunc func(path string) (configurable, []string, error)

// Set this to a non-empty value at link time to enable the HCL2 experiment.
// This is not currently enabled for release builds.
//
// For example:
//    go install -ldflags="-X github.com/hashicorp/terraform/config.enableHCL2Experiment=true" github.com/hashicorp/terraform
var enableHCL2Experiment = ""

// loadTree takes a single file and loads the entire importTree for that
// file. This function detects what kind of configuration file it is an
// executes the proper fileLoaderFunc.
func loadTree(root string) (*importTree, error) {
	var f fileLoaderFunc

	// HCL2 experiment is currently activated at build time via the linker.
	// See the comment on this variable for more information.
	if enableHCL2Experiment == "" {
		// Main-line behavior: always use the original HCL parser
		switch ext(root) {
		case ".tf", ".tf.json":
			f = loadFileHcl
		default:
		}
	} else {
		// Experimental behavior: use the HCL2 parser if the opt-in comment
		// is present.
		switch ext(root) {
		case ".tf":
			// We need to sniff the file for the opt-in comment line to decide
			// if the file is participating in the HCL2 experiment.
			cf, err := os.Open(root)
			if err != nil {
				return nil, err
			}
			sc := bufio.NewScanner(cf)
			for sc.Scan() {
				if sc.Text() == "#terraform:hcl2" {
					f = globalHCL2Loader.loadFile
				}
			}
			if f == nil {
				f = loadFileHcl
			}
		case ".tf.json":
			f = loadFileHcl
		default:
		}
	}

	if f == nil {
		return nil, fmt.Errorf(
			"%s: unknown configuration format. Use '.tf' or '.tf.json' extension",
			root)
	}

	c, imps, err := f(root)
	if err != nil {
		return nil, err
	}

	children := make([]*importTree, len(imps))
	for i, imp := range imps {
		t, err := loadTree(imp)
		if err != nil {
			return nil, err
		}

		children[i] = t
	}

	return &importTree{
		Path:     root,
		Raw:      c,
		Children: children,
	}, nil
}

// Close releases any resources we might be holding open for the importTree.
//
// This can safely be called even while ConfigTree results are alive. The
// importTree is not bound to these.
func (t *importTree) Close() error {
	if c, ok := t.Raw.(io.Closer); ok {
		c.Close()
	}
	for _, ct := range t.Children {
		ct.Close()
	}

	return nil
}

// ConfigTree traverses the importTree and turns each node into a *Config
// object, ultimately returning a *configTree.
func (t *importTree) ConfigTree() (*configTree, error) {
	config, err := t.Raw.Config()
	if err != nil {
		return nil, errwrap.Wrapf(fmt.Sprintf("Error loading %s: {{err}}", t.Path), err)
	}

	// Build our result
	result := &configTree{
		Path:   t.Path,
		Config: config,
	}

	// Build the config trees for the children
	result.Children = make([]*configTree, len(t.Children))
	for i, ct := range t.Children {
		t, err := ct.ConfigTree()
		if err != nil {
			return nil, err
		}

		result.Children[i] = t
	}

	return result, nil
}
