package recordingrule

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"sort"
	"strconv"
	"strings"
)

type Row struct {
	Title     string  `json:"title"`
	ShowTitle bool    `json:"showTitle"`
	Collapse  bool    `json:"collapse"`
	Editable  bool    `json:"editable"`
	Height    Height  `json:"height"`
	Panels    []Panel `json:"panels"`
	Repeat    *string `json:"repeat"`
}

type AlertNotification struct {
	ID                    int64       `json:"id,omitempty"`
	Name                  string      `json:"name"`
	Type                  string      `json:"type"`
	IsDefault             bool        `json:"isDefault"`
	DisableResolveMessage bool        `json:"disableResolveMessage"`
	SendReminder          bool        `json:"sendReminder"`
	Frequency             string      `json:"frequency"`
	Settings              interface{} `json:"settings"`
	UID                   string      `json:"uid,omitempty"`
}

type BoolString struct {
	Flag  bool
	Value string
}

func (s *BoolString) UnmarshalJSON(raw []byte) error {
	if raw == nil || bytes.Equal(raw, []byte(`"null"`)) {
		return nil
	}
	var (
		tmp string
		err error
	)
	if raw[0] != '"' {
		if bytes.Equal(raw, []byte("true")) {
			s.Flag = true
			return nil
		}
		if bytes.Equal(raw, []byte("false")) {
			return nil
		}
		return errors.New("bad boolean value provided")
	}
	if err = json.Unmarshal(raw, &tmp); err != nil {
		return err
	}
	s.Value = tmp
	return nil
}

func (s BoolString) MarshalJSON() ([]byte, error) {
	if s.Value != "" {
		var buf bytes.Buffer
		buf.WriteRune('"')
		buf.WriteString(s.Value)
		buf.WriteRune('"')
		return buf.Bytes(), nil
	}
	return strconv.AppendBool([]byte{}, s.Flag), nil
}

type BoolInt struct {
	Flag  bool
	Value *int64
}

func (s *BoolInt) UnmarshalJSON(raw []byte) error {
	if raw == nil || bytes.Equal(raw, []byte(`"null"`)) {
		return nil
	}
	var (
		tmp int64
		err error
	)
	if tmp, err = strconv.ParseInt(string(raw), 10, 64); err != nil {
		if bytes.Equal(raw, []byte("true")) {
			s.Flag = true
			return nil
		}
		if bytes.Equal(raw, []byte("false")) {
			return nil
		}
		return errors.New("bad value provided")
	}
	s.Value = &tmp
	return nil
}

func (s BoolInt) MarshalJSON() ([]byte, error) {
	if s.Value != nil {
		return strconv.AppendInt([]byte{}, *s.Value, 10), nil
	}
	return strconv.AppendBool([]byte{}, s.Flag), nil
}

func NewIntString(i int64) *IntString {
	return &IntString{
		Value: i,
		Valid: true,
	}
}

// IntString represents special type for json values that could be strings or ints: 100 or "100"
type IntString struct {
	Value int64
	Valid bool
}

// UnmarshalJSON implements custom unmarshalling for IntString type
func (v *IntString) UnmarshalJSON(raw []byte) error {
	if raw == nil || bytes.Equal(raw, []byte(`"null"`)) || bytes.Equal(raw, []byte(`""`)) {
		return nil
	}

	strVal := string(raw)
	if rune(raw[0]) == '"' {
		strVal = strings.Trim(strVal, `"`)
	}

	i, err := strconv.ParseInt(strVal, 10, 64)
	if err != nil {
		return err
	}

	v.Value = i
	v.Valid = true

	return nil
}

// MarshalJSON implements custom marshalling for IntString type
func (v *IntString) MarshalJSON() ([]byte, error) {
	if v.Valid {
		strVal := strconv.FormatInt(v.Value, 10)
		return []byte(strVal), nil
	}

	return []byte(`"null"`), nil
}

func NewFloatString(i float64) *FloatString {
	return &FloatString{
		Value: i,
		Valid: true,
	}
}

// FloatString represents special type for json values that could be strings or ints: 100 or "100"
type FloatString struct {
	Value float64
	Valid bool
}

// UnmarshalJSON implements custom unmarshalling for FloatString type
func (v *FloatString) UnmarshalJSON(raw []byte) error {
	if raw == nil || bytes.Equal(raw, []byte(`"null"`)) || bytes.Equal(raw, []byte(`""`)) {
		return nil
	}

	strVal := string(raw)
	if rune(raw[0]) == '"' {
		strVal = strings.Trim(strVal, `"`)
	}

	i, err := strconv.ParseFloat(strVal, 64)
	if err != nil {
		return err
	}

	v.Value = i
	v.Valid = true

	return nil
}

// MarshalJSON implements custom marshalling for FloatString type
func (v *FloatString) MarshalJSON() ([]byte, error) {
	if v.Valid {
		strVal := strconv.FormatFloat(v.Value, 'g', -1, 64)
		return []byte(strVal), nil
	}

	return []byte(`"null"`), nil
}

// StringSliceString represents special type for json values that could be
// strings or slice of strings: "something" or ["something"].
type StringSliceString struct {
	Value []string
	Valid bool
}

// UnmarshalJSON implements custom unmarshalling for StringSliceString type.
func (v *StringSliceString) UnmarshalJSON(raw []byte) error {
	if raw == nil || bytes.Equal(raw, []byte(`"null"`)) {
		return nil
	}

	// First try with string.
	var str string
	if err := json.Unmarshal(raw, &str); err == nil {
		v.Value = []string{str}
		v.Valid = true
		return nil
	}

	// Lastly try with string slice.
	var strSlice []string
	err := json.Unmarshal(raw, &strSlice)
	if err != nil {
		return err
	}

	v.Value = strSlice
	v.Valid = true
	return nil
}

// MarshalJSON implements custom marshalling for StringSliceString type.
func (v *StringSliceString) MarshalJSON() ([]byte, error) {
	if !v.Valid {
		return []byte(`"null"`), nil
	}

	return json.Marshal(v.Value)
}

var (
	boardID uint
)

// Constants for templating
const (
	TemplatingHideNone = iota
	TemplatingHideLabel
	TemplatingHideVariable
)

