package printer

import (
	"encoding/hex"
	"fmt"
	"io"
	"log"
	"os"
	"strings"

	"golang.org/x/sys/unix"
)

type Printer interface {
	io.Reader
	io.Writer
	io.Closer
}

type usbPrinter struct {
	*os.File
}

const (
	FlagNone  = 0
	FlagDebug = 1 << 0
)

func OpenUSBPrinter(path string, flags int) (Printer, error) {
	file, err := os.OpenFile(path, os.O_RDWR, 0)
	if err != nil {
		return nil, err
	}

	var printer Printer = usbPrinter{file}
	if flags&FlagDebug != 0 {
		printer = withDebugger(printer)
	}
	return printer, nil
}

func (usb usbPrinter) Write(p []byte) (int, error) {
	// The printer often doens't like it if we use multiple write syscalls to
	// transmit the data. usb.File.Write() doesn't seem to guarantee that it
	// will only use a single syscall, so we manually call write instead.
	return unix.Write(int(usb.Fd()), p)
}

type debugger struct {
	Printer
	lastError error
}

func withDebugger(printer Printer) Printer {
	return &debugger{
		Printer:   printer,
		lastError: nil,
	}
}

func (d *debugger) Read(p []byte) (n int, err error) {
	n, err = d.Printer.Read(p)

	if err == io.EOF {
		return
	}

	// don't spam the terminal with the same error again and again
	if err != nil && d.lastError != nil && err.Error() == d.lastError.Error() {
		return
	}
	d.lastError = err

	var builder strings.Builder
	fmt.Fprint(&builder, "          \033[31m")
	for i, b := range p[:n] {
		if i > 0 {
			fmt.Fprint(&builder, " ")
		}
		fmt.Fprintf(&builder, "%08b", b)
	}
	if err != nil {
		if n > 0 {
			fmt.Fprint(&builder, " ")
		}
		fmt.Fprintf(&builder, "read error: %v", err)
	}
	fmt.Fprintf(&builder, "\033[0m\n")

	fmt.Fprint(os.Stderr, builder.String())

	return
}

func (d *debugger) Write(p []byte) (n int, err error) {
	n, err = d.Printer.Write(p)

	printSize := len(p)
	max := 20 * 8 * 2
	if printSize > max {
		printSize = max
	}
	fmt.Fprintf(os.Stderr, "\033[36m%s\033[0m", hex.Dump(p[:printSize]))
	if len(p) > printSize {
		fmt.Fprintf(os.Stderr, "          \033[36m[%d bytes omitted]\033[0m\n", len(p)-printSize)
	}

	if err == nil && n == len(p) {
		log.Printf("wrote %d bytes to printer\n", n)
	} else {
		log.Printf("only wrote %d/%d bytes to printer: %v\n", n, len(p), err)
	}

	return
}