// Package qr can be used to create QR barcodes.
package qr

import (
	"image"

	"github.com/andeya/faygo/ext/barcode"
	"github.com/andeya/faygo/ext/barcode/utils"
)

type encodeFn func(content string, eccLevel ErrorCorrectionLevel) (*utils.BitList, *versionInfo, error)

// Encoding mode for QR Codes.
type Encoding byte

const (
	// Auto will choose ths best matching encoding
	Auto Encoding = iota
	// Numeric encoding only encodes numbers [0-9]
	Numeric
	// AlphaNumeric encoding only encodes uppercase letters, numbers and  [Space], $, %, *, +, -, ., /, :
	AlphaNumeric
	// Unicode encoding encodes the string as utf-8
	Unicode
	// only for testing purpose
	unknownEncoding
)

func (e Encoding) getEncoder() encodeFn {
	switch e {
	case Auto:
		return encodeAuto
	case Numeric:
		return encodeNumeric
	case AlphaNumeric:
		return encodeAlphaNumeric
	case Unicode:
		return encodeUnicode
	}
	return nil
}

func (e Encoding) String() string {
	switch e {
	case Auto:
		return "Auto"
	case Numeric:
		return "Numeric"
	case AlphaNumeric:
		return "AlphaNumeric"
	case Unicode:
		return "Unicode"
	}
	return ""
}

// Encode returns a QR barcode with the given content, error correction level and uses the given encoding
func Encode(content string, level ErrorCorrectionLevel, mode Encoding) (barcode.Barcode, error) {
	bits, vi, err := mode.getEncoder()(content, level)
	if err != nil {
		return nil, err
	}

	blocks := splitToBlocks(bits.IterateBytes(), vi)
	data := blocks.interleave(vi)
	result := render(data, vi)
	result.content = content
	return result, nil
}

func render(data []byte, vi *versionInfo) *qrcode {
	dim := vi.modulWidth()
	results := make([]*qrcode, 8)
	for i := 0; i < 8; i++ {
		results[i] = newBarcode(dim)
	}

	occupied := newBarcode(dim)

	setAll := func(x int, y int, val bool) {
		occupied.Set(x, y, true)
		for i := 0; i < 8; i++ {
			results[i].Set(x, y, val)
		}
	}

	drawFinderPatterns(vi, setAll)
	drawAlignmentPatterns(occupied, vi, setAll)

	// Timing Pattern:
	var i int
	for i = 0; i < dim; i++ {
		if !occupied.Get(i, 6) {
			setAll(i, 6, i%2 == 0)
		}
		if !occupied.Get(6, i) {
			setAll(6, i, i%2 == 0)
		}
	}
	// Dark Module
	setAll(8, dim-8, true)

	drawVersionInfo(vi, setAll)
	drawFormatInfo(vi, -1, occupied.Set)
	for i := 0; i < 8; i++ {
		drawFormatInfo(vi, i, results[i].Set)
	}

	// Write the data
	var curBitNo int

	for pos := range iterateModules(occupied) {
		var curBit bool
		if curBitNo < len(data)*8 {
			curBit = ((data[curBitNo/8] >> uint(7-(curBitNo%8))) & 1) == 1
		} else {
			curBit = false
		}

		for i := 0; i < 8; i++ {
			setMasked(pos.X, pos.Y, curBit, i, results[i].Set)
		}
		curBitNo++
	}

	lowestPenalty := ^uint(0)
	lowestPenaltyIdx := -1
	for i := 0; i < 8; i++ {
		p := results[i].calcPenalty()
		if p < lowestPenalty {
			lowestPenalty = p
			lowestPenaltyIdx = i
		}
	}
	return results[lowestPenaltyIdx]
}

func setMasked(x, y int, val bool, mask int, set func(int, int, bool)) {
	switch mask {
	case 0:
		val = val != (((y + x) % 2) == 0)
		break
	case 1:
		val = val != ((y % 2) == 0)
		break
	case 2:
		val = val != ((x % 3) == 0)
		break
	case 3:
		val = val != (((y + x) % 3) == 0)
		break
	case 4:
		val = val != (((y/2 + x/3) % 2) == 0)
		break
	case 5:
		val = val != (((y*x)%2)+((y*x)%3) == 0)
		break
	case 6:
		val = val != ((((y*x)%2)+((y*x)%3))%2 == 0)
		break
	case 7:
		val = val != ((((y+x)%2)+((y*x)%3))%2 == 0)
	}
	set(x, y, val)
}

