// +build go1.10

package main

import (
	"fmt"
	"log"
	"math/rand"
	"os"
	"strconv"
	"strings"

	"github.com/ncw/rclone/lib/encoder"
)

const (
	edgeLeft = iota
	edgeRight
)

type mapping struct {
	mask     uint
	src, dst []rune
}
type stringPair struct {
	a, b string
}

const header = `// Code generated by ./internal/gen/main.go. DO NOT EDIT.

` + `//go:generate go run ./internal/gen/main.go

package encoder

`

var maskBits = []struct {
	mask uint
	name string
}{
	{encoder.EncodeZero, "EncodeZero"},
	{encoder.EncodeWin, "EncodeWin"},
	{encoder.EncodeSlash, "EncodeSlash"},
	{encoder.EncodeBackSlash, "EncodeBackSlash"},
	{encoder.EncodeHashPercent, "EncodeHashPercent"},
	{encoder.EncodeDel, "EncodeDel"},
	{encoder.EncodeCtl, "EncodeCtl"},
	{encoder.EncodeLeftSpace, "EncodeLeftSpace"},
	{encoder.EncodeLeftTilde, "EncodeLeftTilde"},
	{encoder.EncodeRightSpace, "EncodeRightSpace"},
	{encoder.EncodeRightPeriod, "EncodeRightPeriod"},
	{encoder.EncodeInvalidUtf8, "EncodeInvalidUtf8"},
}
var edges = []struct {
	mask    uint
	name    string
	edge    int
	orig    rune
	replace rune
}{
	{encoder.EncodeLeftSpace, "EncodeLeftSpace", edgeLeft, ' ', '␠'},
	{encoder.EncodeLeftTilde, "EncodeLeftTilde", edgeLeft, '~', '～'},
	{encoder.EncodeRightSpace, "EncodeRightSpace", edgeRight, ' ', '␠'},
	{encoder.EncodeRightPeriod, "EncodeRightPeriod", edgeRight, '.', '．'},
}

var allMappings = []mapping{{
	encoder.EncodeZero, []rune{
		0,
	}, []rune{
		'␀',
	}}, {
	encoder.EncodeWin, []rune{
		':', '?', '"', '*', '<', '>', '|',
	}, []rune{
		'：', '？', '＂', '＊', '＜', '＞', '｜',
	}}, {
	encoder.EncodeSlash, []rune{
		'/',
	}, []rune{
		'／',
	}}, {
	encoder.EncodeBackSlash, []rune{
		'\\',
	}, []rune{
		'＼',
	}}, {
	encoder.EncodeHashPercent, []rune{
		'#', '%',
	}, []rune{
		'＃', '％',
	}}, {
	encoder.EncodeDel, []rune{
		0x7F,
	}, []rune{
		'␡',
	}}, {
	encoder.EncodeCtl,
	runeRange(0x01, 0x1F),
	runeRange('␁', '␟'),
}}

var (
	rng = rand.New(rand.NewSource(42))

	printables          = runeRange(0x20, 0x7E)
	fullwidthPrintables = runeRange(0xFF00, 0xFF5E)
	encodables          = collectEncodables(allMappings)
	encoded             = collectEncoded(allMappings)
	greek               = runeRange(0x03B1, 0x03C9)
)

