package mergefs

import (
	"io"
	"os"
	"sort"
	"unsafe"

	"path/filepath"
	"syscall"

	"github.com/tidbcloud/ffs/pkg/v1/proto"
)

const frameHeaderSize = int(unsafe.Sizeof(frameHeader{}))

type frameHeader struct {
	off  uint64
	size uint64
}

func (h *frameHeader) marshal() []byte {
	var b []byte
	hdr := (*struct {
		data uintptr
		len  int
		cap  int
	})(unsafe.Pointer(&b))
	hdr.data = uintptr(unsafe.Pointer(h))
	hdr.len = frameHeaderSize
	hdr.cap = frameHeaderSize
	return b
}

func (h *frameHeader) unmarshal(b []byte) {
	*h = *(*frameHeader)(unsafe.Pointer(&b[0]))
}

type frame struct {
	frameHeader
	pos uint64
}

func Path(p string) string {
	if p == "." || p == "" {
		p = string(filepath.Separator)
	} else if len(p) > 0 && p[0] != filepath.Separator {
		p = string(filepath.Separator) + p
	}
	return p
}

func Entries(root string) (entries []*proto.DirEntry) {
	filepath.Walk(root, func(path string, fi os.FileInfo, err error) error {
		name, err := filepath.Rel(root, path)
		if err != nil {
			return nil
		}
		e := &proto.DirEntry{
			IsDir:   fi.IsDir(),
			Name:    Path(name),
			Mode:    uint32(fi.Mode()),
			Sz:      uint64(fi.Size()),
			ModTime: uint64(fi.ModTime().UnixNano()),
		}
		if s, ok := fi.Sys().(*syscall.Stat_t); ok {
			e.Nlink = uint32(s.Nlink)
			e.Ino = uint64(s.Ino)
		}

		if !fi.IsDir() {
			f, err := OpenSegFile(path)
			if err != nil {
				return nil
			}
			defer f.Close()
			fi, err = f.Stat()
			if err != nil {
				return nil
			}
			e.Sz = uint64(fi.Size())
		}
		entries = append(entries, e)
		return nil
	})
	return
}

func CompareEntry(entries ...*proto.DirEntry) *proto.DirEntry {
	if len(entries) == 0 {
		return nil
	}
	if len(entries) == 1 {
		return entries[0]
	}
	ret := entries[0]
	for i := 1; i < len(entries); i++ {
		entry := entries[i]
		if ret.IsDir && ret.ModTime < entry.ModTime || ret.Sz < entry.Sz {
			ret = entry
		}
	}
	return ret
}

func MergeEntries(dirs ...[]*proto.DirEntry) []*proto.DirEntry {
	if len(dirs) == 0 {
		return nil
	}
	if len(dirs) == 1 {
		return dirs[0]
	}
	dic := map[string]*proto.DirEntry{}
	for _, dir := range dirs {
		for _, entry := range dir {
			if in, ok := dic[entry.Name]; !ok || in.IsDir && in.ModTime < entry.ModTime || in.Sz < entry.Sz {
				dic[entry.Name] = entry
			}
		}
	}
	entries := make([]*proto.DirEntry, 0, len(dic))
	for _, entry := range dic {
		entries = append(entries, entry)
	}
	return entries
}

func MergeFrames(a []*proto.Frame, b []*proto.Frame) []*proto.Frame {
	if len(a) == 0 {
		return b
	}
	if len(b) == 0 {
		return a
	}
	m, n := a, b
	if len(a) < len(b) {
		m, n = b, a
	}
	i, j, k := len(m)-1, len(n)-1, len(m)+len(n)-1
	m = append(m, n...)
	for i >= 0 && j >= 0 {
		if m[i].Off > n[j].Off || m[i].Off == n[j].Off && (m[i].Len == n[j].Len || m[i].Len > n[j].Len) {
			m[k] = m[i]
			i--
		} else {
			m[k] = n[j]
			j--
		}
		k--
	}
	for j >= 0 {
		m[k] = n[j]
		j--
		k--
	}
	return m
}

