// Copyright © 2016 Asteris, LLC
//
// 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 parse

import (
	"errors"
	"fmt"
	"reflect"
	"sync"
	"unicode"

	log "github.com/Sirupsen/logrus"
	"github.com/hashicorp/hcl"
	"github.com/hashicorp/hcl/hcl/ast"
)

// ErrNotFound is returned from Get and friends when the key does not exist
var ErrNotFound = errors.New("key does not exist")

// Node represents a node in the parsed module
type Node struct {
	*ast.ObjectItem

	values map[string]interface{}
	once   sync.Once
}

// NewNode constructs a new Node from the given ObjectItem
func NewNode(item *ast.ObjectItem) *Node {
	return &Node{ObjectItem: item}
}

// Validate this node
func (n *Node) Validate() error {
	if n == nil {
		return errors.New("node is empty, check for bad input")
	}

	switch len(n.Keys) {
	case 0:
		return fmt.Errorf("%s: no keys", n.Pos())

	case 1:
		if n.IsDefault() {
			break
		}
		return fmt.Errorf("%s: missing name", n.Pos())

	case 2:
		if n.IsModule() {
			return fmt.Errorf("%s: missing source or name in module call", n.Pos())
		}

		if n.IsDefault() {
			return fmt.Errorf("%s: too many keys", n.Pos())
		}

		if n.IsCase() {
			return fmt.Errorf("%s: missing name or predicate in case", n.Pos())
		}

	default:
		if n.IsModule() && len(n.Keys) == 3 {
			break
		}

		if n.IsCase() && len(n.Keys) == 3 {
			break
		}

		return fmt.Errorf("%s: too many keys", n.Pos())
	}
	if err := validateName(n.Name()); err != nil {
		return fmt.Errorf("%s: %s", n.Pos(), err)
	}
	return n.setValues()
}

// ValidateName validates a given name string
func validateName(name string) error {
	var invalidRunes []rune
	for _, letter := range []rune(name) {
		if unicode.IsSpace(letter) {
			return errors.New("resource name may not contain spaces")
		}
		if !(unicode.IsLetter(letter) || unicode.IsNumber(letter) || letter == '-' || letter == '_' || letter == '.') {
			invalidRunes = append(invalidRunes, letter)
		}
	}

	if invalidRunes != nil {
		var badChars []string
		printed := make(map[rune]struct{})
		for _, runeChar := range invalidRunes {
			if _, ok := printed[runeChar]; !ok {
				badChars = append(badChars, string(runeChar))
				printed[runeChar] = struct{}{}
			}
		}
		return fmt.Errorf("invalid character(s) in resource name: %v; valid characters are unicode letters and numbers, dashes '-', underscores '_', and dots '.'", badChars)
	}
	return nil
}

// Kind returns the kind of resource this is
func (n *Node) Kind() string {
	return n.Keys[0].Token.Value().(string)
}

// Name returns the name of the resource
func (n *Node) Name() string {
	return n.Keys[len(n.Keys)-1].Token.Value().(string)
}

// IsModule tests whether this node is a module call
func (n *Node) IsModule() bool {
	return n.Kind() == "module"
}

// IsCase tests whether this node is a case statement
func (n *Node) IsCase() bool {
	return n.Kind() == "case"
}

// IsDefault tests whether this node is a default case statement
func (n *Node) IsDefault() bool {
	return n.Kind() == "default"
}

// Source returns where a module call is to be loaded from
func (n *Node) Source() string {
	if n.IsModule() {
		return n.Keys[1].Token.Value().(string)
	}
	return ""
}

// Group returns the group that the node is a member of
func (n *Node) Group() string {
	group, err := n.GetString("group")
	if err != nil {
		return ""
	}
	return group
}

func (n *Node) setValues() (err error) {
	n.once.Do(func() {
		n.values = map[string]interface{}{}

		err = hcl.DecodeObject(&n.values, n.Val)
	})

	return err
}

// Get a value from the values
func (n *Node) Get(key string) (val interface{}, err error) {
	if err := n.setValues(); err != nil {
		return nil, err
	}

	val, ok := n.values[key]
	if !ok {
		return val, ErrNotFound
	}

	return val, nil
}

// GetString retrieves string value from the values
func (n *Node) GetString(key string) (val string, err error) {
	raw, err := n.Get(key)
	if err != nil {
		return "", err
	}

	val, ok := raw.(string)
	if !ok {
		return "", n.badTypeError(key, "string", raw)
	}

	return val, nil
}

// GetStringSlice retrieves a slice of string from the values
func (n *Node) GetStringSlice(key string) (val []string, err error) {
	raw, err := n.Get(key)
	if err != nil {
		return nil, err
	}

	interfaces, ok := raw.([]interface{})
	if !ok {
		return nil, n.badTypeError(key, "slice", raw)
	}

	for i, iface := range interfaces {
		item, ok := iface.(string)
		if !ok {
			return nil, n.badTypeError(fmt.Sprintf("%s.%d", key, i), "string", iface)
		}

		val = append(val, item)
	}

	return val, nil
}

// GetStrings retrieves all the strings in the node
func (n *Node) GetStrings() (vals []string, err error) {
	if err := n.setValues(); err != nil {
		return nil, err
	}

	toConsider := []interface{}{}
	for _, val := range n.values {
		toConsider = append(toConsider, val)
	}

	for len(toConsider) > 0 {
		val := toConsider[0]
		toConsider = toConsider[1:]

		switch val.(type) {
		case string:
			vals = append(vals, val.(string))

		case []map[string]interface{}:
			for _, sub := range val.([]map[string]interface{}) {
				toConsider = append(toConsider, interface{}(sub))
			}

		case map[string]interface{}:
			for key, value := range val.(map[string]interface{}) {
				toConsider = append(toConsider, key)
				toConsider = append(toConsider, value)
			}

		case []interface{}:
			toConsider = append(toConsider, val.([]interface{})...)

		default:
			log.WithField("type", fmt.Sprintf("%T", val)).WithField("val", val).Debug("unknown value")
		}
	}

	return vals, nil
}

func (n *Node) badTypeError(key, typ string, val interface{}) error {
	article := func(x string) string {
		switch x[0] {
		case 'a', 'e', 'i', 'o', 'u':
			return "an"
		default:
			return "a"
		}
	}

	valTyp := reflect.TypeOf(val).String()

	return fmt.Errorf(
		"%q is not %s %s, it is %s %s",
		key,
		article(typ), typ,
		article(valTyp), valTyp,
	)
}

// ID formats and returns the node ID as kind.name
func (n *Node) ID() string {
	return fmt.Sprintf(
		"%s.%s",
		n.Kind(),
		n.Name(),
	)
}

// String returns the node ID
func (n *Node) String() string {
	return n.ID()
}
