package at

import (
	"fmt"
	"io"
	"log"
	"sync"
	"time"

	"go.bug.st/serial"
)

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

type atPort struct {
	logger *log.Logger

	mutex sync.Mutex // Mutex for all operation with serial port

	baudrate int
	portName string
	port     serial.Port
	inputBuf []byte
}

type Port interface {
	GetName() string
	GetBaudrate() int
	SerialPort() serial.Port // For extra need
	Mutex() sync.Locker      // retruns pointer to mutex for advanced use like readign NMEA reports

	Connect() error
	Disconnect() error
	IsConnected() bool

	RawSend(msg string, timeout time.Duration) (string, error)
	Send(cmd string) (Resp, error)
	SendWithTimeout(cmd string, timeout time.Duration) (Resp, error)

	io.Closer
}

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

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

func (p *atPort) GetBaudrate() int {
	return p.baudrate
}

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

func (p *atPort) Mutex() sync.Locker {
	return &p.mutex
}

func (p *atPort) Connect() error {
	p.mutex.Lock()
	defer p.mutex.Unlock()

	p.logger.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 {
	p.mutex.Lock()
	defer func() {
		p.port = nil
		p.mutex.Unlock()
	}()
	if err := p.port.Close(); err != nil {
		return fmt.Errorf("close port: %w", err)
	}
	return nil
}

func (p *atPort) IsConnected() bool {
	p.mutex.Lock()
	defer p.mutex.Unlock()

	return p.port != nil
}

// Low level write/read function
func (p *atPort) RawSend(msg string, timeout time.Duration) (string, error) {
	p.mutex.Lock()
	defer p.mutex.Unlock()

	// Write
	if _, err := p.port.Write([]byte(msg)); err != nil {
		return "", fmt.Errorf("serial port write: %w", err)
	}
	time.Sleep(timeout)
	// Read
	outBuf := make([]byte, 0)
readLoop:
	for {
		readLen, err := p.port.Read(p.inputBuf)
		if err != nil {
			return "", fmt.Errorf("port read: %w", err)
		}
		if readLen == 0 {
			break readLoop
		}
		outBuf = append(outBuf, p.inputBuf[:readLen]...)
		if readLen < len(p.inputBuf) {
			break readLoop
		}
	}
	// p.logger.Println(msg, "\x1b[38;2;150;150;150mRAWREAD:", string(outBuf), "\x1b[38;2;255;255;255m")

	return string(outBuf), nil
}

func (p *atPort) Send(cmd string) (Resp, error) {
	rawResp, err := p.RawSend(cmd+"\r\n", time.Microsecond)
	if err != nil {
		return RespNil, fmt.Errorf("%s request: %w", cmd, err)
	}
	if len(rawResp) <= 4 {
		return RespNil, fmt.Errorf("%s request: read too small msg: %d byte - %s", cmd, len(rawResp), string(rawResp))
	}
	resp := rawResp[2 : len(rawResp)-2] // Cut \r\n

	return Resp(resp), nil
}

func (p *atPort) SendWithTimeout(cmd string, timeout time.Duration) (Resp, error) {
	rawResp, err := p.RawSend(cmd+"\r\n", timeout)
	if err != nil {
		return RespNil, fmt.Errorf("%s request: %w", cmd, err)
	}
	if len(rawResp) <= 4 {
		return RespNil, fmt.Errorf("%s request: read too small msg: %d byte - %s", cmd, len(rawResp), string(rawResp))
	}
	resp := rawResp[2 : len(rawResp)-2] // Cut \r\n

	return Resp(resp), nil
}

func (p *atPort) Close() error {
	p.mutex.Lock()
	defer p.mutex.Unlock()

	return p.port.Close()
}