// Copyright ©2018 The Gonum Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package cytoscapejs implements marshaling and unmarshaling of Cytoscape.js JSON documents.
//
// See http://js.cytoscape.org/ for Cytoscape.js documentation.
package cytoscapejs // import "gonum.org/v1/gonum/graph/formats/cytoscapejs"

import (
	"encoding/json"
	"errors"
	"fmt"
)

// GraphElem is a Cytoscape.js graph with mixed graph elements.
type GraphElem struct {
	Elements []Element     `json:"elements"`
	Layout   interface{}   `json:"layout,omitempty"`
	Style    []interface{} `json:"style,omitempty"`
}

// Element is a mixed graph element.
type Element struct {
	Group            string      `json:"group,omitempty"`
	Data             ElemData    `json:"data"`
	Position         *Position   `json:"position,omitempty"`
	RenderedPosition *Position   `json:"renderedPosition,omitempty"`
	Selected         bool        `json:"selected,omitempty"`
	Selectable       bool        `json:"selectable,omitempty"`
	Locked           bool        `json:"locked,omitempty"`
	Grabbable        bool        `json:"grabbable,omitempty"`
	Classes          string      `json:"classes,omitempty"`
	Scratch          interface{} `json:"scratch,omitempty"`
}

// ElemType describes an Element type.
type ElemType int

const (
	InvalidElement ElemType = iota - 1
	NodeElement
	EdgeElement
)

// Type returns the element type of the receiver. It returns an error if the Element Group
// is invalid or does not match the Element Data, or if the Element Data is an incomplete
// edge.
func (e Element) Type() (ElemType, error) {
	et := InvalidElement
	switch {
	case e.Data.Source == "" && e.Data.Target == "":
		et = NodeElement
	case e.Data.Source != "" && e.Data.Target != "":
		et = EdgeElement
	default:
		return et, errors.New("cytoscapejs: invalid element: incomplete edge")
	}
	switch {
	case e.Group == "":
		return et, nil
	case e.Group == "node" && et == NodeElement:
		return NodeElement, nil
	case e.Group == "edge" && et == EdgeElement:
		return NodeElement, nil
	default:
		return InvalidElement, errors.New("cytoscapejs: invalid element: mismatched group")
	}
}

// ElemData is a graph element's data container.
type ElemData struct {
	ID         string
	Source     string
	Target     string
	Parent     string
	Attributes map[string]interface{}
}

var (
	_ json.Marshaler   = (*ElemData)(nil)
	_ json.Unmarshaler = (*ElemData)(nil)
)

// MarshalJSON implements the json.Marshaler interface.
func (e *ElemData) MarshalJSON() ([]byte, error) {
	if e.Attributes == nil {
		type elem struct {
			ID     string `json:"id"`
			Source string `json:"source,omitempty"`
			Target string `json:"target,omitempty"`
			Parent string `json:"parent,omitempty"`
		}
		return json.Marshal(elem{ID: e.ID, Source: e.Source, Target: e.Target, Parent: e.Parent})
	}
	e.Attributes["id"] = e.ID
	if e.Source != "" {
		e.Attributes["source"] = e.Source
	}
	if e.Target != "" {
		e.Attributes["target"] = e.Target
	}
	if e.Parent != "" {
		e.Attributes["parent"] = e.Parent
	}
	b, err := json.Marshal(e.Attributes)
	delete(e.Attributes, "id")
	if e.Source != "" {
		delete(e.Attributes, "source")
	}
	if e.Target != "" {
		delete(e.Attributes, "target")
	}
	if e.Parent != "" {
		delete(e.Attributes, "parent")
	}
	return b, err
}

// UnmarshalJSON implements the json.Unmarshaler interface.
func (e *ElemData) UnmarshalJSON(data []byte) error {
	var attrs map[string]interface{}
	err := json.Unmarshal(data, &attrs)
	if err != nil {
		return err
	}
	id, ok := attrs["id"]
	if !ok {
		return errors.New("cytoscapejs: no ID")
	}
	e.ID = fmt.Sprint(id)
	source, ok := attrs["source"]
	if ok {
		e.Source = fmt.Sprint(source)
	}
	target, ok := attrs["target"]
	if ok {
		e.Target = fmt.Sprint(target)
	}
	p, ok := attrs["parent"]
	if ok {
		e.Parent = fmt.Sprint(p)
	}
	delete(attrs, "id")
	delete(attrs, "source")
	delete(attrs, "target")
	delete(attrs, "parent")
	if len(attrs) != 0 {
		e.Attributes = attrs
	}
	return nil
}

// GraphNodeEdge is a Cytoscape.js graph with separated nodes and edges.
type GraphNodeEdge struct {
	Elements Elements      `json:"elements"`
	Layout   interface{}   `json:"layout,omitempty"`
	Style    []interface{} `json:"style,omitempty"`
}

