// Copyright (c) 2024 Celestino Amoroso (celestino.amoroso@gmail.com).
// All rights reserved.

// builtin-os-file.go
package expr

import (
	"bufio"
	"fmt"
	"io"
	"os"
)

const (
	osLimitCh = "limitCh"
)

type osHandle interface {
	getFile() *os.File
}

type osWriter struct {
	fh     *os.File
	writer *bufio.Writer
}

func (h *osWriter) TypeName() string {
	return "osWriter"
}

func (h *osWriter) String() string {
	return "writer"
}

func (h *osWriter) getFile() *os.File {
	return h.fh
}

type osReader struct {
	fh     *os.File
	reader *bufio.Reader
}

func (h *osReader) TypeName() string {
	return "osReader"
}

func (h *osReader) String() string {
	return "reader"
}

func (h *osReader) getFile() *os.File {
	return h.fh
}

func errMissingFilePath(funcName string) error {
	return fmt.Errorf("%s(): missing or invalid file path", funcName)
}

func errInvalidFileHandle(funcName string, v any) error {
	if v != nil {
		return fmt.Errorf("%s(): invalid file handle %v [%T]", funcName, v, v)
	} else {
		return fmt.Errorf("%s(): invalid file handle", funcName)
	}
}

func createFileFunc(ctx ExprContext, name string, args map[string]any) (result any, err error) {
	if filePath, ok := args[ParamFilepath].(string); ok && len(filePath) > 0 {
		var fh *os.File
		if fh, err = os.Create(filePath); err == nil {
			result = &osWriter{fh: fh, writer: bufio.NewWriter(fh)}
		}
	} else {
		err = errMissingFilePath(name)
	}
	return
}

func openFileFunc(ctx ExprContext, name string, args map[string]any) (result any, err error) {
	if filePath, ok := args[ParamFilepath].(string); ok && len(filePath) > 0 {
		var fh *os.File
		if fh, err = os.Open(filePath); err == nil {
			result = &osReader{fh: fh, reader: bufio.NewReader(fh)}
		}
	} else {
		err = errMissingFilePath(name)
	}
	return
}

func appendFileFunc(ctx ExprContext, name string, args map[string]any) (result any, err error) {
	if filePath, ok := args[ParamFilepath].(string); ok && len(filePath) > 0 {
		var fh *os.File
		if fh, err = os.OpenFile(filePath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0660); err == nil {
			result = &osWriter{fh: fh, writer: bufio.NewWriter(fh)}
		}
	} else {
		err = errMissingFilePath(name)
	}
	return
}

func closeFileFunc(ctx ExprContext, name string, args map[string]any) (result any, err error) {
	var handle osHandle
	var invalidFileHandle any
	var ok bool

	if handle, ok = args[ParamHandle].(osHandle); !ok {
		invalidFileHandle = args[ParamHandle]
	}

	if handle != nil {
		if fh := handle.getFile(); fh != nil {
			if w, ok := handle.(*osWriter); ok {
				err = w.writer.Flush()
			}

			if err == nil {
				err = fh.Close()
			}
		}
	}
	if err == nil && (handle == nil || invalidFileHandle != nil) {
		err = errInvalidFileHandle(name, handle)
	}
	result = err == nil
	return
}

func fileWriteTextFunc(ctx ExprContext, name string, args map[string]any) (result any, err error) {
	var handle osHandle
	var invalidFileHandle any
	var ok bool

	if handle, ok = args[ParamHandle].(osHandle); !ok {
		invalidFileHandle = args[ParamHandle]
	}

	if handle != nil {
		if w, ok := handle.(*osWriter); ok {
			if v, exists := args[ParamItem]; exists {
				argv := v.([]any)
				result, err = fmt.Fprint(w.writer, argv...)
			}
		} else {
			invalidFileHandle = handle
		}
	}

	if err == nil && (handle == nil || invalidFileHandle != nil) {
		err = errInvalidFileHandle(name, invalidFileHandle)
	}
	return
}

func fileReadTextFunc(ctx ExprContext, name string, args map[string]any) (result any, err error) {
	var handle osHandle
	var invalidFileHandle any
	var ok bool

	result = nil
	if handle, ok = args[ParamHandle].(osHandle); !ok || args[ParamHandle] == nil {
		invalidFileHandle = args[ParamHandle]
	}

	if handle != nil {
		if r, ok := handle.(*osReader); ok {
			var limit byte = '\n'
			var v string
			if s, ok := args[osLimitCh].(string); ok && len(s) > 0 {
				limit = s[0]
			}

			v, err = r.reader.ReadString(limit)
			if err == io.EOF {
				err = nil
			}
			if len(v) > 0 {
				if v[len(v)-1] == limit {
					result = v[0 : len(v)-1]
				} else {
					result = v
				}
			}
		} else {
			invalidFileHandle = handle
		}
	}

	if err == nil && (handle == nil || invalidFileHandle != nil) {
		err = errInvalidFileHandle(name, invalidFileHandle)
	}
	return
}

func fileReadTextAllFunc(ctx ExprContext, name string, args map[string]any) (result any, err error) {
	var handle osHandle
	var invalidFileHandle any
	var ok bool

	result = nil
	if handle, ok = args[ParamHandle].(osHandle); !ok || args[ParamHandle] == nil {
		invalidFileHandle = args[ParamHandle]
	}

	if handle != nil {
		if r, ok := handle.(*osReader); ok {
			var b []byte
			b, err = io.ReadAll(r.reader)
			result = string(b)
		} else {
			invalidFileHandle = handle
		}
	}

	if err == nil && (handle == nil || invalidFileHandle != nil) {
		err = errInvalidFileHandle(name, invalidFileHandle)
	}
	return
}

func ImportOsFuncs(ctx ExprContext) {
	ctx.RegisterFunc("fileOpen", NewGolangFunctor(openFileFunc), TypeFileHandle, []ExprFuncParam{
		NewFuncParam(ParamFilepath),
	})

	ctx.RegisterFunc("fileAppend", NewGolangFunctor(appendFileFunc), TypeFileHandle, []ExprFuncParam{
		NewFuncParam(ParamFilepath),
	})

	ctx.RegisterFunc("fileCreate", NewGolangFunctor(createFileFunc), TypeFileHandle, []ExprFuncParam{
		NewFuncParam(ParamFilepath),
	})

	ctx.RegisterFunc("fileClose", NewGolangFunctor(closeFileFunc), TypeBoolean, []ExprFuncParam{
		NewFuncParam(ParamHandle),
	})

	ctx.RegisterFunc("fileWriteText", NewGolangFunctor(fileWriteTextFunc), TypeInt, []ExprFuncParam{
		NewFuncParam(ParamHandle),
		NewFuncParamFlagDef(ParamItem, PfDefault|PfRepeat, ""),
	})

	ctx.RegisterFunc("fileReadText", NewGolangFunctor(fileReadTextFunc), TypeString, []ExprFuncParam{
		NewFuncParam(ParamHandle),
		NewFuncParamFlagDef(osLimitCh, PfDefault, "\n"),
	})

	ctx.RegisterFunc("fileReadTextAll", NewGolangFunctor(fileReadTextAllFunc), TypeString, []ExprFuncParam{
		NewFuncParam(ParamHandle),
	})
}

func init() {
	RegisterBuiltinModule("os.file", ImportOsFuncs, "Operating system file functions")
}