// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
// Revel Framework source code and usage is governed by a MIT style
// license that can be found in the LICENSE file.

package revel

import (
	"go/build"
	"net/http"
	"path/filepath"
	"strings"

	"encoding/json"
	"fmt"
	"github.com/revel/config"
	"github.com/revel/revel/logger"
	"github.com/revel/revel/model"
)

const (
	// RevelImportPath Revel framework import path
	RevelImportPath = "github.com/revel/revel"
)

const (
	TEST_MODE_FLAG   = "testModeFlag"
	SPECIAL_USE_FLAG = "specialUseFlag"
)

// App details
var (
	RevelConfig *model.RevelContainer
	AppName    string // e.g. "sample"
	AppRoot    string // e.g. "/app1"
	BasePath   string // e.g. "$GOPATH/src/corp/sample"
	AppPath    string // e.g. "$GOPATH/src/corp/sample/app"
	ViewsPath  string // e.g. "$GOPATH/src/corp/sample/app/views"
	ImportPath string // e.g. "corp/sample"
	SourcePath string // e.g. "$GOPATH/src"

	Config  *config.Context
	RunMode string // Application-defined (by default, "dev" or "prod")
	DevMode bool   // if true, RunMode is a development mode.

	// Revel installation details
	RevelPath string // e.g. "$GOPATH/src/github.com/revel/revel"

	// Where to look for templates
	// Ordered by priority. (Earlier paths take precedence over later paths.)
	CodePaths     []string // Code base directories, for modules and app
	TemplatePaths []string // Template path directories manually added

	// ConfPaths where to look for configurations
	// Config load order
	// 1. framework (revel/conf/*)
	// 2. application (conf/*)
	// 3. user supplied configs (...) - User configs can override/add any from above
	ConfPaths []string

	// Server config.
	//
	// Alert: This is how the app is configured, which may be different from
	// the current process reality.  For example, if the app is configured for
	// port 9000, HTTPPort will always be 9000, even though in dev mode it is
	// run on a random port and proxied.
	HTTPPort    int    // e.g. 9000
	HTTPAddr    string // e.g. "", "127.0.0.1"
	HTTPSsl     bool   // e.g. true if using ssl
	HTTPSslCert string // e.g. "/path/to/cert.pem"
	HTTPSslKey  string // e.g. "/path/to/key.pem"

	// All cookies dropped by the framework begin with this prefix.
	CookiePrefix string
	// Cookie domain
	CookieDomain string
	// Cookie flags
	CookieSecure   bool
	CookieSameSite http.SameSite

	// Revel request access log, not exposed from package.
	// However output settings can be controlled from app.conf

	// True when revel engine has been initialized (Init has returned)
	Initialized bool

	// Private
	secretKey     []byte             // Key used to sign cookies. An empty key disables signing.
	packaged      bool               // If true, this is running from a pre-built package.
	initEventList = []EventHandler{} // Event handler list for receiving events
	packagePathMap = map[string]string{} // The map of the directories needed
)

