// Copyright (c) Huawei Technologies Co., Ltd. 2018-2019. All rights reserved.
// isulad-tools is licensed under the Mulan PSL v1.
// You can use this software according to the terms and conditions of the Mulan PSL v1.
// You may obtain a copy of Mulan PSL v1 at:
//    http://license.coscl.org.cn/MulanPSL
// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR
// PURPOSE.
// See the Mulan PSL v1 for more details.
// Description: bind operation for device
// Author: zhangwei
// Create: 2018-01-18

package libdevice

import (
	"bufio"
	"fmt"
	"os"
	"os/exec"
	"path"
	"strings"

	"github.com/sirupsen/logrus"

	"github.com/opencontainers/runtime-spec/specs-go"
	"isula.org/isulad-tools/types"
	"isula.org/isulad-tools/utils"
)

// ParseBind will parse host path to Bind structure
func ParseBind(bindstr string, spec *specs.Spec, create bool) (*types.Bind, error) {
	var src, dst string
	var isDir bool

	permissions := "rw,rslave"
	arr := strings.Split(bindstr, ":")
	switch len(arr) {
	case 3:
		if validMountOption(arr[2]) == false {
			return nil, fmt.Errorf("invalid permissions: %s", arr[2])
		}
		permissions = arr[2]
		fallthrough
	case 2:
		src = path.Clean(arr[0])
		dst = path.Clean(arr[1])
	default:
		return nil, fmt.Errorf("invalid path specification: %s", bindstr)
	}

	if path.IsAbs(src) == false || path.IsAbs(dst) == false {
		return nil, fmt.Errorf("invalid path specification:%s, only absolute path is allowed", bindstr)
	}

	bind := &types.Bind{
		HostPath:      src,
		ContainerPath: dst,
		MountOption:   permissions,
	}

	symlinkinfo, err := os.Lstat(src)
	if err != nil {
		logrus.Errorf("Lstat returns a FileInfo describing the named file error: %v", err)
	}
	info, err := os.Stat(src)
	if symlinkinfo != nil {
		if ((symlinkinfo.Mode() & os.ModeSymlink) == os.ModeSymlink) && (err != nil) {
			return nil, fmt.Errorf("Parsebind get symlibk source file error: %v", err)
		}
	}

	if create {
		if spec != nil {
			uid, gid := utils.GetUIDGid(spec)
			if uid == -1 {
				uid = 0
			}
			if gid == -1 {
				gid = 0
			}
			bind.UID = uid
			bind.GID = gid
		}
		if err == nil {
			isDir = info.IsDir()
		} else if os.IsNotExist(err) {
			isDir = true
			if spec != nil {
				if err := os.MkdirAll(src, os.FileMode(0755)); err != nil {
					return nil, fmt.Errorf("ParseBind mkdir error: %v", err)
				}
				if err := os.Chown(src, bind.UID, bind.GID); err != nil {
					return nil, fmt.Errorf("ParseBind chown error: %v", err)
				}
			}
		} else {
			return nil, fmt.Errorf("invalid path specification: %s", bindstr)
		}
	} else {
		if err == nil {
			isDir = info.IsDir()
		} else if os.IsNotExist(err) {
			return bind, nil
		} else {
			return nil, fmt.Errorf("invalid path specification: %s", bindstr)
		}
	}
	bind.IsDir = isDir

	return bind, nil
}

func findPathDevice(path string) (*types.Device, string, error) {

	// find path mount entry point.
	cmd := exec.Command("df", "-P", path)
	stdout, err := cmd.StdoutPipe()
	if err != nil {
		return nil, "", err
	}
	if err := cmd.Start(); err != nil {
		return nil, "", err
	}

	defer cmd.Wait()

	reader := bufio.NewReader(stdout)

	// ignore first line.
	reader.ReadString('\n')
	line, err := reader.ReadString('\n')
	if err != nil {
		logrus.Errorf("reader.ReadString error: %v", err)
	}
	line = strings.Trim(line, "\n")

	devs := strings.Split(line, " ")
	device, err := DeviceFromPath(devs[0], "rwm")
	if err != nil {
		return nil, "", err
	}
	return device, devs[len(devs)-1], nil
}

func findDeviceMountEntryPoint(device, mp string) (string, string, string, error) {
	f, err := os.Open("/proc/mounts")
	if err != nil {
		return "", "", "", err
	}
	defer f.Close()

	scanner := bufio.NewScanner(f)
	procMntCols := 6
	for scanner.Scan() {
		line := scanner.Text()
		array := strings.Split(line, " ")
		if len(array) < procMntCols {
			continue
		}
		// the one we wanted.
		if array[0] == device && array[1] == mp {
			entry := array[1]
			fstype := array[2]
			mountOption := array[3]
			return entry, fstype, mountOption, nil
		}
	}
	if err := scanner.Err(); err != nil {
		return "", "", "", err
	}
	return "", "", "", fmt.Errorf("Device Not Found")
}

// validMountOption will validate the mount option for user input
func validMountOption(option string) bool {
	validOp := map[string]bool{
		"ro": true,
		"rw": true,
		// "shared": true,
		"private": true,
		// "slave": true,
		// "rshared": true,
		"rprivate": true,
		"rslave":   true,
	}

	arr := strings.Split(option, ",")
	for _, op := range arr {
		if !validOp[op] {
			return false
		}
		validOp[op] = false
	}
	return true
}
