// Copyright 2017 Google Inc. All rights reserved.
//
// 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 s2

// s2textformat_test contains a collection of functions for converting geometry
// to and from a human-readable format. It is intended for testing and debugging.
// Be aware that the human-readable format is *NOT* designed to preserve the full
// precision of the original object, so it should not be used for data storage.
//
// Most functions here use the same format for inputs, a comma separated set of
// latitude-longitude coordinates in degrees. Functions that expect a different
// input document the values in the function comment.
//
// Examples of the input format:
//     ""                                 // no points
//     "-20:150"                          // one point
//     "-20:150, 10:-120, 0.123:-170.652" // three points

import (
	"bytes"
	"fmt"
	"io"
	"strconv"
	"strings"

	"github.com/golang/geo/r3"
)

// writePoint formats the point and writes it to the given writer.
//
// If roundtripPrecision is true, the coordinates are formatted using
// enough precision to exactly preserve the floating point values.
func writePoint(w io.Writer, p Point, roundtripPrecision bool) {
	ll := LatLngFromPoint(p)
	if roundtripPrecision {
		fmt.Fprintf(w, "%.17g:%.17g", ll.Lat.Degrees(), ll.Lng.Degrees())
	} else {
		fmt.Fprintf(w, "%.15g:%.15g", ll.Lat.Degrees(), ll.Lng.Degrees())
	}
}

// writePoints formats the given points in debug format and writes them to the given writer.
func writePoints(w io.Writer, pts []Point, roundtripPrecision bool) {
	for i, pt := range pts {
		if i > 0 {
			fmt.Fprint(w, ", ")
		}
		writePoint(w, pt, roundtripPrecision)
	}
}

// parsePoint returns a Point from the given string, or the origin if the
// string was invalid. If more than one value is given, only the first is used.
func parsePoint(s string) Point {
	p := parsePoints(s)
	if len(p) > 0 {
		return p[0]
	}

	return Point{r3.Vector{X: 0, Y: 0, Z: 0}}
}

// pointToString returns a string representation suitable for reconstruction
// by the parsePoint method.
func pointToString(point Point, roundtripPrecision bool) string {
	var buf bytes.Buffer
	writePoint(&buf, point, roundtripPrecision)
	return buf.String()
}

// parsePoints returns the values in the input string as Points.
func parsePoints(s string) []Point {
	lls := parseLatLngs(s)
	if len(lls) == 0 {
		return nil
	}
	points := make([]Point, len(lls))
	for i, ll := range lls {
		points[i] = PointFromLatLng(ll)
	}
	return points
}

// pointsToString returns a string representation suitable for reconstruction
// by the parsePoints method.
func pointsToString(points []Point, roundtripPrecision bool) string {
	var buf bytes.Buffer
	writePoints(&buf, points, roundtripPrecision)
	return buf.String()
}

// parseLatLngs returns the values in the input string as LatLngs.
func parseLatLngs(s string) []LatLng {
	var lls []LatLng
	if s == "" {
		return lls
	}

	for _, piece := range strings.Split(s, ",") {
		piece = strings.TrimSpace(piece)

		// Skip empty strings.
		if piece == "" {
			continue
		}

		p := strings.Split(piece, ":")
		if len(p) != 2 {
			continue
		}

		lat, err := strconv.ParseFloat(p[0], 64)
		if err != nil {
			panic(fmt.Sprintf("invalid float in parseLatLngs: %q, err: %v", p[0], err))
		}

		lng, err := strconv.ParseFloat(p[1], 64)
		if err != nil {
			panic(fmt.Sprintf("invalid float in parseLatLngs: %q, err: %v", p[1], err))
		}

		lls = append(lls, LatLngFromDegrees(lat, lng))
	}
	return lls
}

// makeRect returns the minimal bounding Rect that contains the values in the input string.
func makeRect(s string) Rect {
	var rect Rect
	lls := parseLatLngs(s)
	if len(lls) > 0 {
		rect = RectFromLatLng(lls[0])
	}

	for _, ll := range lls[1:] {
		rect = rect.AddPoint(ll)
	}

	return rect
}

