// Copyright 2015 Google Inc. All rights reserved.
//
// 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 common

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"sync"

	"github.com/google/blueprint/proptools"
)

var Bool = proptools.Bool

// The configuration file name
const configFileName = "soong.config"
const productVariablesFileName = "soong.variables"

// A FileConfigurableOptions contains options which can be configured by the
// config file. These will be included in the config struct.
type FileConfigurableOptions struct {
	Mega_device *bool `json:",omitempty"`
}

func (f *FileConfigurableOptions) SetDefaultConfig() {
	*f = FileConfigurableOptions{}
}

type Config struct {
	*config
}

// A config object represents the entire build configuration for Android.
type config struct {
	FileConfigurableOptions
	ProductVariables productVariables

	ConfigFileName           string
	ProductVariablesFileName string

	DeviceArches []Arch
	HostArches   map[HostType][]Arch

	srcDir   string // the path of the root source directory
	buildDir string // the path of the build output directory

	envLock   sync.Mutex
	envDeps   map[string]string
	envFrozen bool

	inMake bool
}

type jsonConfigurable interface {
	SetDefaultConfig()
}

func loadConfig(config *config) error {
	err := loadFromConfigFile(&config.FileConfigurableOptions, config.ConfigFileName)
	if err != nil {
		return err
	}

	return loadFromConfigFile(&config.ProductVariables, config.ProductVariablesFileName)
}

// loads configuration options from a JSON file in the cwd.
func loadFromConfigFile(configurable jsonConfigurable, filename string) error {
	// Try to open the file
	configFileReader, err := os.Open(filename)
	defer configFileReader.Close()
	if os.IsNotExist(err) {
		// Need to create a file, so that blueprint & ninja don't get in
		// a dependency tracking loop.
		// Make a file-configurable-options with defaults, write it out using
		// a json writer.
		configurable.SetDefaultConfig()
		err = saveToConfigFile(configurable, filename)
		if err != nil {
			return err
		}
	} else {
		// Make a decoder for it
		jsonDecoder := json.NewDecoder(configFileReader)
		err = jsonDecoder.Decode(configurable)
		if err != nil {
			return fmt.Errorf("config file: %s did not parse correctly: "+err.Error(), filename)
		}
	}

	// No error
	return nil
}

func saveToConfigFile(config jsonConfigurable, filename string) error {
	data, err := json.MarshalIndent(&config, "", "    ")
	if err != nil {
		return fmt.Errorf("cannot marshal config data: %s", err.Error())
	}

	configFileWriter, err := os.Create(filename)
	if err != nil {
		return fmt.Errorf("cannot create empty config file %s: %s\n", filename, err.Error())
	}
	defer configFileWriter.Close()

	_, err = configFileWriter.Write(data)
	if err != nil {
		return fmt.Errorf("default config file: %s could not be written: %s", filename, err.Error())
	}

	_, err = configFileWriter.WriteString("\n")
	if err != nil {
		return fmt.Errorf("default config file: %s could not be written: %s", filename, err.Error())
	}

	return nil
}

// New creates a new Config object.  The srcDir argument specifies the path to
// the root source directory. It also loads the config file, if found.
func NewConfig(srcDir, buildDir string) (Config, error) {
	// Make a config with default options
	config := Config{
		config: &config{
			ConfigFileName:           filepath.Join(buildDir, configFileName),
			ProductVariablesFileName: filepath.Join(buildDir, productVariablesFileName),

			srcDir:   srcDir,
			buildDir: buildDir,
			envDeps:  make(map[string]string),
		},
	}

	// Sanity check the build and source directories. This won't catch strange
	// configurations with symlinks, but at least checks the obvious cases.
	absBuildDir, err := filepath.Abs(buildDir)
	if err != nil {
		return Config{}, err
	}

	absSrcDir, err := filepath.Abs(srcDir)
	if err != nil {
		return Config{}, err
	}

	if strings.HasPrefix(absSrcDir, absBuildDir) {
		return Config{}, fmt.Errorf("Build dir must not contain source directory")
	}

	// Load any configurable options from the configuration file
	err = loadConfig(config.config)
	if err != nil {
		return Config{}, err
	}

	inMakeFile := filepath.Join(buildDir, ".soong.in_make")
	if _, err := os.Stat(inMakeFile); err == nil {
		config.inMake = true
	}

	hostArches, deviceArches, err := decodeArchProductVariables(config.ProductVariables)
	if err != nil {
		return Config{}, err
	}

	if Bool(config.Mega_device) {
		deviceArches, err = decodeMegaDevice()
		if err != nil {
			return Config{}, err
		}
	}

	config.HostArches = hostArches
	config.DeviceArches = deviceArches

	return config, nil
}

func (c *config) RemoveAbandonedFiles() bool {
	return false
}

// PrebuiltOS returns the name of the host OS used in prebuilts directories
func (c *config) PrebuiltOS() string {
	switch runtime.GOOS {
	case "linux":
		return "linux-x86"
	case "darwin":
		return "darwin-x86"
	default:
		panic("Unknown GOOS")
	}
}

// GoRoot returns the path to the root directory of the Go toolchain.
func (c *config) GoRoot() string {
	return fmt.Sprintf("%s/prebuilts/go/%s", c.srcDir, c.PrebuiltOS())
}

func (c *config) CpPreserveSymlinksFlags() string {
	switch runtime.GOOS {
	case "darwin":
		return "-R"
	case "linux":
		return "-d"
	default:
		return ""
	}
}

func (c *config) Getenv(key string) string {
	var val string
	var exists bool
	c.envLock.Lock()
	if val, exists = c.envDeps[key]; !exists {
		if c.envFrozen {
			panic("Cannot access new environment variables after envdeps are frozen")
		}
		val = os.Getenv(key)
		c.envDeps[key] = val
	}
	c.envLock.Unlock()
	return val
}

func (c *config) EnvDeps() map[string]string {
	c.envLock.Lock()
	c.envFrozen = true
	c.envLock.Unlock()
	return c.envDeps
}

func (c *config) EmbeddedInMake() bool {
	return c.inMake
}

// DeviceName returns the name of the current device target
// TODO: take an AndroidModuleContext to select the device name for multi-device builds
func (c *config) DeviceName() string {
	return *c.ProductVariables.DeviceName
}

func (c *config) DeviceUsesClang() bool {
	if c.ProductVariables.DeviceUsesClang != nil {
		return *c.ProductVariables.DeviceUsesClang
	}
	return true
}

func (c *config) ResourceOverlays() []SourcePath {
	return nil
}

func (c *config) PlatformVersion() string {
	return "M"
}

func (c *config) PlatformSdkVersion() string {
	return "22"
}

func (c *config) BuildNumber() string {
	return "000000"
}

func (c *config) ProductAaptConfig() []string {
	return []string{"normal", "large", "xlarge", "hdpi", "xhdpi", "xxhdpi"}
}

func (c *config) ProductAaptPreferredConfig() string {
	return "xhdpi"
}

func (c *config) ProductAaptCharacteristics() string {
	return "nosdcard"
}

func (c *config) DefaultAppCertificateDir(ctx PathContext) SourcePath {
	return PathForSource(ctx, "build/target/product/security")
}

func (c *config) DefaultAppCertificate(ctx PathContext) SourcePath {
	return c.DefaultAppCertificateDir(ctx).Join(ctx, "testkey")
}

func (c *config) AllowMissingDependencies() bool {
	return Bool(c.ProductVariables.Allow_missing_dependencies)
}

func (c *config) SkipDeviceInstall() bool {
	return c.EmbeddedInMake() || Bool(c.Mega_device)
}