type (
	// Board represents Grafana dashboard.
	Board struct {
		ID              uint       `json:"id,omitempty"`
		UID             string     `json:"uid,omitempty"`
		Slug            string     `json:"slug"`
		Title           string     `json:"title"`
		OriginalTitle   string     `json:"originalTitle"`
		Tags            []string   `json:"tags"`
		Style           string     `json:"style"`
		Timezone        string     `json:"timezone"`
		Editable        bool       `json:"editable"`
		HideControls    bool       `json:"hideControls" graf:"hide-controls"`
		SharedCrosshair bool       `json:"sharedCrosshair" graf:"shared-crosshair"`
		Panels          []*Panel   `json:"panels"`
		Rows            []*Row     `json:"rows"`
		Templating      Templating `json:"templating"`
		Annotations     struct {
			List []Annotation `json:"list"`
		} `json:"annotations"`
		Refresh       *BoolString `json:"refresh,omitempty"`
		SchemaVersion uint        `json:"schemaVersion"`
		Version       uint        `json:"version"`
		Links         []Link      `json:"links"`
		Time          Time        `json:"time"`
		Timepicker    Timepicker  `json:"timepicker"`
		GraphTooltip  int         `json:"graphTooltip,omitempty"`
	}
	Time struct {
		From string `json:"from"`
		To   string `json:"to"`
	}
	Timepicker struct {
		Now              *bool    `json:"now,omitempty"`
		RefreshIntervals []string `json:"refresh_intervals"`
		TimeOptions      []string `json:"time_options"`
	}
	Templating struct {
		List []TemplateVar `json:"list"`
	}
	TemplateVar struct {
		Name        string      `json:"name"`
		Type        string      `json:"type"`
		Auto        bool        `json:"auto,omitempty"`
		AutoCount   *int        `json:"auto_count,omitempty"`
		Datasource  interface{} `json:"datasource"`
		Refresh     BoolInt     `json:"refresh"`
		Options     []Option    `json:"options"`
		IncludeAll  bool        `json:"includeAll"`
		AllFormat   string      `json:"allFormat"`
		AllValue    string      `json:"allValue"`
		Multi       bool        `json:"multi"`
		MultiFormat string      `json:"multiFormat"`
		Query       interface{} `json:"query"`
		Regex       string      `json:"regex"`
		Current     Current     `json:"current"`
		Label       string      `json:"label"`
		Hide        uint8       `json:"hide"`
		Sort        int         `json:"sort"`
	}
	// for templateVar
	Option struct {
		Text     string `json:"text"`
		Value    string `json:"value"`
		Selected bool   `json:"selected"`
	}
	// for templateVar
	Current struct {
		Tags  []*string          `json:"tags,omitempty"`
		Text  *StringSliceString `json:"text"`
		Value interface{}        `json:"value"` // TODO select more precise type
	}
	Annotation struct {
		Name        string      `json:"name"`
		Datasource  interface{} `json:"datasource"`
		ShowLine    bool        `json:"showLine"`
		IconColor   string      `json:"iconColor"`
		LineColor   string      `json:"lineColor"`
		IconSize    uint        `json:"iconSize"`
		Enable      bool        `json:"enable"`
		Query       string      `json:"query"`
		Expr        string      `json:"expr"`
		Step        string      `json:"step"`
		TextField   string      `json:"textField"`
		TextFormat  string      `json:"textFormat"`
		TitleFormat string      `json:"titleFormat"`
		TagsField   string      `json:"tagsField"`
		Tags        []string    `json:"tags"`
		TagKeys     string      `json:"tagKeys"`
		Type        string      `json:"type"`
	}
	// Link represents link to another dashboard or external weblink
	Link struct {
		Title       string   `json:"title"`
		Type        string   `json:"type"`
		AsDropdown  *bool    `json:"asDropdown,omitempty"`
		DashURI     *string  `json:"dashUri,omitempty"`
		Dashboard   *string  `json:"dashboard,omitempty"`
		Icon        *string  `json:"icon,omitempty"`
		IncludeVars bool     `json:"includeVars"`
		KeepTime    *bool    `json:"keepTime,omitempty"`
		Params      *string  `json:"params,omitempty"`
		Tags        []string `json:"tags,omitempty"`
		TargetBlank *bool    `json:"targetBlank,omitempty"`
		Tooltip     *string  `json:"tooltip,omitempty"`
		URL         *string  `json:"url,omitempty"`
	}
)

// Height of rows maybe passed as number (ex 200) or
// as string (ex "200px") or empty string
type Height string

func (h *Height) UnmarshalJSON(raw []byte) error {
	if raw == nil || bytes.Equal(raw, []byte(`"null"`)) {
		return nil
	}
	if raw[0] != '"' {
		tmp := []byte{'"'}
		raw = append(tmp, raw...)
		raw = append(raw, byte('"'))
	}
	var tmp string
	err := json.Unmarshal(raw, &tmp)
	*h = Height(tmp)
	return err
}

func NewBoard(title string) *Board {
	boardID++
	return &Board{
		ID:           boardID,
		Title:        title,
		Style:        "dark",
		Timezone:     "browser",
		Editable:     true,
		HideControls: false,
		Rows:         []*Row{},
	}
}

func (b *Board) AddLink(link Link) {
	b.Links = append(b.Links, link)
}

func (b *Board) RemoveTags(tags ...string) {
	// order might change after removing the tags
	for _, toRemoveTag := range tags {
		tagLen := len(b.Tags)
		for i, tag := range b.Tags {
			if tag == toRemoveTag {
				b.Tags[tagLen-1], b.Tags[i] = b.Tags[i], b.Tags[tagLen-1]
				b.Tags = b.Tags[:tagLen-1]
				break
			}
		}
	}
}

func (b *Board) AddTags(tags ...string) {
	tagFound := make(map[string]bool, len(b.Tags))
	for _, tag := range b.Tags {
		tagFound[tag] = true
	}
	for _, tag := range tags {
		if tagFound[tag] {
			continue
		}
		b.Tags = append(b.Tags, tag)
		tagFound[tag] = true
	}
}

func (b *Board) HasTag(tag string) bool {
	for _, t := range b.Tags {
		if t == tag {
			return true
		}
	}
	return false
}

func (b *Board) AddRow(title string) *Row {
	if title == "" {
		title = "New row"
	}
	row := &Row{
		Title:    title,
		Collapse: false,
		Editable: true,
		Height:   "250px",
	}
	b.Rows = append(b.Rows, row)
	return row
}

const (
	CustomType panelType = iota
	DashlistType
	GraphType
	TableType
	TextType
	PluginlistType
	AlertlistType
	SinglestatType
	StatType
	RowType
	BarGaugeType
	HeatmapType
	TimeseriesType
)

const MixedSource = "-- Mixed --"

