// package fuse/ipns implements a fuse filesystem that interfaces
// with ipns, the naming system for ipfs.
package ipns

import (
	"errors"
	"io"
	"os"
	"path/filepath"
	"time"

	fuse "github.com/jbenet/go-ipfs/Godeps/_workspace/src/bazil.org/fuse"
	fs "github.com/jbenet/go-ipfs/Godeps/_workspace/src/bazil.org/fuse/fs"
	"github.com/jbenet/go-ipfs/Godeps/_workspace/src/code.google.com/p/go.net/context"
	proto "github.com/jbenet/go-ipfs/Godeps/_workspace/src/code.google.com/p/goprotobuf/proto"
	eventlog "github.com/jbenet/go-ipfs/thirdparty/eventlog"

	core "github.com/jbenet/go-ipfs/core"
	chunk "github.com/jbenet/go-ipfs/importer/chunk"
	mdag "github.com/jbenet/go-ipfs/merkledag"
	nsys "github.com/jbenet/go-ipfs/namesys"
	ci "github.com/jbenet/go-ipfs/p2p/crypto"
	path "github.com/jbenet/go-ipfs/path"
	ft "github.com/jbenet/go-ipfs/unixfs"
	uio "github.com/jbenet/go-ipfs/unixfs/io"
	ftpb "github.com/jbenet/go-ipfs/unixfs/pb"
	u "github.com/jbenet/go-ipfs/util"
	lgbl "github.com/jbenet/go-ipfs/util/eventlog/loggables"
)

const IpnsReadonly = true

var log = eventlog.Logger("fuse/ipns")

var (
	shortRepublishTimeout = time.Millisecond * 5
	longRepublishTimeout  = time.Millisecond * 500
)

// InitializeKeyspace sets the ipns record for the given key to
// point to an empty directory.
func InitializeKeyspace(n *core.IpfsNode, key ci.PrivKey) error {
	emptyDir := &mdag.Node{Data: ft.FolderPBData()}
	nodek, err := n.DAG.Add(emptyDir)
	if err != nil {
		return err
	}

	err = n.Pinning.Pin(emptyDir, false)
	if err != nil {
		return err
	}

	err = n.Pinning.Flush()
	if err != nil {
		return err
	}

	pub := nsys.NewRoutingPublisher(n.Routing)
	err = pub.Publish(n.Context(), key, nodek)
	if err != nil {
		return err
	}

	return nil
}

// FileSystem is the readwrite IPNS Fuse Filesystem.
type FileSystem struct {
	Ipfs     *core.IpfsNode
	RootNode *Root
}

// NewFileSystem constructs new fs using given core.IpfsNode instance.
func NewFileSystem(ipfs *core.IpfsNode, sk ci.PrivKey, ipfspath string) (*FileSystem, error) {
	root, err := CreateRoot(ipfs, []ci.PrivKey{sk}, ipfspath)
	if err != nil {
		return nil, err
	}
	return &FileSystem{Ipfs: ipfs, RootNode: root}, nil
}

func CreateRoot(n *core.IpfsNode, keys []ci.PrivKey, ipfsroot string) (*Root, error) {
	root := new(Root)
	root.LocalDirs = make(map[string]*Node)
	root.Ipfs = n
	abspath, err := filepath.Abs(ipfsroot)
	if err != nil {
		return nil, err
	}
	root.IpfsRoot = abspath

	root.Keys = keys

	if len(keys) == 0 {
		log.Warning("No keys given for ipns root creation")
	} else {
		k := keys[0]
		pub := k.GetPublic()
		hash, err := pub.Hash()
		if err != nil {
			log.Errorf("Read Root Error: %s", err)
			return nil, err
		}
		root.LocalLink = &Link{u.Key(hash).Pretty()}
	}

	for _, k := range keys {
		hash, err := k.GetPublic().Hash()
		if err != nil {
			log.Error("failed to hash public key.")
			continue
		}
		name := u.Key(hash).Pretty()
		nd := new(Node)
		nd.Ipfs = n
		nd.key = k
		nd.repub = NewRepublisher(nd, shortRepublishTimeout, longRepublishTimeout)

		go nd.repub.Run()

		pointsTo, err := n.Namesys.Resolve(n.Context(), name)
		if err != nil {
			log.Warning("Could not resolve value for local ipns entry, providing empty dir")
			nd.Nd = &mdag.Node{Data: ft.FolderPBData()}
			root.LocalDirs[name] = nd
			continue
		}

		if !u.IsValidHash(pointsTo.B58String()) {
			log.Criticalf("Got back bad data from namesys resolve! [%s]", pointsTo)
			return nil, nil
		}

		node, err := n.Resolver.ResolvePath(path.Path(pointsTo.B58String()))
		if err != nil {
			log.Warning("Failed to resolve value from ipns entry in ipfs")
			continue
		}

		nd.Nd = node
		root.LocalDirs[name] = nd
	}

	return root, nil
}

