package message

import (
	"context"
	"io"
	"io/fs"

	"go.uber.org/atomic"

	"github.com/gotd/td/telegram/uploader"
	"github.com/gotd/td/telegram/uploader/source"
	"github.com/gotd/td/tg"
)

// Uploader is an abstraction for Telegram file uploader.
type Uploader interface {
	FromFile(ctx context.Context, f uploader.File) (tg.InputFileClass, error)
	FromPath(ctx context.Context, path string) (tg.InputFileClass, error)
	FromFS(ctx context.Context, filesystem fs.FS, path string) (tg.InputFileClass, error)
	FromReader(ctx context.Context, name string, f io.Reader) (tg.InputFileClass, error)
	FromBytes(ctx context.Context, name string, b []byte) (tg.InputFileClass, error)
	FromURL(ctx context.Context, rawURL string) (tg.InputFileClass, error)
	FromSource(ctx context.Context, src source.Source, rawURL string) (tg.InputFileClass, error)
}

type uploadBuilder struct {
	upload Uploader
}

// UploadOption is a UploadBuilder creation option.
type UploadOption interface {
	apply(ctx context.Context, b uploadBuilder) (tg.InputFileClass, error)
}

// uploadOptionFunc is a functional adapter for UploadOption.
type uploadOptionFunc func(ctx context.Context, b uploadBuilder) (tg.InputFileClass, error)

func (f uploadOptionFunc) apply(ctx context.Context, b uploadBuilder) (tg.InputFileClass, error) {
	return f(ctx, b)
}

type fileCache atomic.Value

func (r *fileCache) Store(result tg.InputFileClass) {
	r.Value.Store(result)
}

func (r *fileCache) Load() (result tg.InputFileClass, ok bool) {
	result, ok = r.Value.Load().(tg.InputFileClass)
	return
}

// FilePromise is a upload file promise.
type FilePromise = func(ctx context.Context, b Uploader) (tg.InputFileClass, error)

// Upload creates new upload options using given promise.
func Upload(promise FilePromise) UploadOption {
	once := &fileCache{}
	return uploadOptionFunc(func(ctx context.Context, b uploadBuilder) (r tg.InputFileClass, err error) {
		if v, ok := once.Load(); ok {
			return v, nil
		}
		defer func() {
			if err == nil && r != nil {
				once.Store(r)
			}
		}()

		return promise(ctx, b.upload)
	})
}

// FromFile uploads given File.
// NB: FromFile does not close given file.
func FromFile(f uploader.File) UploadOption {
	return Upload(func(ctx context.Context, b Uploader) (tg.InputFileClass, error) {
		return b.FromFile(ctx, f)
	})
}

// FromPath uploads file from given path.
func FromPath(path string) UploadOption {
	return Upload(func(ctx context.Context, b Uploader) (tg.InputFileClass, error) {
		return b.FromPath(ctx, path)
	})
}

// FromFS uploads file from given path using given fs.FS.
func FromFS(filesystem fs.FS, path string) UploadOption {
	return Upload(func(ctx context.Context, b Uploader) (tg.InputFileClass, error) {
		return b.FromFS(ctx, filesystem, path)
	})
}

// FromReader uploads file from given io.Reader.
// NB: totally stream should not exceed the limit for
// small files (10 MB as docs says, may be a bit bigger).
func FromReader(name string, r io.Reader) UploadOption {
	return Upload(func(ctx context.Context, b Uploader) (tg.InputFileClass, error) {
		return b.FromReader(ctx, name, r)
	})
}

// FromBytes uploads file from given byte slice.
func FromBytes(name string, data []byte) UploadOption {
	return Upload(func(ctx context.Context, b Uploader) (tg.InputFileClass, error) {
		return b.FromBytes(ctx, name, data)
	})
}

// FromURL uploads file from given URL.
func FromURL(rawURL string) UploadOption {
	return Upload(func(ctx context.Context, b Uploader) (tg.InputFileClass, error) {
		return b.FromURL(ctx, rawURL)
	})
}

// FromSource uploads file from given URL using given Source.
func FromSource(src source.Source, rawURL string) UploadOption {
	return Upload(func(ctx context.Context, b Uploader) (tg.InputFileClass, error) {
		return b.FromSource(ctx, src, rawURL)
	})
}
