// Package diskfs implements methods for creating and manipulating disks and filesystems
//
// methods for creating and manipulating disks and filesystems, whether block devices
// in /dev or direct disk images. This does **not**
// mount any disks or filesystems, neither directly locally nor via a VM. Instead, it manipulates the
// bytes directly.
//
// This is not intended as a replacement for operating system filesystem and disk drivers. Instead,
// it is intended to make it easy to work with partitions, partition tables and filesystems directly
// without requiring operating system mounts.
//
// Some examples:
//
// 1. Create a disk image of size 10MB with a FAT32 filesystem spanning the entire disk.
//
//     import diskfs "github.com/diskfs/go-diskfs"
//     size := 10*1024*1024 // 10 MB
//
//     diskImg := "/tmp/disk.img"
//     disk := diskfs.Create(diskImg, size, diskfs.Raw, diskfs.SectorSizeDefault)
//
//     fs, err := disk.CreateFilesystem(0, diskfs.TypeFat32)
//
// 2. Create a disk of size 20MB with an MBR partition table, a single partition beginning at block 2048 (1MB),
//    of size 10MB filled with a FAT32 filesystem.
//
//     import diskfs "github.com/diskfs/go-diskfs"
//
//     diskSize := 10*1024*1024 // 10 MB
//
//     diskImg := "/tmp/disk.img"
//     disk := diskfs.Create(diskImg, size, diskfs.Raw, diskfs.SectorSizeDefault)
//
//     table := &mbr.Table{
//       LogicalSectorSize:  512,
//       PhysicalSectorSize: 512,
//       Partitions: []*mbr.Partition{
//         {
//           Bootable:      false,
//           Type:          Linux,
//           Start:         2048,
//           Size:          20480,
//         },
//       },
//     }
//
//     fs, err := disk.CreateFilesystem(1, diskfs.TypeFat32)
//
// 3. Create a disk of size 20MB with a GPT partition table, a single partition beginning at block 2048 (1MB),
//    of size 10MB, and fill with the contents from the 10MB file "/root/contents.dat"
//
//     import diskfs "github.com/diskfs/go-diskfs"
//
//     diskSize := 10*1024*1024 // 10 MB
//
//     diskImg := "/tmp/disk.img"
//     disk := diskfs.Create(diskImg, size, diskfs.Raw, diskfs.SectorSizeDefault)
//
//     table := &gpt.Table{
//       LogicalSectorSize:  512,
//       PhysicalSectorSize: 512,
//       Partitions: []*gpt.Partition{
//         {
//           LogicalSectorSize:  512,
//           PhysicalSectorSize: 512,
//           ProtectiveMBR:      true,
//         },
//       },
//     }
//
//     f, err := os.Open("/root/contents.dat")
//     written, err := disk.WritePartitionContents(1, f)
//
// 4. Create a disk of size 20MB with an MBR partition table, a single partition beginning at block 2048 (1MB),
//    of size 10MB filled with a FAT32 filesystem, and create some directories and files in that filesystem.
//
//     import diskfs "github.com/diskfs/go-diskfs"
//
//     diskSize := 10*1024*1024 // 10 MB
//
//     diskImg := "/tmp/disk.img"
//     disk := diskfs.Create(diskImg, size, diskfs.Raw, diskfs.SectorSizeDefault)
//
//     table := &mbr.Table{
//       LogicalSectorSize:  512,
//       PhysicalSectorSize: 512,
//       Partitions: []*mbr.Partition{
//         {
//           Bootable:      false,
//           Type:          Linux,
//           Start:         2048,
//           Size:          20480,
//         },
//       },
//     }
//
//     fs, err := disk.CreateFilesystem(1, diskfs.TypeFat32)
//     err := fs.Mkdir("/FOO/BAR")
//     rw, err := fs.OpenFile("/FOO/BAR/AFILE.EXE", os.O_CREATE|os.O_RDRWR)
//     b := make([]byte, 1024, 1024)
//     rand.Read(b)
//     err := rw.Write(b)
//
package diskfs

import (
	"errors"
	"fmt"
	"io"
	"os"

	log "github.com/sirupsen/logrus"

	"github.com/diskfs/go-diskfs/disk"
)

// when we use a disk image with a GPT, we cannot get the logical sector size from the disk via the kernel
//    so we use the default sector size of 512, per Rod Smith
const (
	defaultBlocksize, firstblock int = 512, 2048
	blksszGet                        = 0x1268
	blkpbszGet                       = 0x127b
)

// Format represents the format of the disk
type Format int

const (
	// Raw disk format for basic raw disk
	Raw Format = iota
)

// OpenModeOption represents file open modes
type OpenModeOption int

const (
	// ReadOnly open file in read only mode
	ReadOnly OpenModeOption = iota
	// ReadWriteExclusive open file in read-write exclusive mode
	ReadWriteExclusive
)

// OpenModeOption.String()
func (m OpenModeOption) String() string {
	switch m {
	case ReadOnly:
		return "read-only"
	case ReadWriteExclusive:
		return "read-write exclusive"
	default:
		return "unknown"
	}
}

var openModeOptions = map[OpenModeOption]int{
	ReadOnly:           os.O_RDONLY,
	ReadWriteExclusive: os.O_RDWR | os.O_EXCL,
}

// SectorSize represents the sector size to use
type SectorSize int

const (
	// SectorSizeDefault default behavior, defaulting to defaultBlocksize
	SectorSizeDefault SectorSize = 0
	// SectorSize512 override sector size to 512
	SectorSize512 SectorSize = 512
	// SectorSize4k override sector size to 4k
	SectorSize4k SectorSize = 4096
)

