package gliffy2drawio

import (
	"encoding/base64"
	"encoding/json"
	"fmt"
	"math"
	"regexp"
	"sort"
	"strconv"
	"strings"
)

// Diagram is the root Gliffy document.
type Diagram struct {
	ContentType       string            `json:"contentType"`
	Version           string            `json:"version"`
	Title             string            `json:"title"`
	DefaultPage       string            `json:"defaultPage"`
	Stage             Stage             `json:"stage"`
	Pages             []GliffyPage      `json:"pages"`
	Metadata          Metadata          `json:"metadata"`
	EmbeddedResources EmbeddedResources `json:"embeddedResources"`
}

type Metadata struct {
	Title string `json:"title"`
}

type Stage struct {
	Background      string          `json:"background"`
	Width           float64         `json:"width"`
	Height          float64         `json:"height"`
	AutoFit         bool            `json:"autoFit"`
	GridOn          bool            `json:"gridOn"`
	DrawingGuidesOn bool            `json:"drawingGuidesOn"`
	Objects         []*GliffyObject `json:"objects"`
	Layers          []*GliffyLayer  `json:"layers"`
	TextStyles      TextStyles      `json:"textStyles"`
}

type GliffyPage struct {
	ID    string `json:"id"`
	Title string `json:"title"`
	Index int    `json:"index"`
	Scene Scene  `json:"scene"`
}

type Scene struct {
	Objects         []*GliffyObject `json:"objects"`
	Layers          []*GliffyLayer  `json:"layers"`
	Background      string          `json:"background"`
	Width           float64         `json:"width"`
	Height          float64         `json:"height"`
	AutoFit         bool            `json:"autoFit"`
	GridOn          bool            `json:"gridOn"`
	DrawingGuidesOn bool            `json:"drawingGuidesOn"`
	TextStyles      TextStyles      `json:"textStyles"`
}

type GliffyLayer struct {
	GUID      string `json:"guid"`
	Order     int    `json:"order"`
	Name      string `json:"name"`
	Active    bool   `json:"active"`
	Locked    bool   `json:"locked"`
	Visible   bool   `json:"visible"`
	NodeIndex int    `json:"nodeIndex"`

	MxObject *MxCell `json:"-"`
}

type TextStyles struct {
	Global GlobalTextStyles `json:"global"`
}

type GlobalTextStyles struct {
	Size  string `json:"size"`
	Color string `json:"color"`
}

func (g GlobalTextStyles) SizeValue() string {
	if g.Size == "" {
		return ""
	}
	idx := strings.Index(g.Size, "px")
	if idx == -1 {
		return g.Size
	}
	return g.Size[:idx]
}

type GraphicType string

const (
	GraphicTypeSVG     GraphicType = "Svg"
	GraphicTypeLine    GraphicType = "Line"
	GraphicTypeShape   GraphicType = "Shape"
	GraphicTypeText    GraphicType = "Text"
	GraphicTypeImage   GraphicType = "Image"
	GraphicTypeLink    GraphicType = "Link"
	GraphicTypeMindmap GraphicType = "Mindmap"
	GraphicTypePopup   GraphicType = "PopupNote"
	GraphicTypeUnknown GraphicType = "Unwknown"
)

type Graphic struct {
	Type      GraphicType      `json:"type"`
	Text      *GliffyText      `json:"Text"`
	Line      *GliffyLine      `json:"Line"`
	Link      *GliffyLink      `json:"Link"`
	Shape     *GliffyShape     `json:"Shape"`
	Image     *GliffyImage     `json:"Image"`
	Svg       *GliffySvg       `json:"Svg"`
	Mindmap   *GliffyMindmap   `json:"Mindmap"`
	PopupNote *GliffyPopupNote `json:"PopupNote"`
}

func (g *Graphic) GetType() GraphicType {
	if g == nil {
		return GraphicTypeUnknown
	}
	if g.Type == "" {
		return GraphicTypeUnknown
	}
	return g.Type
}

type GliffyAbstractShape struct {
	StrokeWidth float64 `json:"strokeWidth"`
	StrokeColor string  `json:"strokeColor"`
	FillColor   string  `json:"fillColor"`
	DashStyle   string  `json:"dashStyle"`
}

func (s GliffyAbstractShape) StrokeWidthValue() int {
	return int(math.Round(s.StrokeWidth))
}

type GliffyLine struct {
	GliffyAbstractShape
	StartArrow    *int        `json:"startArrow"`
	EndArrow      *int        `json:"endArrow"`
	Interpolation string      `json:"interpolationType"`
	CornerRadius  *int        `json:"cornerRadius"`
	ControlPath   [][]float64 `json:"controlPath"`
}

