package gliffy2drawio

import (
	"encoding/xml"
	"math"
	"strings"
)

type MxGraph struct {
	Model         *MxGraphModel
	nextID        int
	root          *MxCell
	defaultParent *MxCell
}

func NewMxGraph() *MxGraph {
	root := &MxCell{ID: "0"}
	layer := &MxCell{ID: "1", Parent: "0"}
	graph := &MxGraph{
		Model: &MxGraphModel{
			Root: &mxRoot{Cells: []*MxCell{root, layer}},
		},
		nextID:        2,
		root:          root,
		defaultParent: layer,
	}
	return graph
}

func (g *MxGraph) SetDefaultParent(cell *MxCell) {
	g.defaultParent = cell
}

func (g *MxGraph) NextID() string {
	id := g.nextID
	g.nextID++
	return intToString(id)
}

func (g *MxGraph) AddCell(cell *MxCell, parent *MxCell) {
	if cell == nil {
		return
	}
	// Allow preassigned IDs; otherwise generate.
	if cell.ID == "" {
		cell.ID = g.NextID()
	}
	if parent == nil {
		parent = g.defaultParent
	}
	cell.Parent = parent.ID
	g.Model.Root.Cells = append(g.Model.Root.Cells, cell)
}

// HasCell returns true if a cell with the given ID exists in the root.
func (g *MxGraph) HasCell(id string) bool {
	for _, cell := range g.Model.Root.Cells {
		if cell != nil && cell.ID == id {
			return true
		}
	}
	return false
}

type MxGraphModel struct {
	XMLName    xml.Name `xml:"mxGraphModel"`
	Style      string   `xml:"style,attr,omitempty"`
	Background string   `xml:"background,attr,omitempty"`
	Grid       int      `xml:"grid,attr,omitempty"`
	GridSize   int      `xml:"gridSize,attr,omitempty"`
	Guides     int      `xml:"guides,attr,omitempty"`
	Dx         int      `xml:"dx,attr,omitempty"`
	Dy         int      `xml:"dy,attr,omitempty"`
	Connect    int      `xml:"connect,attr,omitempty"`
	Arrows     int      `xml:"arrows,attr,omitempty"`
	Fold       int      `xml:"fold,attr,omitempty"`
	Page       int      `xml:"page,attr,omitempty"`
	PageScale  int      `xml:"pageScale,attr,omitempty"`
	PageWidth  int      `xml:"pageWidth,attr,omitempty"`
	PageHeight int      `xml:"pageHeight,attr,omitempty"`
	Tooltips   int      `xml:"tooltips,attr,omitempty"`
	Math       int      `xml:"math,attr,omitempty"`
	Shadow     int      `xml:"shadow,attr,omitempty"`
	Root       *mxRoot  `xml:"root"`
}

type mxRoot struct {
	Cells []*MxCell
}

type UserObject struct {
	Label   string
	Link    string
	Tooltip string
}

type MxCell struct {
	ID     string
	Value  string
	Style  string
	Vertex bool
	Edge   bool
	Parent string
	Source string
	Target string

	Geometry   *MxGeometry
	UserObject *UserObject
}

type MxGeometry struct {
	X        float64 `xml:"x,attr,omitempty"`
	Y        float64 `xml:"y,attr,omitempty"`
	Width    float64 `xml:"width,attr,omitempty"`
	Height   float64 `xml:"height,attr,omitempty"`
	Relative int     `xml:"relative,attr,omitempty"`
	As       string  `xml:"as,attr,omitempty"`

	Points      *MxPointArray `xml:"Array,omitempty"`
	SourcePoint *MxPoint      `xml:"-"`
	TargetPoint *MxPoint      `xml:"-"`
}

type MxPoint struct {
	X  float64 `xml:"x,attr,omitempty"`
	Y  float64 `xml:"y,attr,omitempty"`
	As string  `xml:"as,attr,omitempty"`
}

type MxPointArray struct {
	As     string     `xml:"as,attr,omitempty"`
	Points []*MxPoint `xml:"mxPoint"`
}

