package geojson

import (
	"bytes"
	"encoding/binary"
	"math"
	"strconv"
	"unsafe"

	"github.com/tidwall/tile38/geojson/geo"
	"github.com/tidwall/tile38/geojson/poly"
)

const sizeofPosition = 24 // (X,Y,Z) * 8

// Position is a simple point
type Position poly.Point

func pointPositions(positions []Position) []poly.Point {
	return *(*[]poly.Point)(unsafe.Pointer(&positions))
}
func polyPositions(positions []Position) poly.Polygon {
	return *(*poly.Polygon)(unsafe.Pointer(&positions))
}
func polyMultiPositions(positions [][]Position) []poly.Polygon {
	return *(*[]poly.Polygon)(unsafe.Pointer(&positions))
}
func polyExteriorHoles(positions [][]Position) (exterior poly.Polygon, holes []poly.Polygon) {
	switch len(positions) {
	case 0:
	case 1:
		exterior = polyPositions(positions[0])
	default:
		exterior = polyPositions(positions[0])
		holes = polyMultiPositions(positions[1:])
	}
	return
}

func (p Position) writeJSON(buf *bytes.Buffer, isCordZ bool) {
	buf.WriteString(strconv.FormatFloat(p.X, 'f', -1, 64))
	buf.WriteByte(',')
	buf.WriteString(strconv.FormatFloat(p.Y, 'f', -1, 64))
	if isCordZ {
		buf.WriteByte(',')
		buf.WriteString(strconv.FormatFloat(p.Z, 'f', -1, 64))
	}
}

func (p Position) writeBytes(buf *bytes.Buffer, isCordZ bool) {
	b := make([]byte, 8)
	binary.LittleEndian.PutUint64(b, math.Float64bits(p.X))
	buf.Write(b)
	binary.LittleEndian.PutUint64(b, math.Float64bits(p.Y))
	buf.Write(b)
	if isCordZ {
		binary.LittleEndian.PutUint64(b, math.Float64bits(p.Z))
		buf.Write(b)
	}
}

const earthRadius = 6371e3

func toRadians(deg float64) float64 { return deg * math.Pi / 180 }
func toDegrees(rad float64) float64 { return rad * 180 / math.Pi }

// DistanceTo calculates the distance to a position
func (p Position) DistanceTo(position Position) float64 {
	return geo.DistanceTo(p.Y, p.X, position.Y, position.X)
}

// Destination calculates a new position based on the distance and bearing.
func (p Position) Destination(meters, bearingDegrees float64) Position {
	lat, lon := geo.DestinationPoint(p.Y, p.X, meters, bearingDegrees)
	return Position{X: lon, Y: lat, Z: 0}
}

func fillPosition(v []interface{}) (Position, error) {
	var p Position
	var ok bool
	switch len(v) {
	case 0:
		return p, errInvalidNumberOfPositionValues
	case 1:
		if _, ok := v[0].(float64); !ok {
			return p, errInvalidPositionValue
		}
		return p, errInvalidNumberOfPositionValues
	}
	p.Z = nilz
	if p.X, ok = v[0].(float64); !ok {
		return p, errInvalidPositionValue
	}
	if p.Y, ok = v[1].(float64); !ok {
		return p, errInvalidPositionValue
	}
	if len(v) > 2 {
		if p.Z, ok = v[2].(float64); !ok {
			return p, errInvalidPositionValue
		}
	}
	return p, nil
}

func fillPositionBytes(b []byte, isCordZ bool) (Position, []byte, error) {
	var p Position
	if len(b) < 8 {
		return p, b, errNotEnoughData
	}
	p.X = math.Float64frombits(binary.LittleEndian.Uint64(b))
	b = b[8:]
	if len(b) < 8 {
		return p, b, errNotEnoughData
	}
	p.Y = math.Float64frombits(binary.LittleEndian.Uint64(b))
	b = b[8:]
	if isCordZ {
		if len(b) < 8 {
			return p, b, errNotEnoughData
		}
		p.Z = math.Float64frombits(binary.LittleEndian.Uint64(b))
		b = b[8:]
	} else {
		p.Z = nilz
	}
	return p, b, nil
}

// ExternalJSON is the simple json representation of the position used for external applications.
func (p Position) ExternalJSON() string {
	if p.Z != 0 {
		return `{"lat":` + strconv.FormatFloat(p.Y, 'f', -1, 64) + `,"lon":` + strconv.FormatFloat(p.X, 'f', -1, 64) + `,"z":` + strconv.FormatFloat(p.Z, 'f', -1, 64) + `}`
	} else {
		return `{"lat":` + strconv.FormatFloat(p.Y, 'f', -1, 64) + `,"lon":` + strconv.FormatFloat(p.X, 'f', -1, 64) + `}`
	}
}