type GliffyShape struct {
	GliffyAbstractShape
	TID        string  `json:"tid"`
	Gradient   bool    `json:"gradient"`
	DropShadow bool    `json:"dropShadow"`
	State      int     `json:"state"`
	ShadowX    float64 `json:"shadowX"`
	ShadowY    float64 `json:"shadowY"`
	Opacity    float64 `json:"opacity"`
}

func (s GliffyShape) NoFill() bool {
	return s.TID != "" && strings.Contains(s.TID, "no_fill")
}

type GliffyImage struct {
	GliffyShape
	URL string `json:"url"`
}

func (g GliffyImage) CleanURL() string {
	return strings.ReplaceAll(g.URL, ";base64", "")
}

type GliffySvg struct {
	GliffyShape
	EmbeddedResourceID *int `json:"embeddedResourceId"`
}

type GliffyMindmap struct {
	GliffyShape
}

type GliffyPopupNote struct {
	GliffyShape
	Text string `json:"text"`
}

type GliffyLink struct {
	Href       string `json:"href"`
	RenderIcon bool   `json:"renderIcon"`
}

type EmbeddedResource struct {
	ID       int    `json:"id"`
	MimeType string `json:"mimeType"`
	Data     string `json:"data"`
}

func (r EmbeddedResource) Base64EncodedData() string {
	return base64.StdEncoding.EncodeToString([]byte(r.Data))
}

type EmbeddedResources struct {
	Resources   []EmbeddedResource `json:"resources"`
	resourceMap map[int]EmbeddedResource
}

func (e *EmbeddedResources) Get(id int) (EmbeddedResource, bool) {
	if e == nil {
		return EmbeddedResource{}, false
	}
	if e.resourceMap == nil {
		e.resourceMap = make(map[int]EmbeddedResource, len(e.Resources))
		for _, r := range e.Resources {
			e.resourceMap[r.ID] = r
		}
	}
	r, ok := e.resourceMap[id]
	return r, ok
}

type Constraints struct {
	Constraints     []*Constraint `json:"constraints"`
	StartConstraint *Constraint   `json:"startConstraint"`
	EndConstraint   *Constraint   `json:"endConstraint"`
}

type ConstraintType string

const (
	ConstraintTypeStart  ConstraintType = "StartPositionConstraint"
	ConstraintTypeEnd    ConstraintType = "EndPositionConstraint"
	ConstraintTypeHeight ConstraintType = "HeightConstraint"
)

type Constraint struct {
	Type                    ConstraintType  `json:"type"`
	StartPositionConstraint *ConstraintData `json:"StartPositionConstraint"`
	EndPositionConstraint   *ConstraintData `json:"EndPositionConstraint"`
}

type ConstraintData struct {
	NodeID string  `json:"nodeId"`
	PX     float64 `json:"px"`
	PY     float64 `json:"py"`
}

// GliffyText mirrors Gliffy's text object.
type GliffyText struct {
	HTML            string   `json:"html"`
	Valign          string   `json:"valign"`
	Halign          string   `json:"-"`
	VPosition       string   `json:"vposition"`
	HPosition       string   `json:"hposition"`
	PaddingLeft     int      `json:"paddingLeft"`
	PaddingRight    int      `json:"paddingRight"`
	PaddingBottom   int      `json:"paddingBottom"`
	PaddingTop      int      `json:"paddingTop"`
	LineTValue      float64  `json:"lineTValue"`
	LinePerpValue   *float64 `json:"linePerpValue"`
	CardinalityType string   `json:"cardinalityType"`
	Overflow        string   `json:"overflow"`

	forceTopPaddingShift bool
}

func (t *GliffyText) UnmarshalJSON(data []byte) error {
	type raw GliffyText
	var r raw
	if err := json.Unmarshal(data, &r); err != nil {
		return err
	}
	*t = GliffyText(r)
	if t.LineTValue == 0 {
		t.LineTValue = 0.5
	}
	t.postDeserialize()
	return nil
}

func (t *GliffyText) postDeserialize() {
	t.Halign = t.extractHorizontalAlignment()
	t.convertTextAlign()
	t.replaceParagraphWithDiv()
}

