package main

import (
	"encoding/json"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/url"
	"os"
	"os/signal"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"syscall"

	"github.com/elisescu/tty-share/server"
	"github.com/gorilla/websocket"
	"github.com/hashicorp/yamux"
	"github.com/moby/term"
	log "github.com/sirupsen/logrus"
)

type ttyShareClient struct {
	url             string
	ttyWsConn       *websocket.Conn
	tunnelWsConn    *websocket.Conn
	tunnelAddresses *string
	detachKeys      string
	wcChan          chan os.Signal
	ioFlagAtomic    uint32 // used with atomic
	winSizes        struct {
		thisW   uint16
		thisH   uint16
		remoteW uint16
		remoteH uint16
	}
	winSizesMutex    sync.Mutex
	tunnelMuxSession *yamux.Session
}

func newTtyShareClient(url string, detachKeys string, tunnelConfig *string) *ttyShareClient {
	return &ttyShareClient{
		url:             url,
		ttyWsConn:       nil,
		detachKeys:      detachKeys,
		wcChan:          make(chan os.Signal, 1),
		ioFlagAtomic:    1,
		tunnelAddresses: tunnelConfig,
	}
}

func clearScreen() {
	fmt.Fprintf(os.Stdout, "\033[H\033[2J")
}

type keyListener struct {
	wrappedReader io.Reader
	ioFlagAtomicP *uint32
}

func (kl *keyListener) Read(data []byte) (n int, err error) {
	n, err = kl.wrappedReader.Read(data)
	if _, ok := err.(term.EscapeError); ok {
		log.Debug("Escape code detected.")
	}

	// If we are not supposed to do any IO, then return 0 bytes read. This happens the local
	// window is smaller than the remote one
	if atomic.LoadUint32(kl.ioFlagAtomicP) == 0 {
		return 0, err
	}

	return
}

func (c *ttyShareClient) updateAndDecideStdoutMuted() {
	log.Infof("This window: %dx%d. Remote window: %dx%d", c.winSizes.thisW, c.winSizes.thisH, c.winSizes.remoteW, c.winSizes.remoteH)

	if c.winSizes.thisH < c.winSizes.remoteH || c.winSizes.thisW < c.winSizes.remoteW {
		atomic.StoreUint32(&c.ioFlagAtomic, 0)
		clearScreen()
		messageFormat := "\n\rYour window is smaller than the remote window. Please resize or press <C-o C-c> to detach.\n\r\tRemote window: %dx%d \n\r\tYour window:   %dx%d \n\r"
		fmt.Printf(messageFormat, c.winSizes.remoteW, c.winSizes.remoteH, c.winSizes.thisW, c.winSizes.thisH)
	} else {
		if atomic.LoadUint32(&c.ioFlagAtomic) == 0 { // clear the screen when changing back to "write"
			// TODO: notify the remote side to "refresh" the content.
			clearScreen()
		}
		atomic.StoreUint32(&c.ioFlagAtomic, 1)
	}
}

func (c *ttyShareClient) updateThisWinSize() {
	size, err := term.GetWinsize(os.Stdin.Fd())
	if err == nil {
		c.winSizesMutex.Lock()
		c.winSizes.thisW = size.Width
		c.winSizes.thisH = size.Height
		c.winSizesMutex.Unlock()
	}
}