func iterateModules(occupied *qrcode) <-chan image.Point {
	result := make(chan image.Point)
	allPoints := make(chan image.Point)
	go func() {
		curX := occupied.dimension - 1
		curY := occupied.dimension - 1
		isUpward := true

		for true {
			if isUpward {
				allPoints <- image.Pt(curX, curY)
				allPoints <- image.Pt(curX-1, curY)
				curY--
				if curY < 0 {
					curY = 0
					curX -= 2
					if curX == 6 {
						curX--
					}
					if curX < 0 {
						break
					}
					isUpward = false
				}
			} else {
				allPoints <- image.Pt(curX, curY)
				allPoints <- image.Pt(curX-1, curY)
				curY++
				if curY >= occupied.dimension {
					curY = occupied.dimension - 1
					curX -= 2
					if curX == 6 {
						curX--
					}
					isUpward = true
					if curX < 0 {
						break
					}
				}
			}
		}

		close(allPoints)
	}()
	go func() {
		for pt := range allPoints {
			if !occupied.Get(pt.X, pt.Y) {
				result <- pt
			}
		}
		close(result)
	}()
	return result
}

func drawFinderPatterns(vi *versionInfo, set func(int, int, bool)) {
	dim := vi.modulWidth()
	drawPattern := func(xoff int, yoff int) {
		for x := -1; x < 8; x++ {
			for y := -1; y < 8; y++ {
				val := (x == 0 || x == 6 || y == 0 || y == 6 || (x > 1 && x < 5 && y > 1 && y < 5)) && (x <= 6 && y <= 6 && x >= 0 && y >= 0)

				if x+xoff >= 0 && x+xoff < dim && y+yoff >= 0 && y+yoff < dim {
					set(x+xoff, y+yoff, val)
				}
			}
		}
	}
	drawPattern(0, 0)
	drawPattern(0, dim-7)
	drawPattern(dim-7, 0)
}

func drawAlignmentPatterns(occupied *qrcode, vi *versionInfo, set func(int, int, bool)) {
	drawPattern := func(xoff int, yoff int) {
		for x := -2; x <= 2; x++ {
			for y := -2; y <= 2; y++ {
				val := x == -2 || x == 2 || y == -2 || y == 2 || (x == 0 && y == 0)
				set(x+xoff, y+yoff, val)
			}
		}
	}
	positions := vi.alignmentPatternPlacements()

	for _, x := range positions {
		for _, y := range positions {
			if occupied.Get(x, y) {
				continue
			}
			drawPattern(x, y)
		}
	}
}