// Elements contains the nodes and edges of a GraphNodeEdge.
type Elements struct {
	Nodes []Node `json:"nodes"`
	Edges []Edge `json:"edges"`
}

// Node is a Cytoscape.js node.
type Node struct {
	Data             NodeData    `json:"data"`
	Position         *Position   `json:"position,omitempty"`
	RenderedPosition *Position   `json:"renderedPosition,omitempty"`
	Selected         bool        `json:"selected,omitempty"`
	Selectable       bool        `json:"selectable,omitempty"`
	Locked           bool        `json:"locked,omitempty"`
	Grabbable        bool        `json:"grabbable,omitempty"`
	Classes          string      `json:"classes,omitempty"`
	Scratch          interface{} `json:"scratch,omitempty"`
}

// NodeData is a graph node's data container.
type NodeData struct {
	ID         string
	Parent     string
	Attributes map[string]interface{}
}

var (
	_ json.Marshaler   = (*NodeData)(nil)
	_ json.Unmarshaler = (*NodeData)(nil)
)

// MarshalJSON implements the json.Marshaler interface.
func (n *NodeData) MarshalJSON() ([]byte, error) {
	if n.Attributes == nil {
		type node struct {
			ID     string `json:"id"`
			Parent string `json:"parent,omitempty"`
		}
		return json.Marshal(node{ID: n.ID, Parent: n.Parent})
	}
	n.Attributes["id"] = n.ID
	n.Attributes["parent"] = n.Parent
	b, err := json.Marshal(n.Attributes)
	delete(n.Attributes, "id")
	delete(n.Attributes, "parent")
	return b, err
}

// UnmarshalJSON implements the json.Unmarshaler interface.
func (n *NodeData) UnmarshalJSON(data []byte) error {
	var attrs map[string]interface{}
	err := json.Unmarshal(data, &attrs)
	if err != nil {
		return err
	}
	id, ok := attrs["id"]
	if !ok {
		return errors.New("cytoscapejs: no ID")
	}
	n.ID = fmt.Sprint(id)
	delete(attrs, "id")
	p, ok := attrs["parent"]
	if ok {
		n.Parent = fmt.Sprint(p)
	}
	delete(attrs, "parent")
	if len(attrs) != 0 {
		n.Attributes = attrs
	}
	return nil
}

// Edge is a Cytoscape.js edge.
type Edge struct {
	Data       EdgeData    `json:"data"`
	Selected   bool        `json:"selected,omitempty"`
	Selectable bool        `json:"selectable,omitempty"`
	Classes    string      `json:"classes,omitempty"`
	Scratch    interface{} `json:"scratch,omitempty"`
}

// EdgeData is a graph edge's data container.
type EdgeData struct {
	ID         string
	Source     string
	Target     string
	Attributes map[string]interface{}
}

var (
	_ json.Marshaler   = (*EdgeData)(nil)
	_ json.Unmarshaler = (*EdgeData)(nil)
)

// MarshalJSON implements the json.Marshaler interface.
func (e *EdgeData) MarshalJSON() ([]byte, error) {
	if e.Attributes == nil {
		type edge struct {
			ID     string `json:"id"`
			Source string `json:"source"`
			Target string `json:"target"`
		}
		return json.Marshal(edge{ID: e.ID, Source: e.Source, Target: e.Target})
	}
	e.Attributes["id"] = e.ID
	e.Attributes["source"] = e.Source
	e.Attributes["target"] = e.Target
	b, err := json.Marshal(e.Attributes)
	delete(e.Attributes, "id")
	delete(e.Attributes, "source")
	delete(e.Attributes, "target")
	return b, err
}

// UnmarshalJSON implements the json.Unmarshaler interface.
func (e *EdgeData) UnmarshalJSON(data []byte) error {
	var attrs map[string]interface{}
	err := json.Unmarshal(data, &attrs)
	if err != nil {
		return err
	}
	id, ok := attrs["id"]
	if !ok {
		return errors.New("cytoscapejs: no ID")
	}
	source, ok := attrs["source"]
	if !ok {
		return errors.New("cytoscapejs: no source")
	}
	target, ok := attrs["target"]
	if !ok {
		return errors.New("cytoscapejs: no target")
	}
	e.ID = fmt.Sprint(id)
	e.Source = fmt.Sprint(source)
	e.Target = fmt.Sprint(target)
	delete(attrs, "id")
	delete(attrs, "source")
	delete(attrs, "target")
	if len(attrs) != 0 {
		e.Attributes = attrs
	}
	return nil
}

// Position is a node position.
type Position struct {
	X float64 `json:"x"`
	Y float64 `json:"y"`
}
