package daemon

import (
	"encoding/json"
	"fmt"
	"io"
	"os"
	"path"
	"sync"

	core "github.com/jbenet/go-ipfs/core"
	"github.com/jbenet/go-ipfs/core/commands"
	u "github.com/jbenet/go-ipfs/util"

	lock "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/camlistore/lock"
	ma "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-multiaddr"
	manet "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-multiaddr/net"
)

var log = u.Logger("daemon")

// LockFile is the filename of the daemon lock, relative to config dir
const LockFile = "daemon.lock"

// DaemonListener listens to an initialized IPFS node and can send it commands instead of
// starting up a new set of connections
type DaemonListener struct {
	node   *core.IpfsNode
	list   manet.Listener
	closed bool
	wg     sync.WaitGroup
	lk     io.Closer
}

//Command accepts user input and can be sent to the running IPFS node
type Command struct {
	Command string
	Args    []string
	Opts    map[string]interface{}
}

func NewDaemonListener(ipfsnode *core.IpfsNode, addr ma.Multiaddr, confdir string) (*DaemonListener, error) {
	var err error
	confdir, err = u.TildeExpansion(confdir)
	if err != nil {
		return nil, err
	}

	lk, err := daemonLock(confdir)
	if err != nil {
		return nil, err
	}

	ofi, err := os.Create(confdir + "/rpcaddress")
	if err != nil {
		log.Warningf("Could not create rpcaddress file: %s", err)
		return nil, err
	}

	_, err = ofi.Write([]byte(addr.String()))
	if err != nil {
		log.Warningf("Could not write to rpcaddress file: %s", err)
		return nil, err
	}
	ofi.Close()

	list, err := manet.Listen(addr)
	if err != nil {
		return nil, err
	}
	log.Info("New daemon listener initialized.")

	return &DaemonListener{
		node: ipfsnode,
		list: list,
		lk:   lk,
	}, nil
}

func NewCommand() *Command {
	return &Command{
		Opts: make(map[string]interface{}),
	}
}

func (dl *DaemonListener) Listen() {
	if dl.closed {
		panic("attempting to listen on a closed daemon Listener")
	}

	// add ourselves to workgroup. and remove ourselves when done.
	dl.wg.Add(1)
	defer dl.wg.Done()

	log.Info("daemon listening")
	for {
		conn, err := dl.list.Accept()
		if err != nil {
			if !dl.closed {
				log.Warning("DaemonListener Accept: %v", err)
			}
			return
		}
		go dl.handleConnection(conn)
	}
}

func (dl *DaemonListener) handleConnection(conn manet.Conn) {
	defer conn.Close()

	dec := json.NewDecoder(conn)

	var command Command
	err := dec.Decode(&command)
	if err != nil {
		fmt.Fprintln(conn, err)
		return
	}

	log.Debugf("Got command: %v", command)
	switch command.Command {
	case "add":
		err = commands.Add(dl.node, command.Args, command.Opts, conn)
	case "cat":
		err = commands.Cat(dl.node, command.Args, command.Opts, conn)
	case "ls":
		err = commands.Ls(dl.node, command.Args, command.Opts, conn)
	case "pin":
		err = commands.Pin(dl.node, command.Args, command.Opts, conn)
	case "publish":
		err = commands.Publish(dl.node, command.Args, command.Opts, conn)
	case "resolve":
		err = commands.Resolve(dl.node, command.Args, command.Opts, conn)
	case "diag":
		err = commands.Diag(dl.node, command.Args, command.Opts, conn)
	case "blockGet":
		err = commands.BlockGet(dl.node, command.Args, command.Opts, conn)
	case "blockPut":
		err = commands.BlockPut(dl.node, command.Args, command.Opts, conn)
	case "log":
		err = commands.Log(dl.node, command.Args, command.Opts, conn)
	case "unpin":
		err = commands.Unpin(dl.node, command.Args, command.Opts, conn)
	case "updateApply":
		command.Opts["onDaemon"] = true
		err = commands.UpdateApply(dl.node, command.Args, command.Opts, conn)
	default:
		err = fmt.Errorf("Invalid Command: '%s'", command.Command)
	}
	if err != nil {
		log.Errorf("%s: %s", command.Command, err)
		fmt.Fprintln(conn, err)
	}
}

func (dl *DaemonListener) Close() error {
	dl.closed = true
	err := dl.list.Close()
	dl.wg.Wait() // wait till done before releasing lock.
	dl.lk.Close()
	return err
}

func daemonLock(confdir string) (io.Closer, error) {
	return lock.Lock(path.Join(confdir, LockFile))
}
