package excel

import (
	"fmt"
	"gotool/crc16"
	"image"
	"image/png"
	"io/ioutil"
	"math"
	"os"
	"strconv"
	"strings"
)

const (
	AddressHeadOffset     = 0x1000 // 0x0000 ~ 0x0fff reserved, Table info data start address
	AddressAnimationStart = 0x4000 // Animation Frame data start address

	SizeOfOneDataset    = 0x20      // header size, include Table version and Info table
	SizeOfOneInfoTable  = 0x100     // one Info Table Data region size, one Data area include 8 Datesets
	SizeOfOneFrame      = 0x800     // size of one frame in animation
	SizeOfAnimation     = 0x80000   // size of one animation
	SizeOfAnimationArea = 0x1E00000 // whole size of animation area
	SizeOfFlashBin      = 0x4000000 // extflash size

	FrameDefaultValue = 0x64 // if one area is marked as a animation area, but it is a null animation, header will fill with this value
	FlashDefaultValue = 0xff // default value in flash

	// mode to combine, include Day, Night, and Both
	// if mode is Day/Night, only combine Day/Night files
	// if mode is Both, will combine all Day files first, then combine all Night files in one animation
	ModeDay   = "Day"
	ModeNight = "Night"
	ModeBoth  = "Both"
)

func (d *MExcel) CreateBin(pngFilePath string, dstFilePath string, isDay bool) {
	defer func() {
		d.ChOneTaskFinished <- true
	}()

	// create bin file
	src, err := os.Create(dstFilePath[:len(dstFilePath)-4] + "_CHL.bmp.bin")
	if err != nil {
		return
	}
	defer func() {
		if err := src.Close(); err != nil {
			fmt.Println(err)
		}
	}()

	// open the specified png file
	pngFile, err := os.Open(pngFilePath)
	if err != nil {
		return
	}
	defer func() {
		if err := pngFile.Close(); err != nil {
			fmt.Println(err)
		}
	}()

	pngImg, err := png.Decode(pngFile)
	if err != nil {
		return
	}

	// calc rwdata
	var binBody []byte
	for _, s := range d.BinData.Section {
		// if there is a subArea exists, write all data in binBody
		if len(s.subSection) != 0 {
			for _, v := range s.subSection {
				binBody = append(binBody, fillBinBodyData(pngImg, v.PointInfo, v.MainColor, isDay)...)
			}
		} else {
			binBody = append(binBody, fillBinBodyData(pngImg, s.PointInfo, s.MainColor, isDay)...)
		}
	}

	// get every section info table address and write header
	binHead := make([]byte, 0xb8)
	var offset uint16 = 0xa8

	for _, v := range d.BinData.Section {
		pos := 0x10 + 0x08*(v.IsType)
		binHead[pos] = byte(v.IsType)
		binHead[pos+1] = byte(v.IsType >> 8)
		binHead[pos+2] = byte(v.Length)
		binHead[pos+3] = byte(v.Length >> 8)
		binHead[pos+4] = byte(offset)
		binHead[pos+5] = byte(offset >> 8)
		binHead[pos+6] = 0x01
		binHead[pos+7] = 0xff

		offset += v.Length
	}

	// special tag
	binHead[0] = 'B'
	binHead[1] = 'D'
	binHead[2] = 'S'
	binHead[3] = 'L'

	// version
	binHead[4] = 0x00
	binHead[5] = 0x01

	// number of light source
	binHead[8] = byte(len(d.BinData.Section))
	binHead[9] = byte(len(d.BinData.Section) >> 8)

	// size of rawdata
	binHead[10] = byte(offset)
	binHead[11] = byte(offset >> 8)

	// index of animation, calc by file name
	tmp, _ := strconv.Atoi(pngFilePath[strings.LastIndex(pngFilePath, "_")+1 : strings.LastIndex(pngFilePath, ".")])
	tmp++ // 0 --> 1, animation start from 1
	binHead[12] = byte(tmp)
	binHead[13] = byte(tmp >> 8)

	binHead = append(binHead, binBody...)

	// crc16 - MODBUS, calculated CRC is start at 0x10
	checkSum := crc16.CheckSum(binHead[0x10:])
	binHead[6] = byte(checkSum)
	binHead[7] = byte(checkSum >> 8)

	src.Write(binHead)
}