// MarshalXML customizes geometry output to allow multiple mxPoint children.
func (g *MxGeometry) MarshalXML(enc *xml.Encoder, start xml.StartElement) error {
	if start.Name.Local == "" {
		start.Name.Local = "mxGeometry"
	}
	as := g.As
	if as == "" {
		as = "geometry"
	}
	start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "as"}, Value: as})
	if g.X != 0 {
		start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "x"}, Value: floatToString(g.X)})
	}
	if g.Y != 0 {
		start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "y"}, Value: floatToString(g.Y)})
	}
	if g.Width != 0 {
		start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "width"}, Value: floatToString(g.Width)})
	}
	if g.Height != 0 {
		start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "height"}, Value: floatToString(g.Height)})
	}
	if g.Relative != 0 {
		start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "relative"}, Value: intToString(g.Relative)})
	}

	if err := enc.EncodeToken(start); err != nil {
		return err
	}

	if g.Points != nil && len(g.Points.Points) > 0 {
		asVal := g.Points.As
		if asVal == "" {
			asVal = "points"
		}
		arrStart := xml.StartElement{Name: xml.Name{Local: "Array"}, Attr: []xml.Attr{{Name: xml.Name{Local: "as"}, Value: asVal}}}
		if err := enc.EncodeToken(arrStart); err != nil {
			return err
		}
		for _, p := range g.Points.Points {
			if p == nil {
				continue
			}
			if err := enc.EncodeElement(p, xml.StartElement{Name: xml.Name{Local: "mxPoint"}}); err != nil {
				return err
			}
		}
		if err := enc.EncodeToken(xml.EndElement{Name: arrStart.Name}); err != nil {
			return err
		}
	}

	if g.SourcePoint != nil {
		p := *g.SourcePoint
		if p.As == "" {
			p.As = "sourcePoint"
		}
		if err := enc.EncodeElement(p, xml.StartElement{Name: xml.Name{Local: "mxPoint"}}); err != nil {
			return err
		}
	}
	if g.TargetPoint != nil {
		p := *g.TargetPoint
		if p.As == "" {
			p.As = "targetPoint"
		}
		if err := enc.EncodeElement(p, xml.StartElement{Name: xml.Name{Local: "mxPoint"}}); err != nil {
			return err
		}
	}

	return enc.EncodeToken(xml.EndElement{Name: start.Name})
}

// MarshalXML customizes how the mxRoot emits cells, handling UserObject wrappers.
func (r *mxRoot) MarshalXML(enc *xml.Encoder, start xml.StartElement) error {
	start.Name.Local = "root"
	if err := enc.EncodeToken(start); err != nil {
		return err
	}
	for _, cell := range r.Cells {
		if cell == nil {
			continue
		}
		if cell.UserObject != nil {
			uoStart := xml.StartElement{Name: xml.Name{Local: "UserObject"}}
			if cell.UserObject.Label != "" {
				uoStart.Attr = append(uoStart.Attr, xml.Attr{Name: xml.Name{Local: "label"}, Value: cell.UserObject.Label})
			}
			if cell.UserObject.Link != "" {
				uoStart.Attr = append(uoStart.Attr, xml.Attr{Name: xml.Name{Local: "link"}, Value: cell.UserObject.Link})
			}
			if cell.UserObject.Tooltip != "" {
				uoStart.Attr = append(uoStart.Attr, xml.Attr{Name: xml.Name{Local: "tooltip"}, Value: cell.UserObject.Tooltip})
			}
			if err := enc.EncodeToken(uoStart); err != nil {
				return err
			}
			if err := enc.EncodeElement(cell.alias(), xml.StartElement{Name: xml.Name{Local: "mxCell"}}); err != nil {
				return err
			}
			if err := enc.EncodeToken(xml.EndElement{Name: uoStart.Name}); err != nil {
				return err
			}
			continue
		}
		if err := enc.EncodeElement(cell.alias(), xml.StartElement{Name: xml.Name{Local: "mxCell"}}); err != nil {
			return err
		}
	}
	return enc.EncodeToken(xml.EndElement{Name: start.Name})
}

func (c *MxCell) alias() cellAlias {
	return cellAlias{
		ID:       c.ID,
		Value:    c.Value,
		Style:    c.Style,
		Vertex:   boolToAttr(c.Vertex),
		Edge:     boolToAttr(c.Edge),
		Parent:   c.Parent,
		Source:   c.Source,
		Target:   c.Target,
		Geometry: c.geometryWithDefault(),
	}
}

func (c *MxCell) geometryWithDefault() *MxGeometry {
	if c.Geometry == nil {
		return nil
	}
	g := *c.Geometry
	if g.As == "" {
		g.As = "geometry"
	}
	if g.Points != nil && g.Points.As == "" {
		g.Points.As = "points"
	}
	return &g
}

type cellAlias struct {
	ID       string      `xml:"id,attr"`
	Value    string      `xml:"value,attr,omitempty"`
	Style    string      `xml:"style,attr,omitempty"`
	Vertex   string      `xml:"vertex,attr,omitempty"`
	Edge     string      `xml:"edge,attr,omitempty"`
	Parent   string      `xml:"parent,attr,omitempty"`
	Source   string      `xml:"source,attr,omitempty"`
	Target   string      `xml:"target,attr,omitempty"`
	Geometry *MxGeometry `xml:"mxGeometry,omitempty"`
}