func ReadAt(frames []*proto.Frame, b []byte, off int64) (n int) {
	if len(frames) == 0 {
		return 0
	}
	length := len(b)
	for i := 0; i < len(frames) && n < length; i++ {
		s := frames[i]
		segOff := off - int64(s.Off)
		if segOff < 0 {
			return 0
		}
		segRemain := int(s.Len - uint64(segOff))
		readSize := segRemain
		if segRemain > length-n {
			readSize = length - n
		}
		num := copy(b[n:n+readSize], s.Data[segOff:])
		off += int64(num)
		n += num
	}
	return n
}

type SegFile interface {
	Stat() (os.FileInfo, error)
	Truncate(size int64) error
	WriteAt(b []byte, off int64) (n int, err error)
	ReadAt(off, size int64) (frames []*proto.Frame, err error)
	Sync() error
	Close() error
}

type segFile struct {
	file   *os.File
	off    int64
	frames []*frame
}

func OpenSegFile(name string) (SegFile, error) {
	file, err := os.OpenFile(name, os.O_CREATE|os.O_RDWR, os.ModePerm)
	if err != nil {
		return nil, err
	}
	f := &segFile{file: file}
	var a [frameHeaderSize]byte
	var off int64
	for {
		buf := a[:]
		n, err := f.file.ReadAt(buf, off)
		if err != nil {
			if err != io.EOF {
				return nil, err
			}
			break
		}
		if n != frameHeaderSize {
			break
		}
		var s = &frame{}
		h := &frameHeader{}
		h.unmarshal(buf)
		s.frameHeader = *h
		s.pos = uint64(off) + uint64(frameHeaderSize)
		f.frames = append(f.frames, s)
		f.off += int64(frameHeaderSize) + int64(s.size)
		off = int64(f.off)
	}
	return f, nil
}

type FileInfo struct {
	os.FileInfo
	size int64
}

func (f *FileInfo) Size() int64 {
	return f.size
}

func (f *segFile) Stat() (os.FileInfo, error) {
	fi, err := f.file.Stat()
	if err != nil {
		return nil, err
	}
	return &FileInfo{FileInfo: fi, size: f.size()}, nil
}

func (f *segFile) size() int64 {
	if len(f.frames) == 0 {
		return 0
	}
	frame := f.frames[len(f.frames)-1]
	return int64(frame.off + frame.size)
}

func (f *segFile) Truncate(size int64) error {
	length := len(f.frames)
	for i := length - 1; i >= 0; i-- {
		frame := f.frames[i]
		if size <= int64(frame.off) {
			f.frames = f.frames[:i]
			f.off -= int64(frameHeaderSize) + int64(frame.size)
			err := f.file.Truncate(int64(frame.pos) - int64(frameHeaderSize))
			if err != nil {
				return err
			}
		} else {
			if size < int64(frame.off+frame.size) {
				cut := int64(frame.off+frame.size) - size
				frame.size = uint64(size) - frame.off
				f.frames[i] = frame
				f.off -= cut
				return f.file.Truncate(int64(frame.pos + frame.size))
			}
			break
		}
	}
	return nil
}

func (f *segFile) WriteAt(b []byte, off int64) (n int, err error) {
	err = f.Truncate(off)
	if err != nil {
		return
	}
	s := &frame{frameHeader: frameHeader{off: uint64(off), size: uint64(len(b))}, pos: uint64(f.off + int64(frameHeaderSize))}
	f.file.WriteAt(s.frameHeader.marshal(), int64(f.off))
	n, err = f.file.WriteAt(b, int64(s.pos))
	f.frames = append(f.frames, s)
	f.off += int64(frameHeaderSize) + int64(s.size)
	return
}

func (f *segFile) ReadAt(off, size int64) (frames []*proto.Frame, err error) {
	idx := sort.Search(len(f.frames), func(i int) bool {
		return f.frames[i].off > uint64(off)
	})
	start := idx - 1
	if start < 0 {
		start = 0
	}
	for i := start; i < len(f.frames); i++ {
		s := f.frames[i]
		if int64(s.off+s.size) <= off {
			continue
		}
		if int64(s.off) >= off+size {
			break
		}
		b := make([]byte, s.size)
		_, err = f.file.ReadAt(b, int64(s.pos))
		var r = &proto.Frame{Off: s.off, Len: s.size}
		r.Data = b
		frames = append(frames, r)
	}
	return frames, err
}

func (f *segFile) Sync() error {
	return f.file.Sync()
}

func (f *segFile) Close() error {
	return f.file.Close()
}
