package scpi

import (
	"bufio"
	"fmt"
	"net"
	"strings"
	"sync"
	"time"
)

// transport is the request/response channel to an instrument. The raw-socket
// implementation below speaks line-oriented SCPI over TCP (the common
// "SCPI raw" / port 5025 convention). The interface is kept narrow so a VXI-11
// (ONC-RPC) transport can be added later without touching the data source.
type transport interface {
	// query sends cmd and returns the instrument's single-line response.
	query(cmd string) (string, error)
	// write sends cmd and does not wait for a response.
	write(cmd string) error
	close()
}

// rawSocket is a line-based SCPI transport over a single TCP connection. The
// connection is dialled lazily and dropped on any I/O error so the next call
// reconnects. Calls are serialised by mu because SCPI is request/response.
type rawSocket struct {
	addr       string
	timeout    time.Duration
	terminator string

	mu   sync.Mutex
	conn net.Conn
	br   *bufio.Reader
}

func newRawSocket(addr string, timeout time.Duration, terminator string) *rawSocket {
	if timeout <= 0 {
		timeout = 3 * time.Second
	}
	if terminator == "" {
		terminator = "\n"
	}
	return &rawSocket{addr: addr, timeout: timeout, terminator: terminator}
}

func (s *rawSocket) dialLocked() error {
	if s.conn != nil {
		return nil
	}
	conn, err := net.DialTimeout("tcp", s.addr, s.timeout)
	if err != nil {
		return fmt.Errorf("scpi: dial %s: %w", s.addr, err)
	}
	s.conn = conn
	s.br = bufio.NewReader(conn)
	return nil
}

func (s *rawSocket) closeLocked() {
	if s.conn != nil {
		_ = s.conn.Close()
		s.conn = nil
		s.br = nil
	}
}

func (s *rawSocket) close() {
	s.mu.Lock()
	s.closeLocked()
	s.mu.Unlock()
}

func (s *rawSocket) sendLocked(cmd string) error {
	_ = s.conn.SetDeadline(time.Now().Add(s.timeout))
	if _, err := s.conn.Write([]byte(cmd + s.terminator)); err != nil {
		s.closeLocked()
		return fmt.Errorf("scpi: write: %w", err)
	}
	return nil
}

func (s *rawSocket) write(cmd string) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	if err := s.dialLocked(); err != nil {
		return err
	}
	return s.sendLocked(cmd)
}

func (s *rawSocket) query(cmd string) (string, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	if err := s.dialLocked(); err != nil {
		return "", err
	}
	if err := s.sendLocked(cmd); err != nil {
		return "", err
	}
	line, err := s.br.ReadString('\n')
	if err != nil {
		s.closeLocked()
		return "", fmt.Errorf("scpi: read: %w", err)
	}
	return strings.TrimRight(line, "\r\n"), nil
}