func writableMode(mode OpenModeOption) bool {
	m, ok := openModeOptions[mode]
	if ok {
		if m&os.O_RDWR != 0 || m&os.O_WRONLY != 0 {
			return true
		}
	}

	return false
}

func initDisk(f *os.File, openMode OpenModeOption, sectorSize SectorSize) (*disk.Disk, error) {
	var (
		diskType      disk.Type
		size          int64
		lblksize      = int64(defaultBlocksize)
		pblksize      = int64(defaultBlocksize)
		defaultBlocks = true
	)
	log.Debug("initDisk(): start")

	if sectorSize != SectorSizeDefault {
		lblksize = int64(sectorSize)
		pblksize = int64(sectorSize)
	}

	// get device information
	devInfo, err := f.Stat()
	if err != nil {
		return nil, fmt.Errorf("could not get info for device %s: %v", f.Name(), err)
	}
	mode := devInfo.Mode()
	switch {
	case mode.IsRegular():
		log.Debug("initDisk(): regular file")
		diskType = disk.File
		size = devInfo.Size()
		if size <= 0 {
			return nil, fmt.Errorf("could not get file size for device %s", f.Name())
		}
	case mode&os.ModeDevice != 0:
		log.Debug("initDisk(): block device")
		diskType = disk.Device
		file, err := os.Open(f.Name())
		if err != nil {
			return nil, fmt.Errorf("error opening block device %s: %s", f.Name(), err)
		}
		size, err = file.Seek(0, io.SeekEnd)
		if err != nil {
			return nil, fmt.Errorf("error seeking to end of block device %s: %s", f.Name(), err)
		}
		lblksize, pblksize, err = getSectorSizes(f)
		log.Debugf("initDisk(): logical block size %d, physical block size %d", lblksize, pblksize)
		defaultBlocks = false
		if err != nil {
			return nil, fmt.Errorf("Unable to get block sizes for device %s: %v", f.Name(), err)
		}
	default:
		return nil, fmt.Errorf("device %s is neither a block device nor a regular file", f.Name())
	}

	// how many good blocks do we have?
	//var goodBlocks, orphanedBlocks int
	//goodBlocks = size / lblksize

	writable := writableMode(openMode)

	ret := &disk.Disk{
		File:              f,
		Info:              devInfo,
		Type:              diskType,
		Size:              size,
		LogicalBlocksize:  lblksize,
		PhysicalBlocksize: pblksize,
		Writable:          writable,
		DefaultBlocks:     defaultBlocks,
	}

	// try to initialize the partition table.
	// we ignore errors, because it is perfectly fine to open a disk
	// and use it before it has a partition table. This is solely
	// a convenience.
	if table, err := ret.GetPartitionTable(); err == nil && table != nil {
		ret.Table = table
	}
	return ret, nil
}

func checkDevice(device string) error {
	if device == "" {
		return errors.New("must pass device name")
	}
	if _, err := os.Stat(device); os.IsNotExist(err) {
		return fmt.Errorf("provided device %s does not exist", device)
	}

	return nil
}

// Open a Disk from a path to a device in read-write exclusive mode
// Should pass a path to a block device e.g. /dev/sda or a path to a file /tmp/foo.img
// The provided device must exist at the time you call Open()
func Open(device string) (*disk.Disk, error) {
	err := checkDevice(device)
	if err != nil {
		return nil, err
	}
	f, err := os.OpenFile(device, os.O_RDWR|os.O_EXCL, 0600)
	if err != nil {
		return nil, fmt.Errorf("Could not open device %s exclusively for writing", device)
	}
	// return our disk
	return initDisk(f, ReadWriteExclusive, SectorSizeDefault)
}

// OpenWithMode open a Disk from a path to a device with a given open mode
// If the device is open in read-only mode, operations to change disk partitioning will
// return an error
// Should pass a path to a block device e.g. /dev/sda or a path to a file /tmp/foo.img
// The provided device must exist at the time you call OpenWithMode()
func OpenWithMode(device string, mode OpenModeOption) (*disk.Disk, error) {
	err := checkDevice(device)
	if err != nil {
		return nil, err
	}
	m, ok := openModeOptions[mode]
	if !ok {
		return nil, errors.New("unsupported file open mode")
	}
	f, err := os.OpenFile(device, m, 0600)
	if err != nil {
		return nil, fmt.Errorf("Could not open device %s with mode %v: %v", device, mode, err)
	}
	// return our disk
	return initDisk(f, mode, SectorSizeDefault)
}

// Create a Disk from a path to a device
// Should pass a path to a block device e.g. /dev/sda or a path to a file /tmp/foo.img
// The provided device must not exist at the time you call Create()
func Create(device string, size int64, format Format, sectorSize SectorSize) (*disk.Disk, error) {
	if device == "" {
		return nil, errors.New("must pass device name")
	}
	if size <= 0 {
		return nil, errors.New("must pass valid device size to create")
	}
	f, err := os.OpenFile(device, os.O_RDWR|os.O_EXCL|os.O_CREATE, 0666)
	if err != nil {
		return nil, fmt.Errorf("Could not create device %s", device)
	}
	err = os.Truncate(device, size)
	if err != nil {
		return nil, fmt.Errorf("Could not expand device %s to size %d", device, size)
	}
	// return our disk
	return initDisk(f, ReadWriteExclusive, sectorSize)
}