// Root constructs the Root of the filesystem, a Root object.
func (f FileSystem) Root() (fs.Node, fuse.Error) {
	return f.RootNode, nil
}

// Root is the root object of the filesystem tree.
type Root struct {
	Ipfs *core.IpfsNode
	Keys []ci.PrivKey

	// Used for symlinking into ipfs
	IpfsRoot  string
	LocalDirs map[string]*Node

	LocalLink *Link
}

// Attr returns file attributes.
func (*Root) Attr() fuse.Attr {
	return fuse.Attr{Mode: os.ModeDir | 0111} // -rw+x
}

// Lookup performs a lookup under this node.
func (s *Root) Lookup(name string, intr fs.Intr) (fs.Node, fuse.Error) {
	log.Debugf("ipns: Root Lookup: '%s'", name)
	switch name {
	case "mach_kernel", ".hidden", "._.":
		// Just quiet some log noise on OS X.
		return nil, fuse.ENOENT
	}

	if name == "local" {
		if s.LocalLink == nil {
			return nil, fuse.ENOENT
		}
		return s.LocalLink, nil
	}

	nd, ok := s.LocalDirs[name]
	if ok {
		return nd, nil
	}

	log.Debugf("ipns: Falling back to resolution for [%s].", name)
	resolved, err := s.Ipfs.Namesys.Resolve(s.Ipfs.Context(), name)
	if err != nil {
		log.Warningf("ipns: namesys resolve error: %s", err)
		return nil, fuse.ENOENT
	}

	return &Link{s.IpfsRoot + "/" + resolved.B58String()}, nil
}

// ReadDir reads a particular directory. Disallowed for root.
func (r *Root) ReadDir(intr fs.Intr) ([]fuse.Dirent, fuse.Error) {
	log.Debug("Read Root.")
	listing := []fuse.Dirent{
		fuse.Dirent{
			Name: "local",
			Type: fuse.DT_Link,
		},
	}
	for _, k := range r.Keys {
		pub := k.GetPublic()
		hash, err := pub.Hash()
		if err != nil {
			log.Errorf("Read Root Error: %s", err)
			continue
		}
		ent := fuse.Dirent{
			Name: u.Key(hash).Pretty(),
			Type: fuse.DT_Dir,
		}
		listing = append(listing, ent)
	}
	return listing, nil
}

// Node is the core object representing a filesystem tree node.
type Node struct {
	root   *Root
	nsRoot *Node
	parent *Node

	repub *Republisher

	// This nodes name in its parent dir.
	// NOTE: this strategy wont work well if we allow hard links
	// (im all for murdering the thought of hard links)
	name string

	// Private keys held by nodes at the root of a keyspace
	// WARNING(security): the PrivKey interface is currently insecure
	// (holds the raw key). It will be secured later.
	key ci.PrivKey

	Ipfs   *core.IpfsNode
	Nd     *mdag.Node
	dagMod *uio.DagModifier
	cached *ftpb.Data
}

func (s *Node) loadData() error {
	s.cached = new(ftpb.Data)
	return proto.Unmarshal(s.Nd.Data, s.cached)
}

