package btrfs

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

	"github.com/dennwc/ioctl"
)

const SuperMagic uint32 = 0x9123683E

func CloneFile(dst, src *os.File) error {
	return iocClone(dst, src)
}

func Open(path string, ro bool) (*FS, error) {
	if ok, err := IsSubVolume(path); err != nil {
		return nil, err
	} else if !ok {
		return nil, ErrNotBtrfs{Path: path}
	}
	var (
		dir *os.File
		err error
	)
	if ro {
		dir, err = os.OpenFile(path, os.O_RDONLY|syscall.O_NOATIME, 0644)
		if err != nil {
			// Try without O_NOATIME as it requires ownership of the file
			// or other priviliges
			dir, err = os.OpenFile(path, os.O_RDONLY, 0644)
		}
	} else {
		dir, err = os.Open(path)
	}
	if err != nil {
		return nil, err
	} else if st, err := dir.Stat(); err != nil {
		dir.Close()
		return nil, err
	} else if !st.IsDir() {
		dir.Close()
		return nil, fmt.Errorf("not a directory: %s", path)
	}
	return &FS{f: dir}, nil
}

type FS struct {
	f *os.File
}

func (f *FS) Close() error {
	return f.f.Close()
}

type Info struct {
	MaxID          uint64
	NumDevices     uint64
	FSID           FSID
	NodeSize       uint32
	SectorSize     uint32
	CloneAlignment uint32
}

func (f *FS) SubVolumeID() (uint64, error) {
	id, err := getFileRootID(f.f)
	if err != nil {
		return 0, err
	}
	return uint64(id), nil
}

func (f *FS) Info() (out Info, err error) {
	var arg btrfs_ioctl_fs_info_args
	arg, err = iocFsInfo(f.f)
	if err == nil {
		out = Info{
			MaxID:          arg.max_id,
			NumDevices:     arg.num_devices,
			FSID:           arg.fsid,
			NodeSize:       arg.nodesize,
			SectorSize:     arg.sectorsize,
			CloneAlignment: arg.clone_alignment,
		}
	}
	return
}

type DevInfo struct {
	UUID       UUID
	BytesUsed  uint64
	TotalBytes uint64
	Path       string
}

func (f *FS) GetDevInfo(id uint64) (out DevInfo, err error) {
	var arg btrfs_ioctl_dev_info_args
	arg.devid = id

	if err = ioctl.Do(f.f, _BTRFS_IOC_DEV_INFO, &arg); err != nil {
		return
	}
	out.UUID = arg.uuid
	out.BytesUsed = arg.bytes_used
	out.TotalBytes = arg.total_bytes
	out.Path = stringFromBytes(arg.path[:])

	return
}

type DevStats struct {
	WriteErrs uint64
	ReadErrs  uint64
	FlushErrs uint64
	// Checksum error, bytenr error or contents is illegal: this is an
	// indication that the block was damaged during read or write, or written to
	// wrong location or read from wrong location.
	CorruptionErrs uint64
	// An indication that blocks have not been written.
	GenerationErrs uint64
	Unknown        []uint64
}

func (f *FS) GetDevStats(id uint64) (out DevStats, err error) {
	var arg btrfs_ioctl_get_dev_stats
	arg.devid = id
	arg.nr_items = _BTRFS_DEV_STAT_VALUES_MAX
	arg.flags = 0
	if err = ioctl.Do(f.f, _BTRFS_IOC_GET_DEV_STATS, &arg); err != nil {
		return
	}
	i := 0
	out.WriteErrs = arg.values[i]
	i++
	out.ReadErrs = arg.values[i]
	i++
	out.FlushErrs = arg.values[i]
	i++
	out.CorruptionErrs = arg.values[i]
	i++
	out.GenerationErrs = arg.values[i]
	i++
	if int(arg.nr_items) > i {
		out.Unknown = arg.values[i:arg.nr_items]
	}
	return
}

type FSFeatureFlags struct {
	Compatible   FeatureFlags
	CompatibleRO FeatureFlags
	Incompatible IncompatFeatures
}

func (f *FS) GetFeatures() (out FSFeatureFlags, err error) {
	var arg btrfs_ioctl_feature_flags
	if err = ioctl.Do(f.f, _BTRFS_IOC_GET_FEATURES, &arg); err != nil {
		return
	}
	out = FSFeatureFlags{
		Compatible:   arg.compat_flags,
		CompatibleRO: arg.compat_ro_flags,
		Incompatible: arg.incompat_flags,
	}
	return
}