func (t *GliffyText) GetStyle(x, y float64) string {
	var sb strings.Builder
	topPaddingShift := 7

	switch t.VPosition {
	case "above":
		sb.WriteString("verticalLabelPosition=top;verticalAlign=bottom;")
	case "below":
		sb.WriteString("verticalLabelPosition=bottom;verticalAlign=top;")
	case "none":
		sb.WriteString("verticalAlign=" + t.Valign + ";")
		if !t.forceTopPaddingShift && strings.EqualFold(t.Valign, "middle") {
			topPaddingShift = 0
		}
	}

	switch t.HPosition {
	case "left":
		sb.WriteString("labelPosition=left;align=right;")
	case "right":
		sb.WriteString("labelPosition=right;align=left;")
	case "none":
		if t.Halign != "" {
			sb.WriteString("align=" + t.Halign + ";")
			if strings.EqualFold(t.Halign, "right") {
				x = 0
			}
		} else {
			sb.WriteString("align=center;")
		}
	}

	t.PaddingLeft = maxInt(0, t.PaddingLeft-2)
	t.PaddingRight = maxInt(0, t.PaddingRight-2)

	sb.WriteString("spacingLeft=" + floatToString(float64(t.PaddingLeft)+x) + ";")
	sb.WriteString("spacingRight=" + intToString(t.PaddingRight) + ";")

	if t.forceTopPaddingShift || !strings.EqualFold(t.Valign, "middle") {
		sb.WriteString("spacingTop=" + floatToString(float64(t.PaddingTop-topPaddingShift)+y) + ";")
		sb.WriteString("spacingBottom=" + intToString(t.PaddingBottom) + ";")
	}

	if t.Overflow == "none" {
		sb.WriteString("whiteSpace=wrap;")
	}

	return sb.String()
}

func (t *GliffyText) SetHAlign(value string) {
	t.Halign = value
}

func (t *GliffyText) SetVAlign(value string) {
	t.Valign = value
}

func (t *GliffyText) SetForceTopPaddingShift(v bool) {
	t.forceTopPaddingShift = v
}

var (
	spanPattern     = regexp.MustCompile(`<span style="(.*?)">`)
	textAlign       = regexp.MustCompile(`.*(text-align: ?(left|center|right);).*`)
	textAlignToDraw = regexp.MustCompile(`style="text-align:\s?(left|center|right);"`)
	lineHeight      = regexp.MustCompile(`.*(line-height: .*px;).*`)
)

func (t *GliffyText) replaceParagraphWithDiv() {
	m := spanPattern.FindAllStringSubmatchIndex(t.HTML, -1)
	if len(m) == 0 {
		t.HTML = strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(t.HTML, "<p ", "<div "), "<p>", "<div>"), "</p>", "</div>")
		return
	}

	var mod strings.Builder
	last := 0
	for _, match := range m {
		_, end := match[0], match[1]
		styleStart, styleEnd := match[2], match[3]
		span := t.HTML[last:end]
		style := t.HTML[styleStart:styleEnd]
		if style != "" {
			if !lineHeight.MatchString(style) {
				if strings.HasPrefix(strings.ToLower(t.HTML[end:]), "<span") {
					span = span[:styleEnd-last] + " line-height: 0;" + span[styleEnd-last:]
				} else {
					span = span[:styleEnd-last] + " line-height: normal;" + span[styleEnd-last:]
				}
			}
		}
		mod.WriteString(span)
		last = end
	}
	mod.WriteString(t.HTML[last:])
	t.HTML = mod.String()
	t.HTML = strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(t.HTML, "<p ", "<div "), "<p>", "<div>"), "</p>", "</div>")
}

func (t *GliffyText) extractHorizontalAlignment() string {
	m := textAlign.FindStringSubmatch(t.HTML)
	if len(m) == 0 {
		return ""
	}
	return m[2]
}

func (t *GliffyText) convertTextAlign() {
	t.HTML = textAlignToDraw.ReplaceAllString(t.HTML, `align="$1"`)
}

// GliffyObject represents an element on the canvas.
type GliffyObject struct {
	X           float64         `json:"x"`
	Y           float64         `json:"y"`
	ID          string          `json:"id"`
	Width       float64         `json:"width"`
	Height      float64         `json:"height"`
	Rotation    float64         `json:"rotation"`
	UID         string          `json:"uid"`
	TID         string          `json:"tid"`
	Order       OrderValue      `json:"order"`
	LockShape   bool            `json:"lockshape"`
	LayerID     *string         `json:"layerId"`
	Graphic     *Graphic        `json:"graphic"`
	Children    []*GliffyObject `json:"children"`
	Constraints *Constraints    `json:"constraints"`

	MxObject *MxCell       `json:"-"`
	Parent   *GliffyObject `json:"-"`
}

// OrderValue supports both numeric and string orders found in Gliffy files.
type OrderValue string

func (o *OrderValue) UnmarshalJSON(data []byte) error {
	if len(data) == 0 {
		return nil
	}
	if data[0] == '"' {
		var s string
		if err := json.Unmarshal(data, &s); err != nil {
			return err
		}
		*o = OrderValue(s)
		return nil
	}
	var num json.Number
	if err := json.Unmarshal(data, &num); err == nil {
		*o = OrderValue(num.String())
		return nil
	}
	*o = OrderValue(strings.Trim(string(data), "\""))
	return nil
}

func (o OrderValue) String() string {
	return string(o)
}

