// Copyright © 2016 Abcum Ltd
//
// 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 data

import (
	"fmt"
	"reflect"
	"strconv"
	"strings"

	"encoding/json"

	"github.com/abcum/surreal/util/dupe"
	"github.com/abcum/surreal/util/pack"
)

const (
	one int8 = iota
	many
)

const (
	choose int8 = iota
	remove
)

// Doc holds a reference to the core data object, or a selected path.
type Doc struct {
	data interface{}
}

// Fetcher is used when fetching values.
type Fetcher func(key string, val interface{}, path []string) interface{}

// Iterator is used when iterating over items.
type Iterator func(key string, val interface{}) error

// Walker is used when walking over items.
type Walker func(key string, val interface{}, exi bool) error

// New creates a new data object.
func New() *Doc {
	return &Doc{data: map[string]interface{}{}}
}

// Consume converts a GO interface into a data object.
func Consume(input interface{}) *Doc {
	return &Doc{data: input}
}

// Data returns the internal data object as an interface.
func (d *Doc) Data() interface{} {
	return d.data
}

// Copy returns a duplicated copy of the internal data object.
func (d *Doc) Copy() *Doc {
	return Consume(dupe.Duplicate(d.data))
}

// Encode encodes the data object to a byte slice.
func (d *Doc) Encode() (dst []byte) {
	dst = pack.Encode(d.data)
	return
}

// Decode decodes the byte slice into a data object.
func (d *Doc) Decode(src []byte) *Doc {
	pack.Decode(src, &d.data)
	return d
}

func (d *Doc) MarshalJSON() ([]byte, error) {
	return json.Marshal(d.Data())
}

func (d *Doc) UnmarshalJSON(data []byte) error {
	return json.Unmarshal(data, &d.data)
}

// --------------------------------------------------------------------------------

func (d *Doc) path(path ...string) (paths []string) {

	l := len(path)
	for _, p := range path {
		l += strings.Count(p, ".")
		l += strings.Count(p, "[")
	}

	paths = make([]string, 0, l)

	for _, p := range path {
		for j, i, o := 0, 0, false; i < len(p); i++ {
			switch {
			case i == len(p)-1:
				if len(p[j:]) > 0 {
					paths = append(paths, p[j:])
				}
			case p[i] == '.':
				if len(p[j:i]) > 0 {
					paths = append(paths, p[j:i])
				}
				j, i = i+1, i+0
			case p[i] == '[':
				if len(p[j:i]) > 0 {
					paths = append(paths, p[j:i])
				}
				j, i, o = i, i+1, true
			case p[i] == ']' && o:
				if len(p[j:i+1]) > 0 {
					paths = append(paths, p[j:i+1])
				}
				j, i, o = i+1, i+0, false
			}
		}
	}

	return

}

func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}

func max(a, b int) int {
	if a > b {
		return a
	}
	return b
}

func trim(s string) string {
	if s[0] == '[' && s[len(s)-1] == ']' {
		return s[1 : len(s)-1]
	}
	return s
}

