package datastore

import (
	"encoding/json"
	"path"
	"strings"

	dsq "github.com/ipfs/go-datastore/query"

	"github.com/google/uuid"
)

/*
A Key represents the unique identifier of an object.
Our Key scheme is inspired by file systems and Google App Engine key model.

Keys are meant to be unique across a system. Keys are hierarchical,
incorporating more and more specific namespaces. Thus keys can be deemed
'children' or 'ancestors' of other keys::

    Key("/Comedy")
    Key("/Comedy/MontyPython")

Also, every namespace can be parametrized to embed relevant object
information. For example, the Key `name` (most specific namespace) could
include the object type::

    Key("/Comedy/MontyPython/Actor:JohnCleese")
    Key("/Comedy/MontyPython/Sketch:CheeseShop")
    Key("/Comedy/MontyPython/Sketch:CheeseShop/Character:Mousebender")

*/
type Key struct {
	string
}

// NewKey constructs a key from string. it will clean the value.
func NewKey(s string) Key {
	k := Key{s}
	k.Clean()
	return k
}

// RawKey creates a new Key without safety checking the input. Use with care.
func RawKey(s string) Key {
	// accept an empty string and fix it to avoid special cases
	// elsewhere
	if len(s) == 0 {
		return Key{"/"}
	}

	// perform a quick sanity check that the key is in the correct
	// format, if it is not then it is a programmer error and it is
	// okay to panic
	if len(s) == 0 || s[0] != '/' || (len(s) > 1 && s[len(s)-1] == '/') {
		panic("invalid datastore key: " + s)
	}

	return Key{s}
}

// KeyWithNamespaces constructs a key out of a namespace slice.
func KeyWithNamespaces(ns []string) Key {
	return NewKey(strings.Join(ns, "/"))
}

// Clean up a Key, using path.Clean.
func (k *Key) Clean() {
	switch {
	case len(k.string) == 0:
		k.string = "/"
	case k.string[0] == '/':
		k.string = path.Clean(k.string)
	default:
		k.string = path.Clean("/" + k.string)
	}
}

// Strings is the string value of Key
func (k Key) String() string {
	return k.string
}

// Bytes returns the string value of Key as a []byte
func (k Key) Bytes() []byte {
	return []byte(k.string)
}

// Equal checks equality of two keys
func (k Key) Equal(k2 Key) bool {
	return k.string == k2.string
}

// Less checks whether this key is sorted lower than another.
func (k Key) Less(k2 Key) bool {
	list1 := k.List()
	list2 := k2.List()
	for i, c1 := range list1 {
		if len(list2) < (i + 1) {
			return false
		}

		c2 := list2[i]
		if c1 < c2 {
			return true
		} else if c1 > c2 {
			return false
		}
		// c1 == c2, continue
	}

	// list1 is shorter or exactly the same.
	return len(list1) < len(list2)
}

// List returns the `list` representation of this Key.
//   NewKey("/Comedy/MontyPython/Actor:JohnCleese").List()
//   ["Comedy", "MontyPythong", "Actor:JohnCleese"]
func (k Key) List() []string {
	return strings.Split(k.string, "/")[1:]
}

// Reverse returns the reverse of this Key.
//   NewKey("/Comedy/MontyPython/Actor:JohnCleese").Reverse()
//   NewKey("/Actor:JohnCleese/MontyPython/Comedy")
func (k Key) Reverse() Key {
	l := k.List()
	r := make([]string, len(l))
	for i, e := range l {
		r[len(l)-i-1] = e
	}
	return KeyWithNamespaces(r)
}

// Namespaces returns the `namespaces` making up this Key.
//   NewKey("/Comedy/MontyPython/Actor:JohnCleese").Namespaces()
//   ["Comedy", "MontyPython", "Actor:JohnCleese"]
func (k Key) Namespaces() []string {
	return k.List()
}

// BaseNamespace returns the "base" namespace of this key (path.Base(filename))
//   NewKey("/Comedy/MontyPython/Actor:JohnCleese").BaseNamespace()
//   "Actor:JohnCleese"
func (k Key) BaseNamespace() string {
	n := k.Namespaces()
	return n[len(n)-1]
}

// Type returns the "type" of this key (value of last namespace).
//   NewKey("/Comedy/MontyPython/Actor:JohnCleese").Type()
//   "Actor"
func (k Key) Type() string {
	return NamespaceType(k.BaseNamespace())
}

// Name returns the "name" of this key (field of last namespace).
//   NewKey("/Comedy/MontyPython/Actor:JohnCleese").Name()
//   "JohnCleese"
func (k Key) Name() string {
	return NamespaceValue(k.BaseNamespace())
}

