/*
 * Copyright (C) 2018 "IoT.bzh"
 * Author Sebastien Douheret <sebastien@iot.bzh>
 *
 * 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 xdsserver

import (
	"encoding/xml"
	"fmt"
	"log"
	"os"
	"strings"

	common "gerrit.automotivelinux.org/gerrit/src/xds/xds-common.git/golib"
	"gerrit.automotivelinux.org/gerrit/src/xds/xds-server/lib/xdsconfig"
	"gerrit.automotivelinux.org/gerrit/src/xds/xds-server/lib/xsapiv1"
	"github.com/syncthing/syncthing/lib/sync"
)

// Targets Represent a XDS targets
type Targets struct {
	*Context
	fileOnDisk string
	tgts       map[string]*ITARGET
	terminals  map[string]*Terminals
}

// Mutex to make add/delete atomic
var tcMutex = sync.NewMutex()

/***
 * Targets
 ***/

// TargetsConstructor Create a new instance of Model Target
func TargetsConstructor(ctx *Context) *Targets {
	file, _ := xdsconfig.TargetsConfigFilenameGet()
	return &Targets{
		Context:    ctx,
		fileOnDisk: file,
		tgts:       make(map[string]*ITARGET),
		terminals:  make(map[string]*Terminals),
	}
}

// LoadConfig Load targets configuration from disk
func (t *Targets) LoadConfig() error {
	var tgts []xsapiv1.TargetConfig

	if t.fileOnDisk != "" {
		t.Log.Infof("Use target config file: %s", t.fileOnDisk)
		err := targetsConfigRead(t.fileOnDisk, &tgts)
		if err != nil {
			if strings.HasPrefix(err.Error(), "EOF") {
				t.Log.Warnf("Empty target config file")
			} else if strings.HasPrefix(err.Error(), "No target config") {
				t.Log.Warnf(err.Error())
			} else {
				return err
			}
		}
	} else {
		t.Log.Warnf("Targets config filename not set")
	}

	// Update targets
	t.Log.Infof("Loading initial targets config: %d targets found", len(tgts))
	for _, tc := range tgts {
		if _, err := t.createUpdate(tc, false, true, nil); err != nil {
			return err
		}
	}

	return nil
}

// SaveConfig Save targets configuration to disk
func (t *Targets) SaveConfig() error {
	if t.fileOnDisk == "" {
		return fmt.Errorf("Targets config filename not set")
	}

	// FIXME: buffered save or avoid to write on disk each time
	return targetsConfigWrite(t.fileOnDisk, t.getConfigArrUnsafe())
}

// ResolveID Complete a Target ID (helper for user that can use partial ID value)
func (t *Targets) ResolveID(id string) (string, error) {
	if id == "" {
		return "", nil
	}

	match := []string{}
	for iid := range t.tgts {
		if strings.HasPrefix(iid, id) {
			match = append(match, iid)
		}
	}

	if len(match) == 1 {
		return match[0], nil
	} else if len(match) == 0 {
		return id, fmt.Errorf("Unknown id")
	}
	return id, fmt.Errorf("Multiple IDs found %v", match)
}

// Get returns the target config or nil if not existing
func (t *Targets) Get(id string) *ITARGET {
	if id == "" {
		return nil
	}
	tc, exist := t.tgts[id]
	if !exist {
		return nil
	}
	return tc
}

// GetConfigArr returns the config of all targets as an array
func (t *Targets) GetConfigArr() []xsapiv1.TargetConfig {
	tcMutex.Lock()
	defer tcMutex.Unlock()

	return t.getConfigArrUnsafe()
}

// getConfigArrUnsafe Same as GetConfigArr without mutex protection
func (t *Targets) getConfigArrUnsafe() []xsapiv1.TargetConfig {
	conf := []xsapiv1.TargetConfig{}
	for _, v := range t.tgts {
		conf = append(conf, (*v).GetConfig())
	}
	return conf
}

// Add adds a new target
func (t *Targets) Add(newT xsapiv1.TargetConfig, sess *ClientSession) (*xsapiv1.TargetConfig, error) {
	return t.createUpdate(newT, true, false, sess)
}