type (
	// Panel represents panels of different types defined in Grafana.
	Panel struct {
		CommonPanel
		// Should be initialized only one type of panels.
		// OfType field defines which of types below will be used.
		*GraphPanel
		*TablePanel
		*TextPanel
		*SinglestatPanel
		*StatPanel
		*DashlistPanel
		*PluginlistPanel
		*RowPanel
		*AlertlistPanel
		*BarGaugePanel
		*HeatmapPanel
		*TimeseriesPanel
		*CustomPanel
	}
	panelType   int8
	CommonPanel struct {
		Datasource interface{} `json:"datasource,omitempty"` // metrics
		Editable   bool        `json:"editable"`
		Error      bool        `json:"error"`
		GridPos    struct {
			H *int `json:"h,omitempty"`
			W *int `json:"w,omitempty"`
			X *int `json:"x,omitempty"`
			Y *int `json:"y,omitempty"`
		} `json:"gridPos,omitempty"`
		Height           interface{} `json:"height,omitempty"` // general
		HideTimeOverride *bool       `json:"hideTimeOverride,omitempty"`
		ID               uint        `json:"id"`
		IsNew            bool        `json:"isNew"`
		Links            []Link      `json:"links,omitempty"`    // general
		MinSpan          *float32    `json:"minSpan,omitempty"`  // templating options
		OfType           panelType   `json:"-"`                  // it required for defining type of the panel
		Renderer         *string     `json:"renderer,omitempty"` // display styles
		Repeat           *string     `json:"repeat,omitempty"`   // templating options
		// RepeatIteration *int64   `json:"repeatIteration,omitempty"`
		RepeatPanelID *uint `json:"repeatPanelId,omitempty"`
		ScopedVars    map[string]struct {
			Selected bool   `json:"selected"`
			Text     string `json:"text"`
			Value    string `json:"value"`
		} `json:"scopedVars,omitempty"`
		Span        float32 `json:"span"`                  // general
		Title       string  `json:"title"`                 // general
		Description *string `json:"description,omitempty"` // general
		Transparent bool    `json:"transparent"`
		Type        string  `json:"type"`
		Alert       *Alert  `json:"alert,omitempty"`
	}
	AlertEvaluator struct {
		Params []float64 `json:"params,omitempty"`
		Type   string    `json:"type,omitempty"`
	}
	AlertOperator struct {
		Type string `json:"type,omitempty"`
	}
	AlertQuery struct {
		Params []string `json:"params,omitempty"`
	}
	AlertReducer struct {
		Params []string `json:"params,omitempty"`
		Type   string   `json:"type,omitempty"`
	}
	AlertCondition struct {
		Evaluator AlertEvaluator `json:"evaluator,omitempty"`
		Operator  AlertOperator  `json:"operator,omitempty"`
		Query     AlertQuery     `json:"query,omitempty"`
		Reducer   AlertReducer   `json:"reducer,omitempty"`
		Type      string         `json:"type,omitempty"`
	}
	Alert struct {
		AlertRuleTags       map[string]string   `json:"alertRuleTags,omitempty"`
		Conditions          []AlertCondition    `json:"conditions,omitempty"`
		ExecutionErrorState string              `json:"executionErrorState,omitempty"`
		Frequency           string              `json:"frequency,omitempty"`
		Handler             int                 `json:"handler,omitempty"`
		Name                string              `json:"name,omitempty"`
		NoDataState         string              `json:"noDataState,omitempty"`
		Notifications       []AlertNotification `json:"notifications,omitempty"`
		Message             string              `json:"message,omitempty"`
		For                 string              `json:"for,omitempty"`
	}
	GraphPanel struct {
		AliasColors interface{} `json:"aliasColors"` // XXX
		Bars        bool        `json:"bars"`
		DashLength  *uint       `json:"dashLength,omitempty"`
		Dashes      *bool       `json:"dashes,omitempty"`
		Decimals    *int        `json:"decimals,omitempty"`
		Fill        int         `json:"fill"`
		//		Grid        grid        `json:"grid"` obsoleted in 4.1 by xaxis and yaxis

		Legend          Legend           `json:"legend,omitempty"`
		LeftYAxisLabel  *string          `json:"leftYAxisLabel,omitempty"`
		Lines           bool             `json:"lines"`
		Linewidth       uint             `json:"linewidth"`
		NullPointMode   string           `json:"nullPointMode"`
		Percentage      bool             `json:"percentage"`
		Pointradius     float32          `json:"pointradius"`
		Points          bool             `json:"points"`
		RightYAxisLabel *string          `json:"rightYAxisLabel,omitempty"`
		SeriesOverrides []SeriesOverride `json:"seriesOverrides,omitempty"`
		SpaceLength     *uint            `json:"spaceLength,omitempty"`
		Stack           bool             `json:"stack"`
		SteppedLine     bool             `json:"steppedLine"`
		Targets         []Target         `json:"targets,omitempty"`
		Thresholds      []Threshold      `json:"thresholds,omitempty"`
		TimeFrom        *string          `json:"timeFrom,omitempty"`
		TimeShift       *string          `json:"timeShift,omitempty"`
		Tooltip         Tooltip          `json:"tooltip"`
		XAxis           bool             `json:"x-axis,omitempty"`
		YAxis           bool             `json:"y-axis,omitempty"`
		YFormats        []string         `json:"y_formats,omitempty"`
		Xaxis           Axis             `json:"xaxis"` // was added in Grafana 4.x?
		Yaxes           []Axis           `json:"yaxes"` // was added in Grafana 4.x?
		FieldConfig     *FieldConfig     `json:"fieldConfig,omitempty"`
	}
	FieldConfig struct {
		Defaults FieldConfigDefaults `json:"defaults"`
	}
	Options struct {
		Orientation   string `json:"orientation"`
		TextMode      string `json:"textMode"`
		ColorMode     string `json:"colorMode"`
		GraphMode     string `json:"graphMode"`
		JustifyMode   string `json:"justifyMode"`
		DisplayMode   string `json:"displayMode"`
		Content       string `json:"content"`
		Mode          string `json:"mode"`
		ReduceOptions struct {
			Values bool     `json:"values"`
			Fields string   `json:"fields"`
			Calcs  []string `json:"calcs"`
		} `json:"reduceOptions"`
	}
	Threshold struct {
		// the alert threshold value, we do not omitempty, since 0 is a valid
		// threshold
		Value float32 `json:"value"`
		// critical, warning, ok, custom
		ColorMode string `json:"colorMode,omitempty"`
		// gt or lt
		Op   string `json:"op,omitempty"`
		Fill bool   `json:"fill"`
		Line bool   `json:"line"`
		// hexadecimal color (e.g. #629e51, only when ColorMode is "custom")
		FillColor string `json:"fillColor,omitempty"`
		// hexadecimal color (e.g. #629e51, only when ColorMode is "custom")
		LineColor string `json:"lineColor,omitempty"`
		// left or right
		Yaxis string `json:"yaxis,omitempty"`
	}

	Tooltip struct {
		Shared       bool   `json:"shared"`
		ValueType    string `json:"value_type"`
		MsResolution bool   `json:"msResolution,omitempty"` // was added in Grafana 3.x
		Sort         int    `json:"sort,omitempty"`
	}
	TablePanel struct {
		Columns   []Column      `json:"columns"`
		Sort      *Sort         `json:"sort,omitempty"`
		Styles    []ColumnStyle `json:"styles"`
		Transform string        `json:"transform"`
		Targets   []Target      `json:"targets,omitempty"`
		Scroll    bool          `json:"scroll"` // from grafana 3.x
	}
	TextPanel struct {
		Content     string        `json:"content"`
		Mode        string        `json:"mode"`
		PageSize    uint          `json:"pageSize"`
		Scroll      bool          `json:"scroll"`
		ShowHeader  bool          `json:"showHeader"`
		Sort        Sort          `json:"sort"`
		Styles      []ColumnStyle `json:"styles"`
		FieldConfig FieldConfig   `json:"fieldConfig"`
		Options     struct {
			Content string `json:"content"`
			Mode    string `json:"mode"`
		} `json:"options"`
	}
	SinglestatPanel struct {
		Colors          []string    `json:"colors"`
		ColorValue      bool        `json:"colorValue"`
		ColorBackground bool        `json:"colorBackground"`
		Decimals        int         `json:"decimals"`
		Format          string      `json:"format"`
		Gauge           Gauge       `json:"gauge,omitempty"`
		MappingType     *uint       `json:"mappingType,omitempty"`
		MappingTypes    []*MapType  `json:"mappingTypes,omitempty"`
		MaxDataPoints   *IntString  `json:"maxDataPoints,omitempty"`
		NullPointMode   string      `json:"nullPointMode"`
		Postfix         *string     `json:"postfix,omitempty"`
		PostfixFontSize *string     `json:"postfixFontSize,omitempty"`
		Prefix          *string     `json:"prefix,omitempty"`
		PrefixFontSize  *string     `json:"prefixFontSize,omitempty"`
		RangeMaps       []*RangeMap `json:"rangeMaps,omitempty"`
		SparkLine       SparkLine   `json:"sparkline,omitempty"`
		Targets         []Target    `json:"targets,omitempty"`
		Thresholds      string      `json:"thresholds"`
		ValueFontSize   string      `json:"valueFontSize"`
		ValueMaps       []ValueMap  `json:"valueMaps"`
		ValueName       string      `json:"valueName"`
	}
	StatPanel struct {
		Colors          []string    `json:"colors"`
		ColorValue      bool        `json:"colorValue"`
		ColorBackground bool        `json:"colorBackground"`
		Decimals        int         `json:"decimals"`
		Format          string      `json:"format"`
		Gauge           Gauge       `json:"gauge,omitempty"`
		MappingType     *uint       `json:"mappingType,omitempty"`
		MappingTypes    []*MapType  `json:"mappingTypes,omitempty"`
		MaxDataPoints   *IntString  `json:"maxDataPoints,omitempty"`
		NullPointMode   string      `json:"nullPointMode"`
		Postfix         *string     `json:"postfix,omitempty"`
		PostfixFontSize *string     `json:"postfixFontSize,omitempty"`
		Prefix          *string     `json:"prefix,omitempty"`
		PrefixFontSize  *string     `json:"prefixFontSize,omitempty"`
		RangeMaps       []*RangeMap `json:"rangeMaps,omitempty"`
		SparkLine       SparkLine   `json:"sparkline,omitempty"`
		Targets         []Target    `json:"targets,omitempty"`
		Thresholds      string      `json:"thresholds"`
		ValueFontSize   string      `json:"valueFontSize"`
		ValueMaps       []ValueMap  `json:"valueMaps"`
		ValueName       string      `json:"valueName"`
		Options         Options     `json:"options"`
	}
	DashlistPanel struct {
		Mode     string   `json:"mode"`
		Query    string   `json:"query"`
		Tags     []string `json:"tags"`
		FolderID int      `json:"folderId"`
		Limit    int      `json:"limit"`
		Headings bool     `json:"headings"`
		Recent   bool     `json:"recent"`
		Search   bool     `json:"search"`
		Starred  bool     `json:"starred"`
	}
	PluginlistPanel struct {
		Limit int `json:"limit,omitempty"`
	}
	AlertlistPanel struct {
		OnlyAlertsOnDashboard bool     `json:"onlyAlertsOnDashboard"`
		Show                  string   `json:"show"`
		SortOrder             int      `json:"sortOrder"`
		Limit                 int      `json:"limit"`
		StateFilter           []string `json:"stateFilter"`
		NameFilter            string   `json:"nameFilter,omitempty"`
		DashboardTags         []string `json:"dashboardTags,omitempty"`
	}
	BarGaugePanel struct {
		Options     Options     `json:"options"`
		Targets     []Target    `json:"targets,omitempty"`
		FieldConfig FieldConfig `json:"fieldConfig"`
	}
	RowPanel struct {
		Panels    []Panel `json:"panels"`
		Collapsed bool    `json:"collapsed"`
	}
	HeatmapPanel struct {
		Cards struct {
			CardPadding *float64 `json:"cardPadding"`
			CardRound   *float64 `json:"cardRound"`
		} `json:"cards"`
		Color struct {
			CardColor   string   `json:"cardColor"`
			ColorScale  string   `json:"colorScale"`
			ColorScheme string   `json:"colorScheme"`
			Exponent    float64  `json:"exponent"`
			Min         *float64 `json:"min,omitempty"`
			Max         *float64 `json:"max,omitempty"`
			Mode        string   `json:"mode"`
		} `json:"color"`
		DataFormat      string `json:"dataFormat"`
		HideZeroBuckets bool   `json:"hideZeroBuckets"`
		HighlightCards  bool   `json:"highlightCards"`
		Legend          struct {
			Show bool `json:"show"`
		} `json:"legend"`
		ReverseYBuckets bool     `json:"reverseYBuckets"`
		Targets         []Target `json:"targets,omitempty"`
		Tooltip         struct {
			Show          bool `json:"show"`
			ShowHistogram bool `json:"showHistogram"`
		} `json:"tooltip"`
		TooltipDecimals int `json:"tooltipDecimals"`
		XAxis           struct {
			Show bool `json:"show"`
		} `json:"xAxis"`
		XBucketNumber *float64 `json:"xBucketNumber"`
		XBucketSize   *string  `json:"xBucketSize"`
		YAxis         struct {
			Decimals    *int     `json:"decimals"`
			Format      string   `json:"format"`
			LogBase     int      `json:"logBase"`
			Show        bool     `json:"show"`
			Max         *string  `json:"max"`
			Min         *string  `json:"min"`
			SplitFactor *float64 `json:"splitFactor"`
		} `json:"yAxis"`
		YBucketBound  string   `json:"yBucketBound"`
		YBucketNumber *float64 `json:"yBucketNumber"`
		YBucketSize   *float64 `json:"yBucketSize"`
	}
	TimeseriesPanel struct {
		Targets     []Target          `json:"targets,omitempty"`
		Options     TimeseriesOptions `json:"options"`
		FieldConfig FieldConfig       `json:"fieldConfig"`
	}
	TimeseriesOptions struct {
		Legend  TimeseriesLegendOptions  `json:"legend,omitempty"`
		Tooltip TimeseriesTooltipOptions `json:"tooltip,omitempty"`
	}
	TimeseriesLegendOptions struct {
		Calcs       []string `json:"calcs"`
		DisplayMode string   `json:"displayMode"`
		Placement   string   `json:"placement"`
	}
	TimeseriesTooltipOptions struct {
		Mode string `json:"mode"`
	}
	FieldConfigDefaults struct {
		Unit       string            `json:"unit"`
		Decimals   *int              `json:"decimals,omitempty"`
		Min        *float64          `json:"min,omitempty"`
		Max        *float64          `json:"max,omitempty"`
		Color      FieldConfigColor  `json:"color"`
		Thresholds Thresholds        `json:"thresholds"`
		Custom     FieldConfigCustom `json:"custom"`
		Links      []Link            `json:"links,omitempty"`
	}
	FieldConfigCustom struct {
		AxisLabel         string `json:"axisLabel,omitempty"`
		AxisPlacement     string `json:"axisPlacement"`
		AxisSoftMin       *int   `json:"axisSoftMin,omitempty"`
		AxisSoftMax       *int   `json:"axisSoftMax,omitempty"`
		BarAlignment      int    `json:"barAlignment"`
		DrawStyle         string `json:"drawStyle"`
		FillOpacity       int    `json:"fillOpacity"`
		GradientMode      string `json:"gradientMode"`
		LineInterpolation string `json:"lineInterpolation"`
		LineWidth         int    `json:"lineWidth"`
		PointSize         int    `json:"pointSize"`
		ShowPoints        string `json:"showPoints"`
		SpanNulls         bool   `json:"spanNulls"`
		HideFrom          struct {
			Legend  bool `json:"legend"`
			Tooltip bool `json:"tooltip"`
			Viz     bool `json:"viz"`
		} `json:"hideFrom"`
		LineStyle struct {
			Fill string `json:"fill"`
		} `json:"lineStyle"`
		ScaleDistribution struct {
			Type string `json:"type"`
			Log  int    `json:"log,omitempty"`
		} `json:"scaleDistribution"`
		Stacking struct {
			Group string `json:"group"`
			Mode  string `json:"mode"`
		} `json:"stacking"`
		ThresholdsStyle struct {
			Mode string `json:"mode"`
		} `json:"thresholdsStyle"`
	}
	Thresholds struct {
		Mode  string          `json:"mode"`
		Steps []ThresholdStep `json:"steps"`
	}
	ThresholdStep struct {
		Color string   `json:"color"`
		Value *float64 `json:"value"`
	}
	FieldConfigColor struct {
		Mode       string `json:"mode"`
		FixedColor string `json:"fixedColor,omitempty"`
		SeriesBy   string `json:"seriesBy,omitempty"`
	}
	CustomPanel map[string]interface{}
)

