package modem

import (
	"fmt"
	"io"
	"log"
	"os"
	"os/exec"
	"strings"
	"time"

	"github.com/CGSG-2021-AE4/modem-test/api/modem/at"
	"github.com/CGSG-2021-AE4/modem-test/api/modem/gpio"
	"github.com/CGSG-2021-AE4/modem-test/api/modem/internet"
	"github.com/CGSG-2021-AE4/modem-test/api/modem/sms"
)

type ModemData struct {
	Port string `json:"Port"`
	GpsData
}

type modem struct {
	// Internal values
	logger *log.Logger

	// Serial values
	baudrate int

	deviceName string
	port       at.Port

	// Gpio values
	onOffPin gpio.Pin

	// Other values
	lastUpdateTime time.Time

	// GPS
	gpsInfo GpsData

	// Internet connection
	ic internet.Conn

	// Sms and calls
	sms sms.Dialer
}

type Modem interface {
	Init() error
	Validate() bool
	Update() error
	GetInfo() ModemData

	// Temp access to sms interface
	Sms() sms.Dialer

	io.Closer
}

func New(logger *log.Logger) Modem {
	return &modem{
		logger:         logger,
		baudrate:       115200,
		onOffPin:       gpio.New(log.New(logger.Writer(), "gpio", log.LstdFlags), 6),
		lastUpdateTime: time.Now(),
	}
}

func (m *modem) Init() error {
	// Turn module on
	m.logger.Println("=============================== Turn on module")
	if err := m.onOffPin.Init(); err != nil {
		return fmt.Errorf("gpio pin init: %w", err)
	}
	// m.onOffPin.PowerOn()

	// Search
	m.logger.Println("=============================== Search")
	// Soft search
	if err := m.searchPort(true); err != nil {
		return fmt.Errorf("soft port search: %w", err)
	}
	// Wide search
	if m.port == nil {
		if err := m.searchPort(false); err != nil {
			return fmt.Errorf("not soft port search: %w", err)
		}
	}
	if m.port == nil {
		return fmt.Errorf("no port is detected")
	}

	// Connect
	m.logger.Println("=============================== Connect")
	if err := m.connect(); err != nil {
		return fmt.Errorf("connect: %w", err)
	}

	// Tests
	m.logger.Println("=============================== Test")
	if err := m.testGPS(); err != nil {
		return fmt.Errorf("testGPS: %w", err)
	}

	// // Establish internet connection
	// m.logger.Println("=============================== Internet")
	// m.ic = internet.New(log.New(m.logger.Writer(), "internet", log.LstdFlags), m.port)
	// if err := m.ic.Init(); err != nil {
	// 	return fmt.Errorf("internet connection init: %w", err)
	// }

	// Init sms dialer
	m.sms = sms.New(log.New(m.logger.Writer(), "sms", log.LstdFlags), m.port)
	if err := m.sms.Init(); err != nil {
		return fmt.Errorf("sms dialer init %w", err)
	}
	return nil
}

func (m *modem) Validate() bool {
	return m.isConnected()
}

func (m *modem) Update() error {
	if !m.isConnected() {
		m.logger.Println("No connection to module")
		return nil
	}
	m.logger.Println("Update")
	// ans, err := m.port.Request(at.CmdQuestion, "CGPSINFO")
	// if err != nil {
	// 	return fmt.Errorf("check GPS info mode: %w", err)
	// }
	// ok := ans == "1"
	// if !ok {
	// 	_, err := m.port.Request(at.CmdCheck, "CGPSINFO")
	// 	if err != nil {
	// 		return fmt.Errorf("switch to GPS info mode: %w", err)
	// 	}
	// 	m.logger.Println("switched to GPS mode")
	// } else {
	// 	m.logger.Println("mode in right GPS mode")
	// }

	// Update
	m.logger.Println("Receiving GPS data...")
	resp, err := m.port.Send("AT+CGPSINFO")
	if err != nil {
		return fmt.Errorf("receive GPS data: %w", err)
	}
	if !resp.Check() {
		return fmt.Errorf("error response")
	}
	m.logger.Println("Decoding data...")

	if err := m.gpsInfo.decode(strings.Split(strings.Replace(resp.RmFront("+CGPSINFO:").String(), "\r", "", -1), "\n")[0]); err != nil {
		m.logger.Println("Gps info decode error:", err.Error())
		return nil
	}
	m.logger.Println("Decoded successfully")
	return nil
}