func (o *GliffyObject) UnmarshalJSON(data []byte) error {
	type raw GliffyObject
	var r raw
	if err := json.Unmarshal(data, &r); err != nil {
		return err
	}
	*o = GliffyObject(r)
	o.postDeserialize()
	return nil
}

func (o *GliffyObject) postDeserialize() {
	if o.isGroup() && o.hasChildren() {
		o.normalizeChildrenCoordinates()
		o.adjustZOrder()
	}
}

func (o *GliffyObject) hasChildren() bool {
	return len(o.Children) > 0
}

func (o *GliffyObject) GraphicOrChildGraphic() *Graphic {
	if o.Graphic != nil {
		return o.Graphic
	}
	if o.isUml() || graphicalessShapes[o.UID] {
		return o.firstChildGraphic()
	}
	return nil
}

func (o *GliffyObject) firstChildGraphic() *Graphic {
	if len(o.Children) == 0 {
		return nil
	}
	return o.Children[0].Graphic
}

func (o *GliffyObject) TextObject() *GliffyObject {
	return o.textObjectWithOffset(0, 0)
}

func (o *GliffyObject) textObjectWithOffset(x, y float64) *GliffyObject {
	if o.isText() {
		return o
	}
	if len(o.Children) == 0 {
		return nil
	}

	for _, child := range o.Children {
		if g := child.GraphicOrChildGraphic(); g != nil && g.GetType() == GraphicTypeText {
			child.X += x
			child.Y += y
			return child
		}
		if txt := child.textObjectWithOffset(child.X, child.Y); txt != nil {
			return txt
		}
	}
	return nil
}

func (o *GliffyObject) TextHTML() string {
	if o.Graphic == nil || o.Graphic.Text == nil {
		return ""
	}
	text := o.Graphic.Text
	widthDiff := -3
	if text.Overflow != "none" {
		widthDiff = 6
	}
	return "<div style='width: " + floatToString(o.Width+float64(widthDiff)) + "px;height:auto;word-break: break-word;'>" + text.HTML + "</div>"
}

func (o *GliffyObject) isGroup() bool {
	if !o.hasChildren() {
		return false
	}
	if o.UID != "" && (groupShapes[o.UID] || strings.HasPrefix(o.UID, "com.gliffy.shape.table")) {
		return true
	}
	if o.UID == "" && len(o.Children) > 0 && !o.Children[0].isText() {
		return true
	}
	return false
}

func (o *GliffyObject) isSelection() bool {
	return o.UID != "" && strings.Contains(o.UID, "default.selection")
}

func (o *GliffyObject) isMindmap() bool {
	return o.UID != "" && mindmapShapes[o.UID]
}

func (o *GliffyObject) isLine() bool {
	return o.Graphic != nil && o.Graphic.GetType() == GraphicTypeLine
}

func (o *GliffyObject) isLink() bool {
	return o.Graphic != nil && o.Graphic.GetType() == GraphicTypeLink
}

func (o *GliffyObject) isUml() bool {
	return strings.HasPrefix(o.UID, "com.gliffy.shape.uml.uml")
}

func (o *GliffyObject) isShape() bool {
	if o.Graphic != nil {
		t := o.Graphic.GetType()
		return t == GraphicTypeShape || t == GraphicTypeMindmap
	}
	g := o.firstChildGraphic()
	return g != nil && g.GetType() == GraphicTypeShape
}

func (o *GliffyObject) isSvg() bool {
	return o.Graphic != nil && o.Graphic.GetType() == GraphicTypeSVG
}

func (o *GliffyObject) isSwimlane() bool {
	return o.UID != "" && strings.Contains(o.UID, "com.gliffy.shape.swimlanes")
}

func (o *GliffyObject) isText() bool {
	return o.Graphic != nil && o.Graphic.GetType() == GraphicTypeText
}

func (o *GliffyObject) isImage() bool {
	return o.Graphic != nil && o.Graphic.GetType() == GraphicTypeImage
}

func (o *GliffyObject) isVennCircle() bool {
	return strings.HasPrefix(o.UID, "com.gliffy.shape.venn")
}

func (o *GliffyObject) isDeeplyNestedLink() bool {
	return o.UID != "" && deeplyNestedLinks[o.UID]
}

func (o *GliffyObject) GradientColor() string {
	gradientColor := "#FFFFFF"
	if o.Graphic != nil && o.Graphic.Shape != nil && o.UID != "" && !strings.HasPrefix(o.UID, "com.gliffy.shape.radial") {
		hex := o.Graphic.Shape.FillColor
		if len(hex) == 7 && strings.HasPrefix(hex, "#") {
			clr, err := parseHex(hex[1:])
			if err == nil {
				r := math.Min(0xFF0000, float64(clr&0xFF0000)+0xAA0000)
				g := math.Min(0x00FF00, float64(clr&0x00FF00)+0x00AA00)
				b := math.Min(0x0000FF, float64(clr&0x0000FF)+0x0000AA)
				gradientColor = "#" + strings.ToUpper(hexInt(int(r+g+b)))
			}
		}
	}
	return gradientColor
}

