package geojson

import (
	"bytes"
	"encoding/binary"
)

////////////////////////////////
// level 1
////////////////////////////////

func fillLevel1Map(m map[string]interface{}) (
	coordinates Position, bbox *BBox, bytesOut []byte, err error,
) {
	switch v := m["coordinates"].(type) {
	default:
		err = errInvalidCoordinates
		return
	case nil:
		err = errCoordinatesRequired
		return
	case []interface{}:
		coordinates, err = fillPosition(v)
	}
	if err == nil {
		bbox, err = fillBBox(m)
	}
	return
}

func fillLevel1Bytes(b []byte, bbox *BBox, isCordZ bool) (
	coordinates Position, bboxOut *BBox, bytesOut []byte, err error,
) {
	bboxOut = bbox
	coordinates, bytesOut, err = fillPositionBytes(b, isCordZ)
	return
}

func level1CalculatedBBox(coordinates Position, bbox *BBox) BBox {
	if bbox != nil {
		return *bbox
	}
	return BBox{
		Min: coordinates,
		Max: coordinates,
	}
}

func level1PositionCount(coordinates Position, bbox *BBox) int {
	if bbox != nil {
		return 3
	}
	return 1
}

func level1Weight(coordinates Position, bbox *BBox) int {
	return level1PositionCount(coordinates, bbox) * sizeofPosition
}

func level1JSON(name string, coordinates Position, bbox *BBox) string {
	isCordZ := level1IsCoordZDefined(coordinates, bbox)
	var buf bytes.Buffer
	buf.WriteString(`{"type":"`)
	buf.WriteString(name)
	buf.WriteString(`","coordinates":[`)
	coordinates.writeJSON(&buf, isCordZ)
	buf.WriteByte(']')
	bbox.write(&buf)
	buf.WriteByte('}')
	return buf.String()
}

func level1IsCoordZDefined(coordinates Position, bbox *BBox) bool {
	if bbox.isCordZDefined() {
		return true
	}
	return coordinates.Z != nilz
}

func level1Bytes(objType byte, coordinates Position, bbox *BBox) []byte {
	var buf bytes.Buffer
	isCordZ := level1IsCoordZDefined(coordinates, bbox)
	writeHeader(&buf, objType, bbox, isCordZ)
	coordinates.writeBytes(&buf, isCordZ)
	return buf.Bytes()
}

////////////////////////////////
// level 2
////////////////////////////////

func fillLevel2Map(m map[string]interface{}) (
	coordinates []Position, bbox *BBox, bytesOut []byte, err error,
) {
	switch v := m["coordinates"].(type) {
	default:
		err = errInvalidCoordinates
		return
	case nil:
		err = errCoordinatesRequired
		return
	case []interface{}:
		coordinates = make([]Position, len(v))
		for i, v := range v {
			v, ok := v.([]interface{})
			if !ok {
				err = errCoordinatesMustBeArray
				return
			}
			var p Position
			p, err = fillPosition(v)
			if err != nil {
				return
			}
			coordinates[i] = p
		}
	}
	bbox, err = fillBBox(m)
	return
}

func fillLevel2Bytes(b []byte, bbox *BBox, isCordZ bool) (
	coordinates []Position, bboxOut *BBox, bytesOut []byte, err error,
) {
	bboxOut = bbox
	if len(b) < 4 {
		err = errNotEnoughData
		return
	}
	coordinates = make([]Position, int(binary.LittleEndian.Uint32(b)))
	b = b[4:]
	for i := 0; i < len(coordinates); i++ {
		coordinates[i], b, err = fillPositionBytes(b, isCordZ)
		if err != nil {
			return
		}
	}
	bytesOut = b
	return
}

func level2CalculatedBBox(coordinates []Position, bbox *BBox) BBox {
	if bbox != nil {
		return *bbox
	}
	_, bbox2 := positionBBox(0, BBox{}, coordinates)
	return bbox2
}

func level2PositionCount(coordinates []Position, bbox *BBox) int {
	if bbox != nil {
		return 2 + len(coordinates)
	}
	return len(coordinates)
}

func level2Weight(coordinates []Position, bbox *BBox) int {
	return level2PositionCount(coordinates, bbox) * sizeofPosition
}

func level2JSON(name string, coordinates []Position, bbox *BBox) string {
	isCordZ := level2IsCoordZDefined(coordinates, bbox)
	var buf bytes.Buffer
	buf.WriteString(`{"type":"`)
	buf.WriteString(name)
	buf.WriteString(`","coordinates":[`)
	for i, p := range coordinates {
		if i > 0 {
			buf.WriteByte(',')
		}
		buf.WriteByte('[')
		p.writeJSON(&buf, isCordZ)
		buf.WriteByte(']')
	}
	buf.WriteByte(']')
	bbox.write(&buf)
	buf.WriteByte('}')
	return buf.String()
}

func level2IsCoordZDefined(coordinates []Position, bbox *BBox) bool {
	if bbox.isCordZDefined() {
		return true
	}
	for _, p := range coordinates {
		if p.Z != nilz {
			return true
		}
	}
	return false
}