// makeCellUnion returns a CellUnion from the given CellID token strings.
func makeCellUnion(tokens ...string) CellUnion {
	var cu CellUnion

	for _, t := range tokens {
		cu = append(cu, CellIDFromString(t))
	}
	return cu
}

// makeLoop constructs a Loop from the input string.
// The strings "empty" or "full" create an empty or full loop respectively.
func makeLoop(s string) *Loop {
	if s == "full" {
		return FullLoop()
	}
	if s == "empty" {
		return EmptyLoop()
	}

	return LoopFromPoints(parsePoints(s))
}

// makePolygon constructs a polygon from the sequence of loops in the input
// string. Loops are automatically normalized by inverting them if necessary
// so that they enclose at most half of the unit sphere. (Historically this was
// once a requirement of polygon loops. It also hides the problem that if the
// user thinks of the coordinates as X:Y rather than LAT:LNG, it yields a loop
// with the opposite orientation.)
//
// Loops are semicolon separated in the input string with each loop using the
// same format as above.
//
// Examples of the input format:
//
//	"10:20, 90:0, 20:30"                                  // one loop
//	"10:20, 90:0, 20:30; 5.5:6.5, -90:-180, -15.2:20.3"   // two loops
//	""       // the empty polygon (consisting of no loops)
//	"empty"  // the empty polygon (consisting of no loops)
//	"full"   // the full polygon (consisting of one full loop)
func makePolygon(s string, normalize bool) *Polygon {
	var loops []*Loop
	// Avoid the case where strings.Split on empty string will still return
	// one empty value, where we want no values.
	if s == "empty" || s == "" {
		return PolygonFromLoops(loops)
	}

	for _, str := range strings.Split(s, ";") {
		// The polygon test strings mostly have a trailing semicolon
		// (to make concatenating them for tests easy). The C++
		// SplitString doesn't return empty elements where as Go does,
		// so we need to check before using it.
		if str == "" {
			continue
		}
		loop := makeLoop(strings.TrimSpace(str))
		if normalize && !loop.IsFull() {
			loop.Normalize()
		}
		loops = append(loops, loop)
	}
	return PolygonFromLoops(loops)
}

// makePolyline constructs a Polyline from the given string.
func makePolyline(s string) *Polyline {
	p := Polyline(parsePoints(s))
	return &p
}

// makeLaxPolyline constructs a LaxPolyline from the given string.
func makeLaxPolyline(s string) *LaxPolyline {
	return LaxPolylineFromPoints(parsePoints(s))
}

// laxPolylineToString returns a string representation suitable for reconstruction
// by the makeLaxPolyline method.
func laxPolylineToString(l *LaxPolyline) string {
	var buf bytes.Buffer
	writePoints(&buf, l.vertices, false) // TODO(rsned): Add rountripPrecision param.
	return buf.String()

}

// makeLaxPolygon creates a LaxPolygon from the given debug formatted string.
// Similar to makePolygon, except that loops must be oriented so that the
// interior of the loop is always on the left, and polygons with degeneracies
// are supported. As with makePolygon, "full" denotes the full polygon and "empty"
// is not allowed (instead, simply create a LaxPolygon with no loops).
func makeLaxPolygon(s string) *LaxPolygon {
	var points [][]Point
	if s == "" {
		return LaxPolygonFromPoints(points)
	}
	for _, l := range strings.Split(s, ";") {
		if l == "full" {
			points = append(points, []Point{})
		} else if l != "empty" {
			points = append(points, parsePoints(l))
		}
	}
	return LaxPolygonFromPoints(points)
}

