package main

import (
	"bytes"
	"encoding/base64"
	"errors"
	"github.com/gofiber/fiber/v2"
	"image"
	"image/jpeg"
	"log"
	"math/rand"
	"time"
)

type TestCommand struct {
	Command    string `json:"cmd"`
	SequenceId int64  `json:"sequenceId"`
}

type TestData struct {
	State         string  `json:"state"`
	ExceptionType int     `json:"exceptionType,omitempty"`
	ExceptionDesc string  `json:"exceptionDesc,omitempty"`
	Image         string  `json:"image,omitempty"`
	DistanceCM    int     `json:"distanceCM,omitempty"`
	Counts        int     `json:"counts,omitempty"`
	DurationSec   float32 `json:"durationSec,omitempty"`
}

type TestStat struct {
	Scene      string   `json:"scene"`
	Timestamp  int64    `json:"timestamp"`
	SequenceId int64    `json:"sequenceId"`
	TestData   TestData `json:"sceneData"`
}

type TestException struct {
	Type int
	Desc string
}

var currentTest *TestCommand

var random *rand.Rand
var testExceptChannel chan TestException
var testStopChannel chan bool
var testDurations map[string][2]int

func setTestCmd(c *fiber.Ctx) error {
	var testCmd TestCommand
	if err := c.BodyParser(&testCmd); nil != err {
		log.Println("ERROR:", c.Body())
		return failureResponse(c, "-1", err.Error(), 200)
	} else if nil == currentScene {
		return failureResponse(c, "1", "not in a working scene", 200)
	} else {
		switch testCmd.Command {
		case "START":
			log.Println("START ...")
			if nil != currentTest {
				return failureResponse(c, "2", "A test already started", 200)
			}
			if e := recogTaskProc(&testCmd); nil != e {
				return failureResponse(c, "3", e.Error(), 200)
			}
		case "STOP":
			if nil != testStopChannel {
				testStopChannel <- true
				log.Println("STOP ...")
			}

		case "CANCEL":
			if nil != testStopChannel {
				testStopChannel <- true
				log.Println("CANCEL ...")
			}
		default:
			log.Println("UNKNOWN ...")
		}
		return successResponse(c, "Updated successfully.")
	}
}

func pullUpTask(seq int64) error {
	var tk = time.NewTicker(time.Millisecond * time.Duration(1500+random.Intn(800)))
	defer func() {
		tk.Stop()
	}()
	time.Sleep(time.Millisecond * 1300)
	total := 3 + random.Intn(10)
	n := 0
	go pushEvent(currentScene.PushUrl, currentScene.Scene, currentTest.SequenceId, "TEST READY", nil)
	var started = false
	for {
		select {
		case <-testStopChannel:
			go pushEvent(currentScene.PushUrl, currentScene.Scene, seq, "TEST END", capturedImage, n)
			return nil
		case except := <-testExceptChannel:
			if except.Type != 0 {
				go pushException(currentScene.PushUrl, currentScene.Scene, currentTest.SequenceId, except.Type, except.Desc, capturedImage)
			}
		case <-tk.C:
			log.Println("===")
			if !started {
				go pushEvent(currentScene.PushUrl, currentScene.Scene, seq, "START", capturedImage, n)
				started = true
			} else if n >= total {
				go pushEvent(currentScene.PushUrl, currentScene.Scene, seq, "TEST END", capturedImage, n)
				return nil
			} else {
				n += 1
				go func() {
					if e := pushEvent(currentScene.PushUrl, currentScene.Scene, seq, "COUNT", capturedImage, n); nil != e {
						log.Println("pushEvent failed:>", e)
						testStopChannel <- true
					}
				}()
			}
		}
	}
}

func standJumpTask(seq int64) error {
	var tk = time.NewTicker(time.Millisecond * 400)
	defer func() {
		tk.Stop()
	}()
	time.Sleep(time.Millisecond * 500)
	go pushEvent(currentScene.PushUrl, currentScene.Scene, currentTest.SequenceId, "TEST READY", nil)
	var started = false
	for {
		select {
		case <-testStopChannel:
			go pushEvent(currentScene.PushUrl, currentScene.Scene, seq, "TEST END", capturedImage, 1000+random.Intn(1000))
			return nil
		case except := <-testExceptChannel:
			if except.Type != 0 {
				go pushException(currentScene.PushUrl, currentScene.Scene, currentTest.SequenceId, except.Type, except.Desc, capturedImage)
			}
		case <-tk.C:
			log.Println("===")
			if !started {
				go pushEvent(currentScene.PushUrl, currentScene.Scene, seq, "START", capturedImage)
				started = true
			}
		}
	}
}