var formatInfos = map[ErrorCorrectionLevel]map[int][]bool{
	L: {
		0: []bool{true, true, true, false, true, true, true, true, true, false, false, false, true, false, false},
		1: []bool{true, true, true, false, false, true, false, true, true, true, true, false, false, true, true},
		2: []bool{true, true, true, true, true, false, true, true, false, true, false, true, false, true, false},
		3: []bool{true, true, true, true, false, false, false, true, false, false, true, true, true, false, true},
		4: []bool{true, true, false, false, true, true, false, false, false, true, false, true, true, true, true},
		5: []bool{true, true, false, false, false, true, true, false, false, false, true, true, false, false, false},
		6: []bool{true, true, false, true, true, false, false, false, true, false, false, false, false, false, true},
		7: []bool{true, true, false, true, false, false, true, false, true, true, true, false, true, true, false},
	},
	M: {
		0: []bool{true, false, true, false, true, false, false, false, false, false, true, false, false, true, false},
		1: []bool{true, false, true, false, false, false, true, false, false, true, false, false, true, false, true},
		2: []bool{true, false, true, true, true, true, false, false, true, true, true, true, true, false, false},
		3: []bool{true, false, true, true, false, true, true, false, true, false, false, true, false, true, true},
		4: []bool{true, false, false, false, true, false, true, true, true, true, true, true, false, false, true},
		5: []bool{true, false, false, false, false, false, false, true, true, false, false, true, true, true, false},
		6: []bool{true, false, false, true, true, true, true, true, false, false, true, false, true, true, true},
		7: []bool{true, false, false, true, false, true, false, true, false, true, false, false, false, false, false},
	},
	Q: {
		0: []bool{false, true, true, false, true, false, true, false, true, false, true, true, true, true, true},
		1: []bool{false, true, true, false, false, false, false, false, true, true, false, true, false, false, false},
		2: []bool{false, true, true, true, true, true, true, false, false, true, true, false, false, false, true},
		3: []bool{false, true, true, true, false, true, false, false, false, false, false, false, true, true, false},
		4: []bool{false, true, false, false, true, false, false, true, false, true, true, false, true, false, false},
		5: []bool{false, true, false, false, false, false, true, true, false, false, false, false, false, true, true},
		6: []bool{false, true, false, true, true, true, false, true, true, false, true, true, false, true, false},
		7: []bool{false, true, false, true, false, true, true, true, true, true, false, true, true, false, true},
	},
	H: {
		0: []bool{false, false, true, false, true, true, false, true, false, false, false, true, false, false, true},
		1: []bool{false, false, true, false, false, true, true, true, false, true, true, true, true, true, false},
		2: []bool{false, false, true, true, true, false, false, true, true, true, false, false, true, true, true},
		3: []bool{false, false, true, true, false, false, true, true, true, false, true, false, false, false, false},
		4: []bool{false, false, false, false, true, true, true, false, true, true, false, false, false, true, false},
		5: []bool{false, false, false, false, false, true, false, false, true, false, true, false, true, false, true},
		6: []bool{false, false, false, true, true, false, true, false, false, false, false, true, true, false, false},
		7: []bool{false, false, false, true, false, false, false, false, false, true, true, true, false, true, true},
	},
}

func drawFormatInfo(vi *versionInfo, usedMask int, set func(int, int, bool)) {
	var formatInfo []bool

	if usedMask == -1 {
		formatInfo = []bool{true, true, true, true, true, true, true, true, true, true, true, true, true, true, true} // Set all to true cause -1 --> occupied mask.
	} else {
		formatInfo = formatInfos[vi.Level][usedMask]
	}

	if len(formatInfo) == 15 {
		dim := vi.modulWidth()
		set(0, 8, formatInfo[0])
		set(1, 8, formatInfo[1])
		set(2, 8, formatInfo[2])
		set(3, 8, formatInfo[3])
		set(4, 8, formatInfo[4])
		set(5, 8, formatInfo[5])
		set(7, 8, formatInfo[6])
		set(8, 8, formatInfo[7])
		set(8, 7, formatInfo[8])
		set(8, 5, formatInfo[9])
		set(8, 4, formatInfo[10])
		set(8, 3, formatInfo[11])
		set(8, 2, formatInfo[12])
		set(8, 1, formatInfo[13])
		set(8, 0, formatInfo[14])

		set(8, dim-1, formatInfo[0])
		set(8, dim-2, formatInfo[1])
		set(8, dim-3, formatInfo[2])
		set(8, dim-4, formatInfo[3])
		set(8, dim-5, formatInfo[4])
		set(8, dim-6, formatInfo[5])
		set(8, dim-7, formatInfo[6])
		set(dim-8, 8, formatInfo[7])
		set(dim-7, 8, formatInfo[8])
		set(dim-6, 8, formatInfo[9])
		set(dim-5, 8, formatInfo[10])
		set(dim-4, 8, formatInfo[11])
		set(dim-3, 8, formatInfo[12])
		set(dim-2, 8, formatInfo[13])
		set(dim-1, 8, formatInfo[14])
	}
}

