package gliffy2drawio

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

type GliffyDiagramConverter struct {
	diagramString string
	gliffyDiagram Diagram
	drawioDiagram *MxGraph
	vertices      map[string]*GliffyObject
	layers        map[string]*GliffyLayer

	rotationPattern *regexp.Regexp
	pageIDPattern   *regexp.Regexp
	namePattern     *regexp.Regexp

	report     strings.Builder
	translator *StencilTranslator
}

func NewGliffyDiagramConverter(gliffyDiagramString string) (*GliffyDiagramConverter, error) {
	c := &GliffyDiagramConverter{
		diagramString:   gliffyDiagramString,
		drawioDiagram:   NewMxGraph(),
		vertices:        make(map[string]*GliffyObject),
		layers:          make(map[string]*GliffyLayer),
		rotationPattern: regexp.MustCompile(`rotation=(\-?\w+)`),
		pageIDPattern:   regexp.MustCompile(`pageId=([^&]+)`),
		namePattern:     regexp.MustCompile(`name=([^&]+)`),
		translator:      NewStencilTranslator(""),
	}
	if err := c.start(); err != nil {
		return nil, err
	}
	return c, nil
}

func (c *GliffyDiagramConverter) start() error {
	if err := json.Unmarshal([]byte(c.diagramString), &c.gliffyDiagram); err != nil {
		return err
	}

	// Support newer Gliffy files that wrap content in pages/scene.
	if len(c.gliffyDiagram.Stage.Objects) == 0 && len(c.gliffyDiagram.Pages) > 0 {
		selected := c.gliffyDiagram.Pages[0]
		if c.gliffyDiagram.DefaultPage != "" {
			for _, p := range c.gliffyDiagram.Pages {
				if p.ID == c.gliffyDiagram.DefaultPage {
					selected = p
					break
				}
			}
		}
		scene := selected.Scene
		c.gliffyDiagram.Stage = Stage{
			Background:      scene.Background,
			Width:           scene.Width,
			Height:          scene.Height,
			AutoFit:         scene.AutoFit,
			GridOn:          scene.GridOn,
			DrawingGuidesOn: scene.DrawingGuidesOn,
			Objects:         scene.Objects,
			Layers:          scene.Layers,
			TextStyles:      scene.TextStyles,
		}
	}

	c.collectLayersAndConvert(c.layers, c.gliffyDiagram.Stage.Layers)
	c.collectVerticesAndConvert(c.vertices, c.gliffyDiagram.Stage.Objects, nil)

	sortObjectsByOrder(c.gliffyDiagram.Stage.Objects)

	c.importLayers()
	for _, obj := range c.gliffyDiagram.Stage.Objects {
		if err := c.importObject(obj, obj.Parent); err != nil {
			c.report.WriteString("-- Warning, Object " + obj.ID + " cannot be transformed.\n")
		}
	}
	// Shift everything to origin to avoid bottom-right offset in draw.io.
	c.drawioDiagram.NormalizeOrigin()
	return nil
}

func (c *GliffyDiagramConverter) importLayers() {
	if len(c.gliffyDiagram.Stage.Layers) == 0 {
		return
	}
	sortLayersByOrder(c.gliffyDiagram.Stage.Layers)
	for i, layer := range c.gliffyDiagram.Stage.Layers {
		// Avoid duplicating the default layer (id=1 already in root).
		if i == 0 && layer.MxObject != nil && layer.MxObject.ID == "1" && c.drawioDiagram.HasCell("1") {
			continue
		}
		c.drawioDiagram.AddCell(layer.MxObject, c.drawioDiagram.root)
		if i == 0 {
			c.drawioDiagram.SetDefaultParent(layer.MxObject)
		}
	}
}