// Init initializes Revel -- it provides paths for getting around the app.
//
// Params:
//   mode - the run mode, which determines which app.conf settings are used.
//   importPath - the Go import path of the application.
//   srcPath - the path to the source directory, containing Revel and the app.
//     If not specified (""), then a functioning Go installation is required.
func Init(inputmode, importPath, srcPath string) {
	RevelConfig = &model.RevelContainer{}
	// Ignore trailing slashes.
	ImportPath = strings.TrimRight(importPath, "/")
	SourcePath = srcPath

	RunMode = updateLog(inputmode)

	// If the SourcePath is not specified, find it using build.Import.
	var revelSourcePath string // may be different from the app source path
	if SourcePath == "" {
		revelSourcePath, SourcePath = findSrcPaths(importPath)
        BasePath = SourcePath
	} else {
		// If the SourcePath was specified, assume both Revel and the app are within it.
		SourcePath = filepath.Clean(SourcePath)
		revelSourcePath = filepath.Join(SourcePath, filepath.FromSlash(RevelImportPath))
        BasePath = filepath.Join(SourcePath, filepath.FromSlash(importPath))
		packaged = true
	}

	RevelPath = revelSourcePath //filepath.Join(revelSourcePath, filepath.FromSlash(RevelImportPath))
	AppPath = filepath.Join(BasePath, "app")
	ViewsPath = filepath.Join(AppPath, "views")
	RevelLog.Info("Paths","revel", RevelPath,"base", BasePath,"app", AppPath,"views", ViewsPath)

	CodePaths = []string{AppPath}

	if ConfPaths == nil {
		ConfPaths = []string{}
	}

	// Config load order
	// 1. framework (revel/conf/*)
	// 2. application (conf/*)
	// 3. user supplied configs (...) - User configs can override/add any from above
	ConfPaths = append(
		[]string{
			filepath.Join(RevelPath, "conf"),
			filepath.Join(BasePath, "conf"),
		},
		ConfPaths...)

	TemplatePaths = []string{
		ViewsPath,
		filepath.Join(RevelPath, "templates"),
	}

	// Load app.conf
	var err error
	Config, err = config.LoadContext("app.conf", ConfPaths)
	if err != nil || Config == nil {
		RevelLog.Fatal("Failed to load app.conf:", "error", err)
	}

	// After application config is loaded update the logger
	updateLog(inputmode)

	// Configure properties from app.conf
	DevMode = Config.BoolDefault("mode.dev", false)
	HTTPPort = Config.IntDefault("http.port", 9000)
	HTTPAddr = Config.StringDefault("http.addr", "")
	HTTPSsl = Config.BoolDefault("http.ssl", false)
	HTTPSslCert = Config.StringDefault("http.sslcert", "")
	HTTPSslKey = Config.StringDefault("http.sslkey", "")
	if HTTPSsl {
		if HTTPSslCert == "" {
			RevelLog.Fatal("No http.sslcert provided.")
		}
		if HTTPSslKey == "" {
			RevelLog.Fatal("No http.sslkey provided.")
		}
	}

	AppName = Config.StringDefault("app.name", "(not set)")
	AppRoot = Config.StringDefault("app.root", "")
	CookiePrefix = Config.StringDefault("cookie.prefix", "REVEL")
	CookieDomain = Config.StringDefault("cookie.domain", "")
	CookieSecure = Config.BoolDefault("cookie.secure", HTTPSsl)

	switch Config.StringDefault("cookie.samesite", "") {
	case "lax":
		CookieSameSite = http.SameSiteLaxMode
	case "strict":
		CookieSameSite = http.SameSiteStrictMode
	case "none":
		CookieSameSite = http.SameSiteNoneMode
	default:
		CookieSameSite = http.SameSiteDefaultMode
	}

	if secretStr := Config.StringDefault("app.secret", ""); secretStr != "" {
		SetSecretKey([]byte(secretStr))
	}

	RaiseEvent(REVEL_BEFORE_MODULES_LOADED, nil)
	loadModules()
	RaiseEvent(REVEL_AFTER_MODULES_LOADED, nil)

	Initialized = true
	RevelLog.Info("Initialized Revel", "Version", Version, "BuildDate", BuildDate, "MinimumGoVersion", MinimumGoVersion)
}