var versionInfoBitsByVersion = map[byte][]bool{
	7:  []bool{false, false, false, true, true, true, true, true, false, false, true, false, false, true, false, true, false, false},
	8:  []bool{false, false, true, false, false, false, false, true, false, true, true, false, true, true, true, true, false, false},
	9:  []bool{false, false, true, false, false, true, true, false, true, false, true, false, false, true, true, false, false, true},
	10: []bool{false, false, true, false, true, false, false, true, false, false, true, true, false, true, false, false, true, true},
	11: []bool{false, false, true, false, true, true, true, false, true, true, true, true, true, true, false, true, true, false},
	12: []bool{false, false, true, true, false, false, false, true, true, true, false, true, true, false, false, false, true, false},
	13: []bool{false, false, true, true, false, true, true, false, false, false, false, true, false, false, false, true, true, true},
	14: []bool{false, false, true, true, true, false, false, true, true, false, false, false, false, false, true, true, false, true},
	15: []bool{false, false, true, true, true, true, true, false, false, true, false, false, true, false, true, false, false, false},
	16: []bool{false, true, false, false, false, false, true, false, true, true, false, true, true, true, true, false, false, false},
	17: []bool{false, true, false, false, false, true, false, true, false, false, false, true, false, true, true, true, false, true},
	18: []bool{false, true, false, false, true, false, true, false, true, false, false, false, false, true, false, true, true, true},
	19: []bool{false, true, false, false, true, true, false, true, false, true, false, false, true, true, false, false, true, false},
	20: []bool{false, true, false, true, false, false, true, false, false, true, true, false, true, false, false, true, true, false},
	21: []bool{false, true, false, true, false, true, false, true, true, false, true, false, false, false, false, false, true, true},
	22: []bool{false, true, false, true, true, false, true, false, false, false, true, true, false, false, true, false, false, true},
	23: []bool{false, true, false, true, true, true, false, true, true, true, true, true, true, false, true, true, false, false},
	24: []bool{false, true, true, false, false, false, true, true, true, false, true, true, false, false, false, true, false, false},
	25: []bool{false, true, true, false, false, true, false, false, false, true, true, true, true, false, false, false, false, true},
	26: []bool{false, true, true, false, true, false, true, true, true, true, true, false, true, false, true, false, true, true},
	27: []bool{false, true, true, false, true, true, false, false, false, false, true, false, false, false, true, true, true, false},
	28: []bool{false, true, true, true, false, false, true, true, false, false, false, false, false, true, true, false, true, false},
	29: []bool{false, true, true, true, false, true, false, false, true, true, false, false, true, true, true, true, true, true},
	30: []bool{false, true, true, true, true, false, true, true, false, true, false, true, true, true, false, true, false, true},
	31: []bool{false, true, true, true, true, true, false, false, true, false, false, true, false, true, false, false, false, false},
	32: []bool{true, false, false, false, false, false, true, false, false, true, true, true, false, true, false, true, false, true},
	33: []bool{true, false, false, false, false, true, false, true, true, false, true, true, true, true, false, false, false, false},
	34: []bool{true, false, false, false, true, false, true, false, false, false, true, false, true, true, true, false, true, false},
	35: []bool{true, false, false, false, true, true, false, true, true, true, true, false, false, true, true, true, true, true},
	36: []bool{true, false, false, true, false, false, true, false, true, true, false, false, false, false, true, false, true, true},
	37: []bool{true, false, false, true, false, true, false, true, false, false, false, false, true, false, true, true, true, false},
	38: []bool{true, false, false, true, true, false, true, false, true, false, false, true, true, false, false, true, false, false},
	39: []bool{true, false, false, true, true, true, false, true, false, true, false, true, false, false, false, false, false, true},
	40: []bool{true, false, true, false, false, false, true, true, false, false, false, true, true, false, true, false, false, true},
}

func drawVersionInfo(vi *versionInfo, set func(int, int, bool)) {
	versionInfoBits, ok := versionInfoBitsByVersion[vi.Version]

	if ok && len(versionInfoBits) > 0 {
		for i := 0; i < len(versionInfoBits); i++ {
			x := (vi.modulWidth() - 11) + i%3
			y := i / 3
			set(x, y, versionInfoBits[len(versionInfoBits)-i-1])
			set(y, x, versionInfoBits[len(versionInfoBits)-i-1])
		}
	}

}

func addPaddingAndTerminator(bl *utils.BitList, vi *versionInfo) {
	for i := 0; i < 4 && bl.Len() < vi.totalDataBytes()*8; i++ {
		bl.AddBit(false)
	}

	for bl.Len()%8 != 0 {
		bl.AddBit(false)
	}

	for i := 0; bl.Len() < vi.totalDataBytes()*8; i++ {
		if i%2 == 0 {
			bl.AddByte(236)
		} else {
			bl.AddByte(17)
		}
	}
}
