package usn

import (
	"b612.me/stario"
	"b612.me/win32api"
	"fmt"
	"os"
	"reflect"
	"runtime"
	"syscall"
	"unsafe"
)

type DiskInfo struct {
	Driver       string
	Name         string
	Format       string
	SerialNumber uint32
}

func ListDrivers() ([]string, error) {
	drivers := make([]string, 0, 26)
	buf := make([]uint16, 255)
	err := win32api.GetLogicalDriveStringsW(win32api.DWORD(len(buf)), &buf[0])
	if err != nil {
		return drivers, err
	}
	var driver []rune
	for _, v := range buf {
		if v != 0 {
			driver = append(driver, rune(uint8(v)))
			if v == 92 {
				drivers = append(drivers, string(driver))
				driver = []rune{}
			}
		}
	}
	return drivers, nil
}

func GetDiskInfo(disk string) (DiskInfo, error) {
	format := make([]rune, 0, 12)
	name := make([]rune, 0, 128)
	ptr, _ := syscall.UTF16PtrFromString(disk)
	var lpVolumeNameBuffer = make([]uint16, syscall.MAX_PATH+1)
	var nVolumeNameSize = win32api.DWORD(len(lpVolumeNameBuffer))
	var lpVolumeSerialNumber uint32
	var lpMaximumComponentLength uint32
	var lpFileSystemFlags uint32
	var lpFileSystemNameBuffer = make([]uint16, 255)
	var nFileSystemNameSize uint32 = syscall.MAX_PATH + 1
	err := win32api.GetVolumeInformationW(ptr, &lpVolumeNameBuffer[0], nVolumeNameSize, &lpVolumeSerialNumber, &lpMaximumComponentLength,
		&lpFileSystemFlags, &lpFileSystemNameBuffer[0], win32api.DWORD(nFileSystemNameSize))
	for _, v := range lpFileSystemNameBuffer {
		if v != 0 {
			format = append(format, rune(v))
		}
	}
	for _, v := range lpVolumeNameBuffer {
		if v != 0 {
			name = append(name, rune(v))
		}
	}
	return DiskInfo{
		SerialNumber: lpVolumeSerialNumber,
		Driver:       disk,
		Name:         string(name),
		Format:       string(format),
	}, err
}

func DeviceIoControl(handle syscall.Handle, controlCode uint32, in interface{}, out interface{}, done *uint32) (err error) {
	inPtr, inSize := getPointer(in)
	outPtr, outSize := getPointer(out)
	//_,err = syscall.Syscall9(procDeviceIoControl.Addr(), 8, uintptr(handle), uintptr(controlCode), inPtr, uintptr(inSize), outPtr, uintptr(outSize), uintptr(unsafe.Pointer(done)), uintptr(0), 0)
	_, err = win32api.DeviceIoControlPtr(win32api.HANDLE(handle), win32api.DWORD(controlCode), inPtr, win32api.DWORD(inSize), outPtr, win32api.DWORD(outSize), done, nil)
	return
}

func getPointer(i interface{}) (pointer, size uintptr) {
	v := reflect.ValueOf(i)
	switch k := v.Kind(); k {
	case reflect.Ptr:
		t := v.Elem().Type()
		size = t.Size()
		pointer = v.Pointer()
	case reflect.Slice:
		size = uintptr(v.Cap())
		pointer = v.Pointer()
	default:
		fmt.Println("error")
	}
	return
}