func boolToAttr(v bool) string {
	if v {
		return "1"
	}
	return ""
}

// rotateGeometry rotates a rectangle around the given pivot.
func rotateGeometry(geo *MxGeometry, rotation, cx, cy float64) {
	if geo == nil || rotation == 0 {
		return
	}
	rads := rotation * math.Pi / 180
	cos := math.Cos(rads)
	sin := math.Sin(rads)
	x := geo.X - cx
	y := geo.Y - cy
	geo.X = x*cos - y*sin + cx
	geo.Y = x*sin + y*cos + cy
}

// ToXML renders the graph model to draw.io compatible XML.
func (g *MxGraph) ToXML(style, background string, grid, guides bool, width, height float64) (string, error) {
	model := g.Model
	model.Style = style
	model.Background = background
	if grid {
		model.Grid = 1
	}
	if guides {
		model.Guides = 1
	}
	model.GridSize = 10
	model.Connect = 1
	model.Arrows = 1
	model.Fold = 1
	model.Page = 1
	model.PageScale = 1
	model.Dx = 0
	model.Dy = 0
	if width == 0 {
		width = 850
	}
	if height == 0 {
		height = 1100
	}
	model.PageWidth = int(width)
	model.PageHeight = int(height)
	model.Tooltips = 1
	model.Math = 0
	model.Shadow = 0

	output, err := xml.Marshal(model)
	if err != nil {
		return "", err
	}
	return string(output), nil
}

// NormalizeOrigin shifts all absolute coordinates so the top-left-most point is at (0,0).
func (g *MxGraph) NormalizeOrigin() {
	minX, minY := math.MaxFloat64, math.MaxFloat64

	for _, cell := range g.Model.Root.Cells {
		if cell == nil || cell.Geometry == nil {
			continue
		}
		geo := cell.Geometry
		if geo.Relative == 0 {
			if geo.X < minX {
				minX = geo.X
			}
			if geo.Y < minY {
				minY = geo.Y
			}
		}
		if geo.Points != nil {
			for _, p := range geo.Points.Points {
				if p == nil {
					continue
				}
				if p.X < minX {
					minX = p.X
				}
				if p.Y < minY {
					minY = p.Y
				}
			}
		}
		if geo.SourcePoint != nil {
			if geo.SourcePoint.X < minX {
				minX = geo.SourcePoint.X
			}
			if geo.SourcePoint.Y < minY {
				minY = geo.SourcePoint.Y
			}
		}
		if geo.TargetPoint != nil {
			if geo.TargetPoint.X < minX {
				minX = geo.TargetPoint.X
			}
			if geo.TargetPoint.Y < minY {
				minY = geo.TargetPoint.Y
			}
		}
	}

	if minX == math.MaxFloat64 || minY == math.MaxFloat64 {
		return
	}

	dx, dy := -minX, -minY

	for _, cell := range g.Model.Root.Cells {
		if cell == nil || cell.Geometry == nil {
			continue
		}
		geo := cell.Geometry
		if geo.Relative == 0 {
			geo.X += dx
			geo.Y += dy
		}
		if geo.Points != nil {
			for _, p := range geo.Points.Points {
				if p == nil {
					continue
				}
				p.X += dx
				p.Y += dy
			}
		}
		if geo.SourcePoint != nil {
			geo.SourcePoint.X += dx
			geo.SourcePoint.Y += dy
		}
		if geo.TargetPoint != nil {
			geo.TargetPoint.X += dx
			geo.TargetPoint.Y += dy
		}
	}
}

// helper to ensure geometry has relative flag set to 1.
func setRelative(geo *MxGeometry, v bool) {
	if geo == nil {
		return
	}
	if v {
		geo.Relative = 1
	} else {
		geo.Relative = 0
	}
}

// linearly interpolated center used for rotations.
func midpoint(p1, p2 *MxPoint) *MxPoint {
	return &MxPoint{
		X: (p1.X + p2.X) / 2,
		Y: (p1.Y + p2.Y) / 2,
	}
}

// joinStyle appends style fragments with trailing semicolon when needed.
func joinStyle(parts ...string) string {
	var sb strings.Builder
	for _, p := range parts {
		if p == "" {
			continue
		}
		if !strings.HasSuffix(p, ";") {
			p += ";"
		}
		sb.WriteString(p)
	}
	return sb.String()
}