func (m *modem) GetInfo() ModemData {
	return ModemData{
		Port:    m.port.GetName(),
		GpsData: m.gpsInfo,
	}
}

func (m *modem) Sms() sms.Dialer {
	return m.sms
}

func (m *modem) Close() error {
	if err := m.sms.Close(); err != nil {
		return fmt.Errorf("sms: %w", err)
	}

	// Not right way I think
	if err := m.port.Close(); err != nil {
		return fmt.Errorf("serial port: %w", err)
	}
	if err := m.onOffPin.Close(); err != nil {
		return fmt.Errorf("gpio pin: %w", err)
	}
	if err := m.ic.Close(); err != nil {
		return fmt.Errorf("internet connection: %w", err)
	}

	return nil
}

func (m *modem) connect() error {
	if m.port == nil {
		return fmt.Errorf("port is not defined")
	}
	return m.port.Connect()
}

func (m *modem) disconnect() error {
	if m.port == nil {
		return fmt.Errorf("port is not defined")
	}
	return m.port.Disconnect()
}

func (m *modem) isConnected() bool {
	if m.port != nil {
		return m.port.IsConnected()
	}
	return false
}

func (m *modem) testGPS() error {
	m.logger.Println("Testing GPS")

	if err := m.switchToGpsMode(); err != nil {
		return fmt.Errorf("switch to GPS: %w", err)
	}

	if err := m.Update(); err != nil {
		return fmt.Errorf("update: %w", err)
	}

	m.logger.Println("Current coords:", m.getShortInfo())
	return nil
}

// Difference: I do not set \n at the end of string
func (m *modem) getShortInfo() string {
	return fmt.Sprintf("%f,%s,%f,%s", m.gpsInfo.Latitude, m.gpsInfo.LatitudeIndicator, m.gpsInfo.Longitude, m.gpsInfo.LongitudeIndicator)
}

func (m *modem) saveGPS(path string) error {
	f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
	if err != nil {
		return fmt.Errorf("open file: %w", err)
	}

	defer f.Close()

	if _, err = f.WriteString(m.getShortInfo()); err != nil {
		return fmt.Errorf("write file: %W", err)
	}
	return nil
}
func (m *modem) testConsole() {
	for {
		var inStr string
		fmt.Scanln(&inStr)
		if inStr == "exit" {
			return
		}
		resp, err := m.port.Send(inStr)
		if err != nil {
			m.logger.Println("ERROR:", err.Error())
		}
		m.logger.Println(resp)
		m.logger.Println("------------------")
	}
}

