/*
Copyright 2018 The pdfcpu Authors.

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 pdfcpu

import (
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"

	"github.com/pdfcpu/pdfcpu/internal/config"
	"github.com/pdfcpu/pdfcpu/pkg/font"
)

const (
	// ValidationStrict ensures 100% compliance with the spec (PDF 32000-1:2008).
	ValidationStrict int = iota

	// ValidationRelaxed ensures PDF compliance based on frequently encountered validation errors.
	ValidationRelaxed

	// ValidationNone bypasses validation.
	ValidationNone
)

const (

	// StatsFileNameDefault is the standard stats filename.
	StatsFileNameDefault = "stats.csv"

	// PermissionsAll enables all user access permission bits.
	PermissionsAll int16 = -1 // 0xFFFF

	// PermissionsNone disables all user access permissions bits.
	PermissionsNone int16 = -3901 // 0xF0C3

)

// CommandMode specifies the operation being executed.
type CommandMode int

// The available commands.
const (
	VALIDATE CommandMode = iota
	OPTIMIZE
	SPLIT
	MERGECREATE
	MERGEAPPEND
	EXTRACTIMAGES
	EXTRACTFONTS
	EXTRACTPAGES
	EXTRACTCONTENT
	EXTRACTMETADATA
	TRIM
	ADDATTACHMENTS
	ADDATTACHMENTSPORTFOLIO
	REMOVEATTACHMENTS
	EXTRACTATTACHMENTS
	LISTATTACHMENTS
	SETPERMISSIONS
	LISTPERMISSIONS
	ENCRYPT
	DECRYPT
	CHANGEUPW
	CHANGEOPW
	ADDWATERMARKS
	REMOVEWATERMARKS
	IMPORTIMAGES
	INSERTPAGESBEFORE
	INSERTPAGESAFTER
	REMOVEPAGES
	ROTATE
	NUP
	BOOKLET
	INFO
	CHEATSHEETSFONTS
	INSTALLFONTS
	LISTFONTS
	LISTKEYWORDS
	ADDKEYWORDS
	REMOVEKEYWORDS
	LISTPROPERTIES
	ADDPROPERTIES
	REMOVEPROPERTIES
	COLLECT
	CROP
	LISTBOXES
	ADDBOXES
	REMOVEBOXES
)

// Configuration of a Context.
type Configuration struct {
	Path string

	// Enables PDF V1.5 compatible processing of object streams, xref streams, hybrid PDF files.
	Reader15 bool

	// Enables decoding of all streams (fontfiles, images..) for logging purposes.
	DecodeAllStreams bool

	// Validate against ISO-32000: strict or relaxed
	ValidationMode int

	// End of line char sequence for writing.
	Eol string

	// Turns on object stream generation.
	// A signal for compressing any new non-stream-object into an object stream.
	// true enforces WriteXRefStream to true.
	// false does not prevent xRefStream generation.
	WriteObjectStream bool

	// Switches between xRefSection (<=V1.4) and objectStream/xRefStream (>=V1.5) writing.
	WriteXRefStream bool

	// Turns on stats collection.
	// TODO Decision - unused.
	CollectStats bool

	// A CSV-filename holding the statistics.
	StatsFileName string

	// Supplied user password
	UserPW    string
	UserPWNew *string

	// Supplied owner password
	OwnerPW    string
	OwnerPWNew *string

	// EncryptUsingAES ensures AES encryption.
	// true: AES encryption
	// false: RC4 encryption.
	EncryptUsingAES bool

	// AES:40,128,256 RC4:40,128
	EncryptKeyLength int

	// Supplied user access permissions, see Table 22
	Permissions int16

	// Command being executed.
	Cmd CommandMode

	// Display unit in effect.
	Unit DisplayUnit
}

// ConfigPath defines the location of pdfcpu's configuration directory.
// If set to a file path, pdfcpu will ensure the config dir at this location.
// Other possible values:
// 	default:	Ensure config dir at default location
// 	disable:	Disable config dir usage
var ConfigPath string = "default"

var loadedDefaultConfig *Configuration

func generateConfigFile(fileName string) error {
	if err := ioutil.WriteFile(fileName, config.ConfigFileBytes, os.ModePerm); err != nil {
		return err
	}
	loadedDefaultConfig = newDefaultConfiguration()
	loadedDefaultConfig.Path = fileName
	return nil
}

func ensureConfigFileAt(path string) error {
	// Accept config.yml and config.yaml
	f, err := os.Open(path)
	if err != nil {
		// Create path/pdfcpu/config.yml
		//fmt.Printf("writing %s ..\n", path)
		return generateConfigFile(path)
	}
	defer f.Close()
	// Load configuration into loadedDefaultConfig.
	//fmt.Printf("loading %s ...\n", path)
	return parseConfigFile(f, path)
}

// EnsureDefaultConfigAt tries to load the default configuration from path.
// If path/pdfcpu/config.yaml is not found, it will be created.
func EnsureDefaultConfigAt(path string) error {
	configDir := filepath.Join(path, "pdfcpu")
	font.UserFontDir = filepath.Join(configDir, "fonts")
	if err := os.MkdirAll(font.UserFontDir, os.ModePerm); err != nil {
		return err
	}
	if err := ensureConfigFileAt(filepath.Join(configDir, "config.yml")); err != nil {
		return err
	}
	//fmt.Println(loadedDefaultConfig)
	return font.LoadUserFonts()
}

func newDefaultConfiguration() *Configuration {
	// NOTE: pdfcpu/internal/config/config.yml must be updated whenever the default configuration changes.
	return &Configuration{
		Reader15:          true,
		DecodeAllStreams:  false,
		ValidationMode:    ValidationRelaxed,
		Eol:               EolLF,
		WriteObjectStream: true,
		WriteXRefStream:   true,
		EncryptUsingAES:   true,
		EncryptKeyLength:  256,
		Permissions:       PermissionsNone,
	}
}

// NewDefaultConfiguration returns the default pdfcpu configuration.
func NewDefaultConfiguration() *Configuration {
	if loadedDefaultConfig != nil {
		c := *loadedDefaultConfig
		return &c
	}
	if ConfigPath != "disable" {
		path, err := os.UserConfigDir()
		if err != nil {
			path = os.TempDir()
		}
		if err = EnsureDefaultConfigAt(path); err == nil {
			c := *loadedDefaultConfig
			return &c
		}
		fmt.Fprintf(os.Stderr, "pdfcpu: config dir problem: %v\n", err)
		os.Exit(1)
	}
	return newDefaultConfiguration()
}

// NewAESConfiguration returns a default configuration for AES encryption.
func NewAESConfiguration(userPW, ownerPW string, keyLength int) *Configuration {
	c := NewDefaultConfiguration()
	c.UserPW = userPW
	c.OwnerPW = ownerPW
	c.EncryptUsingAES = true
	c.EncryptKeyLength = keyLength
	return c
}

// NewRC4Configuration returns a default configuration for RC4 encryption.
func NewRC4Configuration(userPW, ownerPW string, keyLength int) *Configuration {
	c := NewDefaultConfiguration()
	c.UserPW = userPW
	c.OwnerPW = ownerPW
	c.EncryptUsingAES = false
	c.EncryptKeyLength = keyLength
	return c
}

func (c Configuration) String() string {
	path := "default"
	if len(c.Path) > 0 {
		path = c.Path
	}
	return fmt.Sprintf("pdfcpu configuration:\n"+
		"Path:              %s\n"+
		"Reader15:          %t\n"+
		"DecodeAllStreams:  %t\n"+
		"ValidationMode:    %s\n"+
		"Eol:               %s\n"+
		"WriteObjectStream: %t\n"+
		"WriteXrefStream:   %t\n"+
		"EncryptUsingAES:   %t\n"+
		"EncryptKeyLength:  %d\n"+
		"Permissions:       %d\n"+
		"Unit :             %s\n",
		path,
		c.Reader15,
		c.DecodeAllStreams,
		c.ValidationModeString(),
		c.EolString(),
		c.WriteObjectStream,
		c.WriteXRefStream,
		c.EncryptUsingAES,
		c.EncryptKeyLength,
		c.Permissions,
		c.UnitString())
}

// EolString returns a string rep for the eol in effect.
func (c *Configuration) EolString() string {
	var s string
	switch c.Eol {
	case EolLF:
		s = "EolLF"
	case EolCR:
		s = "EolCR"
	case EolCRLF:
		s = "EolCRLF"
	}
	return s
}

// ValidationModeString returns a string rep for the validation mode in effect.
func (c *Configuration) ValidationModeString() string {
	if c.ValidationMode == ValidationStrict {
		return "strict"
	}
	if c.ValidationMode == ValidationRelaxed {
		return "relaxed"
	}
	return "none"
}

// UnitString returns a string rep for the display unit in effect.
func (c *Configuration) UnitString() string {
	var s string
	switch c.Unit {
	case POINTS:
		s = "points"
	case INCHES:
		s = "inches"
	case CENTIMETRES:
		s = "cm"
	case MILLIMETRES:
		s = "mm"
	}
	return s
}

// ApplyReducedFeatureSet returns true if complex entries like annotations shall not be written.
func (c *Configuration) ApplyReducedFeatureSet() bool {
	switch c.Cmd {
	case SPLIT, TRIM, EXTRACTPAGES, MERGECREATE, MERGEAPPEND, IMPORTIMAGES:
		return true
	}
	return false
}