func sitUpsTask(seq int64) error {
	var tk = time.NewTicker(time.Millisecond * 500)
	defer func() {
		tk.Stop()
	}()
	time.Sleep(time.Millisecond * 600)
	total := 40 + random.Intn(20)
	n := 0
	go pushEvent(currentScene.PushUrl, currentScene.Scene, currentTest.SequenceId, "TEST READY", nil)
	var started = false
	for {
		select {
		case <-testStopChannel:
			go pushEvent(currentScene.PushUrl, currentScene.Scene, seq, "TEST END", capturedImage, n)
			return nil
		case except := <-testExceptChannel:
			if except.Type != 0 {
				go pushException(currentScene.PushUrl, currentScene.Scene, currentTest.SequenceId, except.Type, except.Desc, capturedImage)
			}
		case <-tk.C:
			log.Println("===")
			if !started {
				go pushEvent(currentScene.PushUrl, currentScene.Scene, seq, "START", capturedImage, n)
				started = true
			} else if n >= total {
				go pushEvent(currentScene.PushUrl, currentScene.Scene, seq, "TEST END", capturedImage, n)
				return nil
			} else {
				n += 1
				go func() {
					if e := pushEvent(currentScene.PushUrl, currentScene.Scene, seq, "COUNT", capturedImage, n); nil != e {
						log.Println("pushEvent failed:>", e)
						testStopChannel <- true
					}
				}()
			}
		}
	}
}

func raceTask(seq int64) error {
	var tk = time.NewTicker(time.Millisecond * 800)
	defer func() {
		tk.Stop()
	}()
	time.Sleep(time.Millisecond * 800)
	go pushEvent(currentScene.PushUrl, currentScene.Scene, currentTest.SequenceId, "TEST READY", nil)
	var started = false
	for {
		select {
		case <-testStopChannel:
			go pushEvent(currentScene.PushUrl, currentScene.Scene, currentTest.SequenceId, "TEST END", capturedImage, random.Intn(40)+100)
			return nil
		case except := <-testExceptChannel:
			if except.Type != 0 {
				go pushException(currentScene.PushUrl, currentScene.Scene, currentTest.SequenceId, except.Type, except.Desc, capturedImage)
			}
		case <-tk.C:
			log.Println("===")
			if !started {
				go pushEvent(currentScene.PushUrl, currentScene.Scene, seq, "START", capturedImage)
				started = true
			}
		}
	}
}

func encodeImage(img image.Image) string {
	if nil == img {
		return ""
	}
	log.Println("encodeImage:> Bounds:", img.Bounds())
	//_ = saveToFile(img)
	buf := new(bytes.Buffer)
	if e := jpeg.Encode(buf, img, &jpeg.Options{
		Quality: 60,
	}); nil != e {
		log.Println("encodeImage:> failed:", e)
	}
	log.Println("encodeImage:> image size:", buf.Len())
	s := base64.StdEncoding.EncodeToString(buf.Bytes())
	log.Println("encodeImage:> encoded len:", len(s))
	return s
}

func pushException(url string, scene string, seq int64, tp int, desc string, img image.Image) {
	var st = TestStat{
		Scene:      scene,
		Timestamp:  time.Now().Unix(),
		SequenceId: seq,
		TestData: TestData{
			State:         "EXCEPT",
			ExceptionType: tp,
			ExceptionDesc: desc,
		},
	}
	if nil != img {
		st.TestData.Image = encodeImage(img)
	}
	httpPostEx(url, st, 2)
}

func pushEvent(url string, scene string, seq int64, state string, img image.Image, vals ...int) error {
	var st = TestStat{
		Scene:      scene,
		Timestamp:  time.Now().Unix(),
		SequenceId: seq,
		TestData: TestData{
			State: state,
		},
	}
	if nil != img {
		st.TestData.Image = encodeImage(img)
	}
	if len(vals) > 0 {
		switch scene {
		case "pullUp":
			st.TestData.Counts = vals[0]
		case "standJump":
			st.TestData.DistanceCM = vals[0]
		case "sitUps":
			st.TestData.Counts = vals[0]
		case "race":
			st.TestData.DurationSec = float32(vals[0]) / 10.0
		default:

		}
	}
	return httpPostEx(url, st, 2)
}

func recogTaskProc(cmd *TestCommand) error {
	testStopChannel = make(chan bool)
	testExceptChannel = make(chan TestException)
	random = rand.New(rand.NewSource(time.Now().UnixNano()))
	if nil == currentScene {
		return errors.New("invalid scene state")
	}
	if nil == currentScene.proc {
		return errors.New("invalid scene mode processor")
	}
	currentTest = cmd
	go func() {
		defer func() {
			close(testStopChannel)
			close(testExceptChannel)
			testStopChannel = nil
			testExceptChannel = nil
			currentTest = nil
		}()
		if e := currentScene.proc(cmd.SequenceId); nil != e {
			log.Println(">>>>", e)
		}
	}()
	go func() {
		var min = testDurations[currentScene.Scene][0]
		var max = testDurations[currentScene.Scene][1]
		for i := 0; i < random.Intn(max-min)+min; i++ {
			time.Sleep(time.Second)
		}
		testStopChannel <- true
	}()
	return nil
}

func init() {
	random = rand.New(rand.NewSource(time.Now().UnixNano()))
	currentTest = nil
	testExceptChannel = nil
	testStopChannel = nil
	testDurations = map[string][2]int{
		"pullUp":    [2]int{30, 80},
		"standJump": [2]int{10, 30},
		"sitUps":    [2]int{60, 70},
		"race":      [2]int{9, 15},
	}
}