// for a graph panel
type (
	// TODO look at schema versions carefully
	// grid was obsoleted by xaxis and yaxes
	grid struct { //nolint: unused,deadcode
		LeftLogBase     *int     `json:"leftLogBase"`
		LeftMax         *int     `json:"leftMax"`
		LeftMin         *int     `json:"leftMin"`
		RightLogBase    *int     `json:"rightLogBase"`
		RightMax        *int     `json:"rightMax"`
		RightMin        *int     `json:"rightMin"`
		Threshold1      *float64 `json:"threshold1"`
		Threshold1Color string   `json:"threshold1Color"`
		Threshold2      *float64 `json:"threshold2"`
		Threshold2Color string   `json:"threshold2Color"`
		ThresholdLine   bool     `json:"thresholdLine"`
	}
	xaxis struct { //nolint:unused,deadcode
		Mode   string      `json:"mode"`
		Name   interface{} `json:"name"` // TODO what is this?
		Show   bool        `json:"show"`
		Values *[]string   `json:"values,omitempty"`
	}
	Axis struct {
		Format   string       `json:"format"`
		LogBase  int          `json:"logBase"`
		Decimals int          `json:"decimals,omitempty"`
		Max      *FloatString `json:"max,omitempty"`
		Min      *FloatString `json:"min,omitempty"`
		Show     bool         `json:"show"`
		Label    string       `json:"label,omitempty"`
	}
	SeriesOverride struct {
		Alias         string      `json:"alias"`
		Bars          *bool       `json:"bars,omitempty"`
		Color         *string     `json:"color,omitempty"`
		Dashes        *bool       `json:"dashes,omitempty"`
		Fill          *int        `json:"fill,omitempty"`
		FillBelowTo   *string     `json:"fillBelowTo,omitempty"`
		Legend        *bool       `json:"legend,omitempty"`
		Lines         *bool       `json:"lines,omitempty"`
		LineWidth     *int        `json:"linewidth,omitempty"`
		Stack         *BoolString `json:"stack,omitempty"`
		Transform     *string     `json:"transform,omitempty"`
		YAxis         *int        `json:"yaxis,omitempty"`
		ZIndex        *int        `json:"zindex,omitempty"`
		NullPointMode *string     `json:"nullPointMode,omitempty"`
	}
	Sort struct {
		Col  int  `json:"col"`
		Desc bool `json:"desc"`
	}
	Legend struct {
		AlignAsTable bool  `json:"alignAsTable"`
		Avg          bool  `json:"avg"`
		Current      bool  `json:"current"`
		HideEmpty    bool  `json:"hideEmpty"`
		HideZero     bool  `json:"hideZero"`
		Max          bool  `json:"max"`
		Min          bool  `json:"min"`
		RightSide    bool  `json:"rightSide"`
		Show         bool  `json:"show"`
		SideWidth    *uint `json:"sideWidth,omitempty"`
		Total        bool  `json:"total"`
		Values       bool  `json:"values"`
	}
)