func (o *GliffyObject) GradientIgnored() bool {
	return o.UID != "" && (strings.HasPrefix(o.UID, "com.gliffy.shape.venn.outline") || strings.HasPrefix(o.UID, "com.gliffy.shape.venn.flat"))
}

func (o *GliffyObject) IsSubRoutine() bool {
	return o.UID == "com.gliffy.shape.flowchart.flowchart_v1.default.subroutine"
}

func (o *GliffyObject) IsUnrecognizedGraphicType() bool {
	return o.Graphic != nil && o.Graphic.Type == ""
}

func (o *GliffyObject) GetConstraints() *Constraints {
	return o.Constraints
}

func (o *GliffyObject) String() string {
	if o.UID != "" {
		return o.UID
	}
	return o.TID
}

func (o *GliffyObject) normalizeChildrenCoordinates() {
	if !o.hasChildren() {
		return
	}
	sort.Slice(o.Children, func(i, j int) bool { return o.Children[i].X < o.Children[j].X })
	xMin := o.Children[0].X
	if xMin < 0 {
		o.Width += -xMin
		o.X += xMin
		for _, c := range o.Children {
			c.X += -xMin
		}
	}

	sort.Slice(o.Children, func(i, j int) bool { return o.Children[i].Y < o.Children[j].Y })
	yMin := o.Children[0].Y
	if yMin < 0 {
		o.Height += -yMin
		o.Y += yMin
		for _, c := range o.Children {
			c.Y += -yMin
		}
	}
}

func (o *GliffyObject) adjustZOrder() {
	var maxOrder *int
	for _, c := range o.Children {
		if c.UID == "com.gliffy.shape.basic.basic_v1.default.rectangle" && c.X == 0 && c.Y == 0 && c.Width == o.Width && c.Height == o.Height {
			if val, err := strconv.Atoi(c.Order.String()); err == nil {
				if maxOrder == nil || val > *maxOrder {
					maxOrder = &val
				}
			}
		}
	}
	if maxOrder != nil {
		o.Order = OrderValue(intToString(*maxOrder))
	}
}

func (o *GliffyObject) adjustGeo(geo *MxGeometry) {
	arr := shapeCoordFix[o.UID]
	if arr == nil && o.Graphic != nil && o.Graphic.Shape != nil {
		arr = shapeCoordFix[o.Graphic.Shape.TID]
	}
	if arr == nil {
		return
	}
	x, y, w, h := geo.X, geo.Y, geo.Width, geo.Height
	shifts := o.getAdjustShifts(arr, x, y, w, h)
	geo.X = x + shifts.X
	geo.Y = y + shifts.Y
	geo.Width = w + shifts.Width
	geo.Height = h + shifts.Height
}

func (o *GliffyObject) adjustTextPos(textObj *GliffyObject) {
	arr := shapeCoordFix[o.UID]
	if arr == nil || len(arr) != 4 {
		return
	}
	shifts := o.getAdjustShifts(arr, o.X, o.Y, o.Width, o.Height)
	textObj.X -= shifts.X
	textObj.Y -= shifts.Y
}

func (o *GliffyObject) getAdjustShifts(arr []float64, x, y, w, h float64) *MxGeometry {
	xShift := relativeShift(arr[0], w)
	yShift := relativeShift(arr[1], h)
	wShift := relativeShift(arr[2], w)
	hShift := relativeShift(arr[3], h)
	mod := &MxGeometry{X: x + xShift, Y: y + yShift, Width: w + wShift, Height: h + hShift}
	if o.Rotation > 0 {
		orig := &MxGeometry{X: x, Y: y, Width: w, Height: h}
		rotateGeometry(orig, o.Rotation, 0, 0)
		rotateGeometry(mod, o.Rotation, 0, 0)
		xShift += mod.X - orig.X
		yShift += mod.Y - orig.Y
	}
	mod.X = xShift
	mod.Y = yShift
	mod.Width = wShift
	mod.Height = hShift
	return mod
}

func (o *GliffyObject) AdjustedLink() string {
	if len(o.Children) == 0 {
		return ""
	}
	if o.isDeeplyNestedLink() {
		var sb strings.Builder
		o.collectDeepLink(&sb)
		return sb.String()
	}
	for _, c := range o.Children {
		if c.isLink() && c.Graphic != nil && c.Graphic.Link != nil {
			return c.Graphic.Link.Href
		}
	}
	return ""
}

