// Package twooffive can create interleaved and standard "2 of 5" barcodes.
package twooffive

import (
	"errors"
	"fmt"

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

const patternWidth = 5

type pattern [patternWidth]bool
type encodeInfo struct {
	start  []bool
	end    []bool
	widths map[bool]int
}

var (
	encodingTable = map[rune]pattern{
		'0': pattern{false, false, true, true, false},
		'1': pattern{true, false, false, false, true},
		'2': pattern{false, true, false, false, true},
		'3': pattern{true, true, false, false, false},
		'4': pattern{false, false, true, false, true},
		'5': pattern{true, false, true, false, false},
		'6': pattern{false, true, true, false, false},
		'7': pattern{false, false, false, true, true},
		'8': pattern{true, false, false, true, false},
		'9': pattern{false, true, false, true, false},
	}

	modes = map[bool]encodeInfo{
		false: encodeInfo{ // non-interleaved
			start: []bool{true, true, false, true, true, false, true, false},
			end:   []bool{true, true, false, true, false, true, true},
			widths: map[bool]int{
				true:  3,
				false: 1,
			},
		},
		true: encodeInfo{ // interleaved
			start: []bool{true, false, true, false},
			end:   []bool{true, true, false, true},
			widths: map[bool]int{
				true:  2,
				false: 1,
			},
		},
	}
	nonInterleavedSpace = pattern{false, false, false, false, false}
)

// AddCheckSum calculates the correct check-digit and appends it to the given content.
func AddCheckSum(content string) (string, error) {
	if content == "" {
		return "", errors.New("content is empty")
	}

	even := len(content)%2 == 1
	sum := 0
	for _, r := range content {
		if _, ok := encodingTable[r]; ok {
			value := utils.RuneToInt(r)
			if even {
				sum += value * 3
			} else {
				sum += value
			}
			even = !even
		} else {
			return "", fmt.Errorf("can not encode \"%s\"", content)
		}
	}

	return content + string(utils.IntToRune(sum%10)), nil
}

// Encode creates a codabar barcode for the given content
func Encode(content string, interleaved bool) (barcode.Barcode, error) {
	if content == "" {
		return nil, errors.New("content is empty")
	}

	if interleaved && len(content)%2 == 1 {
		return nil, errors.New("can only encode even number of digits in interleaved mode")
	}

	mode := modes[interleaved]
	resBits := new(utils.BitList)
	resBits.AddBit(mode.start...)

	var lastRune *rune
	for _, r := range content {
		var a, b pattern
		if interleaved {
			if lastRune == nil {
				lastRune = new(rune)
				*lastRune = r
				continue
			} else {
				var o1, o2 bool
				a, o1 = encodingTable[*lastRune]
				b, o2 = encodingTable[r]
				if !o1 || !o2 {
					return nil, fmt.Errorf("can not encode \"%s\"", content)
				}
				lastRune = nil
			}
		} else {
			var ok bool
			a, ok = encodingTable[r]
			if !ok {
				return nil, fmt.Errorf("can not encode \"%s\"", content)
			}
			b = nonInterleavedSpace
		}

		for i := 0; i < patternWidth; i++ {
			for x := 0; x < mode.widths[a[i]]; x++ {
				resBits.AddBit(true)
			}
			for x := 0; x < mode.widths[b[i]]; x++ {
				resBits.AddBit(false)
			}
		}
	}

	resBits.AddBit(mode.end...)

	kindTxt := ""
	if interleaved {
		kindTxt = " (interleaved)"
	}
	return utils.New1DCode("2 of 5"+kindTxt, content, resBits, -1), nil
}