// Need a custom Open to work with backup_semantics
func CreateFile(path string, mode int, attrs uint32) (fd syscall.Handle, err error) {
	if len(path) == 0 {
		return syscall.InvalidHandle, win32api.ERROR_FILE_NOT_FOUND
	}
	pathp, err := syscall.UTF16PtrFromString(path)
	if err != nil {
		return syscall.InvalidHandle, err
	}
	var access uint32
	switch mode & (win32api.O_RDONLY | win32api.O_WRONLY | win32api.O_RDWR) {
	case win32api.O_RDONLY:
		access = win32api.GENERIC_READ
	case win32api.O_WRONLY:
		access = win32api.GENERIC_WRITE
	case win32api.O_RDWR:
		access = win32api.GENERIC_READ | win32api.GENERIC_WRITE
	}
	if mode&win32api.O_CREAT != 0 {
		access |= win32api.GENERIC_WRITE
	}
	if mode&win32api.O_APPEND != 0 {
		access &^= win32api.GENERIC_WRITE
		access |= win32api.FILE_APPEND_DATA
	}
	sharemode := uint32(win32api.FILE_SHARE_READ | win32api.FILE_SHARE_WRITE)
	var sa *syscall.SecurityAttributes
	if mode&win32api.O_CLOEXEC == 0 {
		sa = makeInheritSa()
	}
	var createmode uint32
	switch {
	case mode&(win32api.O_CREAT|win32api.O_EXCL) == (win32api.O_CREAT | win32api.O_EXCL):
		createmode = win32api.CREATE_NEW
	case mode&(win32api.O_CREAT|win32api.O_TRUNC) == (win32api.O_CREAT | win32api.O_TRUNC):
		createmode = win32api.CREATE_ALWAYS
	case mode&win32api.O_CREAT == win32api.O_CREAT:
		createmode = win32api.OPEN_ALWAYS
	case mode&win32api.O_TRUNC == win32api.O_TRUNC:
		createmode = win32api.TRUNCATE_EXISTING
	default:
		createmode = win32api.OPEN_EXISTING
	}
	h, e := syscall.CreateFile(pathp, access, sharemode, sa, createmode, attrs, 0)
	return h, e
}

func makeInheritSa() *syscall.SecurityAttributes {
	var sa syscall.SecurityAttributes
	sa.Length = uint32(unsafe.Sizeof(sa))
	sa.InheritHandle = 1
	return &sa
}

// Query usn journal data
func queryUsnJournal(fd syscall.Handle) (ujd win32api.USN_JOURNAL_DATA, done uint32, err error) {
	err = DeviceIoControl(fd, win32api.FSCTL_QUERY_USN_JOURNAL, []byte{}, &ujd, &done)
	return
}

func readUsnJournal(fd syscall.Handle, rujd *win32api.READ_USN_JOURNAL_DATA) (data []byte, done uint32, err error) {
	data = make([]byte, 0x1000)
	err = DeviceIoControl(fd, win32api.FSCTL_READ_USN_JOURNAL, rujd, data, &done)
	return
}

func enumUsnData(fd syscall.Handle, med *win32api.MFT_ENUM_DATA) (data []byte, done uint32, err error) {
	data = make([]byte, 0x10000)
	err = DeviceIoControl(fd, win32api.FSCTL_ENUM_USN_DATA, med, data, &done)
	return
}

type FileEntry struct {
	Name   string
	Parent win32api.DWORDLONG
	Type   uint8
}

type FileMonitor struct {
	Name   string
	Self   win32api.DWORDLONG
	Parent win32api.DWORDLONG
	Type   uint8
	Reason string
}