func (o *GliffyObject) collectDeepLink(sb *strings.Builder) {
	for _, c := range o.Children {
		if c.isLink() && c.Graphic != nil && c.Graphic.Link != nil {
			sb.WriteString(c.Graphic.Link.Href)
			return
		}
		if len(c.Children) > 0 {
			c.collectDeepLink(sb)
		}
	}
}

func (o *GliffyObject) IsUseFillColorForStroke() bool {
	key := o.UID
	if key == "" && o.Graphic != nil && o.Graphic.Shape != nil {
		key = o.Graphic.Shape.TID
	}
	return fillColorIsStroke[key]
}

func (o *GliffyObject) ContainsTextBracket() bool {
	return o.UID != "" && strings.Contains(o.UID, "com.gliffy.shape.uml.uml_v2.activity.frame")
}

func (o *GliffyObject) ForceConstraints() bool {
	return forceConstraintsShapes[o.UID]
}

func (o *GliffyObject) UMLSequenceCombinedFragmentText() string {
	switch o.UID {
	case "com.gliffy.shape.uml.uml_v2.sequence.interaction_use":
		return "ref"
	case "com.gliffy.shape.uml.uml_v2.sequence.opt_combined_fragment":
		return "opt"
	case "com.gliffy.shape.uml.uml_v2.sequence.loop_combined_fragment":
		return "loop"
	case "com.gliffy.shape.uml.uml_v2.sequence.alt_combined_fragment":
		return "alt"
	default:
		return ""
	}
}

func relativeShift(v, size float64) float64 {
	if math.Abs(v) < 1 {
		return size * v
	}
	return v
}

func parseHex(s string) (int64, error) {
	return strconv.ParseInt(s, 16, 64)
}

func hexInt(v int) string {
	return fmt.Sprintf("%06X", v&0xFFFFFF)
}

// to avoid fmt in hot paths.
func floatToString(v float64) string {
	return strings.TrimRight(strings.TrimRight(strconv.FormatFloat(v, 'f', 4, 64), "0"), ".")
}

func intToString(v int) string {
	return strconv.Itoa(v)
}

func maxInt(a, b int) int {
	if a > b {
		return a
	}
	return b
}

