// 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 session

import (
	"encoding/hex"
	"encoding/json"
	"errors"
	"github.com/twinj/uuid"
	"reflect"
	"strconv"
	"strings"
	"time"
)

const (
	// The key for the identity of the session
	SessionIDKey = "_ID"
	// The expiration date of the session
	TimestampKey = "_TS"
	// The value name indicating how long the session should persist - ie should it persist after the browser closes
	// this is set under the TimestampKey if the session data should expire immediately
	SessionValueName = "session"
	// The key container for the json objects of the data, any non strings found in the map will be placed in here
	// serialized by key using JSON
	SessionObjectKeyName = "_object_"
	// The mapped session object
	SessionMapKeyName = "_map_"
	// The suffix of the session cookie
	SessionCookieSuffix = "_SESSION"
)

// Session data, can be any data, there are reserved keywords used by the storage data
// SessionIDKey Is the key name for the session
// TimestampKey Is the time that the session should expire
//
type Session map[string]interface{}

func NewSession() Session {
	return Session{}
}

// ID retrieves from the cookie or creates a time-based UUID identifying this
// session.
func (s Session) ID() string {
	if sessionIDStr, ok := s[SessionIDKey]; ok {
		return sessionIDStr.(string)
	}

	buffer := uuid.NewV4()

	s[SessionIDKey] = hex.EncodeToString(buffer.Bytes())
	return s[SessionIDKey].(string)
}

// getExpiration return a time.Time with the session's expiration date.
// It uses the passed in expireAfterDuration to add with the current time if the timeout is not
// browser dependent (ie session). If previous session has set to "session", the time returned is time.IsZero()
func (s Session) GetExpiration(expireAfterDuration time.Duration) time.Time {
	if expireAfterDuration == 0 || s[TimestampKey] == SessionValueName {
		// Expire after closing browser
		return time.Time{}
	}
	return time.Now().Add(expireAfterDuration)
}

// SetNoExpiration sets session to expire when browser session ends
func (s Session) SetNoExpiration() {
	s[TimestampKey] = SessionValueName
}

// SetDefaultExpiration sets session to expire after default duration
func (s Session) SetDefaultExpiration() {
	delete(s, TimestampKey)
}

// sessionTimeoutExpiredOrMissing returns a boolean of whether the session
// cookie is either not present or present but beyond its time to live; i.e.,
// whether there is not a valid session.
func (s Session) SessionTimeoutExpiredOrMissing() bool {
	if exp, present := s[TimestampKey]; !present {
		return true
	} else if exp == SessionValueName {
		return false
	} else if expInt, _ := strconv.Atoi(exp.(string)); int64(expInt) < time.Now().Unix() {
		return true
	}
	return false
}

// Constant error if session value is not found
var SESSION_VALUE_NOT_FOUND = errors.New("Session value not found")

// Get an object or property from the session
// it may be embedded inside the session.
func (s Session) Get(key string) (newValue interface{}, err error) {
	// First check to see if it is in the session
	if v, found := s[key]; found {
		return v, nil
	}
	return s.GetInto(key, nil, false)
}

// Get into the specified value.
// If value exists in the session it will just return the value
func (s Session) GetInto(key string, target interface{}, force bool) (result interface{}, err error) {
	if v, found := s[key]; found && !force {
		return v, nil
	}
	splitKey := strings.Split(key, ".")
	rootKey := splitKey[0]

	// Force always recreates the object from the session data map
	if force {
		if target == nil {
			if result, err = s.sessionDataFromMap(key); err != nil {
				return
			}
		} else if result, err = s.sessionDataFromObject(rootKey, target); err != nil {
			return
		}

		return s.getNestedProperty(splitKey, result)
	}

	// Attempt to find the key in the session, this is the most generalized form
	v, found := s[rootKey]
	if !found {
		if target == nil {
			// Try to fetch it from the session

			if v, err = s.sessionDataFromMap(rootKey); err != nil {
				return
			}
		} else if v, err = s.sessionDataFromObject(rootKey, target); err != nil {
			return
		}
	}

	return s.getNestedProperty(splitKey, v)
}

// Returns the default value if the key is not found
func (s Session) GetDefault(key string, value interface{}, defaultValue interface{}) interface{} {
	v, e := s.GetInto(key, value, false)
	if e != nil {
		v = defaultValue
	}
	return v
}

// Extract the values from the session
func (s Session) GetProperty(key string, value interface{}) (interface{}, error) {
	// Capitalize the first letter
	key = strings.Title(key)

	sessionLog.Info("getProperty", "key", key, "value", value)

	// For a map it is easy
	if reflect.TypeOf(value).Kind() == reflect.Map {
		val := reflect.ValueOf(value)
		valueOf := val.MapIndex(reflect.ValueOf(key))
		if valueOf == reflect.Zero(reflect.ValueOf(value).Type()) {
			return nil, nil
		}
		//idx := val.MapIndex(reflect.ValueOf(key))
		if !valueOf.IsValid() {
			return nil, nil
		}

		return valueOf.Interface(), nil
	}

	objValue := s.reflectValue(value)
	field := objValue.FieldByName(key)
	if !field.IsValid() {
		return nil, SESSION_VALUE_NOT_FOUND
	}

	return field.Interface(), nil
}

