package modem

import (
	"errors"
	"fmt"
	"log"
	"math"
	"os"
	"os/exec"
	"strings"
	"time"

	gpio "github.com/warthog618/go-gpiocdev"
	"go.bug.st/serial"
)

type modem struct {
	// Serial stuff
	port        string
	deviceName  string
	baudrate    int
	inputBuf    []byte
	serialPort  serial.Port
	isAvailable bool

	// Gpio stuff
	powerKey int
	gpioLine *gpio.Line

	// Other values
	speed        float64
	latitude_ns  float64
	latitude     float64
	longitude_we float64
	longitude    float64

	lastUpdateTime time.Time
}

type Modem interface {
	Init() error
	SearchPort(isSoft bool) error
	PowerOn() error
	PowerOff() error
	Connect() error
	Ping() (bool, error)
	SwitchToGpsMode() error
	CalculateSpeed(newLatitude, newlongitude float64)
	Update() error
	GetInfo() string
	TestGPS() error
}

func New() Modem {
	return &modem{
		baudrate:       115200,
		powerKey:       6,
		lastUpdateTime: time.Now(),
		inputBuf:       make([]byte, 128),
	}
}

func (m *modem) Init() error {
	if err := m.SearchPort(true); err != nil {
		return fmt.Errorf("soft port search: %w", err)
	}
	if m.port == "" {
		if err := m.SearchPort(false); err != nil {
			return fmt.Errorf("not soft port search: %w", err)
		}
	}
	if m.port == "" {
		return errors.New("no port is detected")
	}
	if err := m.Connect(); err != nil {
		return fmt.Errorf("connect: %w", err)
	}
	if err := m.TestGPS(); err != nil {
		return fmt.Errorf("testGPS: %w", err)
	}
	return nil
}

func (m *modem) SearchPort(isSoft bool) error {
	ports := []string{}

	// Get ports
	if isSoft {
		ports = []string{"ttyUSB1", "ttyUSB2", "ttyUSB3"}
	} else {
		/**/
		log.Print("Search for ports...")
		out, err := exec.Command("/bin/ls", "/dev").Output()
		if err != nil {
			return fmt.Errorf("execute ls command: %w", err)
		}
		allPorts := strings.Split(string(out), "\n")
		for _, p := range allPorts {
			if len(p) > 2 && p[:3] == "tty" {
				ports = append(ports, p)
			}
		}
		// slices.Reverse(ports) // TODO why
	}
	// Check ports
	log.Println("Found ports: ", ports)
	log.Println("Check...")
	defer func() {
		if m.serialPort != nil {
			m.disconnect() // TODO maybe handle
		}
	}()
	for _, p := range ports {
		if m.serialPort != nil {
			if err := m.disconnect(); err != nil {
				return fmt.Errorf("disconnect: %w", err)
			}
		}
		log.Println("Checking port: ", p)
		if _, err := os.Stat("/dev/" + p); err != nil {
			continue
		}
		if err := m.connect("/dev/" + p); err != nil {
			log.Println("Error:", fmt.Errorf("connect: %w", err).Error())
			continue
		}

		// Ping
		log.Println("Ping...")
		if ok, err := m.Ping(); err != nil || !ok {
			if err != nil {
				return fmt.Errorf("ping error: %w", err)
			}
			return errors.New("modem does not ping")
		}

		// Check model
		{
			ok, err := m.makeAtEchoReqAndCheck("AT+CGMM\r\n", "SIMCOM_SIM7600E-H")
			if err != nil {
				return fmt.Errorf("make serial request: %w", err)
			}
			if !ok {
				continue
			}
		}

		log.Print("Found modem on port: ", p)
		m.port = "/dev/" + p
		m.isAvailable = true
		return nil
	}
	// return errors.New("no a compatible modem port found")
	return nil
}

func (m *modem) Connect() error {
	return m.connect(m.port)
}

func (m *modem) Ping() (bool, error) {
	return m.makeAtEchoReqAndCheck("AT\r\n", "OK")
}

func (m *modem) SwitchToGpsMode() error {
	if err := m.serialPort.ResetInputBuffer(); err != nil {
		return fmt.Errorf("reset input buffer: %w", err)
	}
	ans, err := m.makeAtReq("AT+CGPS?\r\n")
	if err != nil {
		return fmt.Errorf("make serial request: %w", err)
	}
	resp := strings.Split(ans, ":")
	if !(len(resp) > 1 && resp[0] == "+CGPS") {
		return errors.New("lost connection while checking gps status")
	}
	switch resp[1][1] {
	case '1':
		log.Println("GPS already enabled")
		return nil
	case '0':
		ok, err := m.makeAtEchoReqAndCheck("AT+CGPS=1", "OK")
		if err != nil {
			return fmt.Errorf("try to switch to gps mode echo reqest: %w", err)
		}
		if !ok {
			return errors.New("lost connection while trying to switch to gps mode")
		}
	default:
		return errors.New("unexpected response of gps status")
	}
	return nil
}

func deg2rad(deg float64) float64 {
	return deg * (math.Pi / 180)
}

func (m *modem) CalculateSpeed(newLatitude, newLongitude float64) {
	log.Println("Calculate speed")
	earthRad := 6371.0 // TODO ?
	dLat := deg2rad(math.Abs(newLatitude - m.latitude))
	dLon := deg2rad(math.Abs(newLongitude - m.longitude))
	a := math.Sin(dLat/2)*math.Sin(dLat/2) +
		math.Cos(deg2rad(newLatitude))*math.Cos(deg2rad(m.latitude))*math.Sin(dLon/2)*math.Sin(dLon/2)
	c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))

	m.speed = earthRad * c / (math.Abs(float64(time.Since(m.lastUpdateTime))))
}

func (m *modem) Update() error {
	log.Println("Update")
	if !m.isAvailable {
		log.Println("No connection to module")
		return nil
	}
	m.serialPort.ResetInputBuffer()
	// MAKE
}

func (m *modem) GetInfo() string {
	return ""
}

func (m *modem) TestGPS() error {
	return nil
}

/*
	TODOs:
		maybe to store read/write buf in obj
	QUESTIONS:
		do many threads?
*/

/*
 */
