// Copyright 2013 Matthew Baird
// 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 elastigo

import (
	"encoding/json"
	"fmt"
	"reflect"
	"strings"
)

type Mapping map[string]MappingOptions

type MappingOptions struct {
	Id               IdOptions                `json:"_id"`
	Timestamp        TimestampOptions         `json:"_timestamp"`
	Analyzer         *AnalyzerOptions         `json:"_analyzer,omitempty"`
	Parent           *ParentOptions           `json:"_parent,omitempty"`
	Routing          *RoutingOptions          `json:"_routing,omitempty"`
	Size             *SizeOptions             `json:"_size,omitempty"`
	Source           *SourceOptions           `json:"_source,omitempty"`
	TTL              *TTLOptions              `json:"_ttl,omitempty"`
	Type             *TypeOptions             `json:"_type,omitempty"`
	Properties       map[string]interface{}   `json:"properties"`
	DynamicTemplates []map[string]interface{} `json:"dynamic_templates,omitempty"`
}

type TimestampOptions struct {
	Enabled bool `json:"enabled"`
}

type AnalyzerOptions struct {
	Path  string `json:"path,omitempty"`
	Index string `json:"index,omitempty"`
}

type ParentOptions struct {
	Type string `json:"type"`
}

type RoutingOptions struct {
	Required bool   `json:"required,omitempty"`
	Path     string `json:"path,omitempty"`
}

type SizeOptions struct {
	Enabled bool `json:"enabled,omitempty"`
	Store   bool `json:"store,omitempty"`
}

type SourceOptions struct {
	Enabled  bool     `json:"enabled,omitempty"`
	Includes []string `json:"includes,omitempty"`
	Excludes []string `json:"excludes,omitempty"`
}

type TypeOptions struct {
	Store bool   `json:"store,omitempty"`
	Index string `json:"index,omitempty"`
}

type TTLOptions struct {
	Enabled bool   `json:"enabled"`
	Default string `json:"default,omitempty"`
}

type IdOptions struct {
	Index string `json:"index,omitempty"`
	Path  string `json:"path,omitempty"`
}

func (m_ Mapping) Options() MappingOptions {
	m := map[string]MappingOptions(m_)
	for _, v := range m {
		return v
	}
	panic(fmt.Errorf("Malformed input: %v", m_))
}

func MappingForType(typeName string, opts MappingOptions) Mapping {
	return map[string]MappingOptions{typeName: opts}
}

func (c *Conn) PutMapping(index string, typeName string, instance interface{}, opt MappingOptions) error {
	instanceType := reflect.TypeOf(instance)
	if instanceType.Kind() != reflect.Struct {
		return fmt.Errorf("instance kind was not struct")
	}

	if opt.Properties == nil {
		opt.Properties = make(map[string]interface{})
	}
	getProperties(instanceType, opt.Properties)
	body, err := json.Marshal(MappingForType(typeName, opt))
	if err != nil {
		return err
	}
	_, err = c.DoCommand("PUT", fmt.Sprintf("/%s/%s/_mapping", index, typeName), nil, string(body))
	if err != nil {
		return err
	}

	return nil
}

//Same as PutMapping, but takes a []byte for mapping and provides no check of structure
func (c *Conn) PutMappingFromJSON(index string, typeName string, mapping []byte) error {
	_, err := c.DoCommand("PUT", fmt.Sprintf("/%s/%s/_mapping", index, typeName), nil, string(mapping))
	return err
}

func getProperties(t reflect.Type, prop map[string]interface{}) {
	n := t.NumField()
	for i := 0; i < n; i++ {
		field := t.Field(i)

		name := strings.Split(field.Tag.Get("json"), ",")[0]
		if name == "-" {
			continue
		} else if name == "" {
			name = field.Name
		}

		attrMap := make(map[string]interface{})
		attrs := splitTag(field.Tag.Get("elastic"))
		for _, attr := range attrs {
			keyvalue := strings.Split(attr, ":")
			attrMap[keyvalue[0]] = keyvalue[1]
		}

		if len(attrMap) == 0 || attrMap["type"] == "nested" {

			// We are looking for tags on any inner struct, independently of
			// whether the field is a struct, a pointer to struct, or a slice of structs
			targetType := field.Type
			if targetType.Kind() == reflect.Ptr ||
				targetType.Kind() == reflect.Slice {
				targetType = field.Type.Elem()
			}
			if targetType.Kind() == reflect.Struct {
				if field.Anonymous {
					getProperties(targetType, prop)
				} else {
					innerStructProp := make(map[string]interface{})
					getProperties(targetType, innerStructProp)
					attrMap["properties"] = innerStructProp
				}
			}
		}
		if len(attrMap) != 0 {
			prop[name] = attrMap
		}
	}
}

func splitTag(tag string) []string {
	tag = strings.Trim(tag, " ")
	if tag == "" {
		return []string{}
	} else {
		return strings.Split(tag, ",")
	}
}