// Attr returns the attributes of a given node.
func (s *Node) Attr() fuse.Attr {
	if s.cached == nil {
		err := s.loadData()
		if err != nil {
			log.Errorf("Error loading PBData for file: '%s'", s.name)
		}
	}
	switch s.cached.GetType() {
	case ftpb.Data_Directory:
		return fuse.Attr{Mode: os.ModeDir | 0555}
	case ftpb.Data_File, ftpb.Data_Raw:
		size, err := ft.DataSize(s.Nd.Data)
		if err != nil {
			log.Errorf("Error getting size of file: %s", err)
			size = 0
		}
		if size == 0 {
			size = s.dagMod.Size()
		}

		mode := os.FileMode(0666)
		if IpnsReadonly {
			mode = 0444
		}

		return fuse.Attr{
			Mode:   mode,
			Size:   size,
			Blocks: uint64(len(s.Nd.Links)),
		}
	default:
		log.Error("Invalid data type.")
		return fuse.Attr{}
	}
}

// Lookup performs a lookup under this node.
func (s *Node) Lookup(name string, intr fs.Intr) (fs.Node, fuse.Error) {
	log.Debugf("ipns: node[%s] Lookup '%s'", s.name, name)
	nd, err := s.Ipfs.Resolver.ResolveLinks(s.Nd, []string{name})
	if err != nil {
		// todo: make this error more versatile.
		return nil, fuse.ENOENT
	}

	return s.makeChild(name, nd), nil
}

func (n *Node) makeChild(name string, node *mdag.Node) *Node {
	child := &Node{
		Ipfs:   n.Ipfs,
		Nd:     node,
		name:   name,
		nsRoot: n.nsRoot,
		parent: n,
	}

	// Always ensure that each child knows where the root is
	if n.nsRoot == nil {
		child.nsRoot = n
	} else {
		child.nsRoot = n.nsRoot
	}

	return child
}

// ReadDir reads the link structure as directory entries
func (s *Node) ReadDir(intr fs.Intr) ([]fuse.Dirent, fuse.Error) {
	log.Debug("Node ReadDir")
	entries := make([]fuse.Dirent, len(s.Nd.Links))
	for i, link := range s.Nd.Links {
		n := link.Name
		if len(n) == 0 {
			n = link.Hash.B58String()
		}
		entries[i] = fuse.Dirent{Name: n, Type: fuse.DT_File}
	}

	if len(entries) > 0 {
		return entries, nil
	}
	return nil, fuse.ENOENT
}

func (s *Node) Read(req *fuse.ReadRequest, resp *fuse.ReadResponse, intr fs.Intr) fuse.Error {
	// intr will be closed by fuse if the request is cancelled. turn this into a context.
	ctx, cancel := context.WithCancel(context.TODO())
	defer cancel() // make sure all operations we started close.

	// we wait on intr and cancel our context if it closes.
	go func() {
		select {
		case <-intr: // closed by fuse
			cancel() // cancel our context
		case <-ctx.Done():
		}
	}()

	k, err := s.Nd.Key()
	if err != nil {
		return err
	}

	// setup our logging event
	lm := make(lgbl.DeferredMap)
	lm["fs"] = "ipns"
	lm["key"] = func() interface{} { return k.Pretty() }
	lm["req_offset"] = req.Offset
	lm["req_size"] = req.Size
	defer log.EventBegin(ctx, "fuseRead", lm).Done()

	r, err := uio.NewDagReader(ctx, s.Nd, s.Ipfs.DAG)
	if err != nil {
		return err
	}
	o, err := r.Seek(req.Offset, os.SEEK_SET)
	lm["res_offset"] = o
	if err != nil {
		return err
	}
	n, err := io.ReadFull(r, resp.Data[:req.Size])
	resp.Data = resp.Data[:n]
	lm["res_size"] = n
	return err // may be non-nil / not succeeded
}

