package mount

import (
	"fmt"
	"os"
	"path/filepath"
	"syscall"

	"github.com/docker/docker/pkg/archive"
	"github.com/docker/docker/pkg/symlink"
	"github.com/docker/libcontainer/label"
	"github.com/docker/libcontainer/mount/mode"
)

type Mount struct {
	Type        string    `json:"type,omitempty"`
	Source      string    `json:"source,omitempty"`      // Source path, in the host namespace
	Destination string    `json:"destination,omitempty"` // Destination path, in the container
	Mode        mode.Mode `json:"mode,omitempty"`
	Private     bool      `json:"private,omitempty"`
	Slave       bool      `json:"slave,omitempty"`
}

func (m *Mount) Mount(rootfs, mountLabel string) error {
	switch m.Type {
	case "bind":
		return m.bindMount(rootfs, mountLabel)
	case "tmpfs":
		return m.tmpfsMount(rootfs, mountLabel)
	default:
		return fmt.Errorf("unsupported mount type %s for %s", m.Type, m.Destination)
	}
}

func (m *Mount) bindMount(rootfs, mountLabel string) error {
	var (
		flags = syscall.MS_BIND | syscall.MS_REC
		dest  = filepath.Join(rootfs, m.Destination)
	)

	if !m.Mode.Writable() {
		flags = flags | syscall.MS_RDONLY
	}

	if m.Slave {
		flags = flags | syscall.MS_SLAVE
	}

	stat, err := os.Stat(m.Source)
	if err != nil {
		return err
	}

	// FIXME: (crosbymichael) This does not belong here and should be done a layer above
	dest, err = symlink.FollowSymlinkInScope(dest, rootfs)
	if err != nil {
		return err
	}

	if err := createIfNotExists(dest, stat.IsDir()); err != nil {
		return fmt.Errorf("creating new bind mount target %s", err)
	}

	if err := syscall.Mount(m.Source, dest, "bind", uintptr(flags), ""); err != nil {
		return fmt.Errorf("mounting %s into %s %s", m.Source, dest, err)
	}

	if !m.Mode.Writable() {
		if err := syscall.Mount(m.Source, dest, "bind", uintptr(flags|syscall.MS_REMOUNT), ""); err != nil {
			return fmt.Errorf("remounting %s into %s %s", m.Source, dest, err)
		}
	}

	if err := label.Relabel(m.Source, mountLabel, m.Mode); err != nil {
		return fmt.Errorf("relabeling %s to %s %s", m.Source, mountLabel, err)
	}

	if m.Private {
		if err := syscall.Mount("", dest, "none", uintptr(syscall.MS_PRIVATE), ""); err != nil {
			return fmt.Errorf("mounting %s private %s", dest, err)
		}
	}

	return nil
}

func (m *Mount) tmpfsMount(rootfs, mountLabel string) error {
	var (
		stat        os.FileInfo
		file        *os.File
		err         error
		l           = label.FormatMountLabel("", mountLabel)
		dest        = filepath.Join(rootfs, m.Destination)
		backup_dest = filepath.Join(rootfs, ".tmpfs"+m.Destination[1:])
	)

	// FIXME: (crosbymichael) This does not belong here and should be done a layer above
	if dest, err = symlink.FollowSymlinkInScope(dest, rootfs); err != nil {
		return err
	}

	if err := createIfNotExists(dest, true); err != nil {
		return fmt.Errorf("creating new tmpfs mount target %s", err)
	}
	if err := archive.CopyWithTar(dest, backup_dest); err != nil {
		return err
	}

	if stat, err = os.Stat(dest); err != nil {
		return err
	}
	if err := syscall.Mount("tmpfs", dest, "tmpfs", uintptr(defaultMountFlags), l); err != nil {
		return fmt.Errorf("%s mounting %s in tmpfs", err, dest)
	}
	if file, err = os.Open(dest); err != nil {
		return err
	}
	defer file.Close()
	if err = file.Chmod(stat.Mode()); err != nil {
		return err
	}
	if err := archive.CopyWithTar(backup_dest, dest); err != nil {
		return err
	}
	return os.RemoveAll(backup_dest)
}
