// Copyright 2018 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Model representing and methods for controlling a remote web display.

package field

import (
	"fmt"
	"math/rand"
	"net/url"
	"reflect"
	"sort"
	"strconv"
	"strings"
	"sync"
)

const (
	minDisplayId = 100
	maxDisplayId = 999
)

type DisplayType int

const (
	InvalidDisplay DisplayType = iota
	PlaceholderDisplay
	AllianceStationDisplay
	AnnouncerDisplay
	AudienceDisplay
	FieldMonitorDisplay
	PitDisplay
	QueueingDisplay
	TwitchStreamDisplay
)

var DisplayTypeNames = map[DisplayType]string{
	PlaceholderDisplay:     "Placeholder",
	AllianceStationDisplay: "Alliance Station",
	AnnouncerDisplay:       "Announcer",
	AudienceDisplay:        "Audience",
	FieldMonitorDisplay:    "Field Monitor",
	PitDisplay:             "Pit",
	QueueingDisplay:        "Queueing",
	TwitchStreamDisplay:    "Twitch Stream",
}

var displayTypePaths = map[DisplayType]string{
	PlaceholderDisplay:     "/display",
	AllianceStationDisplay: "/displays/alliance_station",
	AnnouncerDisplay:       "/displays/announcer",
	AudienceDisplay:        "/displays/audience",
	FieldMonitorDisplay:    "/displays/field_monitor",
	PitDisplay:             "/displays/pit",
	QueueingDisplay:        "/displays/queueing",
	TwitchStreamDisplay:    "/displays/twitch",
}

var displayRegistryMutex sync.Mutex

type Display struct {
	Id              string
	Nickname        string
	Type            DisplayType
	Configuration   map[string]string
	IpAddress       string
	ConnectionCount int
}

// Parses the given display URL path and query string to extract the configuration.
func DisplayFromUrl(path string, query map[string][]string) (*Display, error) {
	if _, ok := query["displayId"]; !ok {
		return nil, fmt.Errorf("Display ID not present in request.")
	}

	var display Display
	display.Id = query["displayId"][0]
	if nickname, ok := query["nickname"]; ok {
		display.Nickname, _ = url.QueryUnescape(nickname[0])
	}

	// Determine type from the websocket connection URL. This way of doing it isn't super efficient, but it's not really
	// a concern since it should happen relatively infrequently.
	for displayType, displayPath := range displayTypePaths {
		if path == displayPath+"/websocket" {
			display.Type = displayType
			break
		}
	}
	if display.Type == InvalidDisplay {
		return nil, fmt.Errorf("Could not determine display type from path %s.", path)
	}

	// Put any remaining query parameters into the per-type configuration map.
	display.Configuration = make(map[string]string)
	for key, value := range query {
		if key != "displayId" && key != "nickname" {
			display.Configuration[key], _ = url.QueryUnescape(value[0])
		}
	}

	return &display, nil
}

// Returns the URL string for the given display that includes all of its configuration parameters.
func (display *Display) ToUrl() string {
	var builder strings.Builder
	builder.WriteString(displayTypePaths[display.Type])
	builder.WriteString("?displayId=")
	builder.WriteString(url.QueryEscape(display.Id))
	if display.Nickname != "" {
		builder.WriteString("&nickname=")
		builder.WriteString(url.QueryEscape(display.Nickname))
	}

	// Sort the keys so that the URL generated is deterministic.
	var keys []string
	for key := range display.Configuration {
		keys = append(keys, key)
	}
	sort.Strings(keys)
	for _, key := range keys {
		builder.WriteString("&")
		builder.WriteString(url.QueryEscape(key))
		builder.WriteString("=")
		builder.WriteString(url.QueryEscape(display.Configuration[key]))
	}
	return builder.String()
}

// Returns an unused ID that can be used for a new display.
func (arena *Arena) NextDisplayId() string {
	displayRegistryMutex.Lock()
	defer displayRegistryMutex.Unlock()

	// Loop until we get an ID that isn't already used. This is inefficient if there is a large number of displays, but
	// that should never be the case.
	for {
		candidateId := strconv.Itoa(rand.Intn(maxDisplayId+1-minDisplayId) + minDisplayId)
		if _, ok := arena.Displays[candidateId]; !ok {
			return candidateId
		}
	}
}

// Adds the given display to the arena registry and triggers a notification.
func (arena *Arena) RegisterDisplay(display *Display) {
	displayRegistryMutex.Lock()
	defer displayRegistryMutex.Unlock()

	existingDisplay, ok := arena.Displays[display.Id]
	if ok && display.Type == PlaceholderDisplay {
		// Don't rewrite the registered configuration if the new one is a placeholder -- if it is reconnecting after a
		// restart, it should adopt the existing configuration.
		arena.Displays[display.Id].ConnectionCount++
	} else {
		if ok {
			display.ConnectionCount = existingDisplay.ConnectionCount + 1
		} else {
			display.ConnectionCount = 1
		}
		arena.Displays[display.Id] = display
	}
	arena.DisplayConfigurationNotifier.Notify()
}

// Updates the given display in the arena registry. Triggers a notification if the display configuration changed.
func (arena *Arena) UpdateDisplay(display *Display) error {
	displayRegistryMutex.Lock()
	defer displayRegistryMutex.Unlock()

	existingDisplay, ok := arena.Displays[display.Id]
	if !ok {
		return fmt.Errorf("Display %s doesn't exist.", display.Id)
	}
	display.ConnectionCount = existingDisplay.ConnectionCount
	if !reflect.DeepEqual(existingDisplay, display) {
		arena.Displays[display.Id] = display
		arena.DisplayConfigurationNotifier.Notify()
	}
	return nil
}

// Marks the given display as having disconnected in the arena registry and triggers a notification.
func (arena *Arena) MarkDisplayDisconnected(display *Display) {
	displayRegistryMutex.Lock()
	defer displayRegistryMutex.Unlock()

	if existingDisplay, ok := arena.Displays[display.Id]; ok {
		if existingDisplay.Type == PlaceholderDisplay && existingDisplay.Nickname == "" &&
			len(existingDisplay.Configuration) == 0 {
			// If the display is an unconfigured placeholder, just remove it entirely to prevent clutter.
			delete(arena.Displays, existingDisplay.Id)
		} else {
			existingDisplay.ConnectionCount -= 1
			arena.DisplayConfigurationNotifier.Notify()

		}
	}
}
