package at

import (
	"fmt"
	"log"
	"strings"
	"time"

	"go.bug.st/serial"
)

// Some constants
const (
	ReadTimeout  = time.Second
	InputBufSize = 128
)

// Command types
type CmdType byte

// Command types base on request/answer semantic:
const (
	CmdTest CmdType = iota
	// AT\r\n
	// OK
	CmdCheck
	// AT+<CMD>\r\n
	// OK
	CmdGet
	// AT+<CMD>\r\n
	// +<CMD>: <ANS>
	CmdQuestion
	// AT+<CMD>?\r\n
	// +<CMD>: <ANS>
)

type atPort struct {
	baudrate int
	portName string
	port     serial.Port
	inputBuf []byte
}

type Port interface {
	Connect() error
	Disconnect() error
	Request(cmdType CmdType, cmd string) (string, error)
	GetName() string
	IsConnected() bool
	GetSerialPort() serial.Port // For extra need
}

func New(portName string, baudrate int) Port {
	return &atPort{
		portName: portName,
		baudrate: baudrate,
		inputBuf: make([]byte, InputBufSize),
	}
}

func (p *atPort) Connect() error {
	log.Println("Connecting to", p.portName, "...")
	s, err := serial.Open(p.portName, &serial.Mode{BaudRate: p.baudrate})
	if err != nil {
		return fmt.Errorf("open port: %w", err)
	}
	// s.Close() There is no open f
	// s.Open()
	p.port = s
	p.port.SetReadTimeout(ReadTimeout)
	p.port.ResetInputBuffer()
	p.port.ResetOutputBuffer()
	return nil
}

func (p *atPort) Disconnect() error {
	defer func() {
		p.port = nil
	}()
	if err := p.port.Close(); err != nil {
		return fmt.Errorf("close port: %w", err)
	}
	return nil
}

// Low level write/read function
func (p *atPort) makeReq(msg string) (string, error) {
	// Write
	p.port.ResetInputBuffer()
	log.Println("Write...") // DEBUG
	if written, err := p.port.Write([]byte(msg)); err != nil {
		return "", fmt.Errorf("serial port write: %w", err)
	} else {
		log.Println("Written:", written) // DEBUG
	}
	// Read
	log.Println("Read...") // DEBUG
	readLen, err := p.port.Read(p.inputBuf)
	if err != nil {
		return "", fmt.Errorf("port read: %w", err)
	}

	log.Println("Read: ", readLen, string(p.inputBuf[:readLen])) // DEBUG
	return string(p.inputBuf[:readLen]), nil
}

func (p *atPort) Request(cmdType CmdType, cmd string) (string, error) {
	msg := "AT"
	// Make command
	// By default it just will be AT check cmd
	switch cmdType {
	case CmdGet, CmdCheck:
		msg += cmd
	case CmdQuestion:
		msg += cmd + "?"
	}
	msg += "\r\n"
	rawResp, err := p.makeReq(msg)
	log.Println("Got")
	if err != nil {
		return "", fmt.Errorf("make at request: %w", err)
	}
	if len(rawResp) == 0 {
		return "", fmt.Errorf("read nothing")
	}
	resp := strings.Split(rawResp, "\n")
	switch cmdType {
	case CmdTest, CmdCheck:
		// Check and test cmds do not suppose anything but OK
		if len(resp[1]) >= 2 && resp[1][:2] == "OK" {
			return "", nil
		}
		return "", fmt.Errorf("connection lost")
	case CmdGet, CmdQuestion:
		checkL := len(cmd) + 1
		if len(resp[1]) >= checkL && resp[1][:checkL] != "+"+cmd {
			return "", fmt.Errorf("connetion lost")
		}
		return strings.Split(resp[1], ":")[1], nil
	}
	return "", fmt.Errorf("undefined command type")
}

func (p *atPort) GetName() string {
	return p.portName
}

func (p *atPort) IsConnected() bool {
	return p.port != nil
}

func (p *atPort) GetSerialPort() serial.Port {
	return p.port
}