func ListUsnFile(driver string) (map[win32api.DWORDLONG]FileEntry, error) {
	fileMap := make(map[win32api.DWORDLONG]FileEntry)
	pDriver := "\\\\.\\" + driver[:len(driver)-1]
	fd, err := CreateFile(pDriver, syscall.O_RDONLY, win32api.FILE_ATTRIBUTE_NORMAL)
	if err != nil {
		return fileMap, err
	}
	ujd, _, err := queryUsnJournal(fd)
	if err != nil {
		return fileMap, err
	}
	med := win32api.MFT_ENUM_DATA{0, 0, ujd.NextUsn}
	for {
		data, done, err := enumUsnData(fd, &med)
		if err != nil && done != 0 {
			return fileMap, err
		}
		if done == 0 {
			return fileMap, nil
		}

		var usn win32api.USN = *(*win32api.USN)(unsafe.Pointer(&data[0]))
		// fmt.Println("usn", usn)

		var ur *win32api.USN_RECORD
		for i := unsafe.Sizeof(usn); i < uintptr(done); i += uintptr(ur.RecordLength) {
			ur = (*win32api.USN_RECORD)(unsafe.Pointer(&data[i]))
			nameLength := uintptr(ur.FileNameLength) / unsafe.Sizeof(ur.FileName[0])
			fnp := unsafe.Pointer(&data[i+uintptr(ur.FileNameOffset)])
			fnUtf := (*[10000]uint16)(fnp)[:nameLength]
			fn := syscall.UTF16ToString(fnUtf)
			(*reflect.SliceHeader)(unsafe.Pointer(&fn)).Cap = int(nameLength)
			typed := uint8(0)
			if ur.FileAttributes&win32api.FILE_ATTRIBUTE_DIRECTORY != 0 {
				typed = 1
			}
			// fmt.Println("len", ur.FileNameLength, ur.FileNameOffset, "fn", fn)
			fileMap[ur.FileReferenceNumber] = FileEntry{Name: fn, Parent: ur.ParentFileReferenceNumber, Type: typed}
		}
		med.StartFileReferenceNumber = win32api.DWORDLONG(usn)
	}
}

func ListUsnFileFn(driver string, searchFn func(string, bool) bool) (map[win32api.DWORDLONG]FileEntry, error) {
	fileMap := make(map[win32api.DWORDLONG]FileEntry)
	pDriver := "\\\\.\\" + driver[:len(driver)-1]
	fd, err := CreateFile(pDriver, syscall.O_RDONLY, win32api.FILE_ATTRIBUTE_NORMAL)
	if err != nil {
		return fileMap, err
	}
	ujd, _, err := queryUsnJournal(fd)
	if err != nil {
		return fileMap, err
	}
	med := win32api.MFT_ENUM_DATA{0, 0, ujd.NextUsn}
	for {
		data, done, err := enumUsnData(fd, &med)
		if err != nil && done != 0 {
			return fileMap, err
		}
		if done == 0 {
			return fileMap, nil
		}

		var usn win32api.USN = *(*win32api.USN)(unsafe.Pointer(&data[0]))
		// fmt.Println("usn", usn)

		var ur *win32api.USN_RECORD
		for i := unsafe.Sizeof(usn); i < uintptr(done); i += uintptr(ur.RecordLength) {
			ur = (*win32api.USN_RECORD)(unsafe.Pointer(&data[i]))
			nameLength := uintptr(ur.FileNameLength) / unsafe.Sizeof(ur.FileName[0])
			fnp := unsafe.Pointer(&data[i+uintptr(ur.FileNameOffset)])
			fnUtf := (*[10000]uint16)(fnp)[:nameLength]
			fn := syscall.UTF16ToString(fnUtf)
			(*reflect.SliceHeader)(unsafe.Pointer(&fn)).Cap = int(nameLength)
			typed := uint8(0)
			if ur.FileAttributes&win32api.FILE_ATTRIBUTE_DIRECTORY != 0 {
				typed = 1
			}
			if typed == 1 || searchFn(fn, typed == 1) {
				// fmt.Println("len", ur.FileNameLength, ur.FileNameOffset, "fn", fn)
				fileMap[ur.FileReferenceNumber] = FileEntry{Name: fn, Parent: ur.ParentFileReferenceNumber, Type: typed}
			}
		}
		med.StartFileReferenceNumber = win32api.DWORDLONG(usn)
	}
}

func GetFullUsnPath(diskName string, fileMap map[win32api.DWORDLONG]FileEntry, id win32api.DWORDLONG) (name string) {
	for id != 0 {
		fe := fileMap[id]
		if id == fe.Parent {
			name = "\\" + name
			break
		}
		if name == "" {
			name = fe.Name
		} else {
			name = fe.Name + "\\" + name
		}
		id = fe.Parent
	}
	name = diskName[:len(diskName)-1] + name
	return
}

