/*
Copyright 2014 Rohith All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package marathon

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/url"
	"time"
)

var (
	// ErrNoApplicationContainer is thrown when a container has been specified yet
	ErrNoApplicationContainer = errors.New("you have not specified a docker container yet")
)

// Applications is a collection of applications
type Applications struct {
	Apps []Application `json:"apps"`
}

// IPAddressPerTask is used by IP-per-task functionality https://mesosphere.github.io/marathon/docs/ip-per-task.html
type IPAddressPerTask struct {
	Groups      *[]string          `json:"groups,omitempty"`
	Labels      *map[string]string `json:"labels,omitempty"`
	Discovery   *Discovery         `json:"discovery,omitempty"`
	NetworkName string             `json:"networkName,omitempty"`
}

// Discovery provides info about ports expose by IP-per-task functionality
type Discovery struct {
	Ports *[]Port `json:"ports,omitempty"`
}

// Port provides info about ports used by IP-per-task
type Port struct {
	Number   int    `json:"number,omitempty"`
	Name     string `json:"name,omitempty"`
	Protocol string `json:"protocol,omitempty"`
}

// Application is the definition for an application in marathon
type Application struct {
	ID                         string               `json:"id,omitempty"`
	Cmd                        *string              `json:"cmd,omitempty"`
	Args                       *[]string            `json:"args,omitempty"`
	Constraints                *[][]string          `json:"constraints,omitempty"`
	Container                  *Container           `json:"container,omitempty"`
	CPUs                       float64              `json:"cpus,omitempty"`
	Disk                       *float64             `json:"disk,omitempty"`
	Env                        *map[string]string   `json:"env,omitempty"`
	Executor                   *string              `json:"executor,omitempty"`
	HealthChecks               *[]HealthCheck       `json:"healthChecks,omitempty"`
	Instances                  *int                 `json:"instances,omitempty"`
	Mem                        *float64             `json:"mem,omitempty"`
	Tasks                      []*Task              `json:"tasks,omitempty"`
	Ports                      []int                `json:"ports"`
	PortDefinitions            *[]PortDefinition    `json:"portDefinitions,omitempty"`
	RequirePorts               *bool                `json:"requirePorts,omitempty"`
	BackoffSeconds             *float64             `json:"backoffSeconds,omitempty"`
	BackoffFactor              *float64             `json:"backoffFactor,omitempty"`
	MaxLaunchDelaySeconds      *float64             `json:"maxLaunchDelaySeconds,omitempty"`
	TaskKillGracePeriodSeconds *float64             `json:"taskKillGracePeriodSeconds,omitempty"`
	Deployments                []map[string]string  `json:"deployments,omitempty"`
	Dependencies               []string             `json:"dependencies"`
	TasksRunning               int                  `json:"tasksRunning,omitempty"`
	TasksStaged                int                  `json:"tasksStaged,omitempty"`
	TasksHealthy               int                  `json:"tasksHealthy,omitempty"`
	TasksUnhealthy             int                  `json:"tasksUnhealthy,omitempty"`
	TaskStats                  map[string]TaskStats `json:"taskStats,omitempty"`
	User                       string               `json:"user,omitempty"`
	UpgradeStrategy            *UpgradeStrategy     `json:"upgradeStrategy,omitempty"`
	Uris                       *[]string            `json:"uris,omitempty"`
	Version                    string               `json:"version,omitempty"`
	VersionInfo                *VersionInfo         `json:"versionInfo,omitempty"`
	Labels                     *map[string]string   `json:"labels,omitempty"`
	AcceptedResourceRoles      []string             `json:"acceptedResourceRoles,omitempty"`
	LastTaskFailure            *LastTaskFailure     `json:"lastTaskFailure,omitempty"`
	Fetch                      *[]Fetch             `json:"fetch,omitempty"`
	IPAddressPerTask           *IPAddressPerTask    `json:"ipAddress,omitempty"`
}

// ApplicationVersions is a collection of application versions for a specific app in marathon
type ApplicationVersions struct {
	Versions []string `json:"versions"`
}

// ApplicationVersion is the application version response from marathon
type ApplicationVersion struct {
	Version string `json:"version"`
}

// VersionInfo is the application versioning details from marathon
type VersionInfo struct {
	LastScalingAt      string `json:"lastScalingAt,omitempty"`
	LastConfigChangeAt string `json:"lastConfigChangeAt,omitempty"`
}

// Fetch will download URI before task starts
type Fetch struct {
	URI        string `json:"uri"`
	Executable bool   `json:"executable"`
	Extract    bool   `json:"extract"`
	Cache      bool   `json:"cache"`
}

// GetAppOpts contains a payload for Application method
//		embed:	Embeds nested resources that match the supplied path.
// 				You can specify this parameter multiple times with different values
type GetAppOpts struct {
	Embed []string `url:"embed,omitempty"`
}

// DeleteAppOpts contains a payload for DeleteApplication method
//		force:		overrides a currently running deployment.
type DeleteAppOpts struct {
	Force bool `url:"force,omitempty"`
}

// TaskStats is a container for Stats
type TaskStats struct {
	Stats Stats `json:"stats"`
}

// Stats is a collection of aggregate statistics about an application's tasks
type Stats struct {
	Counts   map[string]int     `json:"counts"`
	LifeTime map[string]float64 `json:"lifeTime"`
}

// SetIPAddressPerTask defines that the application will have a IP address defines by a external agent.
// This configuration is not allowed to be used with Port or PortDefinitions. Thus, the implementation
// clears both.
func (r *Application) SetIPAddressPerTask(ipAddressPerTask IPAddressPerTask) *Application {
	r.Ports = make([]int, 0)
	r.EmptyPortDefinitions()
	r.IPAddressPerTask = &ipAddressPerTask

	return r
}

// NewDockerApplication creates a default docker application
func NewDockerApplication() *Application {
	application := new(Application)
	application.Container = NewDockerContainer()
	return application
}

// Name sets the name / ID of the application i.e. the identifier for this application
func (r *Application) Name(id string) *Application {
	r.ID = validateID(id)
	return r
}

// Command sets the cmd of the application
func (r *Application) Command(cmd string) *Application {
	r.Cmd = &cmd
	return r
}

// CPU set the amount of CPU shares per instance which is assigned to the application
//		cpu:	the CPU shared (check Docker docs) per instance
func (r *Application) CPU(cpu float64) *Application {
	r.CPUs = cpu
	return r
}

// Storage sets the amount of disk space the application is assigned, which for docker
// application I don't believe is relevant
//		disk:	the disk space in MB
func (r *Application) Storage(disk float64) *Application {
	r.Disk = &disk
	return r
}

// AllTaskRunning checks to see if all the application tasks are running, i.e. the instances is equal
// to the number of running tasks
func (r *Application) AllTaskRunning() bool {
	if r.Instances == nil || *r.Instances == 0 {
		return true
	}
	if r.Tasks == nil {
		return false
	}
	if r.TasksRunning == *r.Instances {
		return true
	}
	return false
}

// DependsOn adds one or more dependencies for this application. Note, if you want to wait for
// an application dependency to actually be UP, i.e. not just deployed, you need a health check
// on the dependant app.
//		names:	the application id(s) this application depends on
func (r *Application) DependsOn(names ...string) *Application {
	if r.Dependencies == nil {
		r.Dependencies = make([]string, 0)
	}
	r.Dependencies = append(r.Dependencies, names...)

	return r
}

// Memory sets he amount of memory the application can consume per instance
//		memory:	the amount of MB to assign
func (r *Application) Memory(memory float64) *Application {
	r.Mem = &memory

	return r
}

// AddPortDefinition adds a port definition. Port definitions are used to define ports that
// should be considered part of a resource. They are necessary when you are using HOST
// networking and no port mappings are specified.
func (r *Application) AddPortDefinition(portDefinition PortDefinition) *Application {
	if r.PortDefinitions == nil {
		r.EmptyPortDefinitions()
	}

	portDefinitions := *r.PortDefinitions
	portDefinitions = append(portDefinitions, portDefinition)
	r.PortDefinitions = &portDefinitions
	return r
}

// EmptyPortDefinitions explicitly empties port definitions -- use this if you need to empty
// port definitions of an application that already has port definitions set (setting port definitions to nil will
// keep the current value)
func (r *Application) EmptyPortDefinitions() *Application {
	r.PortDefinitions = &[]PortDefinition{}

	return r
}

// Count sets the number of instances of the application to run
//		count:	the number of instances to run
func (r *Application) Count(count int) *Application {
	r.Instances = &count

	return r
}

// SetTaskKillGracePeriod sets the number of seconds between escalating from SIGTERM to SIGKILL
// when signalling tasks to terminate. Using this grace period, tasks should perform orderly shut down
// immediately upon receiving SIGTERM.
//		seconds:	the number of seconds
func (r *Application) SetTaskKillGracePeriod(seconds float64) *Application {
	r.TaskKillGracePeriodSeconds = &seconds

	return r
}

// AddArgs adds one or more arguments to the applications
//		arguments:	the argument(s) you are adding
func (r *Application) AddArgs(arguments ...string) *Application {
	if r.Args == nil {
		r.EmptyArgs()
	}

	args := *r.Args
	args = append(args, arguments...)
	r.Args = &args

	return r
}

// EmptyArgs explicitly empties arguments -- use this if you need to empty
// arguments of an application that already has arguments set (setting args to nil will
// keep the current value)
func (r *Application) EmptyArgs() *Application {
	r.Args = &[]string{}

	return r
}

// AddConstraint adds a new constraint
//		constraints:	the constraint definition, one constraint per array element
func (r *Application) AddConstraint(constraints ...string) *Application {
	if r.Constraints == nil {
		r.EmptyConstraints()
	}

	c := *r.Constraints
	c = append(c, constraints)
	r.Constraints = &c

	return r
}

// EmptyConstraints explicitly empties constraints -- use this if you need to empty
// constraints of an application that already has constraints set (setting constraints to nil will
// keep the current value)
func (r *Application) EmptyConstraints() *Application {
	r.Constraints = &[][]string{}

	return r
}

// AddLabel adds a label to the application
//		name:	the name of the label
//		value: value for this label
func (r *Application) AddLabel(name, value string) *Application {
	if r.Labels == nil {
		r.EmptyLabels()
	}
	(*r.Labels)[name] = value

	return r
}

// EmptyLabels explicitly empties the labels -- use this if you need to empty
// the labels of an application that already has labels set (setting labels to nil will
// keep the current value)
func (r *Application) EmptyLabels() *Application {
	r.Labels = &map[string]string{}

	return r
}

// AddEnv adds an environment variable to the application
//		name:	the name of the variable
//		value:	go figure, the value associated to the above
func (r *Application) AddEnv(name, value string) *Application {
	if r.Env == nil {
		r.EmptyEnvs()
	}
	(*r.Env)[name] = value

	return r
}

// EmptyEnvs explicitly empties the envs -- use this if you need to empty
// the environments of an application that already has environments set (setting env to nil will
// keep the current value)
func (r *Application) EmptyEnvs() *Application {
	r.Env = &map[string]string{}

	return r
}

// SetExecutor sets the executor
func (r *Application) SetExecutor(executor string) *Application {
	r.Executor = &executor

	return r
}

// AddHealthCheck adds a health check
// 	healthCheck the health check that should be added
func (r *Application) AddHealthCheck(healthCheck HealthCheck) *Application {
	if r.HealthChecks == nil {
		r.EmptyHealthChecks()
	}

	healthChecks := *r.HealthChecks
	healthChecks = append(healthChecks, healthCheck)
	r.HealthChecks = &healthChecks

	return r
}

// EmptyHealthChecks explicitly empties health checks -- use this if you need to empty
// health checks of an application that already has health checks set (setting health checks to nil will
// keep the current value)
func (r *Application) EmptyHealthChecks() *Application {
	r.HealthChecks = &[]HealthCheck{}

	return r
}

// HasHealthChecks is a helper method, used to check if an application has health checks
func (r *Application) HasHealthChecks() bool {
	return r.HealthChecks != nil && len(*r.HealthChecks) > 0
}

// DeploymentIDs retrieves the application deployments IDs
func (r *Application) DeploymentIDs() []*DeploymentID {
	var deployments []*DeploymentID

	if r.Deployments == nil {
		return deployments
	}

	// step: extract the deployment id from the result
	for _, deploy := range r.Deployments {
		if id, found := deploy["id"]; found {
			deployment := &DeploymentID{
				Version:      r.Version,
				DeploymentID: id,
			}
			deployments = append(deployments, deployment)
		}
	}

	return deployments
}

// CheckHTTP adds a HTTP check to an application
//		port: 		the port the check should be checking
// 		interval:	the interval in seconds the check should be performed
func (r *Application) CheckHTTP(uri string, port, interval int) (*Application, error) {
	if r.Container == nil || r.Container.Docker == nil {
		return nil, ErrNoApplicationContainer
	}
	// step: get the port index
	portIndex, err := r.Container.Docker.ServicePortIndex(port)
	if err != nil {
		return nil, err
	}
	health := NewDefaultHealthCheck()
	health.IntervalSeconds = interval
	*health.Path = uri
	*health.PortIndex = portIndex
	// step: add to the checks
	r.AddHealthCheck(*health)

	return r, nil
}

// CheckTCP adds a TCP check to an application; note the port mapping must already exist, or an
// error will thrown
//		port: 		the port the check should, err, check
// 		interval:	the interval in seconds the check should be performed
func (r *Application) CheckTCP(port, interval int) (*Application, error) {
	if r.Container == nil || r.Container.Docker == nil {
		return nil, ErrNoApplicationContainer
	}
	// step: get the port index
	portIndex, err := r.Container.Docker.ServicePortIndex(port)
	if err != nil {
		return nil, err
	}
	health := NewDefaultHealthCheck()
	health.Protocol = "TCP"
	health.IntervalSeconds = interval
	*health.PortIndex = portIndex
	// step: add to the checks
	r.AddHealthCheck(*health)

	return r, nil
}

// AddUris adds one or more uris to the applications
//		arguments:	the uri(s) you are adding
func (r *Application) AddUris(newUris ...string) *Application {
	if r.Uris == nil {
		r.EmptyUris()
	}

	uris := *r.Uris
	uris = append(uris, newUris...)
	r.Uris = &uris

	return r
}

// EmptyUris explicitly empties uris -- use this if you need to empty
// uris of an application that already has uris set (setting uris to nil will
// keep the current value)
func (r *Application) EmptyUris() *Application {
	r.Uris = &[]string{}

	return r
}

// AddFetchURIs adds one or more fetch URIs to the application.
//		fetchURIs:	the fetch URI(s) to add.
func (r *Application) AddFetchURIs(fetchURIs ...Fetch) *Application {
	if r.Fetch == nil {
		r.EmptyFetchURIs()
	}

	fetch := *r.Fetch
	fetch = append(fetch, fetchURIs...)
	r.Fetch = &fetch

	return r
}

// EmptyFetchURIs explicitly empties fetch URIs -- use this if you need to empty
// fetch URIs of an application that already has fetch URIs set.
// Setting fetch URIs to nil will keep the current value.
func (r *Application) EmptyFetchURIs() *Application {
	r.Fetch = &[]Fetch{}

	return r
}

// SetUpgradeStrategy sets the upgrade strategy.
func (r *Application) SetUpgradeStrategy(us UpgradeStrategy) *Application {
	r.UpgradeStrategy = &us
	return r
}

// EmptyUpgradeStrategy explicitly empties the upgrade strategy -- use this if
// you need to empty the upgrade strategy of an application that already has
// the upgrade strategy set (setting it to nil will keep the current value).
func (r *Application) EmptyUpgradeStrategy() *Application {
	r.UpgradeStrategy = &UpgradeStrategy{}
	return r
}

// String returns the json representation of this application
func (r *Application) String() string {
	s, err := json.MarshalIndent(r, "", "  ")
	if err != nil {
		return fmt.Sprintf(`{"error": "error decoding type into json: %s"}`, err)
	}

	return string(s)
}

// Applications retrieves an array of all the applications which are running in marathon
func (r *marathonClient) Applications(v url.Values) (*Applications, error) {
	query := v.Encode()
	if query != "" {
		query = "?" + query
	}

	applications := new(Applications)
	err := r.apiGet(marathonAPIApps+query, nil, applications)
	if err != nil {
		return nil, err
	}

	return applications, nil
}

// ListApplications retrieves an array of the application names currently running in marathon
func (r *marathonClient) ListApplications(v url.Values) ([]string, error) {
	applications, err := r.Applications(v)
	if err != nil {
		return nil, err
	}
	var list []string
	for _, application := range applications.Apps {
		list = append(list, application.ID)
	}

	return list, nil
}

// HasApplicationVersion checks to see if the application version exists in Marathon
// 		name: 		the id used to identify the application
//		version: 	the version (normally a timestamp) your looking for
func (r *marathonClient) HasApplicationVersion(name, version string) (bool, error) {
	id := trimRootPath(name)
	versions, err := r.ApplicationVersions(id)
	if err != nil {
		return false, err
	}

	return contains(versions.Versions, version), nil
}

// ApplicationVersions is a list of versions which has been deployed with marathon for a specific application
//		name:		the id used to identify the application
func (r *marathonClient) ApplicationVersions(name string) (*ApplicationVersions, error) {
	uri := fmt.Sprintf("%s/versions", buildURI(name))
	versions := new(ApplicationVersions)
	if err := r.apiGet(uri, nil, versions); err != nil {
		return nil, err
	}
	return versions, nil
}

// SetApplicationVersion changes the version of the application
// 		name: 		the id used to identify the application
//		version: 	the version (normally a timestamp) you wish to change to
func (r *marathonClient) SetApplicationVersion(name string, version *ApplicationVersion) (*DeploymentID, error) {
	uri := fmt.Sprintf(buildURI(name))
	deploymentID := new(DeploymentID)
	if err := r.apiPut(uri, version, deploymentID); err != nil {
		return nil, err
	}

	return deploymentID, nil
}

// Application retrieves the application configuration from marathon
// 		name: 		the id used to identify the application
func (r *marathonClient) Application(name string) (*Application, error) {
	var wrapper struct {
		Application *Application `json:"app"`
	}

	if err := r.apiGet(buildURI(name), nil, &wrapper); err != nil {
		return nil, err
	}

	return wrapper.Application, nil
}

// ApplicationBy retrieves the application configuration from marathon
// 		name: 		the id used to identify the application
//		opts:		GetAppOpts request payload
func (r *marathonClient) ApplicationBy(name string, opts *GetAppOpts) (*Application, error) {
	u, err := addOptions(buildURI(name), opts)
	if err != nil {
		return nil, err
	}
	var wrapper struct {
		Application *Application `json:"app"`
	}

	if err := r.apiGet(u, nil, &wrapper); err != nil {
		return nil, err
	}

	return wrapper.Application, nil
}

// ApplicationByVersion retrieves the application configuration from marathon
// 		name: 		the id used to identify the application
// 		version:  the version of the configuration you would like to receive
func (r *marathonClient) ApplicationByVersion(name, version string) (*Application, error) {
	app := new(Application)

	uri := fmt.Sprintf("%s/versions/%s", buildURI(name), version)
	if err := r.apiGet(uri, nil, app); err != nil {
		return nil, err
	}

	return app, nil
}

// ApplicationOK validates that the application, or more appropriately it's tasks have passed all the health checks.
// If no health checks exist, we simply return true
// 		name: 		the id used to identify the application
func (r *marathonClient) ApplicationOK(name string) (bool, error) {
	// step: get the application
	application, err := r.Application(name)
	if err != nil {
		return false, err
	}

	// step: check if all the tasks are running?
	if !application.AllTaskRunning() {
		return false, nil
	}

	// step: if the application has not health checks, just return true
	if application.HealthChecks == nil || len(*application.HealthChecks) == 0 {
		return true, nil
	}

	// step: iterate the application checks and look for false
	for _, task := range application.Tasks {
		// Health check results may not be available immediately. Assume
		// non-healthiness if they are missing for any task.
		if task.HealthCheckResults == nil {
			return false, nil
		}

		for _, check := range task.HealthCheckResults {
			//When a task is flapping in Marathon, this is sometimes nil
			if check == nil || !check.Alive {
				return false, nil
			}
		}
	}

	return true, nil
}

// ApplicationDeployments retrieves an array of Deployment IDs for an application
//       name:       the id used to identify the application
func (r *marathonClient) ApplicationDeployments(name string) ([]*DeploymentID, error) {
	application, err := r.Application(name)
	if err != nil {
		return nil, err
	}

	return application.DeploymentIDs(), nil
}

// CreateApplication creates a new application in Marathon
// 		application:		the structure holding the application configuration
func (r *marathonClient) CreateApplication(application *Application) (*Application, error) {
	result := new(Application)
	if err := r.apiPost(marathonAPIApps, application, result); err != nil {
		return nil, err
	}

	return result, nil
}

// WaitOnApplication waits for an application to be deployed
//		name:		the id of the application
//		timeout:	a duration of time to wait for an application to deploy
func (r *marathonClient) WaitOnApplication(name string, timeout time.Duration) error {
	if r.appExistAndRunning(name) {
		return nil
	}

	timeoutTimer := time.After(timeout)
	ticker := time.NewTicker(r.config.PollingWaitTime)
	defer ticker.Stop()

	for {
		select {
		case <-timeoutTimer:
			return ErrTimeoutError
		case <-ticker.C:
			if r.appExistAndRunning(name) {
				return nil
			}
		}
	}
}

func (r *marathonClient) appExistAndRunning(name string) bool {
	app, err := r.Application(name)
	if apiErr, ok := err.(*APIError); ok && apiErr.ErrCode == ErrCodeNotFound {
		return false
	}
	if err == nil && app.AllTaskRunning() {
		return true
	}
	return false
}

// DeleteApplication deletes an application from marathon
// 		name: 		the id used to identify the application
//		force:		used to force the delete operation in case of blocked deployment
func (r *marathonClient) DeleteApplication(name string, force bool) (*DeploymentID, error) {
	uri := buildURIWithForceParam(name, force)
	// step: check of the application already exists
	deployID := new(DeploymentID)
	if err := r.apiDelete(uri, nil, deployID); err != nil {
		return nil, err
	}

	return deployID, nil
}

// RestartApplication performs a rolling restart of marathon application
// 		name: 		the id used to identify the application
func (r *marathonClient) RestartApplication(name string, force bool) (*DeploymentID, error) {
	deployment := new(DeploymentID)
	var options struct{}
	uri := buildURIWithForceParam(fmt.Sprintf("%s/restart", name), force)
	if err := r.apiPost(uri, &options, deployment); err != nil {
		return nil, err
	}

	return deployment, nil
}

// ScaleApplicationInstances changes the number of instance an application is running
// 		name: 		the id used to identify the application
// 		instances:	the number of instances you wish to change to
//    force: used to force the scale operation in case of blocked deployment
func (r *marathonClient) ScaleApplicationInstances(name string, instances int, force bool) (*DeploymentID, error) {
	changes := new(Application)
	changes.ID = validateID(name)
	changes.Instances = &instances
	uri := buildURIWithForceParam(name, force)
	deployID := new(DeploymentID)
	if err := r.apiPut(uri, changes, deployID); err != nil {
		return nil, err
	}

	return deployID, nil
}

// UpdateApplication updates an application in Marathon
// 		application:		the structure holding the application configuration
func (r *marathonClient) UpdateApplication(application *Application, force bool) (*DeploymentID, error) {
	result := new(DeploymentID)
	uri := buildURIWithForceParam(application.ID, force)
	if err := r.apiPut(uri, application, result); err != nil {
		return nil, err
	}
	return result, nil
}

func buildURIWithForceParam(path string, force bool) string {
	uri := buildURI(path)
	if force {
		uri += "?force=true"
	}
	return uri
}

func buildURI(path string) string {
	return fmt.Sprintf("%s/%s", marathonAPIApps, trimRootPath(path))
}

// EmptyLabels explicitly empties labels -- use this if you need to empty
// labels of an application that already has IP per task with labels defined
func (i *IPAddressPerTask) EmptyLabels() *IPAddressPerTask {
	i.Labels = &map[string]string{}
	return i
}

// AddLabel adds a label to an IPAddressPerTask
//    name: The label name
//   value: The label value
func (i *IPAddressPerTask) AddLabel(name, value string) *IPAddressPerTask {
	if i.Labels == nil {
		i.EmptyLabels()
	}
	(*i.Labels)[name] = value
	return i
}

// EmptyGroups explicitly empties groups -- use this if you need to empty
// groups of an application that already has IP per task with groups defined
func (i *IPAddressPerTask) EmptyGroups() *IPAddressPerTask {
	i.Groups = &[]string{}
	return i
}

// AddGroup adds a group to an IPAddressPerTask
//  group: The group name
func (i *IPAddressPerTask) AddGroup(group string) *IPAddressPerTask {
	if i.Groups == nil {
		i.EmptyGroups()
	}

	groups := *i.Groups
	groups = append(groups, group)
	i.Groups = &groups

	return i
}

// SetDiscovery define the discovery to an IPAddressPerTask
//  discovery: The discovery struct
func (i *IPAddressPerTask) SetDiscovery(discovery Discovery) *IPAddressPerTask {
	i.Discovery = &discovery
	return i
}

// EmptyPorts explicitly empties discovey port -- use this if you need to empty
// discovey port of an application that already has IP per task with discovey ports
// defined
func (d *Discovery) EmptyPorts() *Discovery {
	d.Ports = &[]Port{}
	return d
}

// AddPort adds a port to the discovery info of a IP per task applicable
//   port: The discovery port
func (d *Discovery) AddPort(port Port) *Discovery {
	if d.Ports == nil {
		d.EmptyPorts()
	}
	ports := *d.Ports
	ports = append(ports, port)
	d.Ports = &ports
	return d
}
