//
// gqgmc.go
// Copyright (C) 2017 kevin <kevin@phrye.com>
//
// Distributed under terms of the GPL license.
//

package geiger

import (
	"encoding/binary"
	"encoding/hex"
	"errors"
	"fmt"
	"strconv"
	"sync"
	"time"

	"github.com/go-restruct/restruct"
	"github.com/tarm/serial"
)

// The Ideal and Low voltage threshholds
const (
	VoltageIdeal  = 98
	VoltageTooLow = 75
)

const (
	cmdGetSerial    = "<GETSERIAL>>"    // GMC-280, GMC-300 Re.2.11
	cmdGetVersion   = "<GETVER>>"       // GMC-280, GMC-300 Re.2.0x, Re.2.10
	cmdGetVoltage   = "<GETVOLT>>"      // GMC-280, GMC-300 Re.2.0x, Re.2.10
	cmdGetCPM       = "<GETCPM>>"       // GMC-280, GMC-300 Re.2.0x, Re.2.10
	cmdGetCPS       = "<GETCPS>>"       // GMC-280, GMC-300 Re.2.0x, Re.2.10
	cmdGetCfg       = "<GETCFG>>"       // GMC-280, GMC-300 Re.2.10
	cmdEraseCfg     = "<ECFG>>"         // GMC-280, GMC-300 Re.2.10
	cmdUpdateCfg    = "<CFGUPDATE>>"    // GMC-280, GMC-300 Re.2.20
	cmdTurnOnCPS    = "<HEARTBEAT1>>"   // GMC-280, GMC-300 Re.2.10
	cmdTurnOffCPS   = "<HEARTBEAT0>>"   // Re.2.10
	cmdTurnOffPwr   = "<POWEROFF>>"     // GMC-280, GMC-300 Re.2.11
	cmdTurnOnPwr    = "<POWERON>>"      // GMC-280, GMC-300 Re.3.10
	cmdFactoryReset = "<FACTORYRESET>>" // GMC-280, GMC-300 Re.3.00
	cmdReboot       = "<REBOOT>>"       // GMC-280, GMC-300 Re.3.00
	cmdGetTime      = "<GETDATETIME>>"  // GMC-280, GMC-300 Re.3.00
	cmdGetTemp      = "<GETTEMP>>"      // GMC-320 Re.3.01
	cmdGetGyro      = "<GETGYRO>>"      // GMC-320 Re.3.01
)

var (
	mod280n300 = []string{"GMC-280", "GMC-300"}
	mod320     = []string{"GMC-320"}
)

// GQGMCCounter is a GQ GMC Counter
type GQGMCCounter struct {
	port   *serial.Port
	config *serial.Config
	version,
	model,
	serial string
	mutex *sync.Mutex
}

// DevConfig is the gcgmc config block.
type DevConfig struct {
	PowerOnOff                   int8  `struct:"int8"`
	AlarmOnOff                   int8  `struct:"int8"`
	SpeakerOnOff                 int8  `struct:"int8"`
	GraphicModeOnOff             int8  `struct:"int8"`
	BackLightTimeoutSeconds      int8  `struct:"int8"`
	IdleTitleDisplayMode         int8  `struct:"int8"`
	AlarmCPMValue                int16 `struct:"int16,big"`
	CalibrationCPM0              byte  `struct:"int16,big"`
	CalibrationSvUcByte3p0       byte  `struct:"byte"`
	CalibrationSvUcByte2p0       byte  `struct:"byte"`
	CalibrationSvUcByte1p0       byte  `struct:"byte"`
	CalibrationSvUcByte0p0       byte  `struct:"byte"`
	CalibrationCPM1              byte  `struct:"int16,big"`
	CalibrationSvUcByte3p1       byte  `struct:"byte"`
	CalibrationSvUcByte2p1       byte  `struct:"byte"`
	CalibrationSvUcByte1p1       byte  `struct:"byte"`
	CalibrationSvUcByte0p1       byte  `struct:"byte"`
	CalibrationCPM2              byte  `struct:"int16,big"`
	CalibrationSvUcByte3p2       byte  `struct:"byte"`
	CalibrationSvUcByte2p2       byte  `struct:"byte"`
	CalibrationSvUcByte1p2       byte  `struct:"byte"`
	CalibrationSvUcByte0p2       byte  `struct:"byte"`
	IdleDisplayMode              byte  `struct:"byte"`
	AlarmValueuSvByte3           byte  `struct:"byte"`
	AlarmValueuSvByte2           byte  `struct:"byte"`
	AlarmValueuSvByte1           byte  `struct:"byte"`
	AlarmValueuSvByte0           byte  `struct:"byte"`
	AlarmType                    byte  `struct:"byte"`
	SaveDataType                 byte  `struct:"byte"`
	SwivelDisplay                byte  `struct:"byte"`
	ZoomByte3                    byte  `struct:"byte"`
	ZoomByte2                    byte  `struct:"byte"`
	ZoomByte1                    byte  `struct:"byte"`
	ZoomByte0                    byte  `struct:"byte"`
	SPIDataSaveAddress2          byte  `struct:"byte"`
	SPIDataSaveAddress1          byte  `struct:"byte"`
	SPIDataSaveAddress0          byte  `struct:"byte"`
	SPIDataReadAddress2          byte  `struct:"byte"`
	SPIDataReadAddress1          byte  `struct:"byte"`
	SPIDataReadAddress0          byte  `struct:"byte"`
	PowerSavingMode              int8  `struct:"int8"`
	SensitivityMode              int8  `struct:"int8"`
	CounterDelay                 int16 `struct:"int16,big"`
	VoltageOffset                int8  `struct:"int8"`
	MaxCPM                       int16 `struct:"uint16,big"`
	SensitivityAutoModeThreshold int8  `struct:"int8"`
	SaveDateTimeStamp6           byte  `struct:"byte"`
	SaveDateTimeStamp5           byte  `struct:"byte"`
	SaveDateTimeStamp4           byte  `struct:"byte"`
	SaveDateTimeStamp3           byte  `struct:"byte"`
	SaveDateTimeStamp2           byte  `struct:"byte"`
	SaveDateTimeStamp1           byte  `struct:"byte"`
	MaximumBytes                 byte  `struct:"byte"`
}

