// Recipe "download-file": download the files of a document's latest version to a folder.
//
// Usage: go run ./recipes/download_file [document ID, default 437]
// [folder, default a "mfiles-grpc-samples" folder in the temporary folder]
package main

import (
	"context"
	"fmt"
	"os"
	"path/filepath"

	"github.com/M-Files/mfiles-grpc-api-samples/mfilesclient"
	"github.com/M-Files/mfiles-grpc-api-samples/recipes/internal/recipe"
	"github.com/M-Files/mfiles-grpc-api/go/mfiles"
)

// blockSize is the most one DownloadFile_ReadBlock call asks for: the server's limit per block.
const blockSize = 4 * 1024 * 1024

func main() {
	recipe.Main(run)
}

func run(ctx context.Context) error {
	id := recipe.IntArg(0, 437)
	folder := recipe.StringArg(1, filepath.Join(os.TempDir(), "mfiles-grpc-samples"))
	if err := os.MkdirAll(folder, 0o755); err != nil {
		return err
	}
	client, err := recipe.Connect(ctx)
	if err != nil {
		return err
	}
	defer client.Close()
	objects := mfiles.NewIRPCObjectOperationsClient(client.Conn)

	// The latest version lists the files.
	latest, err := objects.GetObjectInfoAndProperties(ctx, &mfiles.GetObjectInfoAndPropertiesRequest{
		ObjId:         mfilesclient.ObjID(0, id),
		LatestVersion: true,
	})
	if err != nil {
		return err
	}
	object := latest.GetObjectInfo()
	fmt.Println(recipe.Describe(object))

	for _, file := range object.GetVersionInfo().GetFiles() {
		name := file.GetTitle() + "." + file.GetExtension()
		// A title can contain characters that are not allowed in file names, such as "/".
		path := filepath.Join(folder, safeFileName(name))
		size, err := download(ctx, objects, object.GetObjectInfo().GetObjId(), file.GetFileVer(), path)
		if err != nil {
			return err
		}
		fmt.Printf("  Downloaded %s (%d bytes) to %s\n", name, size, path)
	}
	return nil
}

// [START download-file]

// download saves one file version in blocks: DownloadFile_Begin opens a download session,
// DownloadFile_ReadBlock reads it, DownloadFile_Stop closes it.
func download(ctx context.Context, objects mfiles.IRPCObjectOperationsClient, objID *mfiles.ObjID,
	fileVer *mfiles.FileVer, path string) (int64, error) {
	begin, err := objects.DownloadFile_Begin(ctx, &mfiles.DownloadFile_BeginRequest{
		ObjFileVer:                &mfiles.ObjFileVer{ObjId: objID, FileVer: fileVer},
		Format:                    mfiles.FileFormat_FILE_FORMAT_NATIVE,
		PrepareFileForDownloading: true,
	})
	if err != nil {
		return 0, err
	}
	downloadID := begin.GetDownloadId()
	defer objects.DownloadFile_Stop(context.WithoutCancel(ctx),
		&mfiles.DownloadFile_StopRequest{DownloadId: downloadID})

	output, err := os.Create(path)
	if err != nil {
		return 0, err
	}
	defer output.Close()

	size := begin.GetFileOutline().GetSize()
	for offset := int64(0); offset < size; {
		block, err := objects.DownloadFile_ReadBlock(ctx, &mfiles.DownloadFile_ReadBlockRequest{
			DownloadId: downloadID,
			BlockSize:  uint32(min(blockSize, size-offset)),
			Offset:     offset,
		})
		if err != nil {
			return 0, err
		}
		if len(block.GetData()) == 0 {
			return 0, fmt.Errorf("the server returned no data at offset %d of %d", offset, size)
		}
		if _, err := output.Write(block.GetData()); err != nil {
			return 0, err
		}
		offset += int64(len(block.GetData()))
	}
	return size, output.Close()
}

// [END download-file]

// safeFileName replaces the characters Windows does not allow in file names.
func safeFileName(name string) string {
	result := []rune(name)
	for i, r := range result {
		switch r {
		case '<', '>', ':', '"', '/', '\\', '|', '?', '*':
			result[i] = '_'
		}
	}
	return string(result)
}