// Places the object into the session, a nil value will cause remove the key from the session
// (or you can use the Session.Del(key) function
func (s Session) Set(key string, value interface{}) error {
	if value == nil {
		s.Del(key)
		return nil
	}

	s[key] = value
	return nil
}

// Delete the key from the sessionObjects and Session
func (s Session) Del(key string) {
	sessionJsonMap := s.getSessionJsonMap()
	delete(sessionJsonMap, key)
	delete(s, key)
}

// Extracts the session as a map of [string keys] and json values
func (s Session) getSessionJsonMap() map[string]string {
	if sessionJson, found := s[SessionObjectKeyName]; found {
		if _, valid := sessionJson.(map[string]string); !valid {
			sessionLog.Error("Session object key corrupted, reset", "was", sessionJson)
			s[SessionObjectKeyName] = map[string]string{}
		}
		// serialized data inside the session _objects
	} else {
		s[SessionObjectKeyName] = map[string]string{}
	}

	return s[SessionObjectKeyName].(map[string]string)
}

// Convert the map to a simple map[string]string map
// this will marshal any non string objects encountered and store them the the jsonMap
// The expiration time will also be assigned
func (s Session) Serialize() map[string]string {
	sessionJsonMap := s.getSessionJsonMap()
	newMap := map[string]string{}
	newObjectMap := map[string]string{}
	for key, value := range sessionJsonMap {
		newObjectMap[key] = value
	}
	for key, value := range s {
		if key == SessionObjectKeyName || key == SessionMapKeyName {
			continue
		}
		if reflect.ValueOf(value).Kind() == reflect.String {
			newMap[key] = value.(string)
			continue
		}
		if data, err := json.Marshal(value); err != nil {
			sessionLog.Error("Unable to marshal session ", "key", key, "error", err)
			continue
		} else {
			newObjectMap[key] = string(data)
		}
	}
	if len(newObjectMap) > 0 {
		if data, err := json.Marshal(newObjectMap); err != nil {
			sessionLog.Error("Unable to marshal session ", "key", SessionObjectKeyName, "error", err)

		} else {
			newMap[SessionObjectKeyName] = string(data)
		}
	}

	return newMap
}

// Set the session object from the loaded data
func (s Session) Load(data map[string]string) {
	for key, value := range data {
		if key == SessionObjectKeyName {
			target := map[string]string{}
			if err := json.Unmarshal([]byte(value), &target); err != nil {
				sessionLog.Error("Unable to unmarshal session ", "key", SessionObjectKeyName, "error", err)
			} else {
				s[key] = target
			}
		} else {
			s[key] = value
		}

	}
}

// Checks to see if the session is empty
func (s Session) Empty() bool {
	i := 0
	for k := range s {
		i++
		if k == SessionObjectKeyName || k == SessionMapKeyName {
			continue
		}
	}
	return i == 0
}

func (s *Session) reflectValue(obj interface{}) reflect.Value {
	var val reflect.Value

	if reflect.TypeOf(obj).Kind() == reflect.Ptr {
		val = reflect.ValueOf(obj).Elem()
	} else {
		val = reflect.ValueOf(obj)
	}

	return val
}

// Starting at position 1 drill into the object
func (s Session) getNestedProperty(keys []string, newValue interface{}) (result interface{}, err error) {
	for x := 1; x < len(keys); x++ {
		newValue, err = s.GetProperty(keys[x], newValue)
		if err != nil || newValue == nil {
			return newValue, err
		}
	}
	return newValue, nil
}

// Always converts the data from the session mapped objects into the target,
// it will store the results under the session key name SessionMapKeyName
func (s Session) sessionDataFromMap(key string) (result interface{}, err error) {
	var mapValue map[string]interface{}
	uncastMapValue, found := s[SessionMapKeyName]
	if !found {
		mapValue = map[string]interface{}{}
		s[SessionMapKeyName] = mapValue
	} else if mapValue, found = uncastMapValue.(map[string]interface{}); !found {
		// Unusual means that the value in the session was not expected
		sessionLog.Errorf("Unusual means that the value in the session was not expected", "session", uncastMapValue)
		mapValue = map[string]interface{}{}
		s[SessionMapKeyName] = mapValue
	}

	// Try to extract the key from the map
	result, found = mapValue[key]
	if !found {
		result, err = s.convertSessionData(key, nil)
		if err == nil {
			mapValue[key] = result
		}
	}
	return
}

// Unpack the object from the session map and store it in the session when done, if no error occurs
func (s Session) sessionDataFromObject(key string, newValue interface{}) (result interface{}, err error) {
	result, err = s.convertSessionData(key, newValue)
	if err != nil {
		return
	}
	s[key] = result
	return
}

// Converts from the session json map into the target,
func (s Session) convertSessionData(key string, target interface{}) (result interface{}, err error) {
	sessionJsonMap := s.getSessionJsonMap()
	v, found := sessionJsonMap[key]
	if !found {
		return target, SESSION_VALUE_NOT_FOUND
	}

	// Create a target if needed
	if target == nil {
		target = map[string]interface{}{}
		if err := json.Unmarshal([]byte(v), &target); err != nil {
			return target, err
		}
	} else if err := json.Unmarshal([]byte(v), target); err != nil {
		return target, err
	}
	result = target
	return
}