// makeShapeIndex builds a ShapeIndex from the given debug string containing
// the points, polylines, and loops (in the form of a single polygon)
// described by the following format:
//
//	point1|point2|... # line1|line2|... # polygon1|polygon2|...
//
// Examples:
//
//	1:2 | 2:3 # #                     // Two points
//	# 0:0, 1:1, 2:2 | 3:3, 4:4 #      // Two polylines
//	# # 0:0, 0:3, 3:0; 1:1, 2:1, 1:2  // Two nested loops (one polygon)
//	5:5 # 6:6, 7:7 # 0:0, 0:1, 1:0    // One of each
//	# # empty                         // One empty polygon
//	# # empty | full                  // One empty polygon, one full polygon
//
// Loops should be directed so that the region's interior is on the left.
// Loops can be degenerate (they do not need to meet Loop requirements).
//
// Note: Because whitespace is ignored, empty polygons must be specified
// as the string "empty" rather than as the empty string ("").
//
// If roundtripPrecision is true, the coordinates are formatted using
// enough precision to exactly preserve the floating point values.
//
// TODO(rsned): plumb roundtripPrecision parameter through here.
func makeShapeIndex(s string) *ShapeIndex {
	fields := strings.Split(s, "#")
	if len(fields) != 3 {
		panic("shapeIndex debug string must contain 2 '#' characters")
	}

	index := NewShapeIndex()

	var points []Point
	for _, p := range strings.Split(fields[0], "|") {
		p = strings.TrimSpace(p)
		if p == "" {
			continue
		}
		points = append(points, parsePoint(p))
	}
	if len(points) > 0 {
		p := PointVector(points)
		index.Add(&p)
	}

	for _, p := range strings.Split(fields[1], "|") {
		p = strings.TrimSpace(p)
		if p == "" {
			continue
		}
		if polyline := makeLaxPolyline(p); polyline != nil {
			index.Add(polyline)
		}
	}

	for _, p := range strings.Split(fields[2], "|") {
		p = strings.TrimSpace(p)
		if p == "" {
			continue
		}
		if polygon := makeLaxPolygon(p); polygon != nil {
			index.Add(polygon)
		}
	}

	return index
}

// shapeIndexDebugString outputs the contents of this ShapeIndex in debug
// format. The index may contain Shapes of any type. Shapes are reordered
// if necessary so that all point geometry (shapes of dimension 0) are first,
// followed by all polyline geometry, followed by all polygon geometry.
func shapeIndexDebugString(index *ShapeIndex, roundtripPrecision bool) string {
	var buf bytes.Buffer

	for dim := 0; dim <= 2; dim++ {
		if dim > 0 {
			buf.WriteByte('#')
		}

		var count int

		// Use shapes ordered by id rather than ranging over the
		// index.shapes map to ensure that the ordering of shapes in the
		// generated string matches the C++ generated strings.
		for i := int32(0); i < index.nextID; i++ {
			shape := index.Shape(i)
			// Only use shapes that are still in the index and at the
			// current geometry level we are outputting.
			if shape == nil || shape.Dimension() != dim {
				continue
			}
			if count > 0 {
				buf.WriteString(" | ")
			} else {
				if dim > 0 {
					buf.WriteString(" ")
				} else {
					buf.WriteString("")
				}
			}

			for c := 0; c < shape.NumChains(); c++ {
				if c > 0 {
					if dim == 2 {
						buf.WriteString("; ")
					} else {
						buf.WriteString(" | ")
					}
				}
				chain := shape.Chain(c)
				if chain.Length == 0 {
					buf.WriteString("full")
				} else {
					writePoint(&buf, shape.Edge(chain.Start).V0, roundtripPrecision)
				}
				limit := chain.Start + chain.Length
				if dim != 1 {
					limit--
				}

				for e := chain.Start; e < limit; e++ {
					buf.WriteString(", ")
					writePoint(&buf, shape.Edge(e).V1, roundtripPrecision)
				}
				count++
			}
		}

		if dim == 1 || (dim == 0 && count > 0) {
			buf.WriteByte(' ')
		}
	}

	return buf.String()
}

// TODO(roberts): Remaining C++ textformat related methods
// make$S2TYPE methods for missing types.
// to debug string for many types