func main() {
	fd, err := os.Create("encoder_cases_test.go")
	fatal(err, "Unable to open encoder_cases_test.go:")
	defer func() {
		fatal(fd.Close(), "Failed to close encoder_cases_test.go:")
	}()
	fatalW(fd.WriteString(header))("Failed to write header:")

	fatalW(fd.WriteString("var testCasesSingle = []testCase{\n\t"))("Write:")
	_i := 0
	i := func() (r int) {
		r, _i = _i, _i+1
		return
	}
	for _, m := range maskBits {
		if len(getMapping(m.mask).src) == 0 {
			continue
		}
		if _i != 0 {
			fatalW(fd.WriteString(" "))("Write:")
		}
		in, out := buildTestString(
			[]mapping{getMapping(m.mask)},                               // pick
			[]mapping{getMapping(0)},                                    // quote
			printables, fullwidthPrintables, encodables, encoded, greek) // fill
		fatalW(fmt.Fprintf(fd, `{ // %d
		mask: %s,
		in:   %s,
		out:  %s,
	},`, i(), m.name, strconv.Quote(in), strconv.Quote(out)))("Error writing test case:")
	}
	fatalW(fd.WriteString(`
}

var testCasesSingleEdge = []testCase{
	`))("Write:")
	_i = 0
	for _, e := range edges {
		if _i != 0 {
			fatalW(fd.WriteString(" "))("Write:")
		}
		fatalW(fmt.Fprintf(fd, `{ // %d
		mask: %s,
		in:   %s,
		out:  %s,
	},`, i(), e.name, strconv.Quote(string(e.orig)), strconv.Quote(string(e.replace))))("Error writing test case:")
		for _, m := range maskBits {
			if len(getMapping(m.mask).src) == 0 {
				continue
			}
			pairs := buildEdgeTestString(
				e.edge, e.orig, e.replace,
				[]mapping{getMapping(0), getMapping(m.mask)}, // quote
				printables, fullwidthPrintables, encodables, encoded, greek) // fill
			for _, p := range pairs {
				fatalW(fmt.Fprintf(fd, ` { // %d
		mask: %s | %s,
		in:   %s,
		out:  %s,
	},`, i(), m.name, e.name, strconv.Quote(p.a), strconv.Quote(p.b)))("Error writing test case:")
			}
		}
	}
	fatalW(fmt.Fprintf(fd, ` { // %d
		mask: EncodeLeftSpace,
		in:   "  ",
		out:  "␠ ",
	}, { // %d
		mask: EncodeLeftTilde,
		in:   "~~",
		out:  "～~",
	}, { // %d
		mask: EncodeRightSpace,
		in:   "  ",
		out:  " ␠",
	}, { // %d
		mask: EncodeRightPeriod,
		in:   "..",
		out:  ".．",
	}, { // %d
		mask: EncodeLeftSpace | EncodeRightPeriod,
		in:   " .",
		out:  "␠．",
	}, { // %d
		mask: EncodeLeftSpace | EncodeRightSpace,
		in:   " ",
		out:  "␠",
	}, { // %d
		mask: EncodeLeftSpace | EncodeRightSpace,
		in:   "  ",
		out:  "␠␠",
	}, { // %d
		mask: EncodeLeftSpace | EncodeRightSpace,
		in:   "   ",
		out:  "␠ ␠",
	},
}
`, i(), i(), i(), i(), i(), i(), i(), i()))("Error writing test case:")
}

func fatal(err error, s ...interface{}) {
	if err != nil {
		log.Fatalln(append(s, err))
	}
}
func fatalW(_ int, err error) func(...interface{}) {
	if err != nil {
		return func(s ...interface{}) {
			log.Fatalln(append(s, err))
		}
	}
	return func(s ...interface{}) {}
}

// construct a slice containing the runes between (l)ow (inclusive) and (h)igh (inclusive)
func runeRange(l, h rune) []rune {
	if h < l {
		panic("invalid range")
	}
	out := make([]rune, h-l+1)
	for i := range out {
		out[i] = l + rune(i)
	}
	return out
}

func getMapping(mask uint) mapping {
	for _, m := range allMappings {
		if m.mask == mask {
			return m
		}
	}
	return mapping{}
}
func collectEncodables(m []mapping) (out []rune) {
	for _, s := range m {
		for _, r := range s.src {
			out = append(out, r)
		}
	}
	return
}
func collectEncoded(m []mapping) (out []rune) {
	for _, s := range m {
		for _, r := range s.dst {
			out = append(out, r)
		}
	}
	return
}

