/*Package lsession 的目的是，现有的session管理都是把用户所有的session作为一个整体打包存贮
太浪费资源，这个包将每个值分散存贮，各自有超时
*/
package lsession

import (
	"bytes"
	"dbweb/lib/crypter"
	"dbweb/lib/job"
	"encoding/base64"
	"encoding/gob"
	"encoding/json"
	"net/http"
	"time"

	log "github.com/Sirupsen/logrus"

	"github.com/pborman/uuid"
	"github.com/syndtr/goleveldb/leveldb"
	"github.com/syndtr/goleveldb/leveldb/opt"
	"github.com/syndtr/goleveldb/leveldb/util"
)

var (
	sessionStore *leveldb.DB //存贮库
	//Timeout 设置超时分钟数
	Timeout float64
)

const (
	secretSalt      = "sid_llx"
	cookieName      = "_s_"
	sessionLWTPrex  = "l"
	sessionDataPrex = "d"
)

//Session 定义了单个用户Session
type Session struct {
	sid []byte
	Req *http.Request
	Res http.ResponseWriter
}

//SessionInfo 定义了Session的简要信息，在返回所有Session清单时用到
type SessionInfo struct {
	SID      []byte
	LastTime time.Time
}

func init() {
	gob.Register(map[string]interface{}{})
	gob.Register([]interface{}{})
	gob.Register(json.Number(""))
	gob.Register(time.Now())

}
func Open(fileDir string) error {
	var err error
	if sessionStore, err = leveldb.OpenFile(fileDir, &opt.Options{
		NoSync: true,
	}); err != nil {
		log.Error("file:", fileDir, err.Error())
		return err
	}
	job.AddFunc("@every 30s", ClearTimeoutSession)
	return nil
}

//Close 用于关闭底层存贮
func Close() error {
	job.Stop()
	return sessionStore.Close()
}

//NewSession 根据当前请cookie信息，返回一个关联的Session对象
func NewSession(r *http.Request, res http.ResponseWriter) *Session {
	return &Session{nil, r, res}
}

//CrypterSessionID 返回加密的SessionID，用于浏览器
func (s *Session) CrypterSessionID() string {
	return crypter.Encode(s.sid, secretSalt)
}

// SessionID 返原始的ID
func (s *Session) SessionID() []byte {
	if len(s.sid) == 0 {
		//识别特殊的query参数 __cs__
		q := s.Req.URL.Query()
		if cs := q.Get("__cs__"); len(cs) > 0 {
			s.sid = crypter.Decode(cs, secretSalt)
		} else if c, err := s.Req.Cookie(cookieName); err != nil {
			if err == http.ErrNoCookie {
				//没有的话增加一个cookie
				s.sid = uuid.NewUUID()
				http.SetCookie(s.Res, &http.Cookie{
					Name:  cookieName,
					Value: crypter.Encode(s.sid, secretSalt),
				})
			} else {
				log.Panic(err)
			}
		} else {
			s.sid = crypter.Decode(c.Value, secretSalt)
		}
	}
	return s.sid
}
func bytes2Time(b []byte) time.Time {
	result := time.Time{}
	if err := result.UnmarshalBinary(b); err != nil {
		result = time.Unix(0, 0)
	}
	return result
}
func time2Bytes(t time.Time) []byte {
	if bys, err := t.MarshalBinary(); err != nil {
		log.Panic(err)
		return nil
	} else {
		return bys
	}
}

//GetSessionValue 返回具体的Session值
func GetSessionValue(sid []byte, key interface{}) interface{} {
	trueKey := append([]byte(sessionDataPrex), append(sid, encodeKey(key)...)...)
	v, err := sessionStore.Get(trueKey, nil)
	if err == leveldb.ErrNotFound {
		return nil
	}
	if err != nil {
		log.Panic(err)
	}
	var out interface{}
	if err = gob.NewDecoder(bytes.NewReader(v)).Decode(&out); err != nil {
		log.Panic(err)
	}
	return out

}

