package crypto

import (
	"cachefs/pkg/provider"
	"io"
	"io/fs"
	"os"
	"sync"
)

var _ provider.File = (*file)(nil)

type file struct {
	provider.File
	rmu    sync.Mutex
	wmu    sync.Mutex
	fs     *FS
	r      *reader
	w      *writer
	nonce  [NonceSize]byte
	offset int64
	name   string
	stat   *stat
	dir    bool
}

func newFile(key []byte, name string, fs *FS, f provider.File, dir bool) (ef provider.File, err error) {
	if dir {
		return &file{
			File: f,
			fs:   fs,
			name: name,
			dir:  dir,
		}, nil
	}
	nonce, err := initNonce(f)
	if err != nil {
		return
	}
	skey := streamKey(key, nonce)
	w, err := newWriter(skey, f)
	if err != nil {
		return
	}
	r, err := newReader(skey, f)
	if err != nil {
		return
	}
	ef = &file{
		File: f,
		fs:   fs,
		w:    w,
		r:    r,
		name: name,
	}
	return ef, nil
}

func (f *file) Stat() (fs.FileInfo, error) {
	if f.stat != nil {
		return f.stat, nil
	}
	fi, err := f.File.Stat()
	if err != nil {
		return nil, err
	}
	if f.dir {
		return dir{FileInfo: fi, name: f.name}, nil
	}
	f.stat = &stat{
		FileInfo: fi,
		size:     RealSize(fi.Size()),
		name:     f.name,
	}
	return f.stat, nil
}

func (f *file) Name() string {
	return f.name
}

func (f *file) Fd() uintptr {
	osf, ok := f.File.(*os.File)
	if ok {
		return osf.Fd()
	}
	return 0
}

type stat struct {
	fs.FileInfo
	size int64
	name string
}

func (s stat) Size() int64 {
	return s.size
}

func (s stat) Name() string {
	return s.name
}

func (f *file) Truncate(n int64) error {
	return f.File.Truncate(Size(n))
}

func (f *file) Close() error {
	if f.dir {
		return f.File.Close()
	}
	e1 := f.w.Close()
	e2 := f.File.Close()
	if e1 != nil {
		return e1
	}
	return e2
}

func (f *file) Seek(offset int64, whence int) (n int64, err error) {
	var cn, off, roff int64
	switch whence {
	case io.SeekStart:
		cn, off, roff = align(offset)
		f.offset = KDFNonceSize + off
		n, err = f.File.Seek(f.offset, whence)
	case io.SeekCurrent:
		if f.offset == 0 {
			f.offset = KDFNonceSize
		}
		cn, off, roff = align(f.offset + f.r.off + offset)
		f.offset = off
		_, err = f.File.Seek(f.offset, io.SeekStart)
		n = offset
	}
	f.r.err = nil
	f.r.unread = f.r.unread[:0]
	if err != nil {
		return
	}
	f.nonce = [NonceSize]byte{}
	put(&f.nonce, cn)
	f.r.nonce = f.nonce
	f.w.nonce = f.nonce
	f.r.off = roff
	f.r.cn = cn
	return
}

func (f *file) Read(p []byte) (n int, err error) {
	f.r.cn = -1
	n, err = f.r.Read(p)
	return
}

func (f *file) Write(p []byte) (n int, err error) {
	n, err = f.w.Write(p)
	return
}

func (f *file) ReadAt(p []byte, pos int64) (n int, err error) {
	f.rmu.Lock()
	defer f.rmu.Unlock()
	cn, _, roff := align(pos)
	var last bool
	if f.r.cn != cn {
		_, err = f.Seek(pos, io.SeekStart)
		if err != nil {
			return
		}
		last, err = f.r.readChunk()
		if err != nil {
			return
		}
	}
	n = copy(p, f.r.unread[roff:])
	if last && len(f.r.unread) == n+int(roff) {
		err = io.EOF
	}
	return
}

func (f *file) WriteAt(data []byte, pos int64) (n int, err error) {
	f.wmu.Lock()
	defer f.wmu.Unlock()
	for n != len(data) {
		cn, off, woff := align(pos + int64(n))
		c, ok := f.w.cm[cn]
		if !ok {
			c = &chunkWriter{offset: off + KDFNonceSize, w: f.w, cn: cn}
			put(&c.nonce, cn)
			f.w.cm[cn] = c
			if f.w.last == nil {
				f.w.last = c
			} else if cn > f.w.last.cn {
				f.w.last = c
			}
		}
		w, e := c.writeAt(data[n:], woff)
		n += w
		if c.full() {
			if f.w.last != nil && f.w.last.cn == cn {
				f.w.last = nil
			}
			// async flush
			go func() {
				c.flush(notLastChunk)
			}()
			delete(f.w.cm, cn)
		}
		if e != nil {
			err = e
			return
		}
	}
	return
}

func (f *file) Readdir(n int) ([]fs.FileInfo, error) {
	fis, err := f.File.Readdir(n)
	if err != nil {
		return nil, err
	}
	fio := make([]fs.FileInfo, len(fis))
	for i, fi := range fis {
		name, err := f.fs.decPath(fi.Name())
		if err != nil {
			return nil, err
		}
		fio[i] = stat{FileInfo: fi, size: RealSize(fi.Size()), name: name}
	}
	return fio, nil
}

func (f *file) ReadDir(n int) ([]fs.DirEntry, error) {
	fis, err := f.Readdir(n)
	if err != nil {
		return nil, err
	}
	fio := make([]fs.DirEntry, len(fis))
	for i, fi := range fis {
		fio[i] = fs.FileInfoToDirEntry(fi)
	}
	return fio, nil
}

type dir struct {
	fs.FileInfo
	name string
}

func (d dir) Name() string {
	return d.name
}