// calc main color, If the main color is inconsistent, area will fill with 0
func calcMainColor(pngImg image.Image, imgInfo []*MImgPointInfo) string {
	var cntR, cntG, cntB uint32
	var ret string
	cntR = 0
	cntG = 0
	cntB = 0

	for _, v := range imgInfo {
		color := pngImg.At(int(v.SrcX), int(v.SrcY))
		r, g, b, _ := color.RGBA()
		cntR += r
		cntG += g
		cntB += b
	}

	if cntR > cntG && cntR > cntB {
		ret = "r"
	} else if cntG > cntR && cntG > cntB {
		ret = "g"
	} else if cntB > cntR && cntB > cntG {
		ret = "b"
	}

	return ret
}

func fillBinBodyData(pngImg image.Image, pointInfo []*MImgPointInfo, mainColor string, isDay bool) []byte {
	var binBody []byte

	// if the main color is inconsistent, fill with 0
	if mainColor != "" && calcMainColor(pngImg, pointInfo) != mainColor {
		zero := make([]byte, len(pointInfo))
		binBody = append(binBody, zero...)
	} else {
		for _, v := range pointInfo {
			color := pngImg.At(int(v.SrcX), int(v.SrcY))
			r, g, b, _ := color.RGBA()
			// just use the max value of color
			// image color --> light color:  blue --> white, red --> red, green --> yellow
			var gray byte = 0
			if r > g && r > b {
				gray = byte(r)
			} else if g > r && g > b {
				gray = byte(g)
			} else if b > r && b > g {
				gray = byte(b)
			}

			// judge mode is Day or Night
			if isDay {
				gray = byte(math.Floor(float64(gray) * float64(v.BrightnessDay)))
			} else {
				gray = byte(math.Floor(float64(gray) * float64(v.BrightnessNight)))
			}

			// if color < 3, gray set to 0
			// if src pos is ( 0, 0 ), gray set to 0, skip the special light, such as HB
			if gray <= 3 || (v.SrcX == 0 && v.SrcY == 0) {
				gray = 0
			}
			binBody = append(binBody, gray)
		}
	}
	return binBody
}

// list all folder, find all .bin file, mark as Day/Night by using folder name
func (d *MExcel) ListBinFiles(folderPath string) {
	logfile, _ := os.Create(folderPath + "\\log.txt")
	defer logfile.Close()

	folderInfos, err := ioutil.ReadDir(folderPath)
	if err != nil {
		fmt.Println(err)
	}

	// list all files base on theme animation index
	for _, b := range d.BinThemeData {
		for _, a := range b.Animation {
			for _, folder := range folderInfos {
				// match file name and animation index
				// maybe include "Day", "Night" or "Both", meaning this area include animation which have mode include Day/Night or Both
				if a.IndexString != "" && folder.IsDir() &&
					strings.Contains(folder.Name(), a.IndexString) &&
					strings.Contains(folder.Name(), a.Mode) {
					binFiles, _ := ioutil.ReadDir(folderPath + "\\" + folder.Name())
					for _, binfile := range binFiles {
						if !binfile.IsDir() && strings.Contains(binfile.Name(), a.IndexString) &&
							strings.Contains(binfile.Name(), ".bin") {
							logfile.WriteString(binfile.Name() + "\n")
							a.FileName = append(a.FileName, folderPath+"\\"+folder.Name()+"\\"+binfile.Name())
						}
					}
				}
			}
		}
	}
}