// for a table
type (
	Column struct {
		TextType string `json:"text"`
		Value    string `json:"value"`
	}
	ColumnStyle struct {
		Alias           *string    `json:"alias"`
		DateFormat      *string    `json:"dateFormat,omitempty"`
		Pattern         string     `json:"pattern"`
		Type            string     `json:"type"`
		ColorMode       *string    `json:"colorMode,omitempty"`
		Colors          *[]string  `json:"colors,omitempty"`
		Decimals        *int       `json:"decimals,omitempty"`
		Thresholds      *[]string  `json:"thresholds,omitempty"`
		Unit            *string    `json:"unit,omitempty"`
		MappingType     int        `json:"mappingType,omitempty"`
		ValueMaps       []ValueMap `json:"valueMaps,omitempty"`
		Link            bool       `json:"link,omitempty"`
		LinkTooltip     *string    `json:"linkTooltip,omitempty"`
		LinkUrl         *string    `json:"linkUrl,omitempty"`
		LinkTargetBlank bool       `json:"linkTargetBlank,omitempty"`
	}
)

// for a stat
type (
	ValueMap struct {
		Op       string `json:"op"`
		TextType string `json:"text"`
		Value    string `json:"value"`
	}
	Gauge struct {
		MaxValue         float32 `json:"maxValue"`
		MinValue         float32 `json:"minValue"`
		Show             bool    `json:"show"`
		ThresholdLabels  bool    `json:"thresholdLabels"`
		ThresholdMarkers bool    `json:"thresholdMarkers"`
	}
	SparkLine struct {
		FillColor *string  `json:"fillColor,omitempty"`
		Full      bool     `json:"full,omitempty"`
		LineColor *string  `json:"lineColor,omitempty"`
		Show      bool     `json:"show,omitempty"`
		YMin      *float64 `json:"ymin,omitempty"`
		YMax      *float64 `json:"ymax,omitempty"`
	}
)

