package geojson

import (
	"errors"
	"fmt"
	"strconv"

	"github.com/tidwall/geojson/geo"
	"github.com/tidwall/geojson/geometry"
	"github.com/tidwall/gjson"
	"github.com/tidwall/pretty"
)

var (
	fmtErrTypeIsUnknown         = "type '%s' is unknown"
	errDataInvalid              = errors.New("invalid data")
	errTypeInvalid              = errors.New("invalid type")
	errTypeMissing              = errors.New("missing type")
	errCoordinatesInvalid       = errors.New("invalid coordinates")
	errCoordinatesMissing       = errors.New("missing coordinates")
	errGeometryInvalid          = errors.New("invalid geometry")
	errGeometryMissing          = errors.New("missing geometry")
	errFeaturesMissing          = errors.New("missing features")
	errFeaturesInvalid          = errors.New("invalid features")
	errGeometriesMissing        = errors.New("missing geometries")
	errGeometriesInvalid        = errors.New("invalid geometries")
	errCircleRadiusUnitsInvalid = errors.New("invalid circle radius units")
)

// Object is a GeoJSON type
type Object interface {
	Empty() bool
	Valid() bool
	Rect() geometry.Rect
	Center() geometry.Point
	Contains(other Object) bool
	Within(other Object) bool
	Intersects(other Object) bool
	AppendJSON(dst []byte) []byte
	JSON() string
	String() string
	Distance(obj Object) float64
	NumPoints() int
	ForEach(iter func(geom Object) bool) bool
	Spatial() Spatial
	MarshalJSON() ([]byte, error)
}

var _ = []Object{
	&Point{}, &LineString{}, &Polygon{}, &Feature{},
	&MultiPoint{}, &MultiLineString{}, &MultiPolygon{},
	&GeometryCollection{}, &FeatureCollection{},
	&Rect{}, &Circle{}, &SimplePoint{},
}

// Collection is a searchable collection type.
type Collection interface {
	Children() []Object
	Indexed() bool
	Search(rect geometry.Rect, iter func(child Object) bool)
}

var _ = []Collection{
	&MultiPoint{}, &MultiLineString{}, &MultiPolygon{},
	&FeatureCollection{}, &GeometryCollection{},
}

type extra struct {
	dims   byte      // number of extra coordinate values, 1 or 2
	values []float64 // extra coordinate values
	// valid json object that includes extra members such as
	// "bbox", "id", "properties", and foreign members
	members string
}

// ParseOptions ...
type ParseOptions struct {
	// IndexChildren option will cause the object to index their children
	// objects when the number of children is greater than or equal to the
	// provided value. Setting this value to 0 will disable indexing.
	// The default is 64.
	IndexChildren int
	// IndexGeometry option will cause the object to index it's geometry
	// when the number of points in it's base polygon or linestring is greater
	// that or equal to the provided value. Setting this value to 0 will
	// disable indexing.
	// The default is 64.
	IndexGeometry int
	// IndexGeometryKind is the kind of index implementation.
	// Default is QuadTreeCompressed
	IndexGeometryKind geometry.IndexKind
	// RequireValid option cause parse to fail when a geojson object is invalid.
	RequireValid bool
	// AllowSimplePoints options will force to parse to return the SimplePoint
	// type when a geojson point only consists of an 2D x/y coord and no extra
	// json members.
	AllowSimplePoints bool
	// DisableCircleType disables the special Circle syntax that is unique to
	// only Tile38.
	DisableCircleType bool
	// AllowRects options will force to parse and return the Rect type when a
	// geojson polygon only consists of a perfect rectangle, where there are
	// exactly 5 points with the first point being the min x/y and the
	// following point winding counter clockwise creating a closed rectangle.
	AllowRects bool
}

// DefaultParseOptions ...
var DefaultParseOptions = &ParseOptions{
	IndexChildren:     64,
	IndexGeometry:     64,
	IndexGeometryKind: geometry.QuadTree,
	RequireValid:      false,
	AllowSimplePoints: false,
	DisableCircleType: false,
	AllowRects:        false,
}

// Parse a GeoJSON object
func Parse(data string, opts *ParseOptions) (Object, error) {
	if opts == nil {
		// opts should never be nil
		opts = DefaultParseOptions
	}
	// look at the first byte
	for i := 0; ; i++ {
		if len(data) == 0 {
			return nil, errDataInvalid
		}
		switch data[0] {
		default:
			// well-known text is not supported yet
			return nil, errDataInvalid
		case 0, 1:
			if i > 0 {
				// 0x00 or 0x01 must be the first bytes
				return nil, errDataInvalid
			}
			// well-known binary is not supported yet
			return nil, errDataInvalid
		case ' ', '\t', '\n', '\r':
			// strip whitespace
			data = data[1:]
			continue
		case '{':
			return parseJSON(data, opts)
		}
	}
}