// NewGQGMC creates a new GQGMC Counter instance
func NewGQGMC(c Config) (*GQGMCCounter, error) {
	var gc GQGMCCounter
	var buf []byte

	gc.config = &serial.Config{
		Name:        c.Device,
		Baud:        57600,
		ReadTimeout: 500 * time.Millisecond,
	}
	p, err := serial.OpenPort(gc.config)
	if err != nil {
		return nil, err
	}
	gc.port = p
	gc.mutex = &sync.Mutex{}
	buf, err = gc.communicate(cmdGetVersion, 14)
	if err == nil {
		gc.model = string(buf[:7])
		gc.version = string(buf[7:])
	}
	if gc.supportedModels(mod280n300) && !gc.versionLT("Re 2.11") {
		buf, err := gc.communicate(cmdGetSerial, 7)
		if err == nil {
			gc.serial = hex.EncodeToString(buf)
		}
	}
	//getConfiguration()
	return &gc, nil
}

// Clear clears out any remaining data
func (gc *GQGMCCounter) Clear() error {
	// Read up to 10 chars until nothing comes back.
	// error otherwise.
	for i := 0; i < 10; i++ {
		if _, err := gc.recv(1); err != nil {
			break
		}
	}
	return nil
}

// Version gets the version of the device
func (gc *GQGMCCounter) Version() string {
	return gc.version
}

// Model gets the model of the device
func (gc *GQGMCCounter) Model() string {
	return gc.model
}

// Serial gets the serial number of the device
func (gc *GQGMCCounter) Serial() string {
	return gc.serial
}

func (gc *GQGMCCounter) getReading(what string) (uint16, error) {
	buf, err := gc.communicate(what, 2)
	if err != nil {
		return 0, err
	}
	reading := ((uint16(buf[0]) << 8) & 0x3f00)
	reading |= (uint16(buf[1]) & 0x00ff)
	return reading, nil
}

// GetCPM returns CPM
func (gc *GQGMCCounter) GetCPM() (uint16, error) {
	return gc.getReading(cmdGetCPM)
}

// GetCPS returns CPS
func (gc *GQGMCCounter) GetCPS() (uint16, error) {
	return gc.getReading(cmdGetCPS)
}

// Volts returns current battery voltage (times 10)
func (gc *GQGMCCounter) Volts() (int16, error) {
	if !gc.supportedModels(mod280n300) {
		return 0, errors.New("Unsupported model")
	}
	if gc.versionLT("Re 2.00") {
		return 0, errors.New("Unsupported version")
	}

	volts, err := gc.communicate(cmdGetVoltage, 1)
	if err != nil {
		return 0, err
	}
	return int16(volts[0]), err
}