func level2Bytes(objType byte, coordinates []Position, bbox *BBox) []byte {
	var buf bytes.Buffer
	isCordZ := level2IsCoordZDefined(coordinates, bbox)
	writeHeader(&buf, objType, bbox, isCordZ)
	b := make([]byte, 4)
	binary.LittleEndian.PutUint32(b, uint32(len(coordinates)))
	buf.Write(b)
	for _, p := range coordinates {
		p.writeBytes(&buf, isCordZ)
	}
	return buf.Bytes()
}

////////////////////////////////
// level 3
////////////////////////////////

func fillLevel3Map(m map[string]interface{}) (
	coordinates [][]Position, bbox *BBox, bytesOut []byte, err error,
) {
	switch v := m["coordinates"].(type) {
	default:
		err = errInvalidCoordinates
		return
	case nil:
		err = errCoordinatesRequired
		return
	case []interface{}:
		coordinates = make([][]Position, len(v))
		for i, v := range v {
			v, ok := v.([]interface{})
			if !ok {
				err = errInvalidCoordinatesValue
				return
			}
			ps := make([]Position, len(v))
			for i, v := range v {
				v, ok := v.([]interface{})
				if !ok {
					err = errInvalidCoordinatesValue
					return
				}
				var p Position
				p, err = fillPosition(v)
				if err != nil {
					return
				}
				ps[i] = p
			}
			coordinates[i] = ps
		}
	}
	bbox, err = fillBBox(m)
	return
}

func fillLevel3Bytes(b []byte, bbox *BBox, isCordZ bool) (
	coordinates [][]Position, bboxOut *BBox, bytesOut []byte, err error,
) {
	bboxOut = bbox
	if len(b) < 4 {
		err = errNotEnoughData
		return
	}
	coordinates = make([][]Position, int(binary.LittleEndian.Uint32(b)))
	b = b[4:]
	for i := 0; i < len(coordinates); i++ {
		if len(b) < 4 {
			err = errNotEnoughData
			return
		}
		ps := make([]Position, int(binary.LittleEndian.Uint32(b)))
		b = b[4:]
		for j := 0; j < len(ps); j++ {
			ps[j], b, err = fillPositionBytes(b, isCordZ)
			if err != nil {
				return
			}
		}
		coordinates[i] = ps
	}
	bytesOut = b
	return
}

func level3CalculatedBBox(coordinates [][]Position, bbox *BBox, isPolygon bool) BBox {
	if bbox != nil {
		return *bbox
	}
	var bbox2 BBox
	var i = 0
	for _, ps := range coordinates {
		i, bbox2 = positionBBox(i, bbox2, ps)
		if isPolygon {
			break // only the exterior ring should be calculated for a polygon
		}
	}
	return bbox2
}

func level3Weight(coordinates [][]Position, bbox *BBox) int {
	return level3PositionCount(coordinates, bbox) * sizeofPosition
}

func level3PositionCount(coordinates [][]Position, bbox *BBox) int {
	var res int
	for _, p := range coordinates {
		res += len(p)
	}
	if bbox != nil {
		return 2 + res
	}
	return res
}

func level3JSON(name string, coordinates [][]Position, bbox *BBox) string {
	isCordZ := level3IsCoordZDefined(coordinates, bbox)
	var buf bytes.Buffer
	buf.WriteString(`{"type":"`)
	buf.WriteString(name)
	buf.WriteString(`","coordinates":[`)
	for i, p := range coordinates {
		if i > 0 {
			buf.WriteByte(',')
		}
		buf.WriteByte('[')
		for i, p := range p {
			if i > 0 {
				buf.WriteByte(',')
			}
			buf.WriteByte('[')
			p.writeJSON(&buf, isCordZ)
			buf.WriteByte(']')
		}
		buf.WriteByte(']')
	}
	buf.WriteByte(']')
	bbox.write(&buf)
	buf.WriteByte('}')
	return buf.String()
}

func level3IsCoordZDefined(coordinates [][]Position, bbox *BBox) bool {
	if bbox.isCordZDefined() {
		return true
	}
	for _, p := range coordinates {
		for _, p := range p {
			if p.Z != nilz {
				return true
			}
		}
	}
	return false
}

func level3Bytes(objType byte, coordinates [][]Position, bbox *BBox) []byte {
	var buf bytes.Buffer
	isCordZ := level3IsCoordZDefined(coordinates, bbox)
	writeHeader(&buf, objType, bbox, isCordZ)
	b := make([]byte, 4)
	binary.LittleEndian.PutUint32(b, uint32(len(coordinates)))
	buf.Write(b)
	for _, p := range coordinates {
		binary.LittleEndian.PutUint32(b, uint32(len(p)))
		buf.Write(b)
		for _, p := range p {
			p.writeBytes(&buf, isCordZ)
		}
	}
	return buf.Bytes()
}

////////////////////////////////
// level 4
////////////////////////////////

