package main

import (
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"time"
	"unsafe"

	"C"

	"github.com/pdfcpu/pdfcpu/pkg/api"
	"github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model"
)

type TDLLVersionType int
type TDLLCmdId int

const (
	E_DLLFULLNAME TDLLVersionType = iota
	E_DLLMODULNAME
	E_DLLGROUPNAME
	E_DLLFILEVERSION
	E_NEXUSVERSION
	E_COMPILERVERSION
)

const (
	E_DLLCmdExtract TDLLCmdId = iota
	E_DLLCmdAppend
	E_DLLCmdDebugEnable
	E_DLLCmdDebugDisable
)

const (
	ModuleName   = "ZUGFERD"
	GroupName    = "XRECHNUNG"
	NexusVersion = "<NODATABASE>"
)

var debugEnabled = false
var debugFile = "dll/debug.log"

func extractFromPdf(inputFilePath string, outputFilePath string) error {
	if inputFilePath == "" {
		return errors.New("no input file specified")
	}

	if outputFilePath == "" {
		outputFilePath = strings.TrimSuffix(inputFilePath, filepath.Ext(inputFilePath)) + ".xml"
	}

	inputFile, err := os.Open(inputFilePath)
	if err != nil {
		return err
	}
	defer inputFile.Close()

	var (
		attachments []model.Attachment
		rawData     []byte
		ctx         *model.Context
		attachment  *model.Attachment
	)

	ctx, err = api.ReadContextFile(inputFilePath)
	if err != nil {
		return err
	}
	attachments, err = api.Attachments(inputFile, model.NewDefaultConfiguration())
	if err != nil {
		return err
	}

	attachment, err = ctx.ExtractAttachment(attachments[0])
	if err != nil {
		return err
	}

	rawData, err = io.ReadAll(attachment)
	if err != nil {
		return err
	}

	var outputFile *os.File
	outputFile, err = os.Create(outputFilePath)
	if err != nil {
		return err
	}
	defer outputFile.Close()

	_, err = outputFile.Write(rawData)
	if err != nil {
		return err
	}

	return nil
}

func appendToPdf(inputPdfFilePath string, inputXmlFilePath string, outputFilePath string) error {
	if inputPdfFilePath == "" {
		return errors.New("no input pdf file specified")
	}

	if inputXmlFilePath == "" {
		inputXmlFilePath = strings.TrimSuffix(inputPdfFilePath, filepath.Ext(inputPdfFilePath)) + ".xml"
	}

	if outputFilePath == "" {
		outputFilePath = strings.TrimSuffix(
			inputPdfFilePath,
			filepath.Ext(inputPdfFilePath),
		) + "_xml.pdf"
	}
	err := api.AddAttachmentsFile(
		inputPdfFilePath,
		outputFilePath,
		[]string{inputXmlFilePath},
		false,
		model.NewDefaultConfiguration(),
	)
	if err != nil {
		return err
	}

	return nil
}

type DebugLogger struct {
	file *os.File
}

func getDebugLogger() *DebugLogger {
	result := &DebugLogger{}
	runtime.SetFinalizer(
		result, func(logger *DebugLogger) {
			logger.Close()
		},
	)

	if debugEnabled {
		var err error
		_, err = os.Stat(debugFile)

		if err != nil {
			result.file, _ = os.Create(debugFile)
		} else {
			result.file, _ = os.OpenFile(debugFile, os.O_RDWR, 0644)
			result.file.Seek(0, io.SeekEnd)
		}
	}

	return result
}

func (l *DebugLogger) Log(msg ...any) {
	if l.file == nil {
		return
	}

	l.file.WriteString(fmt.Sprintln(append([]any{time.Now().Format("2006-01-02 15:04:05")}, msg...)...))
}

func (l *DebugLogger) Close() error {
	if l.file == nil {
		return nil
	}

	return l.file.Close()
}

//export DLLGETVERSION
func DLLGETVERSION(versionTyp TDLLVersionType) *C.char {
	logger := getDebugLogger()
	defer logger.Close()
	defer logger.Log("Exit  DLLGETVERSION")

	if debugEnabled {
		logger.Log("Enter DLLGETVERSION")
	}

	switch versionTyp {
	case E_DLLMODULNAME:
		return C.CString(ModuleName)
	case E_DLLGROUPNAME:
		return C.CString(GroupName)
	case E_DLLFILEVERSION:
		return C.CString("1.0.0")
	case E_NEXUSVERSION:
		return C.CString(NexusVersion)
	case E_COMPILERVERSION:
		return C.CString(runtime.Version())
	default:
		return C.CString("Unknown type")
	}
}

//export DLLINTERFACE
func DLLINTERFACE(cmdId TDLLCmdId, data unsafe.Pointer) bool {
	var err error

	logger := getDebugLogger()
	defer logger.Close()
	defer logger.Log("Exit  DLLINTERFACE")

	if debugEnabled {
		logger.Log("Enter DLLINTERFACE")
	}

	switch cmdId {
	case E_DLLCmdDebugEnable:
		debugEnabled = true
	case E_DLLCmdDebugDisable:
		debugEnabled = false
	case E_DLLCmdExtract:
		args := strings.Split(C.GoString((*C.char)(data)), "|")
		if debugEnabled {
			logger.Log("Execute  E_DLLCmdExtract: ", args[0]+", "+args[1])
		}
		err = extractFromPdf(args[0], args[1])

		if debugEnabled {
			logger.Log("Finished E_DLLCmdExtract")
		}

		if err != nil {
			logger.Log(err)
			return false
		}
	case E_DLLCmdAppend:
		args := strings.Split(C.GoString((*C.char)(data)), "|")
		if debugEnabled {
			logger.Log("Execute  E_DLLCmdAppend: ", args[0]+", "+args[1]+", "+args[2])
		}

		err = appendToPdf(args[0], args[1], args[2])

		if debugEnabled {
			logger.Log("Finished E_DLLCmdAppend")
		}

		if err != nil {
			logger.Log(err)
			return false
		}
	default:
		return false
	}

	return false
}

//export DLLACTIVATE
func DLLACTIVATE(activate bool) bool {
	logger := getDebugLogger()
	defer logger.Close()
	defer logger.Log("Exit DLLACTIVATE")

	if debugEnabled {
		logger.Log("Enter DLLACTIVATE")
	}

	if !activate {
		// Perform any necessary cleanup
		runtime.GC() // Force garbage collection
		runtime.UnlockOSThread()
	}

	return true
}

func main() {
	// Need a main function to make CGO compile package as C shared library
	DLLINTERFACE(E_DLLCmdDebugEnable, nil)
	DLLGETVERSION(E_DLLGROUPNAME)
	DLLACTIVATE(false)
}

// go build -o pdf.dll -buildmode=c-shared dll.go