func (c *GliffyDiagramConverter) importObject(obj *GliffyObject, gliffyParent *GliffyObject) error {
	var parent *MxCell
	if gliffyParent != nil {
		parent = gliffyParent.MxObject
	}
	if parent == nil && obj.LayerID != nil {
		if layer, ok := c.layers[*obj.LayerID]; ok && layer != nil {
			parent = layer.MxObject
		}
	}

	c.drawioDiagram.AddCell(obj.MxObject, parent)

	if obj.hasChildren() {
		if obj.isSwimlane() {
			if obj.Rotation != 0 {
				reverse(obj.Children)
			}
		} else {
			sortObjectsByOrder(obj.Children)
		}
		for _, child := range obj.Children {
			if err := c.importObject(child, obj); err != nil {
				return err
			}
		}
	}

	if obj.isLine() {
		startTerminal := c.getTerminalCell(obj, true)
		endTerminal := c.getTerminalCell(obj, false)
		if startTerminal != nil {
			obj.MxObject.Source = startTerminal.ID
		}
		if endTerminal != nil {
			obj.MxObject.Target = endTerminal.ID
		}
		c.setWaypoints(obj, startTerminal, endTerminal)
	}
	return nil
}

func (c *GliffyDiagramConverter) getTerminalCell(edge *GliffyObject, start bool) *MxCell {
	cons := edge.GetConstraints()
	if cons == nil {
		return nil
	}
	var con *Constraint
	if start {
		con = cons.StartConstraint
	} else {
		con = cons.EndConstraint
	}
	if con == nil {
		return nil
	}
	var data *ConstraintData
	if start {
		data = con.StartPositionConstraint
	} else {
		data = con.EndPositionConstraint
	}
	if data == nil {
		return nil
	}
	terminal := c.vertices[data.NodeID]
	if terminal == nil {
		return nil
	}
	return terminal.MxObject
}

func (c *GliffyDiagramConverter) collectLayersAndConvert(layerMap map[string]*GliffyLayer, layers []*GliffyLayer) {
	if len(layers) == 0 {
		return
	}

	// Ensure only a single default layer (id=1, parent=0) exists.
	if len(c.drawioDiagram.Model.Root.Cells) > 2 {
		trimmed := []*MxCell{}
		for _, cell := range c.drawioDiagram.Model.Root.Cells {
			if cell == nil {
				continue
			}
			if cell.ID == "0" || cell.ID == "1" {
				trimmed = append(trimmed, cell)
			}
		}
		if len(trimmed) < 2 {
			trimmed = append(trimmed, &MxCell{ID: "1", Parent: c.drawioDiagram.root.ID})
		}
		c.drawioDiagram.Model.Root.Cells = trimmed
	}

	// Reuse existing default layer (id 1, parent 0) for the first Gliffy layer to match draw.io expectations.
	var defaultLayer *MxCell
	if len(c.drawioDiagram.Model.Root.Cells) >= 2 {
		if cell := c.drawioDiagram.Model.Root.Cells[1]; cell != nil && cell.Parent == c.drawioDiagram.root.ID {
			defaultLayer = cell
		}
	}
	if defaultLayer == nil {
		defaultLayer = &MxCell{ID: "1", Parent: c.drawioDiagram.root.ID}
		c.drawioDiagram.Model.Root.Cells = append(c.drawioDiagram.Model.Root.Cells, defaultLayer)
	}

	for i, layer := range layers {
		var cell *MxCell
		if i == 0 {
			cell = defaultLayer
		} else {
			cell = &MxCell{}
			c.drawioDiagram.AddCell(cell, c.drawioDiagram.root)
		}
		cell.Value = layer.Name
		if layer.Locked {
			cell.Style = joinStyle(cell.Style, "locked=1")
		}
		if !layer.Visible {
			cell.Style = joinStyle(cell.Style, "visible=0")
		}
		layer.MxObject = cell
		layerMap[layer.GUID] = layer
		if i == 0 {
			c.drawioDiagram.SetDefaultParent(cell)
		}
	}
}

func (c *GliffyDiagramConverter) collectVerticesAndConvert(vertices map[string]*GliffyObject, objects []*GliffyObject, parent *GliffyObject) {
	for _, obj := range objects {
		obj.Parent = parent
		obj.MxObject = c.convertGliffyObject(obj, parent)
		if !obj.isLine() {
			vertices[obj.ID] = obj
		}
		if obj.isGroup() || obj.isSelection() || (obj.isLine() && obj.hasChildren()) {
			c.collectVerticesAndConvert(vertices, obj.Children, obj)
		}
	}
}

