//Package mcping facilitates the pinging of Minecraft servers using the 1.7+ protocol.
package mcping

import (
	"bufio"
	"bytes"
	"encoding/binary"
	"encoding/json"
	"github.com/jmoiron/jsonq"
	"net"
	"strconv"
	"strings"
	"time"
)

const (
	//DefaultTimeout stores default ping timeout
	DefaultTimeout = time.Second
)

//Ping pings with default timeout
func Ping(addr string) (PingResponse, error) {
	return ping(addr, DefaultTimeout)
}

//PingWithTimeout pings with custom timeout
func PingWithTimeout(addr string, timeout time.Duration) (PingResponse, error) {
	return ping(addr, timeout)
}

//Deprecated: PingTimeout Pings with custom timeout
func PingTimeout(addr string, timeout int) (PingResponse, error) {
	return ping(addr, time.Millisecond * time.Duration(timeout))
}

func ping(addr string, timeout time.Duration) (PingResponse, error) {
	var host string
	var port uint16
	var resp PingResponse

	//Start timer
	timer := pingTimer{}
	timer.Start()

	//Connect
	conn, err := net.DialTimeout("tcp", addr, timeout)
	if err != nil {
		return resp, err
	}
	defer conn.Close()


	connReader := bufio.NewReader(conn)

	var dataBuf bytes.Buffer

	var finBuf bytes.Buffer

	dataBuf.Write([]byte("\x00")) //Packet ID
	dataBuf.Write([]byte("\x6D")) //1.9 protocol

	if addrTokens := strings.Split(addr, ":"); len(addrTokens) == 2 {
		host = addrTokens[0]
		if intport, err := strconv.Atoi(addrTokens[1]); err == nil {
			port = uint16(intport)
		} else {
			return resp, err
		}
	} else {
		return resp, ErrAddress
	}

	//Write host string length + host
	hostLength := uint8(len(host))
	dataBuf.Write([]uint8{hostLength})
	dataBuf.Write([]byte(host))

	//Write port
	b := make([]byte, 2)
	binary.BigEndian.PutUint16(b, port)
	dataBuf.Write(b)

	//Next state ping
	dataBuf.Write([]byte("\x01"))

	//Prepend packet length with data
	packetLength := []byte{uint8(dataBuf.Len())}
	finBuf.Write(append(packetLength, dataBuf.Bytes()...))

	conn.Write(finBuf.Bytes())     //Sending handshake
	conn.Write([]byte("\x01\x00")) //Status ping

	//Get situationally useless full byte length
	binary.ReadUvarint(connReader)

	//Packet type 0 means we're good to receive ping
	packetType, _ := connReader.ReadByte()
	if bytes.Compare([]byte{packetType}, []byte("\x00")) != 0 {
		return resp, ErrPacketType
	}

	//Get data length via Varint
	length, err := binary.ReadUvarint(connReader)
	if err != nil {
		return resp, err
	}
	if length < 10 {
		return resp, ErrSmallPacket
	} else if length > 700000 {
		return resp, ErrBigPacket
	}

	//Recieve json buffer
	bytesRecieved := uint64(0)
	recBytes := make([]byte, length)
	for bytesRecieved < length {
		n, _ := connReader.Read(recBytes[bytesRecieved:length])
		bytesRecieved = bytesRecieved + uint64(n)
	}

	//Stop Timer, collect latency
	latency := timer.End()

	pingString := string(recBytes)

	//Convert buffer into jsonq instance
	pingData := map[string]interface{}{}
	dec := json.NewDecoder(strings.NewReader(pingString))
	dec.Decode(&pingData)
	jq := jsonq.NewQuery(pingData)

	//Assemble PlayerSample
	playerSampleMap, err := jq.ArrayOfObjects("players", "sample")
	playerSamples := []PlayerSample{}
	for k := range playerSampleMap {
		sample := PlayerSample{}
		sample.UUID = playerSampleMap[k]["id"].(string)
		sample.Name = playerSampleMap[k]["name"].(string)
		playerSamples = append(playerSamples, sample)
	}

	//Assemble PingResponse
	resp.Latency = uint(latency)
	resp.Online, _ = jq.Int("players", "online")
	resp.Max, _ = jq.Int("players", "max")
	resp.Protocol, _ = jq.Int("version", "protocol")

	favicon, _ := jq.String("favicon")
	resp.Favicon = []byte(favicon)
	
	resp.Motd, _ = jq.String("description")
	versionStr, _ := jq.String("version", "name")
	arr := strings.Split(versionStr, " ")
	if len(arr) == 0 {
		resp.Server = "Unknown"
		resp.Version = "Unknown"
	} else if len(arr) == 1 {
		resp.Server = "Unknown"
		resp.Version = arr[0]
	} else if len(arr) == 2 {
		resp.Server = arr[0]
		resp.Version = arr[1]
	}
	resp.Sample = playerSamples

	return resp, nil
}
