package storm

import (
	"reflect"

	"github.com/asdine/storm/index"
	"github.com/boltdb/bolt"
	"github.com/fatih/structs"
)

// Storm tags
const (
	tagID        = "id"
	tagIdx       = "index"
	tagUniqueIdx = "unique"
	tagInline    = "inline"
	indexPrefix  = "__storm_index_"
)

type indexInfo struct {
	Type  string
	Field *structs.Field
}

// modelInfo is a structure gathering all the relevant informations about a model
type modelInfo struct {
	Name    string
	Indexes map[string]indexInfo
	ID      identInfo
	data    interface{}
}

func (m *modelInfo) AddIndex(f *structs.Field, indexType string, override bool) {
	fieldName := f.Name()
	if _, ok := m.Indexes[fieldName]; !ok || override {
		m.Indexes[fieldName] = indexInfo{
			Type:  indexType,
			Field: f,
		}
	}
}

func (m *modelInfo) AllByType(indexType string) []indexInfo {
	var idx []indexInfo
	for k := range m.Indexes {
		if m.Indexes[k].Type == indexType {
			idx = append(idx, m.Indexes[k])
		}
	}

	return idx
}

func extract(data interface{}, mi ...*modelInfo) (*modelInfo, error) {
	s := structs.New(data)
	fields := s.Fields()

	var child bool

	var m *modelInfo
	if len(mi) > 0 {
		m = mi[0]
		child = true
	} else {
		m = &modelInfo{}
		m.Indexes = make(map[string]indexInfo)
		m.data = data
	}

	if m.Name == "" {
		m.Name = s.Name()
	}

	for _, f := range fields {
		if !f.IsExported() {
			continue
		}

		err := extractField(f, m, child)
		if err != nil {
			return nil, err
		}
	}

	// ID field or tag detected
	if m.ID.Field != nil {
		if m.ID.Field.IsZero() {
			m.ID.IsZero = true
		} else {
			m.ID.Value = m.ID.Field.Value()
		}
	}

	if child {
		return m, nil
	}

	if m.ID.Field == nil {
		return nil, ErrNoID
	}

	if m.Name == "" {
		return nil, ErrNoName
	}

	return m, nil
}

func extractField(f *structs.Field, m *modelInfo, isChild bool) error {
	tag := f.Tag("storm")
	if tag != "" {
		switch tag {
		case "id":
			m.ID.Field = f
		case tagUniqueIdx, tagIdx:
			m.AddIndex(f, tag, !isChild)
		case tagInline:
			if structs.IsStruct(f.Value()) {
				_, err := extract(f.Value(), m)
				if err != nil {
					return err
				}
			}
		default:
			return ErrUnknownTag
		}
	}

	// the field is named ID and no ID field has been detected before
	if f.Name() == "ID" && m.ID.Field == nil {
		m.ID.Field = f
	}

	return nil
}

// Prefill the most requested informations
type identInfo struct {
	Field  *structs.Field
	IsZero bool
	Value  interface{}
}

func (i *identInfo) Type() reflect.Type {
	return reflect.TypeOf(i.Field.Value())
}

func (i *identInfo) IsOfIntegerFamily() bool {
	return i.Field != nil && i.Field.Kind() >= reflect.Int && i.Field.Kind() <= reflect.Uint64
}

func getIndex(bucket *bolt.Bucket, idxKind string, fieldName string) (index.Index, error) {
	var idx index.Index
	var err error

	switch idxKind {
	case tagUniqueIdx:
		idx, err = index.NewUniqueIndex(bucket, []byte(indexPrefix+fieldName))
	case tagIdx:
		idx, err = index.NewListIndex(bucket, []byte(indexPrefix+fieldName))
	default:
		err = ErrIdxNotFound
	}

	return idx, err
}