func (c *GliffyDiagramConverter) GraphXML() (string, error) {
	grid := c.gliffyDiagram.Stage.GridOn
	guides := c.gliffyDiagram.Stage.DrawingGuidesOn
	w := c.gliffyDiagram.Stage.Width
	h := c.gliffyDiagram.Stage.Height
	if w == 0 {
		w = 1200
	}
	if h == 0 {
		h = 900
	}
	xml, err := c.drawioDiagram.ToXML("default-style2", c.gliffyDiagram.Stage.Background, grid, guides, w, h)
	if err != nil {
		return "", err
	}
	name := c.diagramName()
	return fmt.Sprintf(`<mxfile><diagram name="%s">%s</diagram></mxfile>`, html.EscapeString(name), xml), nil
}

func (c *GliffyDiagramConverter) diagramName() string {
	if c.gliffyDiagram.Title != "" {
		return c.gliffyDiagram.Title
	}
	if len(c.gliffyDiagram.Pages) > 0 {
		selected := c.gliffyDiagram.Pages[0]
		if c.gliffyDiagram.DefaultPage != "" {
			for _, p := range c.gliffyDiagram.Pages {
				if p.ID == c.gliffyDiagram.DefaultPage {
					selected = p
					break
				}
			}
		}
		if selected.Title != "" {
			return selected.Title
		}
	}
	if c.gliffyDiagram.Metadata.Title != "" {
		return c.gliffyDiagram.Metadata.Title
	}
	return "Page-1"
}