func fillLevel4Map(m map[string]interface{}) (
	coordinates [][][]Position, bbox *BBox, bytesOut []byte, err error,
) {
	switch v := m["coordinates"].(type) {
	default:
		err = errInvalidCoordinates
		return
	case nil:
		err = errCoordinatesRequired
		return
	case []interface{}:
		coordinates = make([][][]Position, len(v))
		for i, v := range v {
			v, ok := v.([]interface{})
			if !ok {
				err = errInvalidCoordinatesValue
				return
			}
			ps := make([][]Position, len(v))
			for i, v := range v {
				v, ok := v.([]interface{})
				if !ok {
					err = errInvalidCoordinatesValue
					return
				}
				pss := make([]Position, len(v))
				for i, v := range v {
					v, ok := v.([]interface{})
					if !ok {
						err = errInvalidCoordinatesValue
						return
					}
					var p Position
					p, err = fillPosition(v)
					if err != nil {
						return
					}
					pss[i] = p
				}
				ps[i] = pss
			}
			coordinates[i] = ps
		}
	}
	bbox, err = fillBBox(m)
	return
}

func fillLevel4Bytes(b []byte, bbox *BBox, isCordZ bool) (
	coordinates [][][]Position, bboxOut *BBox, bytesOut []byte, err error,
) {
	bboxOut = bbox
	if len(b) < 4 {
		err = errNotEnoughData
		return
	}
	coordinates = make([][][]Position, int(binary.LittleEndian.Uint32(b)))
	b = b[4:]
	for i := 0; i < len(coordinates); i++ {
		if len(b) < 4 {
			err = errNotEnoughData
			return
		}
		ps := make([][]Position, int(binary.LittleEndian.Uint32(b)))
		b = b[4:]
		for i := 0; i < len(ps); i++ {
			if len(b) < 4 {
				err = errNotEnoughData
				return
			}
			pss := make([]Position, int(binary.LittleEndian.Uint32(b)))
			b = b[4:]
			for i := 0; i < len(pss); i++ {
				pss[i], b, err = fillPositionBytes(b, isCordZ)
				if err != nil {
					return
				}
			}
			ps[i] = pss
		}
		coordinates[i] = ps
	}
	bytesOut = b
	return
}

func level4CalculatedBBox(coordinates [][][]Position, bbox *BBox) BBox {
	if bbox != nil {
		return *bbox
	}
	var bbox2 BBox
	var i = 0
	for _, ps := range coordinates {
		for _, ps := range ps {
			i, bbox2 = positionBBox(i, bbox2, ps)
		}
	}
	return bbox2
}

func level4Weight(coordinates [][][]Position, bbox *BBox) int {
	return level4PositionCount(coordinates, bbox) * sizeofPosition
}

func level4PositionCount(coordinates [][][]Position, bbox *BBox) int {
	var res int
	for _, p := range coordinates {
		for _, p := range p {
			res += len(p)
		}
	}
	if bbox != nil {
		return 2 + res
	}
	return res
}

func level4JSON(name string, coordinates [][][]Position, bbox *BBox) string {
	isCordZ := level4IsCoordZDefined(coordinates, bbox)
	var buf bytes.Buffer
	buf.WriteString(`{"type":"`)
	buf.WriteString(name)
	buf.WriteString(`","coordinates":[`)
	for i, p := range coordinates {
		if i > 0 {
			buf.WriteByte(',')
		}
		buf.WriteByte('[')
		for i, p := range p {
			if i > 0 {
				buf.WriteByte(',')
			}
			buf.WriteByte('[')
			for i, p := range p {
				if i > 0 {
					buf.WriteByte(',')
				}
				buf.WriteByte('[')
				p.writeJSON(&buf, isCordZ)
				buf.WriteByte(']')
			}
			buf.WriteByte(']')
		}
		buf.WriteByte(']')
	}
	buf.WriteByte(']')
	bbox.write(&buf)
	buf.WriteByte('}')
	return buf.String()
}

func level4IsCoordZDefined(coordinates [][][]Position, bbox *BBox) bool {
	if bbox.isCordZDefined() {
		return true
	}
	for _, p := range coordinates {
		for _, p := range p {
			for _, p := range p {
				if p.Z != nilz {
					return true
				}
			}
		}
	}
	return false
}

func level4Bytes(objType byte, coordinates [][][]Position, bbox *BBox) []byte {
	var buf bytes.Buffer
	isCordZ := level4IsCoordZDefined(coordinates, bbox)
	writeHeader(&buf, objType, bbox, isCordZ)
	b := make([]byte, 4)
	binary.LittleEndian.PutUint32(b, uint32(len(coordinates)))
	buf.Write(b)
	for _, p := range coordinates {
		binary.LittleEndian.PutUint32(b, uint32(len(p)))
		buf.Write(b)
		for _, p := range p {
			binary.LittleEndian.PutUint32(b, uint32(len(p)))
			buf.Write(b)
			for _, p := range p {
				p.writeBytes(&buf, isCordZ)
			}
		}
	}
	return buf.Bytes()
}