// The input mode can be as simple as "prod" or it can be a JSON string like
// {"mode":"%s","testModeFlag":true}
// When this function is called it returns the true "inputmode" extracted from the parameter
// and it sets the log context appropriately
func updateLog(inputmode string) (returnMode string) {
	if inputmode == "" {
		returnMode = config.DefaultSection
		return
	} else {
		returnMode = inputmode
	}

	// Check to see if the mode is a json object
	modemap := map[string]interface{}{}

	var testModeFlag, specialUseFlag bool
	if err := json.Unmarshal([]byte(inputmode), &modemap); err == nil {
		returnMode = modemap["mode"].(string)
		if testmode, found := modemap[TEST_MODE_FLAG]; found {
			testModeFlag, _ = testmode.(bool)
		}
		if specialUse, found := modemap[SPECIAL_USE_FLAG]; found {
			specialUseFlag, _ = specialUse.(bool)
		}
		if packagePathMapI, found := modemap["packagePathMap"]; found {
            for k,v := range packagePathMapI.(map[string]interface{}) {
                packagePathMap[k]=v.(string)
            }
        }
	}

	var newContext *config.Context
	// If the Config is nil, set the logger to minimal log messages by adding the option
	if Config == nil {
		newContext = config.NewContext()
		newContext.SetOption(TEST_MODE_FLAG, fmt.Sprint(true))
	} else {
		// Ensure that the selected runmode appears in app.conf.
		// If empty string is passed as the mode, treat it as "DEFAULT"
		if !Config.HasSection(returnMode) {
			RevelLog.Fatal("app.conf: Run mode not found:","runmode", returnMode)
		}
		Config.SetSection(returnMode)
		newContext = Config
	}

	// Only set the testmode flag if it doesnt exist
	if _, found := newContext.Bool(TEST_MODE_FLAG); !found {
		newContext.SetOption(TEST_MODE_FLAG, fmt.Sprint(testModeFlag))
	}
	if _, found := newContext.Bool(SPECIAL_USE_FLAG); !found {
		newContext.SetOption(SPECIAL_USE_FLAG, fmt.Sprint(specialUseFlag))
	}

	appHandle := logger.InitializeFromConfig(BasePath, newContext)

	// Set all the log handlers
	setAppLog(AppLog, appHandle)

	return
}

// Set the secret key
func SetSecretKey(newKey []byte) error {
	secretKey = newKey
	return nil
}

// ResolveImportPath returns the filesystem path for the given import path.
// Returns an error if the import path could not be found.
func ResolveImportPath(importPath string) (string, error) {
	if packaged {
		return filepath.Join(SourcePath, importPath), nil
	}
	if path,found := packagePathMap[importPath];found {
        return path, nil
    }

	modPkg, err := build.Import(importPath, RevelPath, build.FindOnly)
	if err != nil {
		return "", err
	}
	return modPkg.Dir, nil
}

// CheckInit method checks `revel.Initialized` if not initialized it panics
func CheckInit() {
	if !Initialized {
		RevelLog.Panic("CheckInit: Revel has not been initialized!")
	}
}

// findSrcPaths uses the "go/build" package to find the source root for Revel
// and the app.
func findSrcPaths(importPath string) (revelSourcePath, appSourcePath string) {
    if importFsPath,found := packagePathMap[importPath];found {
        return packagePathMap[RevelImportPath],importFsPath
    }
	var (
		gopaths = filepath.SplitList(build.Default.GOPATH)
		goroot  = build.Default.GOROOT
	)

	if len(gopaths) == 0 {
		RevelLog.Fatal("GOPATH environment variable is not set. " +
			"Please refer to http://golang.org/doc/code.html to configure your Go environment.")
	}

	if ContainsString(gopaths, goroot) {
		RevelLog.Fatalf("GOPATH (%s) must not include your GOROOT (%s). "+
			"Please refer to http://golang.org/doc/code.html to configure your Go environment.",
			gopaths, goroot)
	}

	appPkg, err := build.Import(importPath, "", build.FindOnly)
	if err != nil {
		RevelLog.Panic("Failed to import "+importPath+" with error:", "error", err)
	}

	revelPkg, err := build.Import(RevelImportPath, appPkg.Dir, build.FindOnly)
	if err != nil {
		RevelLog.Fatal("Failed to find Revel with error:", "error", err)
	}

	return revelPkg.Dir, appPkg.Dir
}