func (d *Doc) what(p string, a []interface{}, t int8) (o []interface{}, i []int, r int8) {

	p = trim(p)

	i = []int{}

	o = []interface{}{}

	// If there are no items in the
	// original array, then return
	// an empty array immediately.

	if len(a) == 0 {
		if strings.ContainsAny(p, ":*") {
			return o, i, many
		} else {
			return o, i, one
		}
	}

	// If the array index is a star
	// or a colon only, then return
	// the full array immediately

	if p == "*" || p == ":" {
		switch t {
		case choose:
			for k := range a {
				i = append(i, k)
			}
			return a, i, many
		case remove:
			for k := range a {
				i = append(i, k)
			}
			return o, i, many
		}
	}

	// Split the specified array index
	// by colons, so that we can get
	// the specified array items.

	c := strings.Count(p, ":")

	if c == 0 {

		switch p {
		case "0", "first":
			if t == choose {
				i = append(i, 0)
				o = append(o, a[0])
			} else {
				for k := range a[1:] {
					i = append(i, k)
				}
				o = append(o, a[1:]...)
			}
		case "$", "last":
			if t == choose {
				i = append(i, len(a)-1)
				o = append(o, a[len(a)-1])
			} else {
				for k := range a[:len(a)-1] {
					i = append(i, k)
				}
				o = append(o, a[:len(a)-1]...)
			}
		default:
			if z, e := strconv.Atoi(p); e == nil {
				if len(a) > z {
					switch t {
					case choose:
						i = append(i, z)
						o = append(o, a[z])
					case remove:
						for k := range append(a[:z], a[z+1:]...) {
							i = append(i, k)
						}
						o = append(o, append(a[:z], a[z+1:]...)...)
					}
				} else {
					switch t {
					case remove:
						for k := range a {
							i = append(i, k)
						}
						o = append(o, a[:]...)
					}
				}
			}
		}

		return o, i, one

	}

	if c == 1 {

		var e error
		var s, f int

		b := []int{0, len(a)}
		x := strings.Split(p, ":")

		for k := range x {
			switch x[k] {
			case "":
			case "0", "first":
				b[k] = 0
			case "$", "last":
				b[k] = len(a)
			default:
				if b[k], e = strconv.Atoi(x[k]); e != nil {
					return nil, nil, many
				}
			}
		}

		s = b[0]
		s = max(s, 0)
		s = min(s, len(a))

		f = b[1]
		f = max(f, 0)
		f = min(f, len(a))

		if t == choose {
			for k, v := range a[s:f] {
				i = append(i, k)
				o = append(o, v)
			}
		} else {
			for k, v := range append(a[:s], a[f+1:]...) {
				i = append(i, k)
				o = append(o, v)
			}
		}

		return o, i, many

	}

	return nil, nil, many

}

// --------------------------------------------------------------------------------

// Reset empties and resets the data at the specified path.
func (d *Doc) Reset(path ...string) (*Doc, error) {
	return d.Set(map[string]interface{}{}, path...)
}

// Valid checks whether the value at the specified path is nil.
func (d *Doc) Valid(path ...string) bool {
	if !d.Exists(path...) {
		return false
	}
	return d.Get(path...).Data() != nil
}

// Array sets the specified path to an array.
func (d *Doc) Array(path ...string) (*Doc, error) {
	return d.Set([]interface{}{}, path...)
}

// Object sets the specified path to an object.
func (d *Doc) Object(path ...string) (*Doc, error) {
	return d.Set(map[string]interface{}{}, path...)
}

// --------------------------------------------------------------------------------

// New sets the value at the specified path if it does not exist.
func (d *Doc) New(value interface{}, path ...string) (*Doc, error) {
	if !d.Exists(path...) {
		return d.Set(value, path...)
	}
	return d.Get(path...), nil
}

// Iff sets the value at the specified path if it is not nil, or deletes it.
func (d *Doc) Iff(value interface{}, path ...string) (*Doc, error) {
	if value != nil {
		return d.Set(value, path...)
	}
	return &Doc{data: nil}, d.Del(path...)
}

// Keys retrieves the object keys at the specified path.
func (d *Doc) Keys(path ...string) *Doc {

	path = d.path(path...)

	out := []interface{}{}

	if m, ok := d.Get(path...).Data().(map[string]interface{}); ok {
		for k := range m {
			out = append(out, k)
		}
	}

	return &Doc{data: out}

}

// Vals retrieves the object values at the specified path.
func (d *Doc) Vals(path ...string) *Doc {

	path = d.path(path...)

	out := []interface{}{}

	if m, ok := d.Get(path...).Data().(map[string]interface{}); ok {
		for _, v := range m {
			out = append(out, v)
		}
	}

	return &Doc{data: out}

}

// --------------------------------------------------------------------------------