// Instance returns an "instance" of this type key (appends value to namespace).
//   NewKey("/Comedy/MontyPython/Actor").Instance("JohnClesse")
//   NewKey("/Comedy/MontyPython/Actor:JohnCleese")
func (k Key) Instance(s string) Key {
	return NewKey(k.string + ":" + s)
}

// Path returns the "path" of this key (parent + type).
//   NewKey("/Comedy/MontyPython/Actor:JohnCleese").Path()
//   NewKey("/Comedy/MontyPython/Actor")
func (k Key) Path() Key {
	s := k.Parent().string + "/" + NamespaceType(k.BaseNamespace())
	return NewKey(s)
}

// Parent returns the `parent` Key of this Key.
//   NewKey("/Comedy/MontyPython/Actor:JohnCleese").Parent()
//   NewKey("/Comedy/MontyPython")
func (k Key) Parent() Key {
	n := k.List()
	if len(n) == 1 {
		return RawKey("/")
	}
	return NewKey(strings.Join(n[:len(n)-1], "/"))
}

// Child returns the `child` Key of this Key.
//   NewKey("/Comedy/MontyPython").Child(NewKey("Actor:JohnCleese"))
//   NewKey("/Comedy/MontyPython/Actor:JohnCleese")
func (k Key) Child(k2 Key) Key {
	switch {
	case k.string == "/":
		return k2
	case k2.string == "/":
		return k
	default:
		return RawKey(k.string + k2.string)
	}
}

// ChildString returns the `child` Key of this Key -- string helper.
//   NewKey("/Comedy/MontyPython").ChildString("Actor:JohnCleese")
//   NewKey("/Comedy/MontyPython/Actor:JohnCleese")
func (k Key) ChildString(s string) Key {
	return NewKey(k.string + "/" + s)
}

// IsAncestorOf returns whether this key is a prefix of `other`
//   NewKey("/Comedy").IsAncestorOf("/Comedy/MontyPython")
//   true
func (k Key) IsAncestorOf(other Key) bool {
	// equivalent to HasPrefix(other, k.string + "/")

	if len(other.string) <= len(k.string) {
		// We're not long enough to be a child.
		return false
	}

	if k.string == "/" {
		// We're the root and the other key is longer.
		return true
	}

	// "other" starts with /k.string/
	return other.string[len(k.string)] == '/' && other.string[:len(k.string)] == k.string
}

// IsDescendantOf returns whether this key contains another as a prefix.
//   NewKey("/Comedy/MontyPython").IsDescendantOf("/Comedy")
//   true
func (k Key) IsDescendantOf(other Key) bool {
	return other.IsAncestorOf(k)
}

// IsTopLevel returns whether this key has only one namespace.
func (k Key) IsTopLevel() bool {
	return len(k.List()) == 1
}

// MarshalJSON implements the json.Marshaler interface,
// keys are represented as JSON strings
func (k Key) MarshalJSON() ([]byte, error) {
	return json.Marshal(k.String())
}

// UnmarshalJSON implements the json.Unmarshaler interface,
// keys will parse any value specified as a key to a string
func (k *Key) UnmarshalJSON(data []byte) error {
	var key string
	if err := json.Unmarshal(data, &key); err != nil {
		return err
	}
	*k = NewKey(key)
	return nil
}

// RandomKey returns a randomly (uuid) generated key.
//   RandomKey()
//   NewKey("/f98719ea086343f7b71f32ea9d9d521d")
func RandomKey() Key {
	return NewKey(strings.Replace(uuid.New().String(), "-", "", -1))
}

/*
A Key Namespace is like a path element.
A namespace can optionally include a type (delimited by ':')

    > NamespaceValue("Song:PhilosopherSong")
    PhilosopherSong
    > NamespaceType("Song:PhilosopherSong")
    Song
    > NamespaceType("Music:Song:PhilosopherSong")
    Music:Song
*/

// NamespaceType is the first component of a namespace. `foo` in `foo:bar`
func NamespaceType(namespace string) string {
	parts := strings.Split(namespace, ":")
	if len(parts) < 2 {
		return ""
	}
	return strings.Join(parts[0:len(parts)-1], ":")
}

// NamespaceValue returns the last component of a namespace. `baz` in `f:b:baz`
func NamespaceValue(namespace string) string {
	parts := strings.Split(namespace, ":")
	return parts[len(parts)-1]
}

// KeySlice attaches the methods of sort.Interface to []Key,
// sorting in increasing order.
type KeySlice []Key

func (p KeySlice) Len() int           { return len(p) }
func (p KeySlice) Less(i, j int) bool { return p[i].Less(p[j]) }
func (p KeySlice) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }

// EntryKeys
func EntryKeys(e []dsq.Entry) []Key {
	ks := make([]Key, len(e))
	for i, e := range e {
		ks[i] = NewKey(e.Key)
	}
	return ks
}