func toGeometryOpts(opts *ParseOptions) geometry.IndexOptions {
	var gopts geometry.IndexOptions
	if opts == nil {
		gopts = *geometry.DefaultIndexOptions
	} else {
		gopts.Kind = opts.IndexGeometryKind
		gopts.MinPoints = opts.IndexGeometry
	}
	return gopts
}

type parseKeys struct {
	rCoordinates gjson.Result
	rGeometries  gjson.Result
	rGeometry    gjson.Result
	rFeatures    gjson.Result
	members      string // a valid payload with all extra members
}

func parseJSON(data string, opts *ParseOptions) (Object, error) {
	if !gjson.Valid(data) {
		return nil, errDataInvalid
	}
	var keys parseKeys
	var fmembers []byte
	var rType gjson.Result
	gjson.Parse(data).ForEach(func(key, val gjson.Result) bool {
		switch key.String() {
		case "type":
			rType = val
		case "coordinates":
			keys.rCoordinates = val
		case "geometries":
			keys.rGeometries = val
		case "geometry":
			keys.rGeometry = val
		case "features":
			keys.rFeatures = val
		default:
			if len(fmembers) == 0 {
				fmembers = append(fmembers, '{')
			} else {
				fmembers = append(fmembers, ',')
			}
			fmembers = append(fmembers, pretty.UglyInPlace([]byte(key.Raw))...)
			fmembers = append(fmembers, ':')
			fmembers = append(fmembers, pretty.UglyInPlace([]byte(val.Raw))...)
		}
		return true
	})
	if len(fmembers) > 0 {
		fmembers = append(fmembers, '}')
		keys.members = string(fmembers)
	}
	if !rType.Exists() {
		return nil, errTypeMissing
	}
	if rType.Type != gjson.String {
		return nil, errTypeInvalid
	}
	switch rType.String() {
	default:
		return nil, fmt.Errorf(fmtErrTypeIsUnknown, rType.String())
	case "Point":
		return parseJSONPoint(&keys, opts)
	case "LineString":
		return parseJSONLineString(&keys, opts)
	case "Polygon":
		return parseJSONPolygon(&keys, opts)
	case "Feature":
		return parseJSONFeature(&keys, opts)
	case "MultiPoint":
		return parseJSONMultiPoint(&keys, opts)
	case "MultiLineString":
		return parseJSONMultiLineString(&keys, opts)
	case "MultiPolygon":
		return parseJSONMultiPolygon(&keys, opts)
	case "GeometryCollection":
		return parseJSONGeometryCollection(&keys, opts)
	case "FeatureCollection":
		return parseJSONFeatureCollection(&keys, opts)
	}
}

func parseBBoxAndExtras(ex **extra, keys *parseKeys, opts *ParseOptions) error {
	if keys.members == "" {
		return nil
	}
	if *ex == nil {
		*ex = new(extra)
	}
	(*ex).members = keys.members
	return nil
}

func appendJSONPoint(dst []byte, point geometry.Point, ex *extra, idx int) []byte {
	dst = append(dst, '[')
	dst = strconv.AppendFloat(dst, point.X, 'f', -1, 64)
	dst = append(dst, ',')
	dst = strconv.AppendFloat(dst, point.Y, 'f', -1, 64)
	if ex != nil {
		dims := int(ex.dims)
		for i := 0; i < dims; i++ {
			dst = append(dst, ',')
			dst = strconv.AppendFloat(
				dst, ex.values[idx*dims+i], 'f', -1, 64,
			)
		}
	}
	dst = append(dst, ']')
	return dst
}

func (ex *extra) appendJSONExtra(dst []byte, propertiesRequired bool) []byte {
	if ex != nil && ex.members != "" {
		dst = append(dst, ',')
		dst = append(dst, ex.members[1:len(ex.members)-1]...)
		if propertiesRequired {
			if !gjson.Get(ex.members, "properties").Exists() {
				dst = append(dst, `,"properties":{}`...)
			}
		}
	} else if propertiesRequired {
		dst = append(dst, `,"properties":{}`...)
	}

	return dst
}

func appendJSONSeries(
	dst []byte, series geometry.Series, ex *extra, pidx int,
) (ndst []byte, npidx int) {
	dst = append(dst, '[')
	nPoints := series.NumPoints()
	for i := 0; i < nPoints; i++ {
		if i > 0 {
			dst = append(dst, ',')
		}
		dst = appendJSONPoint(dst, series.PointAt(i), ex, pidx)
		pidx++
	}
	dst = append(dst, ']')
	return dst, pidx
}

func unionRects(a, b geometry.Rect) geometry.Rect {
	if b.Min.X < a.Min.X {
		a.Min.X = b.Min.X
	}
	if b.Max.X > a.Max.X {
		a.Max.X = b.Max.X
	}
	if b.Min.Y < a.Min.Y {
		a.Min.Y = b.Min.Y
	}
	if b.Max.Y > a.Max.Y {
		a.Max.Y = b.Max.Y
	}
	return a
}

func geoDistancePoints(a, b geometry.Point) float64 {
	return geo.DistanceTo(a.Y, a.X, b.Y, b.X)
}