func (n *Node) Write(req *fuse.WriteRequest, resp *fuse.WriteResponse, intr fs.Intr) fuse.Error {
	log.Debugf("ipns: Node Write [%s]: flags = %s, offset = %d, size = %d", n.name, req.Flags.String(), req.Offset, len(req.Data))
	if IpnsReadonly {
		log.Error("Attempted to write on readonly ipns filesystem.")
		return fuse.EPERM
	}

	if n.dagMod == nil {
		// Create a DagModifier to allow us to change the existing dag node
		dmod, err := uio.NewDagModifier(n.Nd, n.Ipfs.DAG, chunk.DefaultSplitter)
		if err != nil {
			log.Errorf("Error creating dag modifier: %s", err)
			return err
		}
		n.dagMod = dmod
	}
	wrote, err := n.dagMod.WriteAt(req.Data, uint64(req.Offset))
	if err != nil {
		return err
	}
	resp.Size = wrote
	return nil
}

func (n *Node) Flush(req *fuse.FlushRequest, intr fs.Intr) fuse.Error {
	log.Debugf("Got flush request [%s]!", n.name)
	if IpnsReadonly {
		return nil
	}

	// If a write has happened
	if n.dagMod != nil {
		newNode, err := n.dagMod.GetNode()
		if err != nil {
			log.Errorf("Error getting dag node from dagMod: %s", err)
			return err
		}

		if n.parent != nil {
			log.Debug("updating self in parent!")
			err := n.parent.update(n.name, newNode)
			if err != nil {
				log.Criticalf("error in updating ipns dag tree: %s", err)
				// return fuse.ETHISISPRETTYBAD
				return err
			}
		}
		n.Nd = newNode

		/*/TEMP
		dr, err := mdag.NewDagReader(n.Nd, n.Ipfs.DAG)
		if err != nil {
			log.Critical("Verification read failed.")
		}
		b, err := ioutil.ReadAll(dr)
		if err != nil {
			log.Critical("Verification read failed.")
		}
		fmt.Println("VERIFICATION READ")
		fmt.Printf("READ %d BYTES\n", len(b))
		fmt.Println(string(b))
		fmt.Println(b)
		//*/

		n.dagMod = nil

		n.wasChanged()
	}
	return nil
}

// Signal that a node in this tree was changed so the root can republish
func (n *Node) wasChanged() {
	if IpnsReadonly {
		return
	}
	root := n.nsRoot
	if root == nil {
		root = n
	}

	root.repub.Publish <- struct{}{}
}

func (n *Node) republishRoot() error {
	log.Debug("Republish root")

	// We should already be the root, this is just a sanity check
	var root *Node
	if n.nsRoot != nil {
		root = n.nsRoot
	} else {
		root = n
	}

	// Add any nodes that may be new to the DAG service
	err := n.Ipfs.DAG.AddRecursive(root.Nd)
	if err != nil {
		log.Criticalf("ipns: Dag Add Error: %s", err)
		return err
	}

	ndkey, err := root.Nd.Key()
	if err != nil {
		log.Errorf("getKey error: %s", err)
		return err
	}
	log.Debug("Publishing changes!")

	err = n.Ipfs.Namesys.Publish(n.Ipfs.Context(), root.key, ndkey)
	if err != nil {
		log.Errorf("ipns: Publish Failed: %s", err)
		return err
	}
	return nil
}

func (n *Node) Fsync(req *fuse.FsyncRequest, intr fs.Intr) fuse.Error {
	log.Debug("Got fsync request!")
	return nil
}

func (n *Node) Mkdir(req *fuse.MkdirRequest, intr fs.Intr) (fs.Node, fuse.Error) {
	log.Debug("Got mkdir request!")
	if IpnsReadonly {
		log.Error("Attempted to call mkdir on readonly filesystem.")
		return nil, fuse.EPERM
	}
	dagnd := &mdag.Node{Data: ft.FolderPBData()}
	nnode := n.Nd.Copy()
	nnode.AddNodeLink(req.Name, dagnd)

	child := &Node{
		Ipfs: n.Ipfs,
		Nd:   dagnd,
		name: req.Name,
	}

	if n.nsRoot == nil {
		child.nsRoot = n
	} else {
		child.nsRoot = n.nsRoot
	}

	if n.parent != nil {
		err := n.parent.update(n.name, nnode)
		if err != nil {
			log.Criticalf("Error updating node: %s", err)
			return nil, err
		}
	}
	n.Nd = nnode

	n.wasChanged()

	return child, nil
}