// GetHistory Should return history data but is unimplemented for now
func (gc *GQGMCCounter) GetHistory() {
	// It's not recommended to use this so blank for now.
	return
}

// TurnOnCPS turns on CPS collection
func (gc *GQGMCCounter) TurnOnCPS() error {
	if !gc.supportedModels(mod280n300) {
		return errors.New("Unsupported model")
	}
	if gc.versionLT("Re 2.10") {
		return errors.New("Unsupported version")
	}

	gc.mutex.Lock()
	gc.send(cmdTurnOnCPS)
	gc.mutex.Unlock()
	return nil
}

// TurnOffCPS turns off CPS collection
func (gc *GQGMCCounter) TurnOffCPS() error {
	if !gc.supportedModels(mod280n300) {
		return errors.New("Unsupported model")
	}
	if gc.versionLT("Re 2.10") {
		return errors.New("Unsupported version")
	}

	gc.mutex.Lock()
	gc.send(cmdTurnOffCPS)
	gc.Clear()
	gc.mutex.Unlock()
	return nil
}

// GetAutoCPS gets a reading once auto CPS is turned on
func (gc *GQGMCCounter) GetAutoCPS() (uint16, error) {
	gc.mutex.Lock()
	buf, err := gc.recv(2)
	gc.mutex.Unlock()

	if err != nil {
		return 0, err
	}
	cps := ((uint16(buf[0]) << 8) & 0x3f00)
	cps |= (uint16(buf[1]) & 0x00ff)
	return cps, nil
}

// TurnOnPower turns the device on
func (gc *GQGMCCounter) TurnOnPower() {
	if !gc.supportedModels(mod280n300) {
		return
	}
	if gc.versionLT("Re 3.10") {
		return
	}

	gc.mutex.Lock()
	gc.send(cmdTurnOnPwr)
	gc.mutex.Unlock()
	return
}

// TurnOffPower turns the device off
func (gc *GQGMCCounter) TurnOffPower() {
	if !gc.supportedModels(mod280n300) {
		return
	}
	if gc.versionLT("Re 2.11") {
		return
	}

	gc.mutex.Lock()
	gc.send(cmdTurnOffPwr)
	gc.mutex.Unlock()
	return
}

// GetConfiguration reads configuration data
func (gc *GQGMCCounter) GetConfiguration() (*DevConfig, error) {
	if !gc.supportedModels(mod280n300) {
		return nil, errors.New("Unsupported Model")
	}
	if gc.versionLT("Re 2.10") {
		return nil, errors.New("Unsupported version")
	}

	data, err := gc.communicate(cmdGetCfg, 256)
	if err != nil {
		return nil, err
	}
	var cfg DevConfig
	restruct.Unpack(data[:58], binary.BigEndian, &cfg)
	fmt.Printf("Configuration: %+v\n", data)
	return &cfg, nil
}

// SetConfiguration writes configuration data
func (gc *GQGMCCounter) SetConfiguration() {
	if !gc.supportedModels(mod280n300) {
		return
	}
	if gc.versionLT("Re 2.10") {
		return
	}

	// See the ConfigurationData functions in gqgmc.cc
}

// SetTime sets the time
func (gc *GQGMCCounter) SetTime(t time.Time) {
	if !gc.supportedModels(mod280n300) {
		return
	}
	if gc.versionLT("Re 2.23") {
		return
	}
	gc.mutex.Lock()
	defer gc.mutex.Unlock()
	if gc.versionLT("Re 3.30") {
		gc.setTimeParts(t)
	}
	gc.setTimeAll(t)
}

func (gc *GQGMCCounter) setTimeParts(t time.Time) {
	cmd := make([]byte, 13)
	var timeCmds = []struct {
		cmd  string
		unit int
	}{
		{"<SETDATEYY", t.Year() - 2000},
		{"<SETDATEMM", int(t.Month())},
		{"<SETDATEDD", t.Day()},
		{"<SETTIMEHH", t.Hour()},
		{"<SETTIMEMM", t.Minute()},
		{"<SETTIMESS", t.Second()},
	}

	for _, c := range timeCmds {
		copy(cmd[:], c.cmd)
		cmd[10] = uint8(c.unit)
		copy(cmd[11:], ">>")
		// Mutex acquired in setTime()
		gc.port.Write(cmd)
		gc.recv(1)
	}
}

