package ipldgit

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"sync"

	cid "github.com/ipfs/go-cid"
	node "github.com/ipfs/go-ipld-format"
)

type Tree struct {
	entries     map[string]*TreeEntry
	size        int
	order       []string
	cid         cid.Cid
	rawData     []byte
	rawDataOnce sync.Once
}

type TreeEntry struct {
	name string
	Mode string  `json:"mode"`
	Hash cid.Cid `json:"hash"`
}

func (t *Tree) Cid() cid.Cid {
	return t.cid
}

func (t *Tree) String() string {
	return "[git tree object]"
}

func (t *Tree) GitSha() []byte {
	return cidToSha(t.cid)
}

func (t *Tree) Copy() node.Node {
	out := &Tree{
		entries: make(map[string]*TreeEntry),
		cid:     t.cid,
		size:    t.size,
		order:   t.order, // TODO: make a deep copy of this
	}

	for k, v := range t.entries {
		nv := *v
		out.entries[k] = &nv
	}
	return out
}

func (t *Tree) MarshalJSON() ([]byte, error) {
	return json.Marshal(t.entries)
}

func (t *Tree) Tree(p string, depth int) []string {
	if p != "" {
		_, ok := t.entries[p]
		if !ok {
			return nil
		}

		return []string{"mode", "type", "hash"}
	}

	if depth == 0 {
		return nil
	}

	if depth == 1 {
		return t.order
	}

	var out []string
	for k, _ := range t.entries {
		out = append(out, k, k+"/mode", k+"/type", k+"/hash")
	}
	return out
}

func (t *Tree) Links() []*node.Link {
	var out []*node.Link
	for _, v := range t.entries {
		out = append(out, &node.Link{Cid: v.Hash})
	}
	return out
}

func (t *Tree) Loggable() map[string]interface{} {
	return map[string]interface{}{
		"type": "git tree object",
	}
}

func (t *Tree) RawData() []byte {
	t.rawDataOnce.Do(func() {
		buf := new(bytes.Buffer)

		fmt.Fprintf(buf, "tree %d\x00", t.size)
		for _, s := range t.order {
			t.entries[s].WriteTo(buf)
		}
		t.rawData = buf.Bytes()
	})

	return t.rawData
}

func (t *Tree) Resolve(p []string) (interface{}, []string, error) {
	e, ok := t.entries[p[0]]
	if !ok {
		return nil, nil, errors.New("no such link")
	}

	if len(p) == 1 {
		return e, nil, nil
	}

	switch p[1] {
	case "hash":
		return &node.Link{Cid: e.Hash}, p[2:], nil
	case "mode":
		return e.Mode, p[2:], nil
	default:
		return nil, nil, errors.New("no such link")
	}
}

func (t Tree) ResolveLink(path []string) (*node.Link, []string, error) {
	out, rest, err := t.Resolve(path)
	if err != nil {
		return nil, nil, err
	}

	lnk, ok := out.(*node.Link)
	if !ok {
		return nil, nil, errors.New("not a link")
	}

	return lnk, rest, nil
}

func (t *Tree) Size() (uint64, error) {
	return uint64(len(t.RawData())), nil
}

func (t *Tree) Stat() (*node.NodeStat, error) {
	return &node.NodeStat{}, nil
}

func (te *TreeEntry) WriteTo(w io.Writer) (int, error) {
	n, err := fmt.Fprintf(w, "%s %s\x00", te.Mode, te.name)
	if err != nil {
		return 0, err
	}

	nn, err := w.Write(cidToSha(te.Hash))
	if err != nil {
		return n, err
	}

	return n + nn, nil
}

var _ node.Node = (*Tree)(nil)