func (c *GliffyDiagramConverter) convertGliffyObject(obj *GliffyObject, parent *GliffyObject) *MxCell {
	cell := &MxCell{}
	if obj.ID != "" {
		cell.ID = obj.ID
	}
	if obj.IsUnrecognizedGraphicType() {
		return cell
	}

	var style strings.Builder
	geo := &MxGeometry{X: obj.X, Y: obj.Y, Width: obj.Width, Height: obj.Height}
	obj.adjustGeo(geo)
	cell.Geometry = geo

	var textObject *GliffyObject
	var link string
	graphic := obj.GraphicOrChildGraphic()
	translatedStyle := c.translator.Translate(obj.UID, obj.TID)

	if obj.isGroup() {
		if graphic == nil || translatedStyle == "" {
			style.WriteString("group;")
		}
		cell.Vertex = true
	} else {
		textObject = obj.TextObject()
	}

	if graphic != nil {
		link = obj.AdjustedLink()
		switch graphic.GetType() {
		case GraphicTypeShape, GraphicTypeMindmap:
			shape := graphic.Shape
			if shape == nil {
				shape = &GliffyShape{}
			}
			cell.Vertex = true
			isChevron := strings.Contains(obj.UID, "chevron")
			if translatedStyle != "" {
				style.WriteString("shape=" + translatedStyle + ";")
			}
			if !strings.Contains(style.String(), "shadow=") {
				style.WriteString("shadow=" + intToString(boolToInt(shape.DropShadow)) + ";")
			}
			if !strings.Contains(style.String(), "strokeWidth") {
				style.WriteString("strokeWidth=" + intToString(shape.StrokeWidthValue()) + ";")
				if shape.StrokeWidthValue() == 0 && !isChevron {
					style.WriteString("strokeColor=none;")
				}
			}
			if !strings.Contains(style.String(), "fillColor") {
				if shape.NoFill() && !isChevron {
					style.WriteString("fillColor=none;")
				} else {
					style.WriteString("fillColor=" + shape.FillColor + ";")
				}
				if shape.FillColor == "none" {
					style.WriteString("pointerEvents=0;")
				}
			}
			if !strings.Contains(style.String(), "strokeColor") && !shape.NoFill() {
				stroke := shape.StrokeColor
				if obj.IsUseFillColorForStroke() {
					stroke = shape.FillColor
				}
				style.WriteString("strokeColor=" + stroke + ";")
			}
			if !strings.Contains(style.String(), "gradient") && shape.Gradient && !obj.GradientIgnored() {
				style.WriteString("gradientColor=" + obj.GradientColor() + ";gradientDirection=north;")
			}
			if !obj.isVennCircle() && !strings.Contains(style.String(), "opacity") {
				style.WriteString("opacity=" + floatToString(shape.Opacity*100) + ";")
			}
			style.WriteString(DashStyleMapping(shape.DashStyle, 1))
			if obj.IsSubRoutine() && obj.Width != 0 {
				style.WriteString("size=" + floatToString(10/obj.Width) + ";")
			}
			if fragment := obj.UMLSequenceCombinedFragmentText(); fragment != "" && len(obj.Children) > 0 {
				cell.Value = fragment
				obj.Children = obj.Children[1:]
			}
		case GraphicTypeLine:
			if graphic.Line == nil {
				break
			}
			line := graphic.Line
			cell.Edge = true
			style.WriteString("shape=filledEdge;")
			style.WriteString("strokeWidth=" + intToString(line.StrokeWidthValue()) + ";")
			style.WriteString("strokeColor=" + line.StrokeColor + ";")
			style.WriteString("fillColor=" + line.FillColor + ";")
			style.WriteString(ArrowMapping(line.StartArrow).ToString(true))
			style.WriteString(ArrowMapping(line.EndArrow).ToString(false))
			if line.CornerRadius != nil {
				style.WriteString("rounded=1;")
			} else {
				style.WriteString("rounded=0;")
			}
			style.WriteString(DashStyleMapping(line.DashStyle, line.StrokeWidthValue()))
			style.WriteString(LineMapping(line.Interpolation))
			cell.Geometry.X = 0
			cell.Geometry.Y = 0
		case GraphicTypeText:
			textObject = obj
			cell.Vertex = true
			style.WriteString("text;html=1;nl2Br=0;")
			cell.Value = obj.TextHTML()
			if obj.Parent != nil && !obj.Parent.isGroup() {
				parentGeo := obj.Parent.MxObject.Geometry
				if obj.Parent.isLine() {
					mxGeo := &MxGeometry{X: 0, Y: 0, Width: 0, Height: 0}
					lineT := 0.0
					if graphic.Text != nil {
						lineT = graphic.Text.LineTValue*2 - 1
					}
					mxGeo.X = lineT
					var lblY, lblX float64
					if graphic.Text != nil && graphic.Text.LinePerpValue != nil {
						control := obj.Parent.Graphic.Line.ControlPath
						if len(control) >= 2 {
							i1 := 0
							i2 := len(control) - 1
							noCardinal := false
							switch graphic.Text.CardinalityType {
							case "begin":
								i2 = 1
							case "end":
								i1 = len(control) - 2
							default:
								noCardinal = true
							}
							if noCardinal || control[i1][1] == control[i2][1] {
								lblY = *graphic.Text.LinePerpValue
								if control[i1][0]-control[i2][0] > 0 {
									lblY = -lblY
								}
							} else {
								lblX = *graphic.Text.LinePerpValue
								if control[i1][1]-control[i2][1] < 0 {
									lblX = -lblX
								}
							}
						}
					}
					mxGeo.SourcePoint = &MxPoint{X: lblX, Y: lblY}
					setRelative(mxGeo, true)
					cell.Geometry = mxGeo
					style.WriteString("labelBackgroundColor=" + c.gliffyDiagram.Stage.Background + ";")
					if graphic.Text != nil {
						graphic.Text.SetHAlign("")
					}
				} else {
					cell.Geometry = &MxGeometry{X: 0, Y: 0, Width: parentGeo.Width, Height: parentGeo.Height}
					setRelative(cell.Geometry, true)
				}
			}
		case GraphicTypeImage:
			img := graphic.Image
			cell.Vertex = true
			style.WriteString("shape=" + c.translator.Translate(obj.UID, obj.TID) + ";")
			style.WriteString("image=" + img.CleanURL() + ";")
		case GraphicTypeSVG:
			svg := graphic.Svg
			cell.Vertex = true
			style.WriteString("shape=image;imageAspect=0;")
			if svg != nil && svg.EmbeddedResourceID != nil {
				if res, ok := c.gliffyDiagram.EmbeddedResources.Get(*svg.EmbeddedResourceID); ok {
					svgUtil := SVGImporterUtils{}
					data := svgUtil.SetViewBox(res.Data)
					style.WriteString("image=data:image/svg+xml," + EmbeddedResource{Data: data}.Base64EncodedData() + ";")
				}
			}
		default:
			// Fallback: render as image if possible or apply translated style if found.
			if graphic.Image != nil && graphic.Image.URL != "" {
				cell.Vertex = true
				style.WriteString("shape=image;imageAspect=0;image=" + graphic.Image.CleanURL() + ";")
			} else if graphic.Svg != nil && graphic.Svg.EmbeddedResourceID != nil {
				cell.Vertex = true
				style.WriteString("shape=image;imageAspect=0;")
				if res, ok := c.gliffyDiagram.EmbeddedResources.Get(*graphic.Svg.EmbeddedResourceID); ok {
					svgUtil := SVGImporterUtils{}
					data := svgUtil.SetViewBox(res.Data)
					style.WriteString("image=data:image/svg+xml," + EmbeddedResource{Data: data}.Base64EncodedData() + ";")
				}
			} else if translatedStyle != "" {
				cell.Vertex = true
				style.WriteString("shape=" + translatedStyle + ";")
			}
		}
	} else if obj.isSwimlane() && len(obj.Children) > 0 {
		cell.Vertex = true
		style.WriteString(c.translator.Translate(obj.UID, "") + ";")
		if obj.Rotation == 0 {
			style.WriteString("childLayout=stackLayout;resizeParent=1;resizeParentMax=0;")
		}
		header := obj.Children[0]
		shape := header.Graphic.Shape
		style.WriteString("strokeWidth=" + intToString(shape.StrokeWidthValue()) + ";")
		style.WriteString("shadow=" + intToString(boolToInt(shape.DropShadow)) + ";")
		style.WriteString("fillColor=" + shape.FillColor + ";")
		style.WriteString("strokeColor=" + shape.StrokeColor + ";")
		style.WriteString("startSize=" + floatToString(header.Height) + ";")
		style.WriteString("whiteSpace=wrap;")

		for i := 1; i < len(obj.Children); i++ {
			gLane := obj.Children[i]
			gLane.Parent = obj
			gs := gLane.Graphic.Shape
			var laneStyle strings.Builder
			laneStyle.WriteString("swimlane;collapsible=0;swimlaneLine=0;")
			laneStyle.WriteString("strokeWidth=" + intToString(gs.StrokeWidthValue()) + ";")
			laneStyle.WriteString("shadow=" + intToString(boolToInt(gs.DropShadow)) + ";")
			laneStyle.WriteString("fillColor=" + gs.FillColor + ";")
			laneStyle.WriteString("strokeColor=" + gs.StrokeColor + ";")
			laneStyle.WriteString("whiteSpace=wrap;html=1;fontStyle=0;")

			childGeometry := &MxGeometry{X: gLane.X, Y: gLane.Y, Width: gLane.Width, Height: gLane.Height}
			if obj.Rotation != 0 {
				if obj.Rotation == 270 {
					laneStyle.WriteString("horizontal=0;")
					w := childGeometry.Width
					childGeometry.Width = childGeometry.Height
					childGeometry.Height = w
					x := childGeometry.X
					childGeometry.X = childGeometry.Y
					childGeometry.Y = obj.Width - w - x
				} else {
					laneStyle.WriteString("rotation=" + floatToString(obj.Rotation) + ";")
					rotateGeometry(childGeometry, obj.Rotation, obj.Width/2, obj.Height/2)
				}
			}
			mxLane := &MxCell{Vertex: true}
			laneTxt := gLane.Children[0]
			mxLane.Value = laneTxt.TextHTML()
			if laneTxt.Graphic != nil && laneTxt.Graphic.Text != nil {
				laneStyle.WriteString(laneTxt.Graphic.Text.GetStyle(0, 0))
			}
			laneStyle.WriteString("gliffyId=" + gLane.ID + ";")
			mxLane.Style = laneStyle.String()
			mxLane.Geometry = childGeometry
			gLane.MxObject = mxLane
			cell.Geometry = geo
		}
	} else if obj.isMindmap() && len(obj.Children) > 0 {
		rectangle := obj.Children[0]
		if rectangle.Graphic == nil || rectangle.Graphic.Mindmap == nil {
			obj.MxObject = cell
			return cell
		}
		mindmap := rectangle.Graphic.Mindmap
		style.WriteString("shape=" + c.translator.Translate(obj.UID, "") + ";")
		style.WriteString("shadow=" + intToString(boolToInt(mindmap.DropShadow)) + ";")
		style.WriteString("strokeWidth=" + intToString(mindmap.StrokeWidthValue()) + ";")
		style.WriteString("fillColor=" + mindmap.FillColor + ";")
		style.WriteString("strokeColor=" + mindmap.StrokeColor + ";")
		style.WriteString(DashStyleMapping(mindmap.DashStyle, 1))
		if mindmap.Gradient {
			style.WriteString("gradientColor=#FFFFFF;gradientDirection=north;")
		}
		cell.Vertex = true
	}

	if !obj.isLine() {
		if strings.Contains(style.String(), "rotation") {
			if m := c.rotationPattern.FindStringSubmatch(style.String()); len(m) > 1 {
				initial, _ := strconv.ParseFloat(m[1], 64)
				rotation := initial + obj.Rotation
				styleStr := c.rotationPattern.ReplaceAllString(style.String(), "rotation="+floatToString(rotation))
				style.Reset()
				style.WriteString(styleStr)
			}
		} else if obj.Rotation != 0 {
			if strings.Contains(style.String(), "swimlane;collapsible=0;") && obj.Rotation == 270 {
				w := geo.Width
				h := geo.Height
				geo.X = geo.X + (w-h)/2
				geo.Y = geo.Y + (h-w)/2
				geo.Width = h
				geo.Height = w
				style.WriteString("childLayout=stackLayout;resizeParent=1;resizeParentMax=0;horizontal=0;horizontalStack=0;")
			} else {
				if obj.isGroup() {
					for _, child := range obj.Children {
						c.rotateGroupedObject(obj, child)
					}
				}
				style.WriteString("rotation=" + floatToString(obj.Rotation) + ";")
			}
		}
	}

	if textObject != nil {
		style.WriteString("html=1;nl2Br=0;")
		if !obj.isLine() && textObject.Graphic != nil && textObject.Graphic.Text != nil {
			txt := textObject.Graphic.Text
			if obj.isSwimlane() {
				txt.SetForceTopPaddingShift(true)
				txt.SetVAlign("middle")
			}
			cell.Value = textObject.TextHTML()
			obj.adjustTextPos(textObject)
			if obj.ContainsTextBracket() {
				c.fixFrameTextBorder(obj, &style)
				style.WriteString(strings.ReplaceAll(txt.GetStyle(0, 0), "verticalAlign=middle", "verticalAlign=top"))
			} else {
				isChevron := strings.Contains(obj.UID, "chevron")
				if textObject == obj || isChevron {
					style.WriteString(txt.GetStyle(0, 0))
				} else {
					style.WriteString(txt.GetStyle(textObject.X, textObject.Y))
				}
			}
		}
	}

	popup := c.getGliffyPopup(obj)
	if link != "" || popup != nil {
		userObj := &UserObject{}
		if link != "" {
			if lb := c.extractLightboxDataFromGliffyUrl(link); lb != nil {
				link = "/plugins/drawio/lightbox.action?ceoId=" + intToString(int(lb.Key)) + "&diagramName=" + lb.Value + ".drawio"
			}
			userObj.Link = link
			if textObject != nil {
				userObj.Label = textObject.TextHTML()
			}
		}
		if popup != nil && popup.Graphic != nil && popup.Graphic.PopupNote != nil {
			userObj.Tooltip = popup.Graphic.PopupNote.Text
		}
		cell.UserObject = userObj
	}

	style.WriteString("gliffyId=" + obj.ID + ";")
	cell.Style = style.String()
	obj.MxObject = cell
	return cell
}