// CreateUpdate creates or update a target
func (t *Targets) createUpdate(newT xsapiv1.TargetConfig, create bool, initial bool, sess *ClientSession) (*xsapiv1.TargetConfig, error) {
	var err error

	tcMutex.Lock()
	defer tcMutex.Unlock()

	// Sanity check
	if _, exist := t.tgts[newT.ID]; exist {
		return nil, fmt.Errorf("ID already exists")
	}

	var tgt ITARGET
	switch newT.Type {
	case xsapiv1.TypeTgtStandard:
		tgt = NewTargetStandard(t.Context)
	default:
		return nil, fmt.Errorf("Unsupported target type")
	}

	// Allocate a new UUID
	if create {
		newT.ID = tgt.NewUID("")
	}
	if !create && newT.ID == "" {
		return nil, fmt.Errorf("Cannot update target with null ID")
	}

	if newT.Name == "" {
		newT.Name = "Target"
		if len(newT.ID) > 8 {
			newT.Name += "_" + newT.ID[0:8]
		} else {
			newT.Name += "_" + newT.ID
		}
	}

	// Call terminals constructor the first time
	var terms *Terminals
	if _, exist := t.terminals[newT.ID]; !exist {
		terms = TerminalsConstructor(t.Context)
		t.terminals[newT.ID] = terms
	} else {
		terms = t.terminals[newT.ID]
	}

	var newTarget *xsapiv1.TargetConfig
	if create {
		// Add target
		if newTarget, err = tgt.Add(newT, terms); err != nil {
			newT.Status = xsapiv1.StatusTgtErrorConfig
			log.Printf("ERROR Adding target: %v\n", err)
			return newTarget, err
		}
	} else {
		// Just update target config
		if newTarget, err = tgt.Setup(newT, terms); err != nil {
			newT.Status = xsapiv1.StatusTgtErrorConfig
			log.Printf("ERROR Updating target: %v\n", err)
			return newTarget, err
		}
	}

	// Create terminals
	for _, tc := range newT.Terms {
		_, err := t.CreateUpdateTerminal(newT.ID, tc, initial)
		if err != nil {
			return newTarget, err
		}
	}

	// Add to folders list
	t.tgts[newT.ID] = &tgt

	// Notify target Add
	newTgt := tgt.GetConfig()
	if !initial {
		if err = t.events.Emit(xsapiv1.EVTTargetAdd, &newTgt, sess.ID); err != nil {
			t.Log.Errorf("WS Emit EVTTargetAdd : %v", err)
		}
	}

	// Save config on disk
	if !initial {
		if err := t.SaveConfig(); err != nil {
			return newTarget, err
		}
	}

	return &newTgt, nil
}

// Delete deletes a specific target
func (t *Targets) Delete(id string, sess *ClientSession) (xsapiv1.TargetConfig, error) {
	var err error

	tcMutex.Lock()
	defer tcMutex.Unlock()

	tgc := xsapiv1.TargetConfig{}
	tc, exist := t.tgts[id]
	if !exist {
		return tgc, fmt.Errorf("unknown id")
	}

	tgc = (*tc).GetConfig()

	if err = (*tc).Delete(); err != nil {
		return tgc, err
	}

	delete(t.tgts, id)

	// Save config on disk
	err = t.SaveConfig()

	// Notify target remove
	if err = t.events.Emit(xsapiv1.EVTTargetRemove, &tgc, sess.ID); err != nil {
		t.Log.Errorf("WS Emit EVTTargetRemove : %v", err)
	}

	return tgc, err
}

/***
 * Terminals
 ***/

// GetTerminalsArr Return list of existing terminals
func (t *Targets) GetTerminalsArr(targetID string) ([]xsapiv1.TerminalConfig, error) {
	arr := []xsapiv1.TerminalConfig{}

	tm, exist := t.terminals[targetID]
	if !exist {
		return arr, fmt.Errorf("unknown target id")
	}

	for _, tt := range (*tm).terms {
		arr = append(arr, (*tt).GetConfig())
	}
	return arr, nil
}

// GetTerminal Return info of a specific terminal
func (t *Targets) GetTerminal(targetID, termID string) (*ITERMINAL, error) {
	tm, exist := t.terminals[targetID]
	if !exist {
		return nil, fmt.Errorf("unknown target id")
	}
	term, exist := (*tm).terms[termID]
	if !exist {
		return nil, fmt.Errorf("unknown terminal id")
	}
	return term, nil
}

// ResolveTerminalID Complete a Terminal ID (helper for user that can use partial ID value)
func (t *Targets) ResolveTerminalID(termID string) (string, error) {
	if termID == "" {
		return "", fmt.Errorf("unknown terminal id")
	}

	match := []string{}
	for _, tm := range t.terminals {
		for tid := range tm.terms {
			if strings.HasPrefix(tid, termID) {
				match = append(match, tid)
			}
		}
	}

	if len(match) == 1 {
		return match[0], nil
	} else if len(match) == 0 {
		return termID, fmt.Errorf("Unknown id")
	}
	return termID, fmt.Errorf("Multiple IDs found %v", match)
}

