package escpos

import (
	"errors"
	"fmt"
	"log/slog"
	"strings"
	"syscall"

	"git.sr.ht/~guacamolie/faxmachine/escpos/protocol"
	"github.com/djimenez/iconv-go"
)

type Component interface {
	render(proto protocol.Protocol) ([][]byte, error)
}

type dataComponent struct {
	data []byte
}

func (dc dataComponent) render(proto protocol.Protocol) ([][]byte, error) {
	return [][]byte{dc.data}, nil
}

type groupComponent struct {
	children []Component
}

func (gc groupComponent) render(proto protocol.Protocol) ([][]byte, error) {
	var (
		instructions [][]byte
		errs         []error
	)

	for _, child := range gc.children {
		inst, err := child.render(proto)
		instructions = append(instructions, inst...)
		errs = append(errs, err)
	}

	return instructions, errors.Join(errs...)
}

// Text returns a Component that will print utf8-encoded text in an
// automatically chosen encoding. It will automatically switch between
// encodings mid-sentence if needed. The Component will return an error if the
// printer any part of the input string could not be converted.
//
// Text uses libc iconv to convert the text. Both musl libc and glibc are
// supported.
func Text(text string) Component {
	return textComponent{text}
}

type textComponent struct {
	text string
}

func (tc textComponent) render(proto protocol.Protocol) ([][]byte, error) {
	if len(tc.text) == 0 {
		return nil, nil
	}

	// musl libc uses '*' to represent characters it couldn't convert in the
	// output string. This is more standard compliant than glibc, but does make
	// it more difficult for us to detect if some characters couldn't be
	// converted in a given encoding.
	//
	// So for musl libc we will be checking for '*' characters in the output to
	// determine if the conversion failed. This means however that we can't
	// have any '*' characters in our input, since this would mess up the
	// detection.
	//
	// Luckily the first 127 characters are always the same on this printer for
	// every possible encoding, meaning we can just directly print '*'
	// characters without having to bother with encoding. The beauty of our
	// US-centric world.
	if before, after, found := strings.Cut(tc.text, "*"); found {
		return groupComponent{
			children: []Component{
				textComponent{before},
				dataComponent{[]byte("*")},
				textComponent{after},
			},
		}.render(proto)
	}

	for _, encoding := range allEncodings {
		if encoding.iconvEncoding == "" {
			continue
		}

		converter, err := iconv.NewConverter("utf-8", encoding.iconvEncoding)
		if err == syscall.EINVAL {
			continue
		} else if err != nil {
			return nil, fmt.Errorf("failed to initialize converter from utf-8 to %s: %w", encoding.iconvEncoding, err)
		}
		defer converter.Close()

		converted, err := converter.ConvertString(tc.text)
		slog.Debug("iconv",
			"utf8", tc.text,
			slog.String(encoding.iconvEncoding, converted),
			"error", err)

		// glibc returns EILSEQ, musl libc uses '*' to represent invalid chars.
		// Check for both.
		if err == syscall.EILSEQ ||
			(err == nil && strings.Index(converted, "*") != -1) {
			// This encoding can't properly convert this string, try the next
			// encoding.
			continue
		} else if err != nil {
			return nil, fmt.Errorf("failed to convert text encoding: %w", err)
		}

		// We found a good encoding!
		inst, err := proto.SelectCharacterCodeTable(encoding.page)
		if err != nil {
			// ...or not
			continue
		}

		return [][]byte{
			inst,
			[]byte(converted),
		}, nil
	}

	// None of the supported encoding convert this string. Try to see if it
	// does work when splitting the string up into substrings.

	// Find a midpoint in the string that is between two rune boundaries.
	// TODO: Can this be made more correct? Not every rune boundary is a valid
	// place to chop up a string.
	halfwayIsh := 0
	for i := range tc.text {
		if i >= len(tc.text)/2 {
			halfwayIsh = i
			break
		}
	}

	if halfwayIsh == 0 {
		// We have exhausted all options to convert this, bail out
		return nil, fmt.Errorf("device doesn't support %q characters", tc.text)
	}

	return groupComponent{
		[]Component{
			textComponent{tc.text[:halfwayIsh]},
			textComponent{tc.text[halfwayIsh:]},
		},
	}.render(proto)
}