// This file is part of Gopher2600.
//
// Gopher2600 is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Gopher2600 is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Gopher2600.  If not, see <https://www.gnu.org/licenses/>.

package prefs

import (
	"bufio"
	"fmt"
	"os"
	"sort"
	"strings"
	"unicode"

	"github.com/jetsetilly/gopher2600/resources/fs"
)

// DefaultPrefsFile is the default filename of the global preferences file.
const DefaultPrefsFile = "preferences"

// WarningBoilerPlate is inserted at the beginning of a preferences file.
const WarningBoilerPlate = "*** do not edit this file by hand ***"

// the string the separators the pref key from the value.
const KeySep = " :: "

// the pref values to be stored on disk.
type entryMap map[string]pref

func (e entryMap) String() string {
	// we want to work with sorted keys. this makes it easier to test.
	sorted := make([]string, 0, len(e))
	for k := range e {
		sorted = append(sorted, k)
	}
	sort.Strings(sorted)

	s := strings.Builder{}

	for _, k := range sorted {
		v := e[k]
		s.WriteString(fmt.Sprintf("%s%s%s\n", k, KeySep, v))
	}

	return s.String()
}

// Disk represents preference values as stored on disk.
type Disk struct {
	path    string
	entries entryMap
}

func (dsk Disk) String() string {
	return dsk.entries.String()
}

// NewDisk is the preferred method of initialisation for the Disk type.
func NewDisk(path string) (*Disk, error) {
	dsk := &Disk{
		path:    path,
		entries: make(entryMap),
	}

	return dsk, nil
}

// Add preference value to list of values to store/load from Disk. The key
// value is used to identify the preference value on disk and should only
// consist of alphabetic characters or the period character. The program will
// panic if these constraints are not met.
func (dsk *Disk) Add(key string, p pref) error {
	for _, r := range key {
		if !(r == '.' || r == '_' || unicode.IsLetter(r) || unicode.IsDigit(r)) {
			return fmt.Errorf("prefs: illegal character [%c] in key string [%s]", r, key)
		}
	}

	dsk.entries[key] = p
	return nil
}

// Reset all preferences to the default values. In other words, the value they
// would have on first use before being set.
func (dsk *Disk) Reset() error {
	for _, v := range dsk.entries {
		err := v.Reset()
		if err != nil {
			return fmt.Errorf("prefs: %w", err)
		}
	}

	return nil
}

// DisableSaving is useful for testing when a blanket prohibition on saving to
// disk is required.
var DisableSaving = false

// Save current preference values to disk.
func (dsk *Disk) Save() (rerr error) {
	if DisableSaving {
		return nil
	}

	// load entirity of currently saved prefs file to a temporary entryMap
	entries := make(entryMap)

	// load *all* existing entries to temporary entryMap
	_, err := load(dsk.path, &entries, false)
	if err != nil {
		return fmt.Errorf("prefs: %w", err)
	}

	// copy live values to entryMap, overwriting existing entries
	// if they already exists
	for k, v := range dsk.entries {
		entries[k] = v
	}

	// create a new prefs file
	f, err := fs.Create(dsk.path)
	if err != nil {
		return fmt.Errorf("prefs: %w", err)
	}
	defer func() {
		err := f.Close()
		if err != nil {
			rerr = fmt.Errorf("prefs: %w", err)
		}
	}()

	// number of characters written
	var n int

	// add warning label
	n, err = fmt.Fprintf(f, "%s\n", WarningBoilerPlate)
	if err != nil {
		return fmt.Errorf("prefs: %w", err)
	}
	if n != len(WarningBoilerPlate)+1 {
		return fmt.Errorf("prefs: incorrect number of characters written to file")
	}

	// write entries (combination of old and live entries) to disk
	s := entries.String()
	n, err = fmt.Fprint(f, s)
	if err != nil {
		return fmt.Errorf("prefs: %w", err)
	}
	if n != len(s) {
		return fmt.Errorf("prefs: incorrect number of characters written to file")
	}

	return nil
}

// Load preference values from disk. The saveonFirstUse argument is useful when
// loading preferences on initialisation. It makes sure default preferences are
// saved to disk if they are not present in the preferences file.
func (dsk *Disk) Load(saveOnFirstUse bool) error {
	numLoaded, err := load(dsk.path, &dsk.entries, true)
	if err != nil {
		return err
	}

	// override loaded values
	for k := range dsk.entries {
		if ok, v := GetCommandLinePref(k); ok {
			dsk.entries[k].Set(v)
		}
	}

	// if the number of entries loaded by the load() function is not equal to
	// then number of entries in this Disk instance then we can say that a new
	// preference value has been added since the last save to disk. if
	// saveOnFirstUse is true then save immediately to make sure the default
	// value is on disk.
	if saveOnFirstUse && numLoaded != len(dsk.entries) {
		return dsk.Save()
	}

	return nil
}

// underlying function to load preference value froms disk. the limit boolean
// controls whether to load all valid preference values from the file or to
// ignore those values not already in the entryMap. limit=false is used by the
// save() function in order to avoid clobbering unknown entries.
//
// it returns the number of entries loaded from disk. if limit is false then
// the number returned is the total number of entries in the file.
func load(path string, entries *entryMap, limit bool) (int, error) {
	var numLoaded int

	// open existing prefs file
	f, err := fs.Open(path)
	if err != nil {
		switch err.(type) {
		case *os.PathError:
			return 0, nil
		}
		return numLoaded, fmt.Errorf("prefs: %w", err)
	}
	defer f.Close()

	// new scanner - splitting on newlines
	scanner := bufio.NewScanner(f)

	// check validity of file by checking the first line
	scanner.Scan()
	if len(scanner.Text()) > 0 && scanner.Text() != WarningBoilerPlate {
		return 0, fmt.Errorf("prefs: not a valid prefs file (%s)", path)
	}

	// key and value strings
	var k string
	var v string

	// loop through file until EOF
	for scanner.Scan() {
		// split line into key/value pair
		spt := strings.SplitN(scanner.Text(), KeySep, 2)

		// ignore lines that haven't been split successfully
		if len(spt) != 2 {
			continue
		}

		k = spt[0]
		v = spt[1]

		// assign value to entries if key exists
		if p, ok := (*entries)[k]; ok {
			err = p.Set(v)
			if err != nil {
				return numLoaded, fmt.Errorf("prefs: %w", err)
			}
			numLoaded++
		} else if !limit {
			// if this an unlimited load() and preference key is not in list of
			// dufunct values then store in entryMap
			if !isDefunct(k) {
				var dummy String
				err = dummy.Set(v)
				if err != nil {
					return numLoaded, fmt.Errorf("prefs: %w", err)
				}
				(*entries)[k] = &dummy
			}
		}
	}

	return numLoaded, nil
}

// DoesNotHaveEntry returns true if entry is missing from the written disk.
func (dsk *Disk) DoesNotHaveEntry(entry string) (bool, error) {
	var e entryMap
	var s String
	e = make(entryMap)
	e[entry] = &s

	n, err := load(dsk.path, &e, true)
	if err != nil {
		return false, err
	}

	return n != 1, nil
}