// for an any panel
type Target struct {
	RefID      string      `json:"refId"`
	Datasource interface{} `json:"datasource,omitempty"`
	Hide       bool        `json:"hide,omitempty"`

	// For PostgreSQL
	Table        string `json:"table,omitempty"`
	TimeColumn   string `json:"timeColumn,omitempty"`
	MetricColumn string `json:"metricColumn,omitempty"`
	RawSql       string `json:"rawSql,omitempty"`
	Select       [][]struct {
		Params []string `json:"params,omitempty"`
		Type   string   `json:"type,omitempty"`
	} `json:"select,omitempty"`
	Where []struct {
		Type     string   `json:"type,omitempty"`
		Name     string   `json:"name,omitempty"`
		Params   []string `json:"params,omitempty"`
		Datatype string   `json:"datatype,omitempty"`
	} `json:"where,omitempty"`
	Group []struct {
		Type   string   `json:"type,omitempty"`
		Params []string `json:"params,omitempty"`
	} `json:"group,omitempty"`

	// For Prometheus
	Expr           string `json:"expr,omitempty"`
	IntervalFactor int    `json:"intervalFactor,omitempty"`
	Interval       string `json:"interval,omitempty"`
	Step           int    `json:"step,omitempty"`
	LegendFormat   string `json:"legendFormat,omitempty"`
	Instant        bool   `json:"instant,omitempty"`
	Format         string `json:"format,omitempty"`

	// For InfluxDB
	Measurement string `json:"measurement,omitempty"`

	// For Elasticsearch
	DsType  *string `json:"dsType,omitempty"`
	Metrics []struct {
		ID    string `json:"id"`
		Field string `json:"field"`
		Type  string `json:"type"`
	} `json:"metrics,omitempty"`
	Query      string `json:"query,omitempty"`
	Alias      string `json:"alias,omitempty"`
	RawQuery   bool   `json:"rawQuery,omitempty"`
	TimeField  string `json:"timeField,omitempty"`
	BucketAggs []struct {
		ID       string `json:"id"`
		Field    string `json:"field"`
		Type     string `json:"type"`
		Settings struct {
			Interval    string      `json:"interval,omitempty"`
			MinDocCount interface{} `json:"min_doc_count"`
			Order       string      `json:"order,omitempty"`
			OrderBy     string      `json:"orderBy,omitempty"`
			Size        string      `json:"size,omitempty"`
		} `json:"settings"`
	} `json:"bucketAggs,omitempty"`

	// For Graphite
	Target string `json:"target,omitempty"`

	// For CloudWatch
	Namespace  string            `json:"namespace,omitempty"`
	MetricName string            `json:"metricName,omitempty"`
	Statistics []string          `json:"statistics,omitempty"`
	Dimensions map[string]string `json:"dimensions,omitempty"`
	Period     string            `json:"period,omitempty"`
	Region     string            `json:"region,omitempty"`

	// For the Stackdriver data source. Find out more information at
	// https:/grafana.com/docs/grafana/v6.0/features/datasources/stackdriver/
	ProjectName        string                    `json:"projectName,omitempty"`
	AlignOptions       []StackdriverAlignOptions `json:"alignOptions,omitempty"`
	AliasBy            string                    `json:"aliasBy,omitempty"`
	MetricType         string                    `json:"metricType,omitempty"`
	MetricKind         string                    `json:"metricKind,omitempty"`
	Filters            []string                  `json:"filters,omitempty"`
	AlignmentPeriod    string                    `json:"alignmentPeriod,omitempty"`
	CrossSeriesReducer string                    `json:"crossSeriesReducer,omitempty"`
	PerSeriesAligner   string                    `json:"perSeriesAligner,omitempty"`
	ValueType          string                    `json:"valueType,omitempty"`
	GroupBy            []string                  `json:"groupBy,omitempty"`
	Tags               []struct {
		Key      string `json:"key,omitempty"`
		Operator string `json:"operator,omitempty"`
		Value    string `json:"value,omitempty"`
	} `json:"tags,omitempty"`
}

// StackdriverAlignOptions defines the list of alignment options shown in
// Grafana during query configuration.
type StackdriverAlignOptions struct {
	Expanded bool                     `json:"expanded"`
	Label    string                   `json:"label"`
	Options  []StackdriverAlignOption `json:"options"`
}

// StackdriverAlignOption defines a single alignment option shown in Grafana
// during query configuration.
type StackdriverAlignOption struct {
	Label       string   `json:"label"`
	MetricKinds []string `json:"metricKinds"`
	Text        string   `json:"text"`
	Value       string   `json:"value"`
	ValueTypes  []string `json:"valueTypes"`
}

type MapType struct {
	Name  *string `json:"name,omitempty"`
	Value *int    `json:"value,omitempty"`
}

type RangeMap struct {
	From *string `json:"from,omitempty"`
	Text *string `json:"text,omitempty"`
	To   *string `json:"to,omitempty"`
}

// NewDashlist initializes panel with a dashlist panel.
func NewDashlist(title string) *Panel {
	if title == "" {
		title = "Panel Title"
	}
	render := "flot"
	return &Panel{
		CommonPanel: CommonPanel{
			OfType:   DashlistType,
			Title:    title,
			Type:     "dashlist",
			Renderer: &render,
			IsNew:    true},
		DashlistPanel: &DashlistPanel{}}
}

// NewGraph initializes panel with a graph panel.
func NewGraph(title string) *Panel {
	if title == "" {
		title = "Panel Title"
	}
	render := "flot"
	return &Panel{
		CommonPanel: CommonPanel{
			OfType:   GraphType,
			Title:    title,
			Type:     "graph",
			Renderer: &render,
			Span:     12,
			IsNew:    true},
		GraphPanel: &GraphPanel{
			NullPointMode: "connected",
			Pointradius:   5,
			XAxis:         true,
			YAxis:         true,
		}}
}

// NewTimeseries initializes panel with a timeseries panel.
func NewTimeseries(title string) *Panel {
	if title == "" {
		title = "Panel Title"
	}

	return &Panel{
		CommonPanel: CommonPanel{
			OfType: TimeseriesType,
			Title:  title,
			Type:   "timeseries",
			Span:   12,
			IsNew:  true,
		},
		TimeseriesPanel: &TimeseriesPanel{
			FieldConfig: FieldConfig{
				Defaults: FieldConfigDefaults{
					Color: FieldConfigColor{
						Mode:       "palette-classic",
						FixedColor: "green",
						SeriesBy:   "last",
					},
				},
			},
		},
	}
}

// NewTable initializes panel with a table panel.
func NewTable(title string) *Panel {
	if title == "" {
		title = "Panel Title"
	}
	render := "flot"
	return &Panel{
		CommonPanel: CommonPanel{
			OfType:   TableType,
			Title:    title,
			Type:     "table",
			Renderer: &render,
			IsNew:    true},
		TablePanel: &TablePanel{}}
}

// NewText initializes panel with a text panel.
func NewText(title string) *Panel {
	if title == "" {
		title = "Panel Title"
	}
	render := "flot"
	return &Panel{
		CommonPanel: CommonPanel{
			OfType:   TextType,
			Title:    title,
			Type:     "text",
			Renderer: &render,
			IsNew:    true},
		TextPanel: &TextPanel{}}
}

// NewSinglestat initializes panel with a singlestat panel.
func NewSinglestat(title string) *Panel {
	if title == "" {
		title = "Panel Title"
	}
	render := "flot"
	return &Panel{
		CommonPanel: CommonPanel{
			OfType:   SinglestatType,
			Title:    title,
			Type:     "singlestat",
			Renderer: &render,
			IsNew:    true},
		SinglestatPanel: &SinglestatPanel{}}
}

