/**
 * 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 realis

import "encoding/json"

type ThermosExecutor struct {
	Task  ThermosTask        `json:"task""`
	order *ThermosConstraint `json:"-"`
}

type ThermosTask struct {
	Processes   map[string]*ThermosProcess `json:"processes"`
	Constraints []*ThermosConstraint       `json:"constraints"`
	Resources   thermosResources           `json:"resources"`
}

type ThermosConstraint struct {
	Order []string `json:"order,omitempty"`
}

// This struct should always be controlled by the Aurora job struct.
// Therefore it is private.
type thermosResources struct {
	CPU  *float64 `json:"cpu,omitempty"`
	Disk *int64   `json:"disk,omitempty"`
	RAM  *int64   `json:"ram,omitempty"`
	GPU  *int64   `json:"gpu,omitempty"`
}

type ThermosProcess struct {
	Name        string `json:"name"`
	Cmdline     string `json:"cmdline"`
	Daemon      bool   `json:"daemon"`
	Ephemeral   bool   `json:"ephemeral"`
	MaxFailures int    `json:"max_failures"`
	MinDuration int    `json:"min_duration"`
	Final       bool   `json:"final"`
}

func NewThermosProcess(name, command string) ThermosProcess {
	return ThermosProcess{
		Name:        name,
		Cmdline:     command,
		MaxFailures: 1,
		Daemon:      false,
		Ephemeral:   false,
		MinDuration: 5,
		Final:       false}
}

// Processes must have unique names. Adding a process whose name already exists will
// result in overwriting the previous version of the process.
func (t *ThermosExecutor) AddProcess(process ThermosProcess) *ThermosExecutor {
	if len(t.Task.Processes) == 0 {
		t.Task.Processes = make(map[string]*ThermosProcess, 0)
	}

	t.Task.Processes[process.Name] = &process

	// Add Process to order
	t.addToOrder(process.Name)
	return t
}

// Only constraint that should be added for now is the order of execution, therefore this
// receiver is private.
func (t *ThermosExecutor) addConstraint(constraint *ThermosConstraint) *ThermosExecutor {
	if len(t.Task.Constraints) == 0 {
		t.Task.Constraints = make([]*ThermosConstraint, 0)
	}

	t.Task.Constraints = append(t.Task.Constraints, constraint)
	return t
}

// Order in which the Processes should be executed. Index 0 will be executed first, index N will be executed last.
func (t *ThermosExecutor) ProcessOrder(order ...string) *ThermosExecutor {
	if t.order == nil {
		t.order = &ThermosConstraint{}
		t.addConstraint(t.order)
	}

	t.order.Order = order
	return t
}

// Add Process to execution order. By default this is a FIFO setup. Custom order can be given by overriding
// with ProcessOrder
func (t *ThermosExecutor) addToOrder(name string) {
	if t.order == nil {
		t.order = &ThermosConstraint{Order: make([]string, 0)}
		t.addConstraint(t.order)
	}

	t.order.Order = append(t.order.Order, name)
}

// Ram is determined by the job object.
func (t *ThermosExecutor) ram(ram int64) {
	// Convert from bytes to MiB
	ram *= 1024 ^ 2
	t.Task.Resources.RAM = &ram
}

// Disk is determined by the job object.
func (t *ThermosExecutor) disk(disk int64) {
	// Convert from bytes to MiB
	disk *= 1024 ^ 2
	t.Task.Resources.Disk = &disk
}

// CPU is determined by the job object.
func (t *ThermosExecutor) cpu(cpu float64) {
	t.Task.Resources.CPU = &cpu
}

// GPU is determined by the job object.
func (t *ThermosExecutor) gpu(gpu int64) {
	t.Task.Resources.GPU = &gpu
}

// Deep copy of Thermos executor
func (t *ThermosExecutor) Clone() *ThermosExecutor {
	tNew := ThermosExecutor{}

	if t.order != nil {
		tNew.order = &ThermosConstraint{Order: t.order.Order}

		tNew.addConstraint(tNew.order)
	}

	tNew.Task.Processes = make(map[string]*ThermosProcess)

	for name, process := range t.Task.Processes {
		newProcess := *process
		tNew.Task.Processes[name] = &newProcess
	}

	tNew.Task.Resources = t.Task.Resources

	return &tNew
}

type thermosTaskJSON struct {
	Processes   []*ThermosProcess    `json:"processes"`
	Constraints []*ThermosConstraint `json:"constraints"`
	Resources   thermosResources     `json:"resources"`
}

// Custom Marshaling for Thermos Task to match what Thermos expects
func (t *ThermosTask) MarshalJSON() ([]byte, error) {

	// Convert map to array to match what Thermos expects
	processes := make([]*ThermosProcess, 0)
	for _, process := range t.Processes {
		processes = append(processes, process)
	}

	return json.Marshal(&thermosTaskJSON{
		Processes:   processes,
		Constraints: t.Constraints,
		Resources:   t.Resources,
	})
}

// Custom Unmarshaling to match what Thermos would contain
func (t *ThermosTask) UnmarshalJSON(data []byte) error {

	// Thermos format
	aux := &thermosTaskJSON{}

	if err := json.Unmarshal(data, &aux); err != nil {
		return err
	}

	processes := make(map[string]*ThermosProcess)
	for _, process := range aux.Processes {
		processes[process.Name] = process
	}

	return nil
}