func (f *FS) GetSupportedFeatures() (out FSFeatureFlags, err error) {
	var arg [3]btrfs_ioctl_feature_flags
	if err = ioctl.Do(f.f, _BTRFS_IOC_GET_SUPPORTED_FEATURES, &arg); err != nil {
		return
	}
	out = FSFeatureFlags{
		Compatible:   arg[0].compat_flags,
		CompatibleRO: arg[0].compat_ro_flags,
		Incompatible: arg[0].incompat_flags,
	}
	//for i, a := range arg {
	//	out[i] = FSFeatureFlags{
	//		Compatible:   a.compat_flags,
	//		CompatibleRO: a.compat_ro_flags,
	//		Incompatible: a.incompat_flags,
	//	}
	//}
	return
}

func (f *FS) GetFlags() (SubvolFlags, error) {
	return iocSubvolGetflags(f.f)
}

func (f *FS) SetFlags(flags SubvolFlags) error {
	return iocSubvolSetflags(f.f, flags)
}

func (f *FS) Sync() (err error) {
	if err = ioctl.Ioctl(f.f, _BTRFS_IOC_START_SYNC, 0); err != nil {
		return
	}
	return ioctl.Ioctl(f.f, _BTRFS_IOC_WAIT_SYNC, 0)
}

func (f *FS) CreateSubVolume(name string) error {
	return CreateSubVolume(filepath.Join(f.f.Name(), name))
}

func (f *FS) DeleteSubVolume(name string) error {
	return DeleteSubVolume(filepath.Join(f.f.Name(), name))
}

func (f *FS) Snapshot(dst string, ro bool) error {
	return SnapshotSubVolume(f.f.Name(), filepath.Join(f.f.Name(), dst), ro)
}

func (f *FS) SnapshotSubVolume(name string, dst string, ro bool) error {
	return SnapshotSubVolume(filepath.Join(f.f.Name(), name),
		filepath.Join(f.f.Name(), dst), ro)
}

func (f *FS) Send(w io.Writer, parent string, subvols ...string) error {
	if parent != "" {
		parent = filepath.Join(f.f.Name(), parent)
	}
	sub := make([]string, 0, len(subvols))
	for _, s := range subvols {
		sub = append(sub, filepath.Join(f.f.Name(), s))
	}
	return Send(w, parent, sub...)
}

func (f *FS) Receive(r io.Reader) error {
	return Receive(r, f.f.Name())
}

func (f *FS) ReceiveTo(r io.Reader, mount string) error {
	return Receive(r, filepath.Join(f.f.Name(), mount))
}

func (f *FS) ListSubvolumes(filter func(SubvolInfo) bool) ([]SubvolInfo, error) {
	m, err := listSubVolumes(f.f, filter)
	if err != nil {
		return nil, err
	}
	out := make([]SubvolInfo, 0, len(m))
	for _, v := range m {
		out = append(out, v)
	}
	return out, nil
}

func (f *FS) SubvolumeByUUID(uuid UUID) (*SubvolInfo, error) {
	id, err := lookupUUIDSubvolItem(f.f, uuid)
	if err != nil {
		return nil, err
	}
	return subvolSearchByRootID(f.f, id, "")
}

func (f *FS) SubvolumeByReceivedUUID(uuid UUID) (*SubvolInfo, error) {
	id, err := lookupUUIDReceivedSubvolItem(f.f, uuid)
	if err != nil {
		return nil, err
	}
	return subvolSearchByRootID(f.f, id, "")
}

func (f *FS) SubvolumeByPath(path string) (*SubvolInfo, error) {
	return subvolSearchByPath(f.f, path)
}

func (f *FS) Usage() (UsageInfo, error) { return spaceUsage(f.f) }

func (f *FS) Balance(flags BalanceFlags) (BalanceProgress, error) {
	args := btrfs_ioctl_balance_args{flags: flags}
	err := iocBalanceV2(f.f, &args)
	return args.stat, err
}

func (f *FS) Resize(size int64) error {
	amount := strconv.FormatInt(size, 10)
	args := &btrfs_ioctl_vol_args{}
	args.SetName(amount)
	if err := iocResize(f.f, args); err != nil {
		return fmt.Errorf("resize failed: %v", err)
	}
	return nil
}

func (f *FS) ResizeToMax() error {
	args := &btrfs_ioctl_vol_args{}
	args.SetName("max")
	if err := iocResize(f.f, args); err != nil {
		return fmt.Errorf("resize failed: %v", err)
	}
	return nil
}