func GetFullUsnPathEntry(diskName string, fileMap map[win32api.DWORDLONG]FileEntry, en FileMonitor) (name string) {
	fileMap[en.Self] = FileEntry{
		Name:   en.Name,
		Parent: en.Parent,
		Type:   en.Type,
	}
	id := en.Self
	for id != 0 {
		fe := fileMap[id]
		if id == fe.Parent {
			name = "\\" + name
			break
		}
		if name == "" {
			name = fe.Name
		} else {
			name = fe.Name + "\\" + name
		}
		id = fe.Parent
	}
	name = diskName[:len(diskName)-1] + name
	return
}

const (
	ALL_FILES = iota
	ONLY_FOLDER
	NO_FOLDER
)

func ListNTFSUsnDriverFilesFn(diskName string, searchFn func(string, bool) bool) ([]string, error) {
	var result []string
	data, err := ListUsnFileFn(diskName, searchFn)
	if err != nil {
		return result, err
	}
	return listNTFSUsnDriverFiles(diskName, searchFn, data)
}

func ListNTFSUsnDriverFiles(diskName string, folder uint8) ([]string, error) {
	var result []string
	data, err := ListUsnFile(diskName)
	if err != nil {
		return result, err
	}
	return listNTFSUsnDriverFiles(diskName, func(name string, tp bool) bool {
		if !tp && folder == ONLY_FOLDER {
			return false
		}
		if tp && folder == NO_FOLDER {
			return false
		}
		return true
	}, data)
}

func listNTFSUsnDriverFiles(diskName string, fn func(string, bool) bool, data map[win32api.DWORDLONG]FileEntry) ([]string, error) {
	result := make([]string, len(data))
	i := 0
	for k, v := range data {
		if !fn(v.Name, v.Type == 1) {
			continue
		}
		name := GetFullUsnPath(diskName, data, k)
		result[i] = name
		i++
	}
	(*reflect.SliceHeader)(unsafe.Pointer(&result)).Cap = i
	(*reflect.SliceHeader)(unsafe.Pointer(&result)).Len = i
	data = nil
	data = make(map[win32api.DWORDLONG]FileEntry, 0)
	runtime.GC()
	return result, nil
}

func ListNTFSUsnDriverInfoFn(diskName string, searchFn func(string, bool) bool) ([]FileStat, error) {
	data, err := ListUsnFileFn(diskName, searchFn)
	if err != nil {
		return nil, err
	}
	return listNTFSUsnDriverInfo(diskName, searchFn, data)
}

func ListNTFSUsnDriverInfo(diskName string, folder uint8) ([]FileStat, error) {
	data, err := ListUsnFile(diskName)
	if err != nil {
		return nil, err
	}
	return listNTFSUsnDriverInfo(diskName, func(name string, tp bool) bool {
		if !tp && folder == ONLY_FOLDER {
			return false
		}
		if tp && folder == NO_FOLDER {
			return false
		}
		return true
	}, data)
}

func listNTFSUsnDriverInfo(diskName string, fn func(string, bool) bool, data map[win32api.DWORDLONG]FileEntry) ([]FileStat, error) {
	//fmt.Println("finished 1")
	pDriver := "\\\\.\\" + diskName[:len(diskName)-1]
	fd, err := CreateFile(pDriver, syscall.O_RDONLY, win32api.FILE_ATTRIBUTE_NORMAL)
	if err != nil {
		return nil, err
	}
	defer syscall.Close(fd)
	result := make([]FileStat, len(data))
	i := int(0)
	wg := stario.NewWaitGroup(100)
	for k, v := range data {
		if !fn(v.Name, v.Type == 1) {
			continue
		}
		wg.Add(1)
		go func(k win32api.DWORDLONG, v FileEntry, i int) {
			defer wg.Done()
			//now := time.Now().UnixNano()
			/*
				fd2, err := OpenFileByIdWithfd(fd, k, syscall.O_RDONLY, win32api.FILE_ATTRIBUTE_NORMAL)
				if err != nil {
					return
				}
				//fmt.Println("cost", float64((time.Now().UnixNano()-now)/1000000))
				var info syscall.ByHandleFileInformation
				err = syscall.GetFileInformationByHandle(fd2, &info)
				syscall.Close(fd2)
				//fmt.Println("cost", float64((time.Now().UnixNano()-now)/1000000))
				if err != nil {
					return
				}

			*/
			path := GetFullUsnPath(diskName, data, k)
			fileInfo, err := os.Stat(path)
			if err != nil {
				return
			}
			fs := fileInfo.Sys().(*syscall.Win32FileAttributeData)
			stat := FileStat{
				FileAttributes: fs.FileAttributes,
				CreationTime:   fs.CreationTime,
				LastAccessTime: fs.LastAccessTime,
				LastWriteTime:  fs.LastWriteTime,
				FileSizeHigh:   fs.FileSizeHigh,
				FileSizeLow:    fs.FileSizeLow,
			}
			stat.name = v.Name
			stat.path = path
			return
			result[i] = stat
			//result[i] = newFileStatFromInformation(&info, v.Name, path)
		}(k, v, i)
		i++
	}
	wg.Wait()
	//fmt.Println("finished 2")
	(*reflect.SliceHeader)(unsafe.Pointer(&result)).Cap = i
	(*reflect.SliceHeader)(unsafe.Pointer(&result)).Len = i
	data = nil
	//data = make(map[win32api.DWORDLONG]FileEntry, 0)
	runtime.GC()
	return result, nil
}

