// 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: device config operation
// Author: zhangwei
// Create: 2018-01-18

package config

import (
	"encoding/json"
	"fmt"
	"os"
	"strings"

	"isula.org/isulad-tools/types"
)

// HostMapping host path mapping to container path
type HostMapping struct {
	PathOnHost      string
	PathInContainer string
	Permission      string
}

const (
	// MaxPathNum is max path number for devices
	MaxPathNum = 128
	// ArrayLen is host bind split array len
	ArrayLen = 3
)

func parseMapping(bind string) (*HostMapping, error) {
	array := strings.SplitN(bind, ":", 3)
	if len(array) < ArrayLen {
		// this function is used for host bind.
		// should not get here, in case won't crash.
		return nil, fmt.Errorf("bind must have two : in string")
	}
	mp := &HostMapping{
		PathOnHost:      array[0],
		PathInContainer: array[1],
		Permission:      array[2],
	}
	return mp, nil
}

// Flush will flush the config to filesystem
func (config *ContainerHookConfig) Flush() error {
	if !config.dirty {
		return nil
	}
	file, err := os.Create(config.configPath)
	if err != nil {
		return err
	}
	defer file.Close()

	if err := file.Chmod(0600); err != nil {
		return err
	}

	if err := json.NewEncoder(file).Encode(config); err != nil {
		return err
	}
	return nil
}

func (config *ContainerHookConfig) bindIndexInArray(bind *types.Bind, array []string) int {
	for index, bindstr := range array {
		mp, err := parseMapping(bindstr)
		if err != nil {
			continue
		}
		if mp.PathInContainer == bind.ContainerPath && mp.PathOnHost == bind.HostPath {
			return index
		}
	}
	return -1
}

// DeviceIndexInArray get device index in array
func (config *ContainerHookConfig) DeviceIndexInArray(device *types.Device) int {

	for index, dev := range config.Devices {
		if (device.PathOnHost == "" && dev.PathInContainer == device.Path) ||
			(device.Path == "" && dev.PathOnHost == device.PathOnHost) ||
			(dev.PathInContainer == device.Path && dev.PathOnHost == device.PathOnHost) {
			return index
		}
	}
	return -1
}

func (config *ContainerHookConfig) getConflictIndex(device *types.Device) int {

	for index, dev := range config.Devices {
		if dev.PathInContainer == device.Path || dev.PathOnHost == device.PathOnHost {
			return index
		}
	}
	return -1
}

// FindDeviceByMapping returns if a device in DeviceToAdd Config.
func (config *ContainerHookConfig) FindDeviceByMapping(device *types.Device) *types.Device {
	for _, eDevice := range config.Devices {
		if eDevice.PathOnHost == device.PathOnHost && eDevice.PathInContainer == device.Path {
			return &types.Device{
				Path:        eDevice.PathInContainer,
				PathOnHost:  eDevice.PathOnHost,
				Permissions: eDevice.CgroupPermissions,
				Major:       eDevice.Major,
				Minor:       eDevice.Minor,
				Type:        eDevice.Type,
				Parent:      eDevice.Parent,
			}
		}
	}
	return nil
}

// FindSubPartition returns a set of sub devices of a device by config.
func (config *ContainerHookConfig) FindSubPartition(device *types.Device) []*types.Device {
	var ret []*types.Device
	for _, eDevice := range config.Devices {
		if eDevice.Parent == device.PathOnHost {
			ret = append(ret, &types.Device{
				Path:        eDevice.PathInContainer,
				PathOnHost:  eDevice.PathOnHost,
				Permissions: eDevice.CgroupPermissions,
				Major:       eDevice.Major,
				Minor:       eDevice.Minor,
				Type:        eDevice.Type,
				Parent:      eDevice.Parent,
			})
		}
	}
	return ret
}

// UpdateDevice will update hook config of devices
func (config *ContainerHookConfig) UpdateDevice(device *types.Device, isAddDevice bool) error {
	dev := &DeviceMapping{
		Type:              device.Type,
		Major:             device.Major,
		Minor:             device.Minor,
		PathOnHost:        device.PathOnHost,
		PathInContainer:   device.Path,
		CgroupPermissions: device.Permissions,
		Parent:            device.Parent,
	}

	// add device action:
	if isAddDevice {
		// if not exist in Add array, add it.
		index := config.getConflictIndex(device)
		if index != -1 {
			conflictDev := config.Devices[index]
			return fmt.Errorf("device %s:%s has been already added into container", conflictDev.PathOnHost, conflictDev.PathInContainer)
		}
		config.dirty = true
		config.Devices = append(config.Devices, dev)
	} else {
		if index := config.DeviceIndexInArray(device); index != -1 {
			config.dirty = true
			config.Devices = append(config.Devices[:index], config.Devices[index+1:]...)
		} else {
			return fmt.Errorf("device %s:%s has not been added into container", device.PathOnHost, device.Path)
		}
	}
	return nil

}

// IsBindInConfig returns if a device in DeviceToAdd Config.
func (config *ContainerHookConfig) IsBindInConfig(bind *types.Bind) bool {
	if index := config.bindIndexInArray(bind, config.Binds); index != -1 {
		return true
	}
	return false
}

// GetBindInConfig returns device in DeviceToAdd Config.
func (config *ContainerHookConfig) GetBindInConfig(bind *types.Bind) (*HostMapping, error) {
	if index := config.bindIndexInArray(bind, config.Binds); index != -1 {
		return parseMapping(config.Binds[index])
	}
	return nil, fmt.Errorf("fail to find bind: %v", bind)
}