// NewStat initializes panel with a stat panel.
func NewStat(title string) *Panel {
	if title == "" {
		title = "Panel Title"
	}
	render := "flot"
	return &Panel{
		CommonPanel: CommonPanel{
			OfType:   StatType,
			Title:    title,
			Type:     "stat",
			Renderer: &render,
			IsNew:    true},
		StatPanel: &StatPanel{}}
}

// NewPluginlist initializes panel with a stat panel.
func NewPluginlist(title string) *Panel {
	if title == "" {
		title = "Panel Title"
	}
	render := "flot"
	return &Panel{
		CommonPanel: CommonPanel{
			OfType:   PluginlistType,
			Title:    title,
			Type:     "pluginlist",
			Renderer: &render,
			IsNew:    true},
		PluginlistPanel: &PluginlistPanel{}}
}

func NewAlertlist(title string) *Panel {
	if title == "" {
		title = "Panel Title"
	}
	render := "flot"
	return &Panel{
		CommonPanel: CommonPanel{
			OfType:   AlertlistType,
			Title:    title,
			Type:     "alertlist",
			Renderer: &render,
			IsNew:    true},
		AlertlistPanel: &AlertlistPanel{}}
}

func NewHeatmap(title string) *Panel {
	if title == "" {
		title = "Panel Title"
	}
	render := "flot"
	return &Panel{
		CommonPanel: CommonPanel{
			OfType:   HeatmapType,
			Title:    title,
			Type:     "heatmap",
			Renderer: &render,
			IsNew:    true},
		HeatmapPanel: &HeatmapPanel{}}
}

// NewCustom initializes panel with a stat panel.
func NewCustom(title string) *Panel {
	if title == "" {
		title = "Panel Title"
	}
	render := "flot"
	return &Panel{
		CommonPanel: CommonPanel{
			OfType:   CustomType,
			Title:    title,
			Type:     "singlestat",
			Renderer: &render,
			IsNew:    true},
		CustomPanel: &CustomPanel{}}
}

// ResetTargets delete all targets defined for a panel.
func (p *Panel) ResetTargets() {
	switch p.OfType {
	case GraphType:
		p.GraphPanel.Targets = nil
	case SinglestatType:
		p.SinglestatPanel.Targets = nil
	case StatType:
		p.StatPanel.Targets = nil
	case TableType:
		p.TablePanel.Targets = nil
	case BarGaugeType:
		p.BarGaugePanel.Targets = nil
	case HeatmapType:
		p.HeatmapPanel.Targets = nil
	case TimeseriesType:
		p.TimeseriesPanel.Targets = nil
	}
}

// AddTarget adds a new target as defined in the argument
// but with refId letter incremented. Value of refID from
// the argument will be used only if no target with such
// value already exists.
func (p *Panel) AddTarget(t *Target) {
	switch p.OfType {
	case GraphType:
		p.GraphPanel.Targets = append(p.GraphPanel.Targets, *t)
	case SinglestatType:
		p.SinglestatPanel.Targets = append(p.SinglestatPanel.Targets, *t)
	case StatType:
		p.StatPanel.Targets = append(p.StatPanel.Targets, *t)
	case TableType:
		p.TablePanel.Targets = append(p.TablePanel.Targets, *t)
	case HeatmapType:
		p.HeatmapPanel.Targets = append(p.HeatmapPanel.Targets, *t)
	case TimeseriesType:
		p.TimeseriesPanel.Targets = append(p.TimeseriesPanel.Targets, *t)
	}
	// TODO check for existing refID
}

// SetTarget updates a target if target with such refId exists
// or creates a new one.
func (p *Panel) SetTarget(t *Target) {
	setTarget := func(t *Target, targets *[]Target) {
		for i, target := range *targets {
			if t.RefID == target.RefID {
				(*targets)[i] = *t
				return
			}
		}
		(*targets) = append((*targets), *t)
	}
	switch p.OfType {
	case GraphType:
		setTarget(t, &p.GraphPanel.Targets)
	case SinglestatType:
		setTarget(t, &p.SinglestatPanel.Targets)
	case StatType:
		setTarget(t, &p.StatPanel.Targets)
	case TableType:
		setTarget(t, &p.TablePanel.Targets)
	case HeatmapType:
		setTarget(t, &p.HeatmapPanel.Targets)
	case TimeseriesType:
		setTarget(t, &p.TimeseriesPanel.Targets)
	}
}

// MapDatasources on all existing targets for the panel.
func (p *Panel) RepeatDatasourcesForEachTarget(dsNames ...string) {
	repeatDS := func(dsNames []string, targets *[]Target) {
		var refID = "A"
		originalTargets := *targets
		cleanedTargets := make([]Target, 0, len(originalTargets)*len(dsNames))
		*targets = cleanedTargets
		for _, target := range originalTargets {
			for _, ds := range dsNames {
				newTarget := target
				newTarget.RefID = refID
				newTarget.Datasource = ds
				refID = incRefID(refID)
				*targets = append(*targets, newTarget)
			}
		}
	}
	switch p.OfType {
	case GraphType:
		repeatDS(dsNames, &p.GraphPanel.Targets)
	case SinglestatType:
		repeatDS(dsNames, &p.SinglestatPanel.Targets)
	case StatType:
		repeatDS(dsNames, &p.StatPanel.Targets)
	case TableType:
		repeatDS(dsNames, &p.TablePanel.Targets)
	case HeatmapType:
		repeatDS(dsNames, &p.HeatmapPanel.Targets)
	case TimeseriesType:
		repeatDS(dsNames, &p.TimeseriesPanel.Targets)
	}
}

// RepeatTargetsForDatasources repeats all existing targets for a panel
// for all provided in the argument datasources. Existing datasources of
// targets are ignored.
func (p *Panel) RepeatTargetsForDatasources(dsNames ...string) {
	repeatTarget := func(dsNames []string, targets *[]Target) {
		var lastRefID string
		lenTargets := len(*targets)
		for i, name := range dsNames {
			if i < lenTargets {
				(*targets)[i].Datasource = name
				lastRefID = (*targets)[i].RefID
			} else {
				newTarget := (*targets)[i%lenTargets]
				lastRefID = incRefID(lastRefID)
				newTarget.RefID = lastRefID
				newTarget.Datasource = name
				*targets = append(*targets, newTarget)
			}
		}
	}
	switch p.OfType {
	case GraphType:
		repeatTarget(dsNames, &p.GraphPanel.Targets)
	case SinglestatType:
		repeatTarget(dsNames, &p.SinglestatPanel.Targets)
	case StatType:
		repeatTarget(dsNames, &p.StatPanel.Targets)
	case TableType:
		repeatTarget(dsNames, &p.TablePanel.Targets)
	case HeatmapType:
		repeatTarget(dsNames, &p.HeatmapPanel.Targets)
	case TimeseriesType:
		repeatTarget(dsNames, &p.TimeseriesPanel.Targets)
	}
}