func (c *GliffyDiagramConverter) getGliffyPopup(obj *GliffyObject) *GliffyObject {
	if !obj.hasChildren() {
		return nil
	}
	for _, child := range obj.Children {
		if child.Graphic != nil && child.Graphic.GetType() == GraphicTypePopup {
			return child
		}
	}
	return nil
}

func (c *GliffyDiagramConverter) rotateGroupedObject(group, child *GliffyObject) {
	pivot := &MxPoint{X: group.Width/2 - child.Width/2, Y: group.Height/2 - child.Height/2}
	temp := &MxPoint{X: child.X, Y: child.Y}
	if group.Rotation != 0 {
		rads := group.Rotation * math.Pi / 180
		cos := math.Cos(rads)
		sin := math.Sin(rads)
		temp = &MxPoint{
			X: temp.X*cos - temp.Y*sin + pivot.X,
			Y: temp.X*sin + temp.Y*cos + pivot.Y,
		}
		child.X = temp.X
		child.Y = temp.Y
		child.Rotation += group.Rotation
	}
}

func (c *GliffyDiagramConverter) fixFrameTextBorder(obj *GliffyObject, style *strings.Builder) {
	if obj.TextObject() == nil {
		return
	}
	wrong := "labelX=32"
	correct := "labelX=" + floatToString(obj.TextObject().Width*1.1)
	styleStr := strings.Replace(style.String(), wrong, correct, 1)
	style.Reset()
	style.WriteString(styleStr)
}