// Exists checks whether the specified path exists.
func (d *Doc) Exists(path ...string) bool {

	path = d.path(path...)

	// If the value found at the current
	// path part is undefined, then just
	// return false immediately

	if d.data == nil {
		return false
	}

	// Define the temporary object so
	// that we can loop over and traverse
	// down the path parts of the data

	object := d.data

	// Loop over each part of the path
	// whilst detecting if the data at
	// the current path is an {} or []

	for k, p := range path {

		p = trim(p)

		// If the value found at the current
		// path part is an object, then move
		// to the next part of the path

		if m, ok := object.(map[string]interface{}); ok {
			if object, ok = m[p]; !ok {
				return false
			}
			continue
		}

		// If the value found at the current
		// path part is an array, then perform
		// the query on the specified items

		if a, ok := object.([]interface{}); ok {

			c, _, r := d.what(p, a, choose)

			if len(c) == 0 {
				return false
			}

			if r == one {
				return Consume(c[0]).Exists(path[k+1:]...)
			}

			if r == many {
				for _, v := range c {
					if !Consume(v).Exists(path[k+1:]...) {
						return false
					}
				}
				return true
			}

		}

		return false

	}

	return true

}

// --------------------------------------------------------------------------------

// Get gets the value or values at a specified path.
func (d *Doc) Get(path ...string) *Doc {
	return d.Fetch(nil, path...)
}

// Fetch gets the value or values at a specified path, allowing for a custom fetch method.
func (d *Doc) Fetch(call Fetcher, path ...string) *Doc {

	path = d.path(path...)

	// If the value found at the current
	// path part is undefined, then just
	// return false immediately

	if d.data == nil {
		return &Doc{data: nil}
	}

	// Define the temporary object so
	// that we can loop over and traverse
	// down the path parts of the data

	object := d.data

	// Loop over each part of the path
	// whilst detecting if the data at
	// the current path is an {} or []

	for k, p := range path {

		p = trim(p)

		// If the value found at the current
		// path part is an object, then move
		// to the next part of the path

		if m, ok := object.(map[string]interface{}); ok {
			switch p {
			default:
				if call != nil {
					object = call(p, m[p], path[k+1:])
				} else {
					object = m[p]
				}
			case "*":
				object = m
			}
			continue
		}

		// If the value found at the current
		// path part is an array, then perform
		// the query on the specified items

		if a, ok := object.([]interface{}); ok {

			c, _, r := d.what(p, a, choose)

			if len(c) == 0 {
				switch r {
				case one:
					return &Doc{data: nil}
				case many:
					return &Doc{data: []interface{}{}}
				}
			}

			if r == one {
				if call != nil {
					c[0] = call(p, c[0], path[k+1:])
				}
				return Consume(c[0]).Fetch(call, path[k+1:]...)
			}

			if r == many {
				out := []interface{}{}
				for _, v := range c {
					if call != nil {
						v = call(p, v, path[k+1:])
					}
					res := Consume(v).Fetch(call, path[k+1:]...)
					out = append(out, res.data)
				}
				return &Doc{data: out}
			}

		}

		return &Doc{data: nil}

	}

	return &Doc{data: object}

}

// --------------------------------------------------------------------------------

// Set sets the value or values at a specified path.
func (d *Doc) Set(value interface{}, path ...string) (*Doc, error) {

	path = d.path(path...)

	if len(path) == 0 {
		d.data = value
		return d, nil
	}

	// If the value found at the current
	// path part is undefined, then ensure
	// that it is an object

	if d.data == nil {
		d.data = map[string]interface{}{}
	}

	// Define the temporary object so
	// that we can loop over and traverse
	// down the path parts of the data

	object := d.data

	// Loop over each part of the path
	// whilst detecting if the data at
	// the current path is an {} or []

	for k, p := range path {

		p = trim(p)

		// If the value found at the current
		// path part is an object, then move
		// to the next part of the path

		if m, ok := object.(map[string]interface{}); ok {
			if k == len(path)-1 {
				m[p] = value
			} else if m[p] == nil {
				m[p] = map[string]interface{}{}
			}
			object = m[p]
			continue
		}

		// If the value found at the current
		// path part is an array, then perform
		// the query on the specified items

		if a, ok := object.([]interface{}); ok {

			c, i, r := d.what(p, a, choose)

			if len(c) == 0 {
				switch r {
				case one:
					return &Doc{data: nil}, nil
				case many:
					return &Doc{data: []interface{}{}}, nil
				}
			}

			if r == one {
				if k == len(path)-1 {
					a[i[0]] = value
					object = a[i[0]]
					continue
				} else {
					return Consume(a[i[0]]).Set(value, path[k+1:]...)
				}
			}

			if r == many {
				out := []interface{}{}
				for j, v := range c {
					if k == len(path)-1 {
						a[i[j]] = value
						out = append(out, value)
					} else {
						res, _ := Consume(v).Set(value, path[k+1:]...)
						if res.data != nil {
							out = append(out, res.data)
						}
					}
				}
				return &Doc{data: out}, nil
			}

		}

	}

	return &Doc{data: object}, nil

}

