package savegame

import (
	"bufio"
	"crypto/sha1"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"time"

	"git.kirsle.net/SketchyMaze/doodle/pkg/level"
	"git.kirsle.net/SketchyMaze/doodle/pkg/usercfg"
	"git.kirsle.net/SketchyMaze/doodle/pkg/userdir"
)

// SaveGame holds the user's progress thru level packs.
type SaveGame struct {
	LevelPacks map[string]*LevelPack `json:"levelPacks"`
}

// LevelPack holds savegame process for a level pack.
type LevelPack struct {
	Levels map[string]*Level `json:"levels"`
}

// Level holds high score information for a level.
type Level struct {
	Completed   bool           `json:"completed"`
	BestTime    *time.Duration `json:"bestTime"`
	PerfectTime *time.Duration `json:"perfectTime"`
}

// New creates a new SaveGame.
func New() *SaveGame {
	return &SaveGame{
		LevelPacks: map[string]*LevelPack{},
	}
}

// NewLevelPack initializes a LevelPack struct.
func NewLevelPack() *LevelPack {
	return &LevelPack{
		Levels: map[string]*Level{},
	}
}

// GetOrCreate the save game JSON. If the save file isn't found OR has an
// invalid checksum, it is created. Always returns a valid SaveGame struct
// and the error may communicate if there was a problem reading an existing file.
func GetOrCreate() (*SaveGame, error) {
	if sg, err := Load(); err == nil {
		return sg, nil
	} else {
		return New(), err
	}
}

// Load the save game JSON from the user's profile directory.
func Load() (*SaveGame, error) {
	fh, err := os.Open(userdir.SaveFile)
	if err != nil {
		return nil, err
	}

	// Read the checksum line.
	scanner := bufio.NewScanner(fh)
	scanner.Scan()
	var (
		checksum = scanner.Text()
		jsontext []byte
	)
	for scanner.Scan() {
		jsontext = append(jsontext, scanner.Bytes()...)
	}

	// Validate the checksum.
	if !verifyChecksum(jsontext, checksum) {
		return nil, errors.New("checksum error")
	}

	// Parse the JSON.
	var sg = New()
	err = json.Unmarshal(jsontext, sg)
	if err != nil {
		return nil, err
	}

	return sg, nil
}

// Save the savegame.json to disk.
func (sg *SaveGame) Save() error {
	// Encode to JSON.
	text, err := json.Marshal(sg)
	if err != nil {
		return err
	}

	// Create the checksum.
	checksum := makeChecksum(text)

	// Write the file.
	fh, err := os.Create(userdir.SaveFile)
	if err != nil {
		return err
	}
	defer fh.Close()

	fh.Write([]byte(checksum))
	fh.Write([]byte{'\n'})
	fh.Write(text)

	return nil
}

// MarkCompleted is a helper function to mark a levelpack level completed.
// Parameters are the filename of the levelpack and the level therein.
// Extra path info except the base filename is stripped from both.
func (sg *SaveGame) MarkCompleted(levelpack, filename string) {
	lvl := sg.GetLevelScore(levelpack, filename)
	lvl.Completed = true
}

// NewHighScore may set a new highscore for a level.
//
// The level will be marked Completed and if the given score is better
// than the stored one it will update.
//
// Returns true if a new high score was logged.
func (sg *SaveGame) NewHighScore(levelpack, filename string, isPerfect bool, elapsed time.Duration, rules level.GameRule) bool {
	levelpack = filepath.Base(levelpack)
	filename = filepath.Base(filename)

	score := sg.GetLevelScore(levelpack, filename)
	score.Completed = true
	var newHigh bool

	if isPerfect {
		if score.PerfectTime == nil || *score.PerfectTime > elapsed {
			score.PerfectTime = &elapsed
			newHigh = true
		}
	} else {
		// GameRule: Survival (silver) - high score is based on longest time left alive rather
		// than fastest time completed.
		if rules.Survival {
			if score.BestTime == nil || *score.BestTime < elapsed {
				score.BestTime = &elapsed
				newHigh = true
			}
		} else {
			// Normally: fastest time is best time.
			if score.BestTime == nil || *score.BestTime > elapsed {
				score.BestTime = &elapsed
				newHigh = true
			}
		}
	}

	if newHigh {
		if sg.LevelPacks[levelpack] == nil {
			sg.LevelPacks[levelpack] = NewLevelPack()
		}
		sg.LevelPacks[levelpack].Levels[filename] = score
	}

	return newHigh
}

// GetLevelScore finds or creates a default Level score.
func (sg *SaveGame) GetLevelScore(levelpack, filename string) *Level {
	levelpack = filepath.Base(levelpack)
	filename = filepath.Base(filename)

	if _, ok := sg.LevelPacks[levelpack]; !ok {
		sg.LevelPacks[levelpack] = NewLevelPack()
	}

	if row, ok := sg.LevelPacks[levelpack].Levels[filename]; ok {
		return row
	} else {
		row = &Level{}
		sg.LevelPacks[levelpack].Levels[filename] = row
		return row
	}
}

// CountCompleted returns the number of completed levels in a levelpack.
func (sg *SaveGame) CountCompleted(levelpack string) int {
	var count int
	levelpack = filepath.Base(levelpack)

	if lp, ok := sg.LevelPacks[levelpack]; ok {
		for _, lvl := range lp.Levels {
			if lvl.Completed {
				count++
			}
		}
	}

	return count
}

// FormatDuration pretty prints a time.Duration in MM:SS format.
func FormatDuration(d time.Duration) string {
	var (
		millisecond = d.Milliseconds()
		second      = (millisecond / 1000) % 60
		minute      = (millisecond / (1000 * 60)) % 60
		hour        = (millisecond / (1000 * 60 * 60)) % 24
		ms          = fmt.Sprintf("%d", millisecond%1000)
	)

	// Limit milliseconds to 2 digits.
	if len(ms) > 2 {
		ms = ms[:2]
	}

	return strings.TrimPrefix(
		fmt.Sprintf("%02d:%02d:%02d.%s", hour, minute, second, ms),
		"00:",
	)
}

// Hashing key that goes into the level's save data.
var secretKey = []byte(`Sc\x96R\x8e\xba\x96\x8e\x1fg\x01Q\xf5\xcbIX`)

func makeChecksum(jsontext []byte) string {
	h := sha1.New()
	h.Write(jsontext)
	h.Write(secretKey)
	h.Write(usercfg.Current.Entropy)
	return hex.EncodeToString(h.Sum(nil))
}

func verifyChecksum(jsontext []byte, checksum string) bool {
	expect := makeChecksum(jsontext)
	return expect == checksum
}