// GetTargets is iterate over all panel targets. It just returns nil if
// no targets defined for panel of concrete type.
func (p *Panel) GetTargets() *[]Target {
	switch p.OfType {
	case GraphType:
		return &p.GraphPanel.Targets
	case SinglestatType:
		return &p.SinglestatPanel.Targets
	case StatType:
		return &p.StatPanel.Targets
	case TableType:
		return &p.TablePanel.Targets
	case BarGaugeType:
		return &p.BarGaugePanel.Targets
	case HeatmapType:
		return &p.HeatmapPanel.Targets
	case TimeseriesType:
		return &p.TimeseriesPanel.Targets
	default:
		return nil
	}
}

type probePanel struct {
	CommonPanel
	//	json.RawMessage
}

func (p *Panel) Cyclo(probe probePanel, b []byte) (err error) {
	switch probe.Type {
	case "graph":
		var graph GraphPanel
		p.OfType = GraphType
		if err = json.Unmarshal(b, &graph); err == nil {
			p.GraphPanel = &graph
		}
	case "table":
		var table TablePanel
		p.OfType = TableType
		if err = json.Unmarshal(b, &table); err == nil {
			p.TablePanel = &table
		}
	case "text":
		var text TextPanel
		p.OfType = TextType
		if err = json.Unmarshal(b, &text); err == nil {
			p.TextPanel = &text
		}
	case "singlestat":
		var singlestat SinglestatPanel
		p.OfType = SinglestatType
		if err = json.Unmarshal(b, &singlestat); err == nil {
			p.SinglestatPanel = &singlestat
		}
	case "stat":
		var stat StatPanel
		p.OfType = StatType
		if err = json.Unmarshal(b, &stat); err == nil {
			p.StatPanel = &stat
		}
	case "dashlist":
		var dashlist DashlistPanel
		p.OfType = DashlistType
		if err = json.Unmarshal(b, &dashlist); err == nil {
			p.DashlistPanel = &dashlist
		}
	case "bargauge":
		var bargauge BarGaugePanel
		p.OfType = BarGaugeType
		if err = json.Unmarshal(b, &bargauge); err == nil {
			p.BarGaugePanel = &bargauge
		}
	}
	return err
}

func (p *Panel) UnmarshalJSON(b []byte) (err error) {
	var probe probePanel
	if err = json.Unmarshal(b, &probe); err != nil {
		return err
	}

	p.CommonPanel = probe.CommonPanel
	if probe.Type == "graph" || probe.Type == "table" || probe.Type == "text" ||
		probe.Type == "singlestat" || probe.Type == "stat" || probe.Type == "dashlist" ||
		probe.Type == "bargauge" {
		err = p.Cyclo(probe, b)
	} else {
		switch probe.Type {
		case "heatmap":
			var heatmap HeatmapPanel
			p.OfType = HeatmapType
			if err = json.Unmarshal(b, &heatmap); err == nil {
				p.HeatmapPanel = &heatmap
			}
		case "timeseries":
			var timeseries TimeseriesPanel
			p.OfType = TimeseriesType
			if err = json.Unmarshal(b, &timeseries); err == nil {
				p.TimeseriesPanel = &timeseries
			}
		case "row":
			var rowpanel RowPanel
			p.OfType = RowType
			if err = json.Unmarshal(b, &rowpanel); err == nil {
				p.RowPanel = &rowpanel
			}
		default:
			var custom = make(CustomPanel)
			p.OfType = CustomType
			if err = json.Unmarshal(b, &custom); err == nil {
				p.CustomPanel = &custom
			}
		}
	}

	if err != nil && (probe.Title != "" || probe.Type != "") {
		err = fmt.Errorf("%w (panel %q of type %q)", err, probe.Title, probe.Type)
	}

	return err
}

func (p *Panel) MarshalJSON() ([]byte, error) {
	switch p.OfType {
	case GraphType:
		var outGraph = struct {
			CommonPanel
			GraphPanel
		}{p.CommonPanel, *p.GraphPanel}
		return json.Marshal(outGraph)
	case TableType:
		var outTable = struct {
			CommonPanel
			TablePanel
		}{p.CommonPanel, *p.TablePanel}
		return json.Marshal(outTable)
	case TextType:
		var outText = struct {
			CommonPanel
			TextPanel
		}{p.CommonPanel, *p.TextPanel}
		return json.Marshal(outText)
	case SinglestatType:
		var outSinglestat = struct {
			CommonPanel
			SinglestatPanel
		}{p.CommonPanel, *p.SinglestatPanel}
		return json.Marshal(outSinglestat)
	case StatType:
		var outSinglestat = struct {
			CommonPanel
			StatPanel
		}{p.CommonPanel, *p.StatPanel}
		return json.Marshal(outSinglestat)
	case DashlistType:
		var outDashlist = struct {
			CommonPanel
			DashlistPanel
		}{p.CommonPanel, *p.DashlistPanel}
		return json.Marshal(outDashlist)
	case BarGaugeType:
		var outBarGauge = struct {
			CommonPanel
			BarGaugePanel
		}{p.CommonPanel, *p.BarGaugePanel}
		return json.Marshal(outBarGauge)
	case PluginlistType:
		var outPluginlist = struct {
			CommonPanel
			PluginlistPanel
		}{p.CommonPanel, *p.PluginlistPanel}
		return json.Marshal(outPluginlist)
	case AlertlistType:
		var outAlertlist = struct {
			CommonPanel
			AlertlistPanel
		}{p.CommonPanel, *p.AlertlistPanel}
		return json.Marshal(outAlertlist)
	case RowType:
		var outRow = struct {
			CommonPanel
			RowPanel
		}{p.CommonPanel, *p.RowPanel}
		return json.Marshal(outRow)
	case HeatmapType:
		var outHeatmap = struct {
			CommonPanel
			HeatmapPanel
		}{p.CommonPanel, *p.HeatmapPanel}
		return json.Marshal(outHeatmap)
	case TimeseriesType:
		var outTimeseries = struct {
			CommonPanel
			TimeseriesPanel
		}{p.CommonPanel, *p.TimeseriesPanel}
		return json.Marshal(outTimeseries)
	case CustomType:
		var outCustom = customPanelOutput{
			p.CommonPanel,
			*p.CustomPanel,
		}
		return json.Marshal(outCustom)
	}
	return nil, errors.New("can't marshal unknown panel type")
}

type customPanelOutput struct {
	CommonPanel
	CustomPanel
}

func (c customPanelOutput) MarshalJSON() ([]byte, error) {
	b, err := json.Marshal(c.CommonPanel)
	if err != nil {
		return b, err
	}
	// Append custom keys to marshalled CommonPanel.
	buf := bytes.NewBuffer(b[:len(b)-1])

	// Sort keys to make output idempotent
	keys := make([]string, 0, len(c.CustomPanel))
	for k := range c.CustomPanel {
		keys = append(keys, k)
	}
	sort.Strings(keys)

	for _, k := range keys {
		buf.WriteString(`,"`)
		buf.WriteString(k)
		buf.WriteString(`":`)
		b, err := json.Marshal(c.CustomPanel[k])
		if err != nil {
			return b, err
		}
		buf.Write(b)
	}
	buf.WriteString("}")
	return buf.Bytes(), nil
}

func incRefID(refID string) string {
	firstLetter := refID[0]
	ordinal := int(firstLetter)
	ordinal++
	return string(rune(ordinal))
}