// --------------------------------------------------------------------------------

// Del deletes the value or values at a specified path.
func (d *Doc) Del(path ...string) error {

	path = d.path(path...)

	// If the value found at the current
	// path part is undefined, then return
	// a not an object error

	if d.data == nil {
		return fmt.Errorf("Item is not an object")
	}

	// Define the temporary object so
	// that we can loop over and traverse
	// down the path parts of the data

	object := d.data

	// Loop over each part of the path
	// whilst detecting if the data at
	// the current path is an {} or []

	for k, p := range path {

		p = trim(p)

		// If the value found at the current
		// path part is an object, then move
		// to the next part of the path

		if m, ok := object.(map[string]interface{}); ok {
			if k == len(path)-1 {
				delete(m, p)
			} else if m[p] == nil {
				return fmt.Errorf("Item at path %s is not an object", path)
			}
			object = m[p]
			continue
		}

		// If the value found at the current
		// path part is an array, then perform
		// the query on the specified items

		if a, ok := object.([]interface{}); ok {

			var r int8
			var c []interface{}

			if k == len(path)-1 {
				c, _, r = d.what(p, a, remove)
			} else {
				c, _, r = d.what(p, a, choose)
			}

			if r == one {
				if k == len(path)-1 {
					d.Set(c, path[:len(path)-1]...)
					continue
				} else {
					if len(c) != 0 {
						return Consume(c[0]).Del(path[k+1:]...)
					}
				}
			}

			if r == many {
				if k == len(path)-1 {
					d.Set(c, path[:len(path)-1]...)
					continue
				} else {
					for _, v := range c {
						Consume(v).Del(path[k+1:]...)
					}
					break
				}
			}

		}

	}

	return nil

}

// --------------------------------------------------------------------------------

// Append appends an item or an array of items to an array at the specified path.
func (d *Doc) Append(value interface{}, path ...string) (*Doc, error) {

	a, ok := d.Get(path...).Data().([]interface{})
	if !ok {
		return &Doc{data: nil}, fmt.Errorf("Not an array")
	}

	a = append(a, value)

	return d.Set(a, path...)

}

// ArrayAdd appends an item or an array of items to an array at the specified path.
func (d *Doc) ArrayAdd(value interface{}, path ...string) (*Doc, error) {

	a, ok := d.Get(path...).Data().([]interface{})
	if !ok {
		return &Doc{data: nil}, fmt.Errorf("Not an array")
	}

	if values, ok := value.([]interface{}); ok {
	outer:
		for _, value := range values {
			for _, v := range a {
				if reflect.DeepEqual(v, value) {
					continue outer
				}
			}
			a = append(a, value)
		}
	} else {
		for _, v := range a {
			if reflect.DeepEqual(v, value) {
				return Consume(a), nil
			}
		}
		a = append(a, value)
	}

	return d.Set(a, path...)

}

