// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package server

import (
	"encoding/base64"
	"fmt"
	"net"
	"net/http"
	"strconv"

	"github.com/gorilla/websocket"

	"yunion.io/x/jsonutils"
	"yunion.io/x/log"

	"yunion.io/x/onecloud/pkg/webconsole/session"
)

const (
	BINARY_PROTOL = "binary"
	BASE64_PROTOL = "base64"
)

var upgrader = websocket.Upgrader{
	ReadBufferSize:  1024,
	WriteBufferSize: 1024,
	CheckOrigin: func(r *http.Request) bool {
		return true
	},
	Subprotocols: []string{BINARY_PROTOL, BASE64_PROTOL},
}

type WebsockifyServer struct {
	Session    *session.SSession
	TargetHost string
	TargetPort int64
}

func NewWebsockifyServer(s *session.SSession) (*WebsockifyServer, error) {
	info := s.ISessionData.(*session.RemoteConsoleInfo)
	if info.Host == "" {
		return nil, fmt.Errorf("Empty remote host")
	}
	if info.Port <= 0 {
		return nil, fmt.Errorf("Invalid remote port: %d", info.Port)
	}

	server := &WebsockifyServer{
		Session:    s,
		TargetHost: info.Host,
		TargetPort: info.Port,
	}
	return server, nil
}

func (s *WebsockifyServer) isBase64Subprotocol(wsConn *websocket.Conn) bool {
	return wsConn.Subprotocol() == BASE64_PROTOL
}

func (s *WebsockifyServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	log.Debugf("ServeHTTP: %s, %s", r.URL.String(), jsonutils.Marshal(r.Header))
	wsConn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Errorf("New websocket connection error: %v", err)
		return
	}
	log.Debugf("Get coordinate subprotocol: %s", wsConn.Subprotocol())

	targetAddr := net.JoinHostPort(s.TargetHost, strconv.Itoa(int(s.TargetPort)))
	log.Debugf("Handle websocket connect, target: %s", targetAddr)
	targetConn, err := net.Dial("tcp", targetAddr)
	if err != nil {
		log.Errorf("Connection to target %s error: %v", targetConn, err)
		wsConn.Close()
		return
	}

	s.doProxy(wsConn, targetConn)
}

func (s *WebsockifyServer) doProxy(wsConn *websocket.Conn, tcpConn net.Conn) {
	log.Infof("doProxy bewteen ws: %s <--> tcp: %s", wsConn.RemoteAddr(), tcpConn.RemoteAddr())
	s.Session.RegisterDuplicateHook(func() {
		wsConn.Close()
		tcpConn.Close()
	})
	go s.wsToTcp(wsConn, tcpConn)
	s.tcpToWs(wsConn, tcpConn)
}

func (s *WebsockifyServer) ReadFromWs(wsConn *websocket.Conn) ([]byte, error) {
	_, data, err := wsConn.ReadMessage()
	if err != nil {
		return nil, err
	}
	if s.isBase64Subprotocol(wsConn) {
		data, err = base64.StdEncoding.DecodeString(string(data))
		if err != nil {
			return nil, err
		}
	}

	return data, nil
}

func (s *WebsockifyServer) wsToTcp(wsConn *websocket.Conn, tcpConn net.Conn) {
	defer s.onExit(wsConn, tcpConn)

	for {
		data, err := s.ReadFromWs(wsConn)
		if err != nil {
			log.Errorf("Read from websocket error: %v", err)
			return
		}

		_, err = tcpConn.Write(data)
		if err != nil {
			log.Errorf("Write to tcp socket error: %v", err)
			return
		}
	}
}

func (s *WebsockifyServer) WriteToWs(wsConn *websocket.Conn, data []byte) error {
	msg := string(data)
	msgType := websocket.BinaryMessage
	if s.isBase64Subprotocol(wsConn) {
		msg = base64.StdEncoding.EncodeToString(data)
		msgType = websocket.TextMessage
	}
	return wsConn.WriteMessage(msgType, []byte(msg))
}

func (s *WebsockifyServer) tcpToWs(wsConn *websocket.Conn, tcpConn net.Conn) {
	defer s.onExit(wsConn, tcpConn)

	buffer := make([]byte, 1024)
	for {
		n, err := tcpConn.Read(buffer)
		if err != nil {
			log.Errorf("Read from tcp socket error: %v", err)
			return
		}

		err = s.WriteToWs(wsConn, buffer[0:n])
		if err != nil {
			log.Errorf("Write to websocket error: %v", err)
			return
		}
	}
}

func (s *WebsockifyServer) onExit(wsConn *websocket.Conn, tcpConn net.Conn) {
	wsConn.Close()
	tcpConn.Close()
	s.Session.Close()
}