// Static shape metadata replicated from Java implementation.
var graphicalessShapes = map[string]bool{
	"com.gliffy.shape.uml.uml_v1.default.package":                         true,
	"com.gliffy.shape.uml.uml_v1.default.class":                           true,
	"com.gliffy.shape.uml.uml_v1.default.simple_class":                    true,
	"com.gliffy.shape.uml.uml_v1.default.object_timeline":                 true,
	"com.gliffy.shape.uml.uml_v1.default.lifeline":                        true,
	"com.gliffy.shape.uml.uml_v1.default.use_case":                        true,
	"com.gliffy.shape.uml.uml_v1.default.actor":                           true,
	"com.gliffy.shape.uml.uml_v1.default.message":                         true,
	"com.gliffy.shape.uml.uml_v1.default.activation":                      true,
	"com.gliffy.shape.uml.uml_v1.default.dependency":                      true,
	"com.gliffy.shape.uml.uml_v1.default.composition":                     true,
	"com.gliffy.shape.uml.uml_v1.default.aggregation":                     true,
	"com.gliffy.shape.uml.uml_v1.default.association":                     true,
	"com.gliffy.shape.uml.uml_v2.class.package":                           true,
	"com.gliffy.shape.uml.uml_v2.class.simple_class":                      true,
	"com.gliffy.shape.uml.uml_v2.class.class":                             true,
	"com.gliffy.shape.uml.uml_v2.class.class2":                            true,
	"com.gliffy.shape.uml.uml_v2.class.interface":                         true,
	"com.gliffy.shape.uml.uml_v2.class.enumeration":                       true,
	"com.gliffy.shape.uml.uml_v2.sequence.lifeline":                       true,
	"com.gliffy.shape.uml.uml_v2.sequence.boundary_lifeline":              true,
	"com.gliffy.shape.uml.uml_v2.sequence.control_lifeline":               true,
	"com.gliffy.shape.uml.uml_v2.sequence.entity_lifeline":                true,
	"com.gliffy.shape.uml.uml_v2.deployment.package":                      true,
	"com.gliffy.shape.uml.uml_v2.component.package":                       true,
	"com.gliffy.shape.uml.uml_v2.use_case.package":                        true,
	"com.gliffy.shape.erd.erd_v1.default.entity_with_attributes":          true,
	"com.gliffy.shape.erd.erd_v1.default.entity_with_multiple_attributes": true,
	"com.gliffy.shape.bpmn.bpmn_v1.data_artifacts.annotation":             true,
	"com.gliffy.shape.ui.ui_v3.navigation.navbar":                         true,
	"com.gliffy.shape.ui.ui_v3.forms_controls.combo_box":                  true,
	"com.gliffy.shape.ui.ui_v3.containers_content.tooltip_top":            true,
	"com.gliffy.shape.ui.ui_v3.containers_content.tooltip_bottom":         true,
	"com.gliffy.shape.ui.ui_v3.containers_content.tooltip_left":           true,
	"com.gliffy.shape.ui.ui_v3.containers_content.tooltip_right":          true,
	"com.gliffy.shape.ui.ui_v3.containers_content.popover_top":            true,
	"com.gliffy.shape.ui.ui_v3.containers_content.popover_bottom":         true,
	"com.gliffy.shape.ui.ui_v3.forms_controls.selector":                   true,
	"com.gliffy.shape.ui.ui_v3.icon_symbols.annotate_left":                true,
	"com.gliffy.shape.ui.ui_v3.icon_symbols.annotate_right":               true,
	"com.gliffy.shape.ui.ui_v3.icon_symbols.annotate_top":                 true,
	"com.gliffy.shape.ui.ui_v3.containers_content.speech_bubble_right":    true,
	"com.gliffy.shape.ui.ui_v3.containers_content.speech_bubble_left":     true,
	"com.gliffy.shape.sitemap.sitemap_v2.page":                            true,
	"com.gliffy.shape.sitemap.sitemap_v2.home":                            true,
	"com.gliffy.shape.sitemap.sitemap_v2.gliffy":                          true,
	"com.gliffy.shape.sitemap.sitemap_v2.form":                            true,
	"com.gliffy.shape.sitemap.sitemap_v2.shopping_cart":                   true,
	"com.gliffy.shape.sitemap.sitemap_v2.text":                            true,
	"com.gliffy.shape.sitemap.sitemap_v2.video":                           true,
	"com.gliffy.shape.sitemap.sitemap_v2.upload":                          true,
	"com.gliffy.shape.sitemap.sitemap_v2.slideshow":                       true,
	"com.gliffy.shape.sitemap.sitemap_v2.sitemap":                         true,
	"com.gliffy.shape.sitemap.sitemap_v2.settings":                        true,
	"com.gliffy.shape.sitemap.sitemap_v2.search":                          true,
	"com.gliffy.shape.sitemap.sitemap_v2.script":                          true,
	"com.gliffy.shape.sitemap.sitemap_v2.print":                           true,
	"com.gliffy.shape.sitemap.sitemap_v2.pricing":                         true,
	"com.gliffy.shape.sitemap.sitemap_v2.photo":                           true,
	"com.gliffy.shape.sitemap.sitemap_v2.map":                             true,
	"com.gliffy.shape.sitemap.sitemap_v2.login":                           true,
	"com.gliffy.shape.sitemap.sitemap_v2.game":                            true,
	"com.gliffy.shape.sitemap.sitemap_v2.gallery":                         true,
	"com.gliffy.shape.sitemap.sitemap_v2.download":                        true,
	"com.gliffy.shape.sitemap.sitemap_v2.document":                        true,
	"com.gliffy.shape.sitemap.sitemap_v2.chat":                            true,
	"com.gliffy.shape.sitemap.sitemap_v2.calendar":                        true,
	"com.gliffy.shape.sitemap.sitemap_v2.audio":                           true,
	"com.gliffy.shape.sitemap.sitemap_v2.profile":                         true,
	"com.gliffy.shape.sitemap.sitemap_v2.error":                           true,
	"com.gliffy.shape.sitemap.sitemap_v2.success":                         true,
	"com.gliffy.shape.sitemap.sitemap_v2.cloud":                           true,
}

