// Copyright 2015 Google Inc. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.

package search

import (
	"fmt"
	"reflect"
	"strings"
	"sync"
)

// ErrFieldMismatch is returned when a field is to be loaded into a different
// than the one it was stored from, or when a field is missing or unexported in
// the destination struct.
type ErrFieldMismatch struct {
	FieldName string
	Reason    string
}

func (e *ErrFieldMismatch) Error() string {
	return fmt.Sprintf("search: cannot load field %q: %s", e.FieldName, e.Reason)
}

// ErrFacetMismatch is returned when a facet is to be loaded into a different
// type than the one it was stored from, or when a field is missing or
// unexported in the destination struct. StructType is the type of the struct
// pointed to by the destination argument passed to Iterator.Next.
type ErrFacetMismatch struct {
	StructType reflect.Type
	FacetName  string
	Reason     string
}

func (e *ErrFacetMismatch) Error() string {
	return fmt.Sprintf("search: cannot load facet %q into a %q: %s", e.FacetName, e.StructType, e.Reason)
}

// structCodec defines how to convert a given struct to/from a search document.
type structCodec struct {
	// byIndex returns the struct tag for the i'th struct field.
	byIndex []structTag

	// fieldByName returns the index of the struct field for the given field name.
	fieldByName map[string]int

	// facetByName returns the index of the struct field for the given facet name,
	facetByName map[string]int
}

// structTag holds a structured version of each struct field's parsed tag.
type structTag struct {
	name   string
	facet  bool
	ignore bool
}

var (
	codecsMu sync.RWMutex
	codecs   = map[reflect.Type]*structCodec{}
)

func loadCodec(t reflect.Type) (*structCodec, error) {
	codecsMu.RLock()
	codec, ok := codecs[t]
	codecsMu.RUnlock()
	if ok {
		return codec, nil
	}

	codecsMu.Lock()
	defer codecsMu.Unlock()
	if codec, ok := codecs[t]; ok {
		return codec, nil
	}

	codec = &structCodec{
		fieldByName: make(map[string]int),
		facetByName: make(map[string]int),
	}

	for i, I := 0, t.NumField(); i < I; i++ {
		f := t.Field(i)
		name, opts := f.Tag.Get("search"), ""
		if i := strings.Index(name, ","); i != -1 {
			name, opts = name[:i], name[i+1:]
		}
		ignore := false
		if name == "-" {
			ignore = true
		} else if name == "" {
			name = f.Name
		} else if !validFieldName(name) {
			return nil, fmt.Errorf("search: struct tag has invalid field name: %q", name)
		}
		facet := opts == "facet"
		codec.byIndex = append(codec.byIndex, structTag{name: name, facet: facet, ignore: ignore})
		if facet {
			codec.facetByName[name] = i
		} else {
			codec.fieldByName[name] = i
		}
	}

	codecs[t] = codec
	return codec, nil
}

// structFLS adapts a struct to be a FieldLoadSaver.
type structFLS struct {
	v     reflect.Value
	codec *structCodec
}

func (s structFLS) Load(fields []Field, meta *DocumentMetadata) error {
	var err error
	for _, field := range fields {
		i, ok := s.codec.fieldByName[field.Name]
		if !ok {
			// Note the error, but keep going.
			err = &ErrFieldMismatch{
				FieldName: field.Name,
				Reason:    "no such struct field",
			}
			continue

		}
		f := s.v.Field(i)
		if !f.CanSet() {
			// Note the error, but keep going.
			err = &ErrFieldMismatch{
				FieldName: field.Name,
				Reason:    "cannot set struct field",
			}
			continue
		}
		v := reflect.ValueOf(field.Value)
		if ft, vt := f.Type(), v.Type(); ft != vt {
			err = &ErrFieldMismatch{
				FieldName: field.Name,
				Reason:    fmt.Sprintf("type mismatch: %v for %v data", ft, vt),
			}
			continue
		}
		f.Set(v)
	}
	if meta == nil {
		return err
	}
	for _, facet := range meta.Facets {
		i, ok := s.codec.facetByName[facet.Name]
		if !ok {
			// Note the error, but keep going.
			if err == nil {
				err = &ErrFacetMismatch{
					StructType: s.v.Type(),
					FacetName:  facet.Name,
					Reason:     "no matching field found",
				}
			}
			continue
		}
		f := s.v.Field(i)
		if !f.CanSet() {
			// Note the error, but keep going.
			if err == nil {
				err = &ErrFacetMismatch{
					StructType: s.v.Type(),
					FacetName:  facet.Name,
					Reason:     "unable to set unexported field of struct",
				}
			}
			continue
		}
		v := reflect.ValueOf(facet.Value)
		if ft, vt := f.Type(), v.Type(); ft != vt {
			if err == nil {
				err = &ErrFacetMismatch{
					StructType: s.v.Type(),
					FacetName:  facet.Name,
					Reason:     fmt.Sprintf("type mismatch: %v for %d data", ft, vt),
				}
				continue
			}
		}
		f.Set(v)
	}
	return err
}

func (s structFLS) Save() ([]Field, *DocumentMetadata, error) {
	fields := make([]Field, 0, len(s.codec.fieldByName))
	var facets []Facet
	for i, tag := range s.codec.byIndex {
		if tag.ignore {
			continue
		}
		f := s.v.Field(i)
		if !f.CanSet() {
			continue
		}
		if tag.facet {
			facets = append(facets, Facet{Name: tag.name, Value: f.Interface()})
		} else {
			fields = append(fields, Field{Name: tag.name, Value: f.Interface()})
		}
	}
	return fields, &DocumentMetadata{Facets: facets}, nil
}

// newStructFLS returns a FieldLoadSaver for the struct pointer p.
func newStructFLS(p interface{}) (FieldLoadSaver, error) {
	v := reflect.ValueOf(p)
	if v.Kind() != reflect.Ptr || v.IsNil() || v.Elem().Kind() != reflect.Struct {
		return nil, ErrInvalidDocumentType
	}
	codec, err := loadCodec(v.Elem().Type())
	if err != nil {
		return nil, err
	}
	return structFLS{v.Elem(), codec}, nil
}

func loadStructWithMeta(dst interface{}, f []Field, meta *DocumentMetadata) error {
	x, err := newStructFLS(dst)
	if err != nil {
		return err
	}
	return x.Load(f, meta)
}

func saveStructWithMeta(src interface{}) ([]Field, *DocumentMetadata, error) {
	x, err := newStructFLS(src)
	if err != nil {
		return nil, nil, err
	}
	return x.Save()
}

// LoadStruct loads the fields from f to dst. dst must be a struct pointer.
func LoadStruct(dst interface{}, f []Field) error {
	return loadStructWithMeta(dst, f, nil)
}

// SaveStruct returns the fields from src as a slice of Field.
// src must be a struct pointer.
func SaveStruct(src interface{}) ([]Field, error) {
	f, _, err := saveStructWithMeta(src)
	return f, err
}