// ArrayDel deletes an item or an array of items from an array at the specified path.
func (d *Doc) ArrayDel(value interface{}, path ...string) (*Doc, error) {

	a, ok := d.Get(path...).Data().([]interface{})
	if !ok {
		return &Doc{data: nil}, fmt.Errorf("Not an array")
	}

	if values, ok := value.([]interface{}); ok {
		for _, value := range values {
			for i := len(a) - 1; i >= 0; i-- {
				v := a[i]
				if reflect.DeepEqual(v, value) {
					copy(a[i:], a[i+1:])
					a[len(a)-1] = nil
					a = a[:len(a)-1]
				}
			}
		}
	} else {
		for i := len(a) - 1; i >= 0; i-- {
			v := a[i]
			if reflect.DeepEqual(v, value) {
				copy(a[i:], a[i+1:])
				a[len(a)-1] = nil
				a = a[:len(a)-1]
			}
		}
	}

	return d.Set(a, path...)

}

// --------------------------------------------------------------------------------

// Contains checks whether the value exists within the array at the specified path.
func (d *Doc) Contains(value interface{}, path ...string) bool {

	a, ok := d.Get(path...).Data().([]interface{})
	if !ok {
		return false
	}

	for _, v := range a {
		if reflect.DeepEqual(v, value) {
			return true
		}
	}

	return false

}

// --------------------------------------------------------------------------------

// Inc increments an item, or appends an item to an array at the specified path.
func (d *Doc) Inc(value interface{}, path ...string) (*Doc, error) {

	switch cur := d.Get(path...).Data().(type) {
	case nil:
		switch inc := value.(type) {
		case int64:
			return d.Set(0+inc, path...)
		case float64:
			return d.Set(0+inc, path...)
		default:
			switch value.(type) {
			case []interface{}:
				return d.Set(value, path...)
			default:
				return d.Set([]interface{}{value}, path...)
			}
		}
	case int64:
		switch inc := value.(type) {
		case int64:
			return d.Set(cur+inc, path...)
		case float64:
			return d.Set(float64(cur)+inc, path...)
		}
	case float64:
		switch inc := value.(type) {
		case int64:
			return d.Set(cur+float64(inc), path...)
		case float64:
			return d.Set(cur+inc, path...)
		}
	case []interface{}:
		return d.ArrayAdd(value, path...)
	}

	return &Doc{data: nil}, fmt.Errorf("Not possible to increment.")

}

// Dec decrements an item, or removes an item from an array at the specified path.
func (d *Doc) Dec(value interface{}, path ...string) (*Doc, error) {

	switch cur := d.Get(path...).Data().(type) {
	case nil:
		switch inc := value.(type) {
		case int64:
			return d.Set(0-inc, path...)
		case float64:
			return d.Set(0-inc, path...)
		}
	case int64:
		switch inc := value.(type) {
		case int64:
			return d.Set(cur-inc, path...)
		case float64:
			return d.Set(float64(cur)-inc, path...)
		}
	case float64:
		switch inc := value.(type) {
		case int64:
			return d.Set(cur-float64(inc), path...)
		case float64:
			return d.Set(cur-inc, path...)
		}
	case []interface{}:
		return d.ArrayDel(value, path...)
	}

	return &Doc{data: nil}, fmt.Errorf("Not possible to decrement.")

}

// --------------------------------------------------------------------------------

func (d *Doc) Same(n *Doc) bool {

	switch a := d.data.(type) {
	case []interface{}:
		switch b := n.data.(type) {
		case map[string]interface{}:
			return false
		case []interface{}:
			if len(a) != len(b) {
				return false
			}
			break
		}
	case map[string]interface{}:
		switch b := n.data.(type) {
		case []interface{}:
			return false
		case map[string]interface{}:
			if len(a) != len(b) {
				return false
			}
			break
		}
	}

	return reflect.DeepEqual(d, n)

}

func (d *Doc) Diff(n *Doc) map[string]interface{} {

	var initial = make(map[string]interface{})
	var current = make(map[string]interface{})
	var changes = make(map[string]interface{})

	d.Each(func(key string, val interface{}) error {
		initial[key] = val
		return nil
	})

	n.Each(func(key string, val interface{}) error {
		current[key] = val
		return nil
	})

	for k, v := range current {
		if o, ok := initial[k]; ok {
			if reflect.DeepEqual(o, v) {
				continue
			}
		}
		changes[k] = v
	}

	for k := range initial {
		if _, ok := current[k]; !ok {
			changes[k] = nil
		}
	}

	return changes

}