func getUsnJournalReasonString(reason win32api.DWORD) (s string) {
	var reasons = []string{
		"DataOverwrite",       // 0x00000001
		"DataExtend",          // 0x00000002
		"DataTruncation",      // 0x00000004
		"0x00000008",          // 0x00000008
		"NamedDataOverwrite",  // 0x00000010
		"NamedDataExtend",     // 0x00000020
		"NamedDataTruncation", // 0x00000040
		"0x00000080",          // 0x00000080
		"FileCreate",          // 0x00000100
		"FileDelete",          // 0x00000200
		"PropertyChange",      // 0x00000400
		"SecurityChange",      // 0x00000800
		"RenameOldName",       // 0x00001000
		"RenameNewName",       // 0x00002000
		"IndexableChange",     // 0x00004000
		"BasicInfoChange",     // 0x00008000
		"HardLinkChange",      // 0x00010000
		"CompressionChange",   // 0x00020000
		"EncryptionChange",    // 0x00040000
		"ObjectIdChange",      // 0x00080000
		"ReparsePointChange",  // 0x00100000
		"StreamChange",        // 0x00200000
		"0x00400000",          // 0x00400000
		"0x00800000",          // 0x00800000
		"0x01000000",          // 0x01000000
		"0x02000000",          // 0x02000000
		"0x04000000",          // 0x04000000
		"0x08000000",          // 0x08000000
		"0x10000000",          // 0x10000000
		"0x20000000",          // 0x20000000
		"0x40000000",          // 0x40000000
		"*Close*",             // 0x80000000
	}
	for i := 0; reason != 0; {
		if reason&1 == 1 {
			s = s + ", " + reasons[i]
		}
		reason >>= 1
		i++
	}
	return
}