// CreateUpdateTerminal Create or Update a target terminal definition
func (t *Targets) CreateUpdateTerminal(targetID string, tmCfg xsapiv1.TerminalConfig, initial bool) (*xsapiv1.TerminalConfig, error) {

	var term *xsapiv1.TerminalConfig

	iTerm, err := t.GetTerminal(targetID, tmCfg.ID)
	if err != nil && strings.Contains(err.Error(), "unknown target") {
		return nil, err
	}

	if iTerm != nil {
		// Update terminal config
		term = (*iTerm).UpdateConfig(tmCfg)
	} else {
		// Auto create a new terminal when needed
		var err error
		if term, err = t.terminals[targetID].New(tmCfg, targetID); err != nil {
			return nil, err
		}
	}

	if !initial {
		// Save config on disk
		if err := t.SaveConfig(); err != nil {
			return term, err
		}
	}

	return term, nil
}

// DeleteTerminal Delete a target terminal definition
func (t *Targets) DeleteTerminal(targetID, termID string) (*xsapiv1.TerminalConfig, error) {
	terms, exist := t.terminals[targetID]
	if !exist {
		return nil, fmt.Errorf("unknown target id")
	}

	term, err := (*terms).Free(termID)
	if err != nil {
		return term, err
	}

	// Save config on disk
	if err := t.SaveConfig(); err != nil {
		return term, err
	}

	return term, nil
}

// OpenTerminal Open a target terminal
func (t *Targets) OpenTerminal(targetID, termID string, sess *ClientSession) (*xsapiv1.TerminalConfig, error) {
	terms, exist := t.terminals[targetID]
	if !exist {
		return nil, fmt.Errorf("unknown target id")
	}

	return (*terms).Open(termID, sess)
}

// CloseTerminal Close a target terminal
func (t *Targets) CloseTerminal(targetID, termID string, sess *ClientSession) (*xsapiv1.TerminalConfig, error) {
	terms, exist := t.terminals[targetID]
	if !exist {
		return nil, fmt.Errorf("unknown target id")
	}
	return (*terms).Close(termID, sess)
}

// ResizeTerminal Set size (row+col) of a target terminal
func (t *Targets) ResizeTerminal(targetID, termID string, cols, rows uint16, sess *ClientSession) (*xsapiv1.TerminalConfig, error) {
	terms, exist := t.terminals[targetID]
	if !exist {
		return nil, fmt.Errorf("unknown target id")
	}
	return (*terms).Resize(termID, cols, rows, sess)
}

// SignalTerminal Send a signal to a target terminal
func (t *Targets) SignalTerminal(targetID, termID, sigNum string) error {
	terms, exist := t.terminals[targetID]
	if !exist {
		return fmt.Errorf("unknown target id")
	}
	return (*terms).Signal(termID, sigNum)
}

/**
 * Private functions
 **/

// Use XML format and not json to be able to save/load all fields including
// ones that are masked in json (IOW defined with `json:"-"`)
type xmlTargets struct {
	XMLName xml.Name               `xml:"targets"`
	Version string                 `xml:"version,attr"`
	Targets []xsapiv1.TargetConfig `xml:"targets"`
}

// targetsConfigRead reads targets config from disk
func targetsConfigRead(file string, targets *[]xsapiv1.TargetConfig) error {
	if !common.Exists(file) {
		return fmt.Errorf("No target config file found (%s)", file)
	}

	ffMutex.Lock()
	defer ffMutex.Unlock()

	fd, err := os.Open(file)
	defer fd.Close()
	if err != nil {
		return err
	}

	data := xmlTargets{}
	err = xml.NewDecoder(fd).Decode(&data)
	if err == nil {
		*targets = data.Targets
	}
	return err
}

// targetsConfigWrite writes targets config on disk
func targetsConfigWrite(file string, targets []xsapiv1.TargetConfig) error {
	ffMutex.Lock()
	defer ffMutex.Unlock()

	fd, err := os.OpenFile(file, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
	defer fd.Close()
	if err != nil {
		return err
	}

	data := &xmlTargets{
		Version: "1",
		Targets: targets,
	}

	enc := xml.NewEncoder(fd)
	enc.Indent("", "  ")
	return enc.Encode(data)
}