func (c *ttyShareClient) Run() (err error) {
	log.Debugf("Connecting as a client to %s ..", c.url)

	resp, err := http.Get(c.url)

	if err != nil {
		return
	}

	// Get the path of the websockts route from the header
	ttyWsPath := resp.Header.Get("TTYSHARE-TTY-WSPATH")
	ttyWSProtocol := resp.Header.Get("TTYSHARE-VERSION")

	ttyTunnelPath := resp.Header.Get("TTYSHARE-TUNNEL-WSPATH")

	// Build the WS URL from the host part of the given http URL and the wsPath
	httpURL, err := url.Parse(c.url)
	if err != nil {
		return
	}
	wsScheme := "ws"
	if httpURL.Scheme == "https" {
		wsScheme = "wss"
	}
	ttyWsURL := wsScheme + "://" + httpURL.Host + ttyWsPath
	ttyTunnelURL := wsScheme + "://" + httpURL.Host + ttyTunnelPath

	log.Debugf("Built the WS URL from the headers: %s", ttyWsURL)

	c.ttyWsConn, _, err = websocket.DefaultDialer.Dial(ttyWsURL, nil)
	if err != nil {
		return
	}
	defer c.ttyWsConn.Close()

	tunnelFunc := func() {
		if *c.tunnelAddresses == "" {
			// Don't build a tunnel
			return
		}

		if ver, err := strconv.Atoi(ttyWSProtocol); err != nil || ver < 2 {
			log.Fatalf("Cannot create a tunnel. Server too old (protocol %d, required min. 2)", ver)
		}

		c.tunnelWsConn, _, err = websocket.DefaultDialer.Dial(ttyTunnelURL, nil)
		if err != nil {
			log.Errorf("Cannot create a tunnel connection with the server. Server needs to allow that")
			return
		}
		defer c.tunnelWsConn.Close()

		a := strings.Split(*c.tunnelAddresses, ":")
		tunnelRemoteAddress := fmt.Sprintf("%s:%s", a[1], a[2])
		tunnelLocalAddress := fmt.Sprintf(":%s", a[0])

		initMsg := server.TunInitMsg{
			Address: tunnelRemoteAddress,
		}

		data, err := json.Marshal(initMsg)
		if err != nil {
			log.Errorf("Could not marshal the tunnel init message: %s", err.Error())
			return
		}

		err = c.tunnelWsConn.WriteMessage(websocket.TextMessage, data)

		if err != nil {
			log.Errorf("Could not initiate the tunnel: %s", err.Error())
			return
		}

		wsWRC := server.WSConnReadWriteCloser{
			WsConn: c.tunnelWsConn,
		}

		localListener, err := net.Listen("tcp", tunnelLocalAddress)
		if err != nil {
			log.Errorf("Could not listen locally for the tunnel: %s", err.Error())

		}

		c.tunnelMuxSession, err = yamux.Server(&wsWRC, nil)
		if err != nil {
			log.Errorf("Could not create mux server: %s", err.Error())
		}

		for {
			localTunconn, err := localListener.Accept()

			if err != nil {
				log.Warnf("Cannot accept local tunnel connections: ", err.Error())
				return
			}

			muxClient, err := c.tunnelMuxSession.Open()
			if err != nil {
				log.Warnf("Cannot create a muxer to the remote, over ws: ", err.Error())
				return
			}

			go func() {
				io.Copy(muxClient, localTunconn)
				defer localTunconn.Close()
				defer muxClient.Close()
			}()

			go func() {
				io.Copy(localTunconn, muxClient)
				defer localTunconn.Close()
				defer muxClient.Close()
			}()

		}
	}

	detachBytes, err := term.ToBytes(c.detachKeys)
	if err != nil {
		log.Errorf("Invalid dettaching keys: %s", c.detachKeys)
		return
	}

	state, err := term.MakeRaw(os.Stdin.Fd())
	defer term.RestoreTerminal(os.Stdin.Fd(), state)
	clearScreen()

	protoWS := server.NewTTYProtocolWSLocked(c.ttyWsConn)

	monitorWinChanges := func() {
		// start monitoring the size of the terminal
		signal.Notify(c.wcChan, syscall.SIGWINCH)

		for {
			select {
			case <-c.wcChan:
				c.updateThisWinSize()
				c.updateAndDecideStdoutMuted()
				protoWS.SetWinSize(int(c.winSizes.thisW), int(c.winSizes.thisH))
			}
		}
	}

	readLoop := func() {

		var err error
		for {
			err = protoWS.ReadAndHandle(
				// onWrite
				func(data []byte) {
					if atomic.LoadUint32(&c.ioFlagAtomic) != 0 {
						os.Stdout.Write(data)
					}
				},
				// onWindowSize
				func(cols, rows int) {
					c.winSizesMutex.Lock()
					c.winSizes.remoteW = uint16(cols)
					c.winSizes.remoteH = uint16(rows)
					c.winSizesMutex.Unlock()
					c.updateThisWinSize()
					c.updateAndDecideStdoutMuted()
				},
			)

			if err != nil {
				log.Errorf("Error parsing remote message: %s", err.Error())
				if err == io.EOF {
					// Remote WS connection closed
					return
				}
			}
		}
	}

	writeLoop := func() {
		kl := &keyListener{
			wrappedReader: term.NewEscapeProxy(os.Stdin, detachBytes),
			ioFlagAtomicP: &c.ioFlagAtomic,
		}
		_, err := io.Copy(protoWS, kl)

		if err != nil {
			log.Debugf("Connection closed: %s", err.Error())
			c.Stop()
			return
		}
	}

	go monitorWinChanges()
	go writeLoop()
	go tunnelFunc()
	readLoop()

	clearScreen()
	return
}

func (c *ttyShareClient) Stop() {
	// if we had a tunnel, close it
	if c.tunnelMuxSession != nil {
		c.tunnelMuxSession.Close()
		c.tunnelWsConn.Close()
	}
	c.ttyWsConn.Close()
	signal.Stop(c.wcChan)
}