//Get 根据Key返回值
func (s *Session) Get(key interface{}) interface{} {
	if key == "" {
		return nil
	}
	//写入时间,每个session的所有值写一个时间
	if err := sessionStore.Put(append([]byte(sessionLWTPrex), s.SessionID()...), time2Bytes(time.Now()), nil); err != nil {
		log.Panic(err)
	}
	return GetSessionValue(s.SessionID(), key)
}

//SessionIDString 返当前SessionID的字符串形式，为base64
func (s *Session) SessionIDString() string {
	return base64.RawStdEncoding.EncodeToString(s.SessionID())
}

//Clear 清除当前所有内容
func (s *Session) Clear() {
	clear(s.SessionID())
}
func clear(sid []byte) {
	var icount int64

	batch := new(leveldb.Batch)
	batch.Delete(append([]byte(sessionLWTPrex), sid...))
	iterData := sessionStore.NewIterator(util.BytesPrefix(append([]byte(sessionDataPrex), sid...)), nil)
	defer iterData.Release()
	for iterData.Next() {
		batch.Delete(iterData.Key())
		icount++
	}
	if err := iterData.Error(); err != nil {
		log.Panic(err)
	}
	if err := sessionStore.Write(batch, nil); err != nil {
		log.Panic(err)
	}
}
func setSessionValue(sid []byte, key, val interface{}) {
	trueKey := append(sid, encodeKey(key)...)

	bys := bytes.NewBuffer(nil)
	if err := gob.NewEncoder(bys).Encode(&val); err != nil {
		log.Panic(err)
	}
	batch := new(leveldb.Batch)
	batch.Put(append([]byte(sessionLWTPrex), sid...), time2Bytes(time.Now()))
	batch.Put(append([]byte(sessionDataPrex), trueKey...), bys.Bytes())
	if err := sessionStore.Write(batch, nil); err != nil {
		log.Panic(err)
	}

}

//Set 设置值
func (s *Session) Set(key interface{}, val interface{}) {
	setSessionValue(s.SessionID(), key, val)
}

//Delete 删除值
func (s *Session) Delete(key interface{}) {
	trueKey := append(s.SessionID(), encodeKey(key)...)
	batch := new(leveldb.Batch)
	batch.Put(append([]byte(sessionLWTPrex), s.SessionID()...), time2Bytes(time.Now()))
	batch.Delete(append([]byte(sessionDataPrex), trueKey...))
	if err := sessionStore.Write(batch, nil); err != leveldb.ErrNotFound && err != nil {
		log.Panic(err)
	}

}
func encodeKey(key interface{}) []byte {
	bys := bytes.NewBuffer(nil)
	if err := gob.NewEncoder(bys).Encode(key); err != nil {
		log.Panic(err)
	}
	return bys.Bytes()
}

//GetSessionList 获取当前所有Sessio简要信息
func GetSessionList() []*SessionInfo {
	rev := []*SessionInfo{}
	iter := sessionStore.NewIterator(util.BytesPrefix([]byte(sessionLWTPrex)), nil)
	defer iter.Release()
	for iter.Next() {
		key := iter.Key()
		sid := make([]byte, len(key)-len(sessionLWTPrex))
		copy(sid, key[len(sessionLWTPrex):])
		t := bytes2Time(iter.Value())
		rev = append(rev, &SessionInfo{sid, t})
	}
	if err := iter.Error(); err != nil {
		log.Panic(err)
	}
	return rev
}

//踢掉一个用户，并注明原因，sid是加密的
func Kickout(sid []byte) {
	clear(sid)
}

//ClearTimeoutSession 清除已经超时的Session
func ClearTimeoutSession() {
	iter := sessionStore.NewIterator(util.BytesPrefix([]byte(sessionLWTPrex)), nil)
	icount := 0
	defer iter.Release()
	for iter.Next() {
		key := iter.Key()
		sid := key[len(sessionLWTPrex):]
		t := bytes2Time(iter.Value())
		if time.Now().Sub(t).Minutes() > Timeout {
			clear(sid)
			icount++
		}
	}
	if err := iter.Error(); err != nil {
		log.Panic(err)
	}
}