var groupShapes = map[string]bool{
	"com.gliffy.shape.basic.basic_v1.default.group":                       true,
	"com.gliffy.shape.erd.erd_v1.default.entity_with_attributes":          true,
	"com.gliffy.shape.erd.erd_v1.default.entity_with_multiple_attributes": true,
	"com.gliffy.shape.uml.uml_v2.sequence.interaction_use":                true,
	"com.gliffy.shape.uml.uml_v2.sequence.opt_combined_fragment":          true,
	"com.gliffy.shape.uml.uml_v2.sequence.loop_combined_fragment":         true,
	"com.gliffy.shape.uml.uml_v2.sequence.alt_combined_fragment":          true,
	"com.gliffy.shape.uml.uml_v2.class.object":                            true,
	"com.gliffy.shape.uml.uml_v2.class.enumeration":                       true,
	"com.gliffy.shape.uml.uml_v2.class.interface":                         true,
	"com.gliffy.shape.uml.uml_v2.class.class2":                            true,
	"com.gliffy.shape.uml.uml_v2.class.class":                             true,
	"com.gliffy.shape.uml.uml_v2.class.data_type":                         true,
	"com.gliffy.shape.uml.uml_v2.state_machine.composite_state":           true,
	"com.gliffy.shape.uml.uml_v2.state_machine.orthoganal_state":          true,
	"com.gliffy.shape.uml.uml_v2.class.package":                           true,
	"com.gliffy.shape.uml.uml_v2.sequence.boundary_lifeline":              true,
	"com.gliffy.shape.uml.uml_v2.sequence.lifeline":                       true,
	"com.gliffy.shape.uml.uml_v2.sequence.entity_lifeline":                true,
	"com.gliffy.shape.uml.uml_v2.sequence.control_lifeline":               true,
	"com.gliffy.shape.uml.uml_v1.default.object_timeline":                 true,
	"com.gliffy.shape.uml.uml_v1.default.class":                           true,
	"com.gliffy.shape.uml.uml_v1.default.object":                          true,
	"com.gliffy.shape.iphone.iphone_ios7.containers_content.table":        true,
	"com.gliffy.shape.iphone.iphone_ios7.forms_controls.button_stack":     true,
	"com.gliffy.shape.iphone.iphone_ios7.forms_controls.alert_2options":   true,
	"com.gliffy.shape.iphone.iphone_ios7.forms_controls.alert":            true,
	"com.gliffy.shape.iphone.iphone_ios7.navigation.contextual_menu":      true,
	"com.gliffy.shape.iphone.iphone_ios7.navigation.nav_3tabs":            true,
	"com.gliffy.shape.iphone.iphone_ios7.containers_content.title_bar":    true,
	"com.gliffy.shape.iphone.iphone_ios7.navigation.tab_bar":              true,
	"com.gliffy.shape.iphone.iphone_ios7.forms_controls.search":           true,
	"com.gliffy.shape.android.android_v1.general.dialog":                  true,
	"com.gliffy.shape.android.android_v1.general.list_1line":              true,
	"com.gliffy.shape.android.android_v1.general.list_2lines":             true,
	"com.gliffy.shape.android.android_v1.general.tabs01":                  true,
	"com.gliffy.shape.android.android_v1.general.tabs02":                  true,
	"com.gliffy.shape.network.network_v3.business.user_group":             true,
	"com.gliffy.shape.ui.ui_v3.navigation.navbar":                         true,
	"com.gliffy.shape.ui.ui_v3.navigation.navbar_vertical":                true,
	"com.gliffy.shape.ui.ui_v3.forms_controls.dropdown":                   true,
	"com.gliffy.shape.uml.uml_v2.sequence.recursive_message":              true,
}

var mindmapShapes = map[string]bool{
	"com.gliffy.shape.mindmap.mindmap_v1.default.main_topic": true,
	"com.gliffy.shape.mindmap.mindmap_v1.default.subtopic":   true,
	"com.gliffy.shape.mindmap.mindmap_v1.default.child_node": true,
}

var fillColorIsStroke = map[string]bool{
	"com.gliffy.stencil.rectangle.no_fill_line_bottom_2px_off": true,
}

var deeplyNestedLinks = map[string]bool{
	"com.gliffy.shape.basic.basic_v1.default.chevron_box_right":  true,
	"com.gliffy.shape.basic.basic_v1.default.chevron_box_left":   true,
	"com.gliffy.shape.basic.basic_v1.default.chevron_tail_right": true,
	"com.gliffy.shape.basic.basic_v1.default.chevron_tail_left":  true,
}

var shapeCoordFix = map[string][]float64{
	"com.gliffy.shape.flowchart.flowchart_v1.default.paper_tape":        {0, -0.1, 0, 0.2},
	"com.gliffy.shape.uml.uml_v1.default.node":                          {0, -10, 10, 10},
	"com.gliffy.shape.uml.uml_v2.deployment.node":                       {0, -10, 10, 10},
	"com.gliffy.shape.uml.uml_v2.deployment.device_node":                {0, -10, 10, 10},
	"com.gliffy.shape.uml.uml_v2.deployment.execution_environment_node": {0, -10, 10, 10},
	"com.gliffy.shape.flowchart.flowchart_v1.default.data_storage":      {0, 0, 0.115, 0},
	"com.gliffy.shape.flowchart.flowchart_v1.default.database":          {0, 0, 0, 0.15},
	"com.gliffy.stencil.entity_lifeline.uml_v2":                         {10, 0, -20, 0},
	"com.gliffy.stencil.boundary_lifeline.uml_v2":                       {35, 0, -70, 0},
	"com.gliffy.stencil.control_lifeline.uml_v2":                        {10, 0, -20, 0},
	"com.gliffy.shape.ui.ui_v3.containers_content.browser":              {0, -40, 0, 40, 1},
}

var forceConstraintsShapes = map[string]bool{
	"com.gliffy.shape.uml.uml_v2.class.association": true,
}
