// Copyright 2017 The Hugo Authors. All rights reserved.
//
// 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 collections

import (
	"errors"
	"fmt"
	"reflect"

	"github.com/spf13/cast"

	"github.com/gohugoio/hugo/common/hmaps"
	"github.com/gohugoio/hugo/common/hreflect"
)

// Index returns the result of indexing its first argument by the following
// arguments. Thus "index x 1 2 3" is, in Go syntax, x[1][2][3]. Each
// indexed item must be a map, slice, or array.
//
// Adapted from Go stdlib src/text/template/funcs.go.
//
// We deviate from the stdlib mostly because of https://github.com/golang/go/issues/14751.
func (ns *Namespace) Index(item any, args ...any) (any, error) {
	v, err := ns.doIndex(item, args...)
	if err != nil {
		return nil, fmt.Errorf("index of type %T with args %v failed: %s", item, args, err)
	}
	return v, nil
}

func (ns *Namespace) doIndex(item any, args ...any) (any, error) {
	// TODO(moorereason): merge upstream changes.
	v := reflect.ValueOf(item)
	if !v.IsValid() {
		// See issue 10489
		// This used to be an error.
		return nil, nil
	}

	var indices []any

	if len(args) == 1 {
		v := reflect.ValueOf(args[0])
		if v.Kind() == reflect.Slice {
			for i := range v.Len() {
				indices = append(indices, v.Index(i).Interface())
			}
		} else {
			indices = append(indices, args[0])
		}
	} else {
		indices = args
	}

	lowerm, ok := item.(hmaps.Params)
	if ok {
		return lowerm.GetNested(cast.ToStringSlice(indices)...), nil
	}

	for _, i := range indices {
		index := reflect.ValueOf(i)
		var isNil bool
		if v, isNil = hreflect.Indirect(v); isNil {
			// See issue 10489
			// This used to be an error.
			return nil, nil
		}
		switch v.Kind() {
		case reflect.Array, reflect.Slice, reflect.String:
			var x int64
			switch index.Kind() {
			case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
				x = index.Int()
			case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
				x = int64(index.Uint())
			case reflect.Invalid:
				return nil, errors.New("cannot index slice/array with nil")
			default:
				return nil, fmt.Errorf("cannot index slice/array with type %s", index.Type())
			}
			if x < 0 || x >= int64(v.Len()) {
				// We deviate from stdlib here.  Don't return an error if the
				// index is out of range.
				return nil, nil
			}
			v = v.Index(int(x))
		case reflect.Map:
			index, err := prepareArg(index, v.Type().Key())
			if err != nil {
				return nil, err
			}

			if x := v.MapIndex(index); x.IsValid() {
				v = x
			} else {
				v = reflect.Zero(v.Type().Elem())
			}
		case reflect.Invalid:
			// the loop holds invariant: v.IsValid()
			panic("unreachable")
		default:
			return nil, fmt.Errorf("can't index item of type %s", v.Type())
		}
	}
	return v.Interface(), nil
}

// prepareArg checks if value can be used as an argument of type argType, and
// converts an invalid value to appropriate zero if possible.
//
// Copied from Go stdlib src/text/template/funcs.go.
func prepareArg(value reflect.Value, argType reflect.Type) (reflect.Value, error) {
	if !value.IsValid() {
		if !canBeNil(argType) {
			return reflect.Value{}, fmt.Errorf("value is nil; should be of type %s", argType)
		}
		value = reflect.Zero(argType)
	}
	if !value.Type().AssignableTo(argType) {
		return reflect.Value{}, fmt.Errorf("value has type %s; should be %s", value.Type(), argType)
	}
	return value, nil
}

// canBeNil reports whether an untyped nil can be assigned to the type. See reflect.Zero.
//
// Copied from Go stdlib src/text/template/exec.go.
func canBeNil(typ reflect.Type) bool {
	switch typ.Kind() {
	case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
		return true
	}
	return false
}
