// Copyright 2011 The Walk Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// +build windows

package walk

import (
	"bufio"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"time"
)

const iniFileTimeStampFormat = "2006-01-02"

type IniFileSettings struct {
	fileName       string
	key2Record     map[string]iniFileRecord
	expireDuration time.Duration
	portable       bool
}

type iniFileRecord struct {
	value     string
	timestamp time.Time
}

func NewIniFileSettings(fileName string) *IniFileSettings {
	return &IniFileSettings{
		fileName:   fileName,
		key2Record: make(map[string]iniFileRecord),
	}
}

func (ifs *IniFileSettings) Get(key string) (string, bool) {
	record, ok := ifs.key2Record[key]
	return record.value, ok
}

func (ifs *IniFileSettings) Timestamp(key string) (time.Time, bool) {
	record, ok := ifs.key2Record[key]
	return record.timestamp, ok
}

func (ifs *IniFileSettings) Put(key, value string) error {
	return ifs.put(key, value, false)
}

func (ifs *IniFileSettings) PutExpiring(key, value string) error {
	return ifs.put(key, value, true)
}

func (ifs *IniFileSettings) put(key, value string, expiring bool) error {
	if key == "" {
		return newError("key must not be empty")
	}
	if strings.IndexAny(key, "|=\r\n") > -1 {
		return newError("key contains at least one of the invalid characters '|=\\r\\n'")
	}
	if strings.IndexAny(value, "\r\n") > -1 {
		return newError("value contains at least one of the invalid characters '\\r\\n'")
	}

	var timestamp time.Time
	if expiring {
		timestamp = time.Now()
	}

	ifs.key2Record[key] = iniFileRecord{value, timestamp}

	return nil
}

func (ifs *IniFileSettings) Remove(key string) error {
	delete(ifs.key2Record, key)

	return nil
}

func (ifs *IniFileSettings) ExpireDuration() time.Duration {
	return ifs.expireDuration
}

func (ifs *IniFileSettings) SetExpireDuration(expireDuration time.Duration) {
	ifs.expireDuration = expireDuration
}

func (ifs *IniFileSettings) Portable() bool {
	return ifs.portable
}

func (ifs *IniFileSettings) SetPortable(portable bool) {
	ifs.portable = portable
}

func (ifs *IniFileSettings) FilePath() string {
	if ifs.portable {
		absPath, err := filepath.Abs(ifs.fileName)
		if err != nil {
			return ""
		}

		return absPath
	}

	appDataPath, err := AppDataPath()
	if err != nil {
		return ""
	}

	return filepath.Join(
		appDataPath,
		App().OrganizationName(),
		App().ProductName(),
		ifs.fileName)
}

func (ifs *IniFileSettings) fileExists() (bool, error) {
	filePath := ifs.FilePath()

	if _, err := os.Stat(filePath); err != nil {
		// FIXME: Not necessarily a file does not exist error.
		return false, nil
	}

	return true, nil
}

func (ifs *IniFileSettings) withFile(flags int, f func(file *os.File) error) error {
	filePath := ifs.FilePath()

	dirPath, _ := filepath.Split(filePath)
	if err := os.MkdirAll(dirPath, 0644); err != nil {
		return wrapError(err)
	}

	file, err := os.OpenFile(filePath, flags, 0644)
	if err != nil {
		return wrapError(err)
	}
	defer file.Close()

	return f(file)
}

func (ifs *IniFileSettings) Load() error {
	exists, err := ifs.fileExists()
	if err != nil {
		return err
	}

	if !exists {
		return nil
	}

	return ifs.withFile(os.O_RDONLY, func(file *os.File) error {
		scanner := bufio.NewScanner(file)

		for scanner.Scan() {
			line := scanner.Text()

			assignIndex := strings.Index(line, "=")
			if assignIndex == -1 {
				return newError("bad line format: missing '='")
			}

			key := strings.TrimSpace(line[:assignIndex])

			var ts time.Time
			if parts := strings.Split(key, "|"); len(parts) > 1 {
				key = parts[0]
				if ts, _ = time.Parse(iniFileTimeStampFormat, parts[1]); ts.IsZero() {
					ts = time.Now()
				}
			}

			value := strings.TrimSpace(line[assignIndex+1:])

			ifs.key2Record[key] = iniFileRecord{value, ts}
		}

		return scanner.Err()
	})
}

func (ifs *IniFileSettings) Save() error {
	return ifs.withFile(os.O_CREATE|os.O_TRUNC|os.O_WRONLY, func(file *os.File) error {
		bufWriter := bufio.NewWriter(file)

		keys := make([]string, 0, len(ifs.key2Record))

		for key, record := range ifs.key2Record {
			if ifs.expireDuration <= 0 || record.timestamp.IsZero() || time.Since(record.timestamp) < ifs.expireDuration {
				keys = append(keys, key)
			}
		}

		sort.Strings(keys)

		for _, key := range keys {
			record := ifs.key2Record[key]

			if _, err := bufWriter.WriteString(key); err != nil {
				return wrapError(err)
			}
			if !record.timestamp.IsZero() {
				if _, err := bufWriter.WriteString("|"); err != nil {
					return wrapError(err)
				}
				if _, err := bufWriter.WriteString(record.timestamp.Format(iniFileTimeStampFormat)); err != nil {
					return wrapError(err)
				}
			}
			if _, err := bufWriter.WriteString("="); err != nil {
				return wrapError(err)
			}
			if _, err := bufWriter.WriteString(record.value); err != nil {
				return wrapError(err)
			}
			if _, err := bufWriter.WriteString("\r\n"); err != nil {
				return wrapError(err)
			}
		}

		return bufWriter.Flush()
	})
}