func MonitorUsnChange(driver string, rec chan FileMonitor) error {
	pDriver := "\\\\.\\" + driver[:len(driver)-1]
	fd, err := CreateFile(pDriver, syscall.O_RDONLY, win32api.FILE_ATTRIBUTE_NORMAL)
	if err != nil {
		return err
	}
	ujd, _, err := queryUsnJournal(fd)
	if err != nil {
		return err
	}

	rujd := win32api.READ_USN_JOURNAL_DATA{ujd.NextUsn, 0xFFFFFFFF, 0, 0, 1, ujd.UsnJournalID}

	for {
		var usn win32api.USN
		data, done, err := readUsnJournal(fd, &rujd)
		if err != nil || done <= uint32(unsafe.Sizeof(usn)) {
			return err
		}

		usn = *(*win32api.USN)(unsafe.Pointer(&data[0]))

		var ur *win32api.USN_RECORD
		for i := unsafe.Sizeof(usn); i < uintptr(done); i += uintptr(ur.RecordLength) {
			ur = (*win32api.USN_RECORD)(unsafe.Pointer(&data[i]))
			nameLength := uintptr(ur.FileNameLength) / unsafe.Sizeof(ur.FileName[0])
			fnp := unsafe.Pointer(&data[i+uintptr(ur.FileNameOffset)])
			fn := syscall.UTF16ToString((*[10000]uint16)(fnp)[:nameLength])
			(*reflect.SliceHeader)(unsafe.Pointer(&fn)).Cap = int(nameLength)
			// fmt.Println("len", ur.FileNameLength, ur.FileNameOffset, "fn", getFullPath(folders, ur.ParentFileReferenceNumber), syscall.UTF16ToString(fn), getUsnJournalReasonString(ur.Reason))
			typed := uint8(0)
			if ur.FileAttributes&win32api.FILE_ATTRIBUTE_DIRECTORY != 0 {
				typed = 1
			}
			// fmt.Println("len", ur.FileNameLength, ur.FileNameOffset, "fn", fn)
			rec <- FileMonitor{Name: fn, Parent: ur.ParentFileReferenceNumber, Type: typed, Self: ur.FileReferenceNumber, Reason: getUsnJournalReasonString(ur.Reason)}
		}
		rujd.StartUsn = usn
		if usn == 0 {
			return nil
		}
	}
}

func GetUsnFileInfo(diskName string, fileMap map[win32api.DWORDLONG]FileEntry, id win32api.DWORDLONG) (FileStat, error) {
	name := fileMap[id].Name
	path := GetFullUsnPath(diskName, fileMap, id)
	fd, err := OpenFileById(diskName, id, syscall.O_RDONLY, win32api.FILE_ATTRIBUTE_NORMAL)
	if err != nil {
		return FileStat{}, err
	}
	var info syscall.ByHandleFileInformation
	err = syscall.GetFileInformationByHandle(fd, &info)
	return newFileStatFromInformation(&info, name, path), err
}

// Need a custom Open to work with backup_semantics
func OpenFileById(diskName string, id win32api.DWORDLONG, mode int, attrs uint32) (syscall.Handle, error) {
	pDriver := "\\\\.\\" + diskName[:len(diskName)-1]
	fd, err := CreateFile(pDriver, syscall.O_RDONLY, win32api.FILE_ATTRIBUTE_NORMAL)
	if err != nil {
		return syscall.InvalidHandle, err
	}
	defer syscall.Close(fd)
	return OpenFileByIdWithfd(fd, id, mode, attrs)
}

func OpenFileByIdWithfd(fd syscall.Handle, id win32api.DWORDLONG, mode int, attrs uint32) (syscall.Handle, error) {
	var access uint32
	switch mode & (win32api.O_RDONLY | win32api.O_WRONLY | win32api.O_RDWR) {
	case win32api.O_RDONLY:
		access = win32api.GENERIC_READ
	case win32api.O_WRONLY:
		access = win32api.GENERIC_WRITE
	case win32api.O_RDWR:
		access = win32api.GENERIC_READ | win32api.GENERIC_WRITE
	}
	if mode&win32api.O_CREAT != 0 {
		access |= win32api.GENERIC_WRITE
	}
	if mode&win32api.O_APPEND != 0 {
		access &^= win32api.GENERIC_WRITE
		access |= win32api.FILE_APPEND_DATA
	}
	sharemode := uint32(win32api.FILE_SHARE_READ | win32api.FILE_SHARE_WRITE)
	var sa *syscall.SecurityAttributes
	if mode&win32api.O_CLOEXEC == 0 {
		sa = makeInheritSa()
	}
	fid := win32api.FILE_ID_DESCRIPTOR{
		DwSize: 16,
		Type:   0,
		FileId: id,
	}
	fid.DwSize = win32api.DWORD(unsafe.Sizeof(fid))
	h, e := win32api.OpenFileById(win32api.HANDLE(fd), &fid, win32api.DWORD(access),
		win32api.DWORD(sharemode), sa, win32api.DWORD(attrs))
	return syscall.Handle(h), e
}