func buildTestString(mappings, testMappings []mapping, fill ...[]rune) (string, string) {
	combinedMappings := append(mappings, testMappings...)
	var (
		rIn  []rune
		rOut []rune
	)
	for _, m := range mappings {
		if len(m.src) == 0 || len(m.src) != len(m.dst) {
			panic("invalid length")
		}
		rIn = append(rIn, m.src...)
		rOut = append(rOut, m.dst...)
	}
	inL := len(rIn)
	testL := inL * 3
	if testL < 30 {
		testL = 30
	}
	rIn = append(rIn, make([]rune, testL-inL)...)
	rOut = append(rOut, make([]rune, testL-inL)...)
	quoteOut := make([]bool, testL)
	set := func(i int, in, out rune, quote bool) {
		rIn[i] = in
		rOut[i] = out
		quoteOut[i] = quote
	}
	for i, r := range rOut[:inL] {
		set(inL+i, r, r, true)
	}

outer:
	for pos := inL * 2; pos < testL; pos++ {
		m := pos % len(fill)
		i := rng.Intn(len(fill[m]))
		r := fill[m][i]
		for _, m := range combinedMappings {
			if pSrc := runePos(r, m.src); pSrc != -1 {
				set(pos, r, m.dst[pSrc], false)
				continue outer
			} else if pDst := runePos(r, m.dst); pDst != -1 {
				set(pos, r, r, true)
				continue outer
			}
		}
		set(pos, r, r, false)
	}

	rng.Shuffle(testL, func(i, j int) {
		rIn[i], rIn[j] = rIn[j], rIn[i]
		rOut[i], rOut[j] = rOut[j], rOut[i]
		quoteOut[i], quoteOut[j] = quoteOut[j], quoteOut[i]
	})

	var bOut strings.Builder
	bOut.Grow(testL)
	for i, r := range rOut {
		if quoteOut[i] {
			bOut.WriteRune(encoder.QuoteRune)
		}
		bOut.WriteRune(r)
	}
	return string(rIn), bOut.String()
}

func buildEdgeTestString(edge int, orig, replace rune, testMappings []mapping, fill ...[]rune) (out []stringPair) {
	testL := 30
	rIn := make([]rune, testL)
	rOut := make([]rune, testL)
	quoteOut := make([]bool, testL)

	set := func(i int, in, out rune, quote bool) {
		rIn[i] = in
		rOut[i] = out
		quoteOut[i] = quote
	}

outer:
	for pos := 0; pos < testL; pos++ {
		m := pos % len(fill)
		i := rng.Intn(len(fill[m]))
		r := fill[m][i]
		for _, m := range testMappings {
			if pSrc := runePos(r, m.src); pSrc != -1 {
				set(pos, r, m.dst[pSrc], false)
				continue outer
			} else if pDst := runePos(r, m.dst); pDst != -1 {
				set(pos, r, r, true)
				continue outer
			}
		}
		set(pos, r, r, false)
	}

	rng.Shuffle(testL, func(i, j int) {
		rIn[i], rIn[j] = rIn[j], rIn[i]
		rOut[i], rOut[j] = rOut[j], rOut[i]
		quoteOut[i], quoteOut[j] = quoteOut[j], quoteOut[i]
	})
	set(10, orig, orig, false)

	out = append(out, stringPair{string(rIn), quotedToString(rOut, quoteOut)})
	for _, i := range []int{0, 1, testL - 2, testL - 1} {
		for _, j := range []int{1, testL - 2, testL - 1} {
			if j < i {
				continue
			}
			rIn := append([]rune{}, rIn...)
			rOut := append([]rune{}, rOut...)
			quoteOut := append([]bool{}, quoteOut...)

			for _, in := range []rune{orig, replace} {
				expect, quote := in, false
				if i == 0 && edge == edgeLeft ||
					i == testL-1 && edge == edgeRight {
					expect, quote = replace, in == replace
				}
				rIn[i], rOut[i], quoteOut[i] = in, expect, quote

				if i != j {
					for _, in := range []rune{orig, replace} {
						expect, quote = in, false
						if j == testL-1 && edge == edgeRight {
							expect, quote = replace, in == replace
						}
						rIn[j], rOut[j], quoteOut[j] = in, expect, quote
					}
				}
				out = append(out, stringPair{string(rIn), quotedToString(rOut, quoteOut)})
			}
		}
	}
	return
}

func runePos(r rune, s []rune) int {
	for i, c := range s {
		if c == r {
			return i
		}
	}
	return -1
}

// quotedToString returns a string for the chars slice where a encoder.QuoteRune is
// inserted before a char[i] when quoted[i] is true.
func quotedToString(chars []rune, quoted []bool) string {
	var out strings.Builder
	out.Grow(len(chars))
	for i, r := range chars {
		if quoted[i] {
			out.WriteRune(encoder.QuoteRune)
		}
		out.WriteRune(r)
	}
	return out.String()
}
