// Copyright (C) 2022 Marius Schellenberger

package fs

import (
	"cachefs/pkg/provider"
	"cachefs/pkg/provider/crypto"
	"context"
	"errors"
	"io"
	"os"

	"github.com/go-logr/logr"
)

type File struct {
	log     logr.Logger
	f       provider.File
	cache   provider.File
	md      *Metadata
	offset  int64
	offline bool
}

type preload struct {
	f       *File
	skipped int64
	written int64
	ctx     context.Context
}

func (p *preload) Read(data []byte) (n int, err error) {
	select {
	case <-p.ctx.Done():
		return 0, p.ctx.Err()
	default:
	}
	if p.f.offset >= p.f.size() {
		return 0, io.EOF
	}
	n = len(data)
	if p.f.hasChunk(n) {
		p.skipped++
		_, err = p.f.Seek(int64(n), io.SeekCurrent)
		return
	}
	n, err = p.f.readToCache(data)
	p.written++
	return
}

func (f *File) Preload(ctx context.Context, fin func(), errf func(), stop func()) {
	log := f.log
	defer f.Close()
	if f.offline {
		log.V(2).Error(errors.New("no preload in offline mode"), "error preloading file")
		errf()
		return
	}
	if f.md.FullyCached() {
		log.V(2).Info("skipped preload for fully cached file")
		fin()
		return
	}

	log.V(2).Info("preload started")
	p := &preload{f: f, ctx: ctx}
	_, err := io.Copy(Discard, p)
	if err == context.Canceled {
		log.V(2).Info("preload canceled", "skipped", p.skipped, "written", p.written)
		stop()
		return
	}
	perr := false
	if err != nil && err != io.EOF {
		log.Error(err, "error preloading file")
		perr = true
	}
	err = f.md.Close()
	if err != nil {
		log.Error(err, "error closing cache file")
	}
	if perr {
		errf()
		return
	}
	log.V(2).Info("preload finished", "skipped", p.skipped, "written", p.written)
	fin()
}

func (f *File) Read(p []byte) (n int, err error) {
	log := f.log
	if f.hasChunk(len(p)) {
		n, err = f.cache.ReadAt(p, f.offset)
		if err == io.EOF {
			return
		}
		if err != nil {
			if !IsIOErr(err) && err != crypto.ErrDecrypt {
				log.Error(err, "error reading cache file")
				return
			}
			n, err = f.readSource(p)
			if err != nil {
				log.Error(err, "error reading fallback source file")
				return
			}
		}
		_, err = f.Seek(int64(n), io.SeekCurrent)
		if err != nil {
			log.Error(err, "error seeking source file")
		}
		return
	}
	return f.readToCache(p)
}

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

func (f *File) hasChunk(n int) bool {
	return f.md.HasChunk(f.offset, n)
}

func (f *File) readSource(p []byte) (n int, err error) {
	if f.offline {
		return 0, io.EOF
	}
	return f.f.Read(p)
}

func (f *File) readToCache(p []byte) (n int, err error) {
	log := f.log
	n, err = f.readSource(p)
	if n > 0 {
		// async cache write
		if _, err = f.md.WriteAt(p[:n], f.offset); err != nil {
			log.Error(err, "error writing cache file")
			return
		}
	}
	if err != nil && err != io.EOF {
		log.Error(err, "error reading source file")
		return
	}
	f.offset += int64(n)
	if n > 0 && n < len(p) {
		_, err = f.Seek(f.offset, io.SeekStart)
	}
	return
}

func (f *File) Seek(offset int64, whence int) (n int64, err error) {
	if !f.offline {
		n, err = f.f.Seek(offset, whence)
		if err != nil {
			f.log.Error(err, "error seeking source file")
			return
		}
	}
	switch whence {
	case io.SeekStart:
		f.offset = offset
	case io.SeekCurrent:
		f.offset += offset
	}
	return
}

func (f *File) Readdir(_ int) ([]os.FileInfo, error) {
	return nil, nil
}

func (f *File) Stat() (os.FileInfo, error) {
	if f.offline {
		return f.md.Stat()
	}
	return f.f.Stat()
}

func (f *File) Close() error {
	if !f.offline && f.cache != nil {
		f.cache.Close()
	}
	return f.f.Close()
}