// generate flash bin file by ThemeInfo, size: 64MB
// Concatenate Reserved area  (0x0000000 ~ 0x0000FFF),
// Info Table area(0x0001000 ~ 0x0004FFF),
// Animation area (0x0005000 ~ 0x1E04FFF)
func (d *MExcel) CreateFlashBin(filePath string) {
	var binData []byte
	d.ChTaskAdd <- 4
	////////////////////////// Reserved area ///////////////////////////////
	reservedArea := make([]byte, AddressHeadOffset)
	for i := 0; i < len(reservedArea); i++ {
		reservedArea[i] = FlashDefaultValue
	}
	binData = append(binData, reservedArea...)
	d.ChOneTaskFinished <- true

	////////////////////////// Info Table area /////////////////////////////
	infoTable := getInfoTableArea(d.BinThemeData)
	binData = append(binData, infoTable...)
	d.ChOneTaskFinished <- true

	///////////////////////// Animation area ///////////////////////////////
	animationArea := getAnimationArea(d.BinThemeData)
	binData = append(binData, animationArea...)
	d.ChOneTaskFinished <- true

	alignData(&binData, SizeOfFlashBin)

	f, _ := os.Create(filePath)
	f.Write(binData)
	f.Close()
	d.ChOneTaskFinished <- true
	d.ChDone <- true
}

func getInfoTableArea(themeInfo []*ThemeInfo) []byte {
	/////////////// head ////////////////////
	head := make([]byte, SizeOfOneInfoTable)
	for i := 0; i < len(head); i++ {
		head[i] = FlashDefaultValue
	}

	// magic number
	head[0] = 0x54
	head[1] = 0x44
	head[2] = 0x53
	head[3] = 0x49
	// version
	head[4] = 0x01
	head[5] = 0x00
	// crc
	head[6] = 0xff
	head[7] = 0xff
	// size of themes
	size := len(themeInfo)
	head[8] = byte(size)
	head[9] = byte(size >> 8)

	for _, v := range themeInfo {
		table := make([]byte, SizeOfOneInfoTable)
		for i := 0; i < len(table); i++ {
			table[i] = FlashDefaultValue
		}

		table[0] = byte(v.ID)
		table[1] = byte(v.ID >> 8)
		size := len(v.Animation)
		table[2] = byte(size)
		table[3] = byte(size >> 8)

		var cnt int = 0
		for i := 0; i < len(v.Animation); i++ {
			frameLen := len(v.Animation[i].FileName)
			// 长度为0时，默认使用FrameDefaultValue
			if frameLen == 0 {
				frameLen = FrameDefaultValue
			}
			table[4+cnt] = byte(frameLen)
			cnt += 1
		}

		head = append(head, table...)
	}

	// add tail default data
	tail := make([]byte, AddressAnimationStart-len(head))
	for i := 0; i < len(tail); i++ {
		tail[i] = FlashDefaultValue
	}

	return append(head, tail...)
}

func getAnimationArea(themeInfo []*ThemeInfo) []byte {
	var bodyData []byte
	for _, binData := range themeInfo {
		for _, a := range binData.Animation {
			var animations []byte
			// get every animation data and aligh with SizeOfAnimation by fill FlashDefaultValue
			for _, filename := range a.FileName {
				// get every frame data and aligh with SizeOfOneFrame by fill FlashDefaultValue
				f, _ := os.Open(filename)
				frame, _ := ioutil.ReadAll(f)
				alignData(&frame, SizeOfOneFrame)
				animations = append(animations, frame...)
			}
			alignData(&animations, SizeOfAnimation)
			bodyData = append(bodyData, animations...)
		}
	}
	alignData(&bodyData, SizeOfAnimationArea)
	return bodyData
}

func alignData(data *[]byte, length int) {
	tail := make([]byte, length-len(*data))
	for i := 0; i < len(tail); i++ {
		tail[i] = FlashDefaultValue
	}
	*data = append(*data, tail...)
}