func (c *GliffyDiagramConverter) addConstraint(obj *GliffyObject, terminal *MxCell, source bool, orthogonal bool) bool {
	cons := obj.GetConstraints()
	if cons == nil {
		return orthogonal
	}
	var con *Constraint
	if source {
		con = cons.StartConstraint
	} else {
		con = cons.EndConstraint
	}
	if con == nil {
		return orthogonal
	}
	var data *ConstraintData
	if source {
		data = con.StartPositionConstraint
	} else {
		data = con.EndPositionConstraint
	}
	if data != nil {
		direction := getStyleValue(terminal, "direction", "east")
		temp := &MxPoint{X: data.PX, Y: data.PY}
		rotation := 0
		switch strings.ToLower(direction) {
		case "south":
			rotation = 270
		case "west":
			rotation = 180
		case "north":
			rotation = 90
		}
		if rotation != 0 {
			rad := float64(rotation) * math.Pi / 180
			temp = &MxPoint{
				X: temp.X*math.Cos(rad) - temp.Y*math.Sin(rad),
				Y: temp.X*math.Sin(rad) + temp.Y*math.Cos(rad),
			}
		}
		if !orthogonal || (temp.X == 0.5 && temp.Y == 0.5) || obj.ForceConstraints() {
			cell := obj.MxObject
			if source {
				cell.Style += "exitX=" + floatToString(temp.X) + ";exitY=" + floatToString(temp.Y) + ";exitPerimeter=0;"
			} else {
				cell.Style += "entryX=" + floatToString(temp.X) + ";entryY=" + floatToString(temp.Y) + ";entryPerimeter=0;"
			}
		}
		return true
	}
	return orthogonal
}

