package main

import (
	"image"
	"image/color"
	"io"
)

// Printer wraps an io.Writer (USB file, TCP conn, etc.)
type Printer struct {
	w io.Writer
}

func NewPrinter(w io.Writer) *Printer {
	return &Printer{w: w}
}

func (p *Printer) Write(data []byte) error {
	_, err := p.w.Write(data)
	return err
}

// Print plain text
func (p *Printer) PrintText(text string) error {
	return p.Write([]byte(text))
}

// New line
func (p *Printer) LF(num int) error {
	for range num {
		err := p.PrintText("\n")
		if err != nil {
			return err
		}
	}
	return nil
}

// Bold text ON
func (p *Printer) BoldOn() error {
	return p.Write([]byte{0x1b, 0x45, 0x01})
}

// Bold text OFF
func (p *Printer) BoldOff() error {
	return p.Write([]byte{0x1b, 0x45, 0x00})
}

// Alignment: 0=left, 1=center, 2=right
func (p *Printer) Align(n int) error {
	return p.Write([]byte{0x1b, 0x61, byte(n)})
}

// Cut paper
func (p *Printer) Cut() error {
	return p.Write([]byte{0x1d, 0x56, 0x00})
}

// SelectFont chooses printer's built-in font (0=A, 1=B, 2=C if supported)
func (p *Printer) SelectFont(n int) error {
	return p.Write([]byte{0x1b, 0x4d, byte(n)})
}

// SetFontSize scales text (x, y from 1–8)
func (p *Printer) SetFontSize(x, y int) error {
	n := byte(((y - 1) << 4) | (x - 1))
	return p.Write([]byte{0x1d, 0x21, n})
}

// Underline: 0=off, 1=thin, 2=thick
func (p *Printer) Underline(n int) error {
	return p.Write([]byte{0x1b, 0x2d, byte(n)})
}

// UpsideDown: 0=off, 1=on
func (p *Printer) UpsideDown(n int) error {
	return p.Write([]byte{0x1b, 0x7b, byte(n)})
}

// SetCharSpacing sets extra spacing between characters
func (p *Printer) SetCharSpacing(n int) error {
	return p.Write([]byte{0x1b, 0x20, byte(n)})
}

// ---------------- IMAGE PRINTING ----------------

// convert to monochrome
func toMonochrome(img image.Image) *image.Gray {
	bounds := img.Bounds()
	gray := image.NewGray(bounds)
	for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
		for x := bounds.Min.X; x < bounds.Max.X; x++ {
			r, g, b, _ := img.At(x, y).RGBA()
			grayValue := uint8((r + g + b) / 3 >> 8)
			if grayValue > 128 {
				gray.Set(x, y, color.White)
			} else {
				gray.Set(x, y, color.Black)
			}
		}
	}
	return gray
}

// ESC/POS: GS v 0
func escposRaster(img *image.Gray) []byte {
	bounds := img.Bounds()
	w := bounds.Dx()
	h := bounds.Dy()

	// Width must be in bytes (8 pixels per byte)
	wBytes := (w + 7) / 8
	data := []byte{}

	// ESC/POS raster format: GS v 0 m xL xH yL yH [data]
	// GS v 0
	data = append(data, 0x1d, 0x76, 0x30, 0x00)

	// xL, xH
	data = append(data, byte(wBytes%256), byte(wBytes/256))
	// yL, yH
	data = append(data, byte(h%256), byte(h/256))

	// Image data
	for y := 0; y < h; y++ {
		for xByte := 0; xByte < wBytes; xByte++ {
			var b byte
			for bit := 0; bit < 8; bit++ {
				x := xByte*8 + bit
				if x < w {
					grayColor := img.GrayAt(x, y).Y
					if grayColor < 128 {
						// Black pixel = 1
						b |= (1 << (7 - bit))
					}
				}
			}
			data = append(data, b)
		}
	}

	return data
}

// PrintImage sends an image using GS v 0 (raster bit image)
func (p *Printer) PrintImage(img image.Image) error {
	gray := toMonochrome(img)
	data := escposRaster(gray)
	return p.Write(data)
}

// PrintBarcode prints a CODE128 barcode (most supported)
func (p *Printer) PrintBarcode(data string) error {
	// Select barcode height
	p.Write([]byte{0x1d, 0x68, 100}) // height in dots
	// Select barcode width
	p.Write([]byte{0x1d, 0x77, 2}) // module width (2 = medium)
	// HRI (human readable interpretation) below barcode
	p.Write([]byte{0x1d, 0x48, 2})

	// GS k m d1...dk NUL
	cmd := []byte{0x1d, 0x6b, 73, byte(len(data))} // 73 = CODE128 auto
	cmd = append(cmd, []byte(data)...)
	return p.Write(cmd)
}

// ---------------- QR CODE ----------------

// PrintQRCode prints a QR code
func (p *Printer) PrintQRCode(data string, size int, ecLevel byte) error {
	// size: module size (1–16)
	// ecLevel: error correction (48=L, 49=M, 50=Q, 51=H)

	// [1] Store QR data
	storeLen := len(data) + 3
	cmdStore := []byte{0x1d, 0x28, 0x6b, byte(storeLen % 256), byte(storeLen / 256), 49, 80, 48}
	cmdStore = append(cmdStore, []byte(data)...)

	// [2] Set module size
	cmdSize := []byte{0x1d, 0x28, 0x6b, 3, 0, 49, 67, byte(size)}

	// [3] Set error correction level
	cmdErr := []byte{0x1d, 0x28, 0x6b, 3, 0, 49, 69, ecLevel}

	// [4] Print
	cmdPrint := []byte{0x1d, 0x28, 0x6b, 3, 0, 49, 81, 48}

	p.Write(cmdSize)
	p.Write(cmdErr)
	p.Write(cmdStore)
	return p.Write(cmdPrint)
}