func (n *Node) Open(req *fuse.OpenRequest, resp *fuse.OpenResponse, intr fs.Intr) (fs.Handle, fuse.Error) {
	//log.Debug("[%s] Received open request! flags = %s", n.name, req.Flags.String())
	//TODO: check open flags and truncate if necessary
	if req.Flags&fuse.OpenTruncate != 0 {
		log.Warning("Need to truncate file!")
		n.cached = nil
		n.Nd = &mdag.Node{Data: ft.FilePBData(nil, 0)}
	} else if req.Flags&fuse.OpenAppend != 0 {
		log.Warning("Need to append to file!")
	}
	return n, nil
}

func (n *Node) Mknod(req *fuse.MknodRequest, intr fs.Intr) (fs.Node, fuse.Error) {
	log.Debug("Got mknod request!")
	return nil, nil
}

func (n *Node) Create(req *fuse.CreateRequest, resp *fuse.CreateResponse, intr fs.Intr) (fs.Node, fs.Handle, fuse.Error) {
	log.Debugf("Got create request: %s", req.Name)
	if IpnsReadonly {
		log.Error("Attempted to call Create on a readonly filesystem.")
		return nil, nil, fuse.EPERM
	}

	// New 'empty' file
	nd := &mdag.Node{Data: ft.FilePBData(nil, 0)}
	child := n.makeChild(req.Name, nd)

	nnode := n.Nd.Copy()

	err := nnode.AddNodeLink(req.Name, nd)
	if err != nil {
		log.Errorf("Error adding child to node: %s", err)
		return nil, nil, err
	}
	if n.parent != nil {
		err := n.parent.update(n.name, nnode)
		if err != nil {
			log.Criticalf("Error updating node: %s", err)
			// Can we panic, please?
			return nil, nil, err
		}
	}
	n.Nd = nnode
	n.wasChanged()

	return child, child, nil
}

func (n *Node) Remove(req *fuse.RemoveRequest, intr fs.Intr) fuse.Error {
	log.Debugf("[%s] Got Remove request: %s", n.name, req.Name)
	if IpnsReadonly {
		log.Error("Attempted to call Remove on a readonly filesystem.")
		return fuse.EPERM
	}

	nnode := n.Nd.Copy()
	err := nnode.RemoveNodeLink(req.Name)
	if err != nil {
		log.Error("Remove: No such file.")
		return fuse.ENOENT
	}

	if n.parent != nil {
		err := n.parent.update(n.name, nnode)
		if err != nil {
			log.Criticalf("Error updating node: %s", err)
			return err
		}
	}
	n.Nd = nnode
	n.wasChanged()
	return nil
}

func (n *Node) Rename(req *fuse.RenameRequest, newDir fs.Node, intr fs.Intr) fuse.Error {
	log.Debugf("Got Rename request '%s' -> '%s'", req.OldName, req.NewName)
	if IpnsReadonly {
		log.Error("Attempted to call Rename on a readonly filesystem.")
		return fuse.EPERM
	}

	var mdn *mdag.Node
	for _, l := range n.Nd.Links {
		if l.Name == req.OldName {
			mdn = l.Node
		}
	}
	if mdn == nil {
		log.Critical("nil Link found on rename!")
		return fuse.ENOENT
	}
	n.Nd.RemoveNodeLink(req.OldName)

	switch newDir := newDir.(type) {
	case *Node:
		err := newDir.Nd.AddNodeLink(req.NewName, mdn)
		if err != nil {
			log.Errorf("Error adding node to new dir on rename: %s", err)
			return err
		}
	default:
		log.Critical("Unknown node type for rename target dir!")
		return errors.New("Unknown fs node type!")
	}
	return nil
}

// Updates the child of this node, specified by name to the given newnode
func (n *Node) update(name string, newnode *mdag.Node) error {
	log.Debugf("update '%s' in '%s'", name, n.name)
	nnode := n.Nd.Copy()
	err := nnode.RemoveNodeLink(name)
	if err != nil {
		return err
	}
	nnode.AddNodeLink(name, newnode)

	if n.parent != nil {
		err := n.parent.update(n.name, nnode)
		if err != nil {
			return err
		}
	}
	n.Nd = nnode
	return nil
}