func (c *GliffyDiagramConverter) setWaypoints(obj *GliffyObject, start, end *MxCell) {
	if obj.Graphic == nil || obj.Graphic.Line == nil {
		return
	}
	cell := obj.MxObject
	geo := cell.Geometry
	if geo == nil {
		geo = &MxGeometry{}
		cell.Geometry = geo
	}
	setRelative(geo, true)

	points := obj.Graphic.Line.ControlPath
	if len(points) < 2 {
		return
	}
	mxPoints := make([]*MxPoint, 0, len(points))
	pivot := &MxPoint{X: obj.X + obj.Width/2, Y: obj.Y + obj.Height/2}
	for _, p := range points {
		wp := &MxPoint{X: p[0] + obj.X, Y: p[1] + obj.Y}
		if obj.Rotation != 0 {
			rads := obj.Rotation * math.Pi / 180
			cos := math.Cos(rads)
			sin := math.Sin(rads)
			wp = &MxPoint{
				X: pivot.X + (wp.X-pivot.X)*cos - (wp.Y-pivot.Y)*sin,
				Y: pivot.Y + (wp.X-pivot.X)*sin + (wp.Y-pivot.Y)*cos,
			}
		}
		mxPoints = append(mxPoints, wp)
	}

	orthogonal := true
	last := mxPoints[0]
	for _, p := range mxPoints[1:] {
		orthogonal = orthogonal && (last.X == p.X || last.Y == p.Y)
		last = p
	}

	p0 := mxPoints[0]
	pe := mxPoints[len(mxPoints)-1]
	if start == nil {
		geo.SourcePoint = &MxPoint{X: p0.X, Y: p0.Y, As: "sourcePoint"}
		mxPoints = mxPoints[1:]
	} else {
		if c.addConstraint(obj, start, true, orthogonal) {
			// keep point
		}
	}
	if end == nil {
		geo.TargetPoint = &MxPoint{X: pe.X, Y: pe.Y, As: "targetPoint"}
		mxPoints = mxPoints[:len(mxPoints)-1]
	} else {
		if c.addConstraint(obj, end, false, orthogonal) {
			// keep
		}
	}

	if orthogonal {
		cell.Style += "edgeStyle=orthogonalEdgeStyle;"
		result := make([]*MxPoint, 0, len(mxPoints))
		var prev *MxPoint
		for _, p := range mxPoints {
			if prev == nil || prev.X != p.X || prev.Y != p.Y {
				result = append(result, p)
				prev = p
			}
		}
		if len(result) == 0 && ((start == nil && end != nil) || (start != nil && end == nil)) {
			center := &MxPoint{X: p0.X + (pe.X-p0.X)/2, Y: p0.Y + (pe.Y-p0.Y)/2}
			result = []*MxPoint{center, center}
		}
		mxPoints = result
	}

	if len(mxPoints) > 0 {
		geo.Points = &MxPointArray{Points: mxPoints}
	}
	cell.Geometry = geo
}