func (m *modem) checkPort(portName string) (outErr error) {
	defer func() {
		if outErr != nil { // Clear port if there is error
			m.port = nil
		}
	}()

	// Check device for existance
	if _, err := os.Stat(portName); err != nil {
		return fmt.Errorf("device does not exist")
	}

	// Check serial connection
	// Connect
	m.port = at.New(m.logger, portName, m.baudrate)
	if err := m.port.Connect(); err != nil {
		return fmt.Errorf("connect: %w", err)
	}
	defer m.port.Disconnect() // Do not bother about errors...

	// Reset input
	if err := m.port.GetSerialPort().ResetInputBuffer(); err != nil {
		return fmt.Errorf("reset input buffer: %w", err)
	}
	// Reset output
	if err := m.port.GetSerialPort().ResetOutputBuffer(); err != nil {
		return fmt.Errorf("reset output buffer: %w", err)
	}
	m.port.Send("ATE0") // This shit sometimes enables echo mode... why... when... but it can

	// Ping
	m.logger.Println("Ping...")
	if err := m.ping(); err != nil {
		return fmt.Errorf("ping error: %w", err)
	}
	m.logger.Println("\x1b[38;2;0;255;0mOK\x1b[38;2;255;255;255m")

	// Check model
	m.logger.Println("Check model...")
	resp, err := m.port.Send("AT+CGMM")
	if err != nil {
		return fmt.Errorf("get model: %w", err)
	}
	if !resp.Check() {
		return fmt.Errorf("error response: %s", resp)
	}
	model := strings.Split(resp.String(), "\n")[0]
	if err != nil {
		return fmt.Errorf("get model: %w", err)
	}
	rightModel := "SIMCOM_SIM7600E-H"
	// m.logger.Printf("[% x]\n [% x]", []byte("SIMCOM_SIM7600E-H"), []byte(model))
	if model[:len(rightModel)] != rightModel {
		return fmt.Errorf("invalid modem model: %s", model)
	}
	m.logger.Println("\x1b[38;2;0;255;0mOK\x1b[38;2;255;255;255m")
	return nil
}

func (m *modem) searchPort(isSoft bool) error {
	// Get ports
	ports := []string{"ttyUSB1", "ttyUSB2", "ttyUSB3"}
	if !isSoft {
		ps, err := getTtyDevices()
		if err != nil {
			fmt.Errorf("get serial devices: %w", err)
		}
		ports = ps
	}

	// Check ports
SearchLoop:
	for _, p := range ports {
		m.logger.Printf("Checking port %s ...\n", p)

		if err := m.checkPort("/dev/" + p); err != nil {
			m.logger.Printf("\x1b[38;2;255;0;0mCheck failed: %s\x1b[38;2;255;255;255m\n", err.Error())
			continue SearchLoop
		}

		m.logger.Print("Found modem on port: ", p)
		m.port = at.New(m.logger, "/dev/"+p, m.baudrate)
		return nil
	}
	return nil
}

func (m *modem) ping() error {
	resp, err := m.port.Send("AT")
	if err != nil {
		return err
	}
	if !resp.Check() {
		return fmt.Errorf("connection lost")
	}
	return nil
}

func (m *modem) switchToGpsMode() error {
	m.logger.Println("Enabling GPS mode...")
	// Reset input
	if err := m.port.GetSerialPort().ResetInputBuffer(); err != nil {
		return fmt.Errorf("reset input buffer: %w", err)
	}
	// Reset output
	if err := m.port.GetSerialPort().ResetOutputBuffer(); err != nil {
		return fmt.Errorf("reset output buffer: %w", err)
	}

	// Check gps mode status
	resp, err := m.port.Send("AT+CGPS?")
	if err != nil {
		return fmt.Errorf("make at ask: %w", err)
	}
	if !resp.Check() {
		return fmt.Errorf("error response")
	}
	ans := strings.Replace(strings.Split(strings.Split(resp.RmFront("+CGPS:").String(), "\n")[0], ",")[0], " ", "", -1)
	if ans == "1" {
		m.logger.Println("GPS already enabled")
		return nil
	}
	m.logger.Println(ans)

	// Modem is not in GPS mode
	resp, err = m.port.Send("AT+CGPS=1")
	if err != nil {
		return fmt.Errorf("try to switch to gps: %w", err)
	}
	if !resp.Check() {
		return fmt.Errorf("switch tp GPS failed")
	}
	m.logger.Println("GPS mode enabled")
	return nil
}

func getTtyDevices() ([]string, error) {
	// Get ports
	/**/
	out, err := exec.Command("ls", "--", "/dev/tty[!0-9]*").Output()
	if err != nil {
		return nil, fmt.Errorf("execute ls command: %w", err)
	}
	ports := strings.Split(string(out), "\n")
	return ports, nil
}

/*
	TODOs:
		maybe to store read/write buf in obj
	QUESTIONS:
		JSON why you clamp
*/

/*
 */