func (gc *GQGMCCounter) setTimeAll(t time.Time) {
	cmd := make([]byte, 20)
	copy(cmd[:], "<SETDATETIME")
	cmd[12] = uint8(t.Year() - 2000)
	cmd[13] = uint8(int(t.Month()))
	cmd[14] = uint8(t.Day())
	cmd[15] = uint8(t.Hour())
	cmd[16] = uint8(t.Minute())
	cmd[17] = uint8(t.Second())
	copy(cmd[18:], ">>")
	// Mutex acquired in setTime()
	gc.port.Write(cmd)
	gc.recv(1)
}

// GetTime gets the time
func (gc *GQGMCCounter) GetTime() (time.Time, error) {
	if !gc.supportedModels(mod280n300) {
		return time.Unix(0, 0), errors.New("Unsupported model")
	}
	if gc.versionLT("Re 3.00") {
		return time.Unix(0, 0), errors.New("Unsupported version")
	}

	b, err := gc.communicate(cmdGetTime, 7)
	if err != nil {
		return time.Unix(0, 0), err
	}
	t := time.Date(int(b[0])+2000, time.Month(b[1]), int(b[2]),
		int(b[3]), int(b[4]), int(b[5]), 0, time.Local)
	return t, nil
}

// GetTemp gets the temp
func (gc *GQGMCCounter) GetTemp() (float64, error) {
	if !gc.supportedModels(mod320) {
		return 0, errors.New("Unsupported model")
	}
	if gc.versionLT("Re 3.01") {
		return 0, errors.New("Unsupported version")
	}

	t, err := gc.communicate(cmdGetTemp, 4)
	if err != nil {
		return 0, err
	}

	var temp float64
	temp, err = strconv.ParseFloat(fmt.Sprintf("%d.%d", uint8(t[0]), uint8(t[1])), 64)
	if err != nil {
		return 0, err
	}
	if t[2] != 0 {
		temp = -temp
	}
	return temp, nil
}

// GetGyro gets the position in space
func (gc *GQGMCCounter) GetGyro() (int16, int16, int16, error) {
	if !gc.supportedModels(mod320) {
		return 0, 0, 0, errors.New("Unsupported model")
	}
	if gc.versionLT("Re 3.01") {
		return 0, 0, 0, errors.New("Unsupported version")
	}

	buf, err := gc.communicate(cmdGetGyro, 7)
	if err != nil {
		return 0, 0, 0, err
	}
	x := (int16(buf[0]) << 8)
	x |= (int16(buf[1]) & 0x00ff)
	y := (int16(buf[0]) << 8)
	y |= (int16(buf[1]) & 0x00ff)
	z := (int16(buf[0]) << 8)
	z |= (int16(buf[1]) & 0x00ff)
	return x, y, z, nil
}

// FactoryReset does a factory reset
func (gc *GQGMCCounter) FactoryReset() {
	if !gc.supportedModels(mod280n300) {
		return
	}
	if gc.versionLT("Re 3.00") {
		return
	}

	gc.mutex.Lock()
	gc.send(cmdFactoryReset)
	gc.mutex.Unlock()
	return
}

// Reboot reboots the device
func (gc *GQGMCCounter) Reboot() {
	if !gc.supportedModels(mod280n300) {
		return
	}
	if gc.versionLT("Re 3.00") {
		return
	}

	gc.mutex.Lock()
	gc.send(cmdReboot)
	gc.mutex.Unlock()
	return
}

func (gc *GQGMCCounter) supportedModels(models []string) bool {
	for _, model := range models {
		if model == gc.model {
			return true
		}
	}
	return false
}

func (gc *GQGMCCounter) versionLT(version string) bool {
	return gc.version < version
}

func (gc *GQGMCCounter) communicate(cmd string, length int) ([]byte, error) {
	gc.mutex.Lock()
	defer gc.mutex.Unlock()
	gc.Clear()
	if len(cmd) > 0 {
		gc.send(cmd)
	}
	if length != 0 {
		return gc.recv(length)
	}
	return nil, nil
}

func (gc *GQGMCCounter) send(cmd string) {
	gc.port.Write([]byte(cmd))
	return
}

func (gc *GQGMCCounter) recv(length int) ([]byte, error) {
	buf := make([]byte, length)
	n, err := gc.port.Read(buf)
	if err != nil {
		return nil, err
	}
	read := n
	if n != length {
		// Handle the case where we couldn't read it all.
		// Really only happens for length > 32.
		for i := 0; i < 20; i++ {
			n, err = gc.port.Read(buf[read:])
			if err != nil {
				return nil, err
			}
			read += n
			if read == length {
				break
			}
		}
	}
	if read != length {
		return nil, fmt.Errorf("Short read (got: %d, wanted: %d)", n, length)
	}
	return buf, nil
}