func (config *ContainerHookConfig) addBind(bind *types.Bind) (bool, error) {
	// if not in Add array, add it.
	exist, err := config.bi.add(bind.ToString())
	if err != nil {
		return exist, err
	}
	if index := config.bindIndexInArray(bind, config.Binds); index == -1 {
		config.dirty = true
		config.Binds = append(config.Binds, bind.ToString())
	}
	return exist, nil

}
func (config *ContainerHookConfig) removeBind(bind *types.Bind) (bool, error) {
	// if in add array, remove it, will not restore to Rm array, as it is added by isulad-tools.
	if index := config.bindIndexInArray(bind, config.Binds); index != -1 {
		config.dirty = true
		config.Binds = append(config.Binds[:index], config.Binds[index+1:]...)
	}

	return config.bi.remove(bind.ToString())

}

// UpdateBind will update binds of hook config
func (config *ContainerHookConfig) UpdateBind(bind *types.Bind, isAddBind bool) (bool, error) {
	if isAddBind {
		return config.addBind(bind)
	}
	return config.removeBind(bind)
}

// GetBinds get binds of hook config
func (config *ContainerHookConfig) GetBinds() []string {
	return config.Binds[:]
}

// GetAllDevices get all devices of hook config
func (config *ContainerHookConfig) GetAllDevices() []*DeviceMapping {
	return config.Devices[:]
}

// UpdateDeviceQos will update the qos for device
func (config *ContainerHookConfig) UpdateDeviceQos(qos *types.Qos, qType QosType) error {
	update := func(qosArr []*types.Qos, qos *types.Qos) []*types.Qos {
		for _, q := range qosArr {
			if q.Major == qos.Major && q.Minor == qos.Minor {
				if q.Value != qos.Value {
					config.dirty = true
					q.Value = qos.Value
				}
				return qosArr
			}
		}
		config.dirty = true
		qosArr = append(qosArr, qos)
		return qosArr
	}
	switch qType {
	case QosReadIOPS:
		config.ReadIOPS = update(config.ReadIOPS, qos)
	case QosWriteIOPS:
		config.WriteIOPS = update(config.WriteIOPS, qos)
	case QosReadBps:
		config.ReadBps = update(config.ReadBps, qos)
	case QosWriteBps:
		config.WriteBps = update(config.WriteBps, qos)
	case QosBlkioWeight:
		config.BlkioWeight = update(config.BlkioWeight, qos)
	}
	return nil
}

// RemoveDeviceQos remove qos for device
func (config *ContainerHookConfig) RemoveDeviceQos(device *types.Device, qType QosType) (bool, error) {
	remove := func(qosArr []*types.Qos, device *types.Device) ([]*types.Qos, bool) {
		for index, q := range qosArr {
			if q.Major == device.Major && q.Minor == device.Minor {
				config.dirty = true
				qosArr = append(qosArr[:index], qosArr[index+1:]...)
				return qosArr, true
			}
		}
		return qosArr, false
	}
	var ret bool
	switch qType {
	case QosReadIOPS:
		config.ReadIOPS, ret = remove(config.ReadIOPS, device)
	case QosWriteIOPS:
		config.WriteIOPS, ret = remove(config.WriteIOPS, device)
	case QosReadBps:
		config.ReadBps, ret = remove(config.ReadBps, device)
	case QosWriteBps:
		config.WriteBps, ret = remove(config.WriteBps, device)
	case QosBlkioWeight:
		config.BlkioWeight, ret = remove(config.BlkioWeight, device)
	}
	return ret, nil
}

// CheckPathNum check path num reach max limit or not
func (config *ContainerHookConfig) CheckPathNum() error {
	if len(config.Binds) > MaxPathNum {
		return fmt.Errorf("Path already reach max limit")
	}
	return nil
}

// SetConfigDirty set config dir dirty
func (config *ContainerHookConfig) SetConfigDirty() {
	config.dirty = true
}

// UpdateQosDevNum update qos major and minor for device
func (config *ContainerHookConfig) UpdateQosDevNum(qos *types.Qos, major int64, minor int64) {
	if qos.Major != major || qos.Minor != minor {
		qos.Major = major
		qos.Minor = minor
		config.dirty = true
	}
}

// UpdateDeviceNode update device node
func (config *ContainerHookConfig) UpdateDeviceNode(device string, major, minor int64) {
	for index, dev := range config.Devices {
		if dev.PathOnHost == device {
			config.Devices[index].Major = major
			config.Devices[index].Minor = minor
			config.dirty = true
		}
	}

	for index, qos := range config.ReadIOPS {
		if qos.Path == device {
			config.ReadIOPS[index].Major = major
			config.ReadIOPS[index].Minor = minor
			config.dirty = true
		}
	}

	for index, qos := range config.WriteIOPS {
		if qos.Path == device {
			config.WriteIOPS[index].Major = major
			config.WriteIOPS[index].Minor = minor
			config.dirty = true
		}
	}

	for index, qos := range config.ReadBps {
		if qos.Path == device {
			config.ReadBps[index].Major = major
			config.ReadBps[index].Minor = minor
			config.dirty = true
		}
	}

	for index, qos := range config.WriteBps {
		if qos.Path == device {
			config.WriteBps[index].Major = major
			config.WriteBps[index].Minor = minor
			config.dirty = true
		}
	}

	for index, qos := range config.BlkioWeight {
		if qos.Path == device {
			config.BlkioWeight[index].Major = major
			config.BlkioWeight[index].Minor = minor
			config.dirty = true
		}
	}
}