type lightboxInfo struct {
	Key   int64
	Value string
}

func (c *GliffyDiagramConverter) extractLightboxDataFromGliffyUrl(link string) *lightboxInfo {
	pagem := c.pageIDPattern.FindStringSubmatch(link)
	namem := c.namePattern.FindStringSubmatch(link)
	if len(pagem) > 1 {
		oldPageID, err := strconv.ParseInt(pagem[1], 10, 64)
		if err != nil {
			return nil
		}
		if len(namem) > 1 {
			return &lightboxInfo{Key: oldPageID, Value: namem[1]}
		}
	}
	return nil
}

func getStyleValue(cell *MxCell, key, defaultValue string) string {
	if cell == nil {
		return defaultValue
	}
	style := cell.Style
	if style == "" {
		return defaultValue
	}
	pairs := strings.Split(style, ";")
	for _, p := range pairs {
		if p == "" {
			continue
		}
		parts := strings.SplitN(p, "=", 2)
		if len(parts) == 2 && strings.EqualFold(parts[0], key) {
			return parts[1]
		}
	}
	return defaultValue
}

func sortObjectsByOrder(objs []*GliffyObject) {
	sort.SliceStable(objs, func(i, j int) bool {
		o1 := objs[i].Order.String()
		o2 := objs[j].Order.String()
		if o1 == "" && o2 == "" {
			return false
		}
		if o1 == "" {
			return false
		}
		if o2 == "" {
			return true
		}
		f1, err1 := strconv.ParseFloat(o1, 64)
		f2, err2 := strconv.ParseFloat(o2, 64)
		if err1 == nil && err2 == nil {
			return f1 < f2
		}
		return o1 < o2
	})
}

func sortLayersByOrder(layers []*GliffyLayer) {
	sort.SliceStable(layers, func(i, j int) bool {
		return layers[i].Order < layers[j].Order
	})
}

func reverse(objs []*GliffyObject) {
	for i, j := 0, len(objs)-1; i < j; i, j = i+1, j-1 {
		objs[i], objs[j] = objs[j], objs[i]
	}
}

func boolToInt(v bool) int {
	if v {
		return 1
	}
	return 0
}

// Convenience entry point for users wanting a single call.
func ConvertGliffyJSONToDrawioXML(gliffyJSON string) (string, error) {
	converter, err := NewGliffyDiagramConverter(gliffyJSON)
	if err != nil {
		return "", err
	}
	return converter.GraphXML()
}