// --------------------------------------------------------------------------------

func (d *Doc) join(parts ...[]string) string {
	var path []string
	for _, part := range parts {
		path = append(path, part...)
	}
	return strings.Join(path, ".")
}

// --------------------------------------------------------------------------------

// Each loops through the values in the data doc.
func (d *Doc) Each(exec Iterator) error {

	return d.each(exec, nil)

}

func (d *Doc) each(exec Iterator, prev []string) error {

	// Define the temporary object so
	// that we can loop over and traverse
	// down the path parts of the data

	object := d.data

	// If the value found at the current
	// path part is an object, then move
	// to the next part of the path

	if m, ok := object.(map[string]interface{}); ok {
		exec(d.join(prev), make(map[string]interface{}))
		for k, v := range m {
			var keep []string
			keep = append(keep, prev...)
			keep = append(keep, k)
			Consume(v).each(exec, keep)
		}
		return nil
	}

	// If the value found at the current
	// path part is an array, then perform
	// the query on the specified items

	if a, ok := object.([]interface{}); ok {
		exec(d.join(prev), make([]interface{}, len(a)))
		for i, v := range a {
			var keep []string
			keep = append(keep, prev...)
			keep = append(keep, fmt.Sprintf("[%d]", i))
			Consume(v).each(exec, keep)
		}
		return nil
	}

	return exec(d.join(prev), object)

}

// --------------------------------------------------------------------------------

// Walk walks the value or values at a specified path.
func (d *Doc) Walk(exec Walker, path ...string) error {

	path = d.path(path...)

	return d.walk(exec, nil, path...)

}

func (d *Doc) walk(exec Walker, prev []string, path ...string) error {

	if len(path) == 0 {
		return nil
	}

	// If the value found at the current
	// path part is undefined, then ensure
	// that it is an object

	if d.data == nil {
		d.data = map[string]interface{}{}
	}

	// Define the temporary object so
	// that we can loop over and traverse
	// down the path parts of the data

	object := d.data

	// Loop over each part of the path
	// whilst detecting if the data at
	// the current path is an {} or []

	for k, p := range path {

		p = trim(p)

		// If the value found at the current
		// path part is an object, then move
		// to the next part of the path

		if m, ok := object.(map[string]interface{}); ok {
			if object, ok = m[p]; !ok {
				return exec(d.join(prev, path), nil, false)
			}
			continue
		}

		// If the value found at the current
		// path part is an array, then perform
		// the query on the specified items

		if a, ok := object.([]interface{}); ok {

			c, i, r := d.what(p, a, choose)

			if r == one && len(c) == 0 {
				return nil
			}

			if r == one {
				if k == len(path)-1 {
					var keep []string
					keep = append(keep, prev...)
					keep = append(keep, path[:k]...)
					keep = append(keep, fmt.Sprintf("[%d]", i[0]))
					return exec(d.join(keep), c[0], true)
				} else {
					var keep []string
					keep = append(keep, prev...)
					keep = append(keep, path[:k]...)
					keep = append(keep, fmt.Sprintf("[%d]", i[0]))
					return Consume(c[0]).walk(exec, keep, path[k+1:]...)
				}
			}

			if r == many {
				for j, v := range c {
					if k == len(path)-1 {
						var keep []string
						keep = append(keep, prev...)
						keep = append(keep, path[:k]...)
						keep = append(keep, fmt.Sprintf("[%d]", i[j]))
						if err := exec(d.join(keep), v, true); err != nil {
							return err
						}
					} else {
						var keep []string
						keep = append(keep, prev...)
						keep = append(keep, path[:k]...)
						keep = append(keep, fmt.Sprintf("[%d]", i[j]))
						if err := Consume(v).walk(exec, keep, path[k+1:]...); err != nil {
							return err
						}
					}
				}
				return nil
			}

		}

		// The current path item is not an object or an array
		// but there are still other items in the search path.

		return nil

	}

	return exec(d.join(prev, path), object, true)

}