package rest

import (
	"archive/zip"
	"fmt"
	"github.com/eyebluecn/tank/code/core"
	"github.com/eyebluecn/tank/code/tool/builder"
	"github.com/eyebluecn/tank/code/tool/download"
	"github.com/eyebluecn/tank/code/tool/i18n"
	"github.com/eyebluecn/tank/code/tool/result"
	"github.com/eyebluecn/tank/code/tool/util"
	"io"
	"io/ioutil"
	"net/http"
	"os"
	"path"
	"path/filepath"
	"regexp"
	"strings"
	"time"
)

/**
 * Methods start with Atomic has Lock. These method cannot be invoked by other Atomic Methods.
 */
//@Service
type MatterService struct {
	BaseBean
	matterDao         *MatterDao
	userDao           *UserDao
	userService       *UserService
	imageCacheDao     *ImageCacheDao
	imageCacheService *ImageCacheService
	preferenceService *PreferenceService
}

func (this *MatterService) Init() {
	this.BaseBean.Init()

	b := core.CONTEXT.GetBean(this.matterDao)
	if b, ok := b.(*MatterDao); ok {
		this.matterDao = b
	}

	b = core.CONTEXT.GetBean(this.userDao)
	if b, ok := b.(*UserDao); ok {
		this.userDao = b
	}

	b = core.CONTEXT.GetBean(this.userService)
	if b, ok := b.(*UserService); ok {
		this.userService = b
	}

	b = core.CONTEXT.GetBean(this.imageCacheDao)
	if b, ok := b.(*ImageCacheDao); ok {
		this.imageCacheDao = b
	}

	b = core.CONTEXT.GetBean(this.imageCacheService)
	if b, ok := b.(*ImageCacheService); ok {
		this.imageCacheService = b
	}

	b = core.CONTEXT.GetBean(this.preferenceService)
	if b, ok := b.(*PreferenceService); ok {
		this.preferenceService = b
	}

}

//Download. Support chunk download.
func (this *MatterService) DownloadFile(
	writer http.ResponseWriter,
	request *http.Request,
	filePath string,
	filename string,
	withContentDisposition bool) {

	download.DownloadFile(writer, request, filePath, filename, withContentDisposition)
}

//Download specified matters. matters must have the same puuid.
func (this *MatterService) DownloadZip(
	writer http.ResponseWriter,
	request *http.Request,
	matters []*Matter) {

	if matters == nil || len(matters) == 0 {
		panic(result.BadRequest("matters cannot be nil."))
	}
	userUuid := matters[0].UserUuid
	puuid := matters[0].Puuid

	for _, m := range matters {
		if m.UserUuid != userUuid {
			panic(result.BadRequest("userUuid not same"))
		} else if m.Puuid != puuid {
			panic(result.BadRequest("puuid not same"))
		}
	}

	preference := this.preferenceService.Fetch()

	//count the num of files will be downloaded.
	var count int64 = 0
	for _, matter := range matters {
		count = count + this.matterDao.CountByUserUuidAndPath(matter.UserUuid, matter.Path)
	}

	if preference.DownloadDirMaxNum >= 0 {
		if count > preference.DownloadDirMaxNum {
			panic(result.BadRequestI18n(request, i18n.MatterSelectNumExceedLimit, count, preference.DownloadDirMaxNum))
		}
	}

	//count the size of files will be downloaded.
	var sumSize int64 = 0
	for _, matter := range matters {
		sumSize = sumSize + this.matterDao.SumSizeByUserUuidAndPath(matter.UserUuid, matter.Path)
	}

	if preference.DownloadDirMaxSize >= 0 {
		if sumSize > preference.DownloadDirMaxSize {
			panic(result.BadRequestI18n(request, i18n.MatterSelectSizeExceedLimit, util.HumanFileSize(sumSize), util.HumanFileSize(preference.DownloadDirMaxSize)))
		}
	}

	//prepare the temp zip dir
	destZipDirPath := fmt.Sprintf("%s/%d", GetUserZipRootDir(matters[0].Username), time.Now().UnixNano()/1e6)
	util.MakeDirAll(destZipDirPath)

	destZipName := fmt.Sprintf("%s.zip", matters[0].Name)
	if len(matters) > 1 || !matters[0].Dir {
		destZipName = "archive.zip"
	}

	destZipPath := fmt.Sprintf("%s/%s", destZipDirPath, destZipName)

	this.zipMatters(request, matters, destZipPath)

	download.DownloadFile(writer, request, destZipPath, destZipName, true)

	//delete the temp zip file.
	err := os.Remove(destZipPath)
	if err != nil {
		this.logger.Error("error while deleting zip file %s", err.Error())
	}
	util.DeleteEmptyDir(destZipDirPath)

}

//zip matters.
func (this *MatterService) zipMatters(request *http.Request, matters []*Matter, destPath string) {

	if util.PathExists(destPath) {
		panic(result.BadRequest("%s exists", destPath))
	}

	//matters must have the same puuid.
	if matters == nil || len(matters) == 0 {
		panic(result.BadRequest("matters cannot be nil."))
	}
	userUuid := matters[0].UserUuid
	puuid := matters[0].Puuid
	baseDirPath := util.GetDirOfPath(matters[0].AbsolutePath()) + "/"

	for _, m := range matters {
		if m.UserUuid != userUuid {
			panic(result.BadRequest("userUuid not same"))
		} else if m.Puuid != puuid {
			panic(result.BadRequest("puuid not same"))
		}
	}

	//wrap children for every matter.
	for _, m := range matters {
		this.WrapChildrenDetail(request, m)
	}

	// create temp zip file.
	fileWriter, err := os.Create(destPath)
	this.PanicError(err)

	defer func() {
		err := fileWriter.Close()
		this.PanicError(err)
	}()

	zipWriter := zip.NewWriter(fileWriter)
	defer func() {
		err := zipWriter.Close()
		this.PanicError(err)
	}()

	//DFS algorithm
	var walkFunc func(matter *Matter)
	walkFunc = func(matter *Matter) {

		path := matter.AbsolutePath()

		fileInfo, err := os.Stat(path)
		this.PanicError(err)

		// Create file info.
		fileHeader, err := zip.FileInfoHeader(fileInfo)
		this.PanicError(err)

		// Trim the baseDirPath
		fileHeader.Name = strings.TrimPrefix(path, baseDirPath)

		// directory has prefix /
		if matter.Dir {
			fileHeader.Name += "/"
		}

		writer, err := zipWriter.CreateHeader(fileHeader)
		this.PanicError(err)

		// only regular file has things to write.
		if fileHeader.Mode().IsRegular() {

			fileToBeZip, err := os.Open(path)
			defer func() {
				err = fileToBeZip.Close()
				this.PanicError(err)
			}()
			this.PanicError(err)

			_, err = io.Copy(writer, fileToBeZip)
			this.PanicError(err)

		}

		//dfs.
		for _, m := range matter.Children {
			walkFunc(m)
		}
	}

	for _, m := range matters {
		walkFunc(m)
	}
}

//delete files.
func (this *MatterService) Delete(request *http.Request, matter *Matter, user *User) {

	if matter == nil {
		panic(result.BadRequest("matter cannot be nil"))
	}

	this.matterDao.Delete(matter)

	//re compute the size of Route.
	this.ComputeRouteSize(matter.Puuid, user)
}

//soft delete files.
func (this *MatterService) SoftDelete(request *http.Request, matter *Matter, user *User) {

	if matter == nil {
		panic(result.BadRequest("matter cannot be nil"))
	}

	if matter.Deleted {
		panic(result.BadRequest("matter has been deleted"))
	}

	this.matterDao.SoftDelete(matter)
	//no need to recompute size.
}

//recovery delete files.
func (this *MatterService) Recovery(request *http.Request, matter *Matter, user *User) {

	if matter == nil {
		panic(result.BadRequest("matter cannot be nil"))
	}

	if !matter.Deleted {
		panic(result.BadRequest("matter has not been deleted"))
	}

	this.matterDao.Recovery(matter)
	//no need to recompute size.
}

//atomic delete files
func (this *MatterService) AtomicDelete(request *http.Request, matter *Matter, user *User) {

	if matter == nil {
		panic(result.BadRequest("matter cannot be nil"))
	}

	//lock
	this.userService.MatterLock(matter.UserUuid)
	defer this.userService.MatterUnlock(matter.UserUuid)

	this.Delete(request, matter, user)
}

//atomic soft delete files
func (this *MatterService) AtomicSoftDelete(request *http.Request, matter *Matter, user *User) {

	if matter == nil {
		panic(result.BadRequest("matter cannot be nil"))
	}

	if matter.Deleted {
		panic(result.BadRequest("matter has been deleted"))
	}

	//lock
	this.userService.MatterLock(matter.UserUuid)
	defer this.userService.MatterUnlock(matter.UserUuid)

	//if disabled the recycle feature. then we hard delete.
	preference := this.preferenceService.Fetch()
	if preference.DeletedKeepDays == 0 {
		this.Delete(request, matter, user)
	} else {
		this.SoftDelete(request, matter, user)
	}

}

//atomic recovery delete files
func (this *MatterService) AtomicRecovery(request *http.Request, matter *Matter, user *User) {

	if matter == nil {
		panic(result.BadRequest("matter cannot be nil"))
	}

	if !matter.Deleted {
		panic(result.BadRequest("matter has not been deleted"))
	}

	//lock
	this.userService.MatterLock(matter.UserUuid)
	defer this.userService.MatterUnlock(matter.UserUuid)

	this.Recovery(request, matter, user)
}

//upload files.
func (this *MatterService) Upload(request *http.Request, file io.Reader, user *User, dirMatter *Matter, filename string, privacy bool) *Matter {

	if user == nil {
		panic(result.BadRequest("user cannot be nil."))
	}

	if dirMatter == nil {
		panic(result.BadRequest("dirMatter cannot be nil."))
	}

	if dirMatter.Deleted {
		panic(result.BadRequest("Dir has been deleted. Cannot upload under it."))
	}

	if len(filename) > MATTER_NAME_MAX_LENGTH {
		panic(result.BadRequestI18n(request, i18n.MatterNameLengthExceedLimit, len(filename), MATTER_NAME_MAX_LENGTH))
	}

	dirAbsolutePath := dirMatter.AbsolutePath()

	count := this.matterDao.CountByUserUuidAndPuuidAndDirAndName(user.Uuid, dirMatter.Uuid, false, filename)
	if count > 0 {
		panic(result.BadRequestI18n(request, i18n.MatterExist, filename))
	}

	fileAbsolutePath := dirAbsolutePath + "/" + filename

	util.MakeDirAll(dirAbsolutePath)

	//if exist, overwrite it.
	exist := util.PathExists(fileAbsolutePath)
	if exist {
		this.logger.Error("%s exits, overwrite it.", fileAbsolutePath)
		removeError := os.Remove(fileAbsolutePath)
		this.PanicError(removeError)
	}

	destFile, err := os.OpenFile(fileAbsolutePath, os.O_WRONLY|os.O_CREATE, 0777)
	this.PanicError(err)

	defer func() {
		err := destFile.Close()
		this.PanicError(err)
	}()

	fileSize, err := io.Copy(destFile, file)
	this.PanicError(err)

	this.logger.Info("upload %s %v ", filename, util.HumanFileSize(fileSize))

	//check the size limit.
	if user.SizeLimit >= 0 {
		if fileSize > user.SizeLimit {
			//delete the file on disk.
			err = os.Remove(fileAbsolutePath)
			this.PanicError(err)

			panic(result.BadRequestI18n(request, i18n.MatterSizeExceedLimit, util.HumanFileSize(fileSize), util.HumanFileSize(user.SizeLimit)))
		}
	}

	//check total size.
	if user.TotalSizeLimit >= 0 {
		if user.TotalSize+fileSize > user.TotalSizeLimit {

			//delete the file on disk.
			err = os.Remove(fileAbsolutePath)
			this.PanicError(err)

			panic(result.BadRequestI18n(request, i18n.MatterSizeExceedTotalLimit, util.HumanFileSize(user.TotalSize), util.HumanFileSize(user.TotalSizeLimit)))
		}
	}

	matter := this.createNonDirMatter(dirMatter, filename, fileSize, privacy, user)

	return matter
}

// create a non dir matter.
func (this *MatterService) createNonDirMatter(dirMatter *Matter, filename string, fileSize int64, privacy bool, user *User) *Matter {
	dirRelativePath := dirMatter.Path
	fileRelativePath := dirRelativePath + "/" + filename

	//write to db.
	matter := &Matter{
		Puuid:     dirMatter.Uuid,
		UserUuid:  user.Uuid,
		Username:  user.Username,
		Dir:       false,
		Name:      filename,
		Md5:       "",
		Size:      fileSize,
		Privacy:   privacy,
		Path:      fileRelativePath,
		Prop:      EMPTY_JSON_MAP,
		VisitTime: time.Now(),
	}
	matter = this.matterDao.Create(matter)

	//compute the size of directory
	go core.RunWithRecovery(func() {
		this.ComputeRouteSize(dirMatter.Uuid, user)
	})

	return matter
}

// create a non dir matter.
func (this *MatterService) updateNonDirMatter(matter *Matter, fileSize int64, user *User) *Matter {

	matter.Size = fileSize

	matter = this.matterDao.Save(matter)

	//compute the size of directory
	go core.RunWithRecovery(func() {
		this.ComputeRouteSize(matter.Puuid, user)
	})

	return matter
}

// compute route size. It will compute upward until root directory
func (this *MatterService) ComputeRouteSize(matterUuid string, user *User) {

	//if to root directory, then update to user's info.
	if matterUuid == MATTER_ROOT {

		size := this.matterDao.SizeByPuuidAndUserUuid(MATTER_ROOT, user.Uuid)

		db := core.CONTEXT.GetDB().Model(&User{}).Where("uuid = ?", user.Uuid).Update("total_size", size)
		this.PanicError(db.Error)

		//update user total size info in cache.
		user.TotalSize = size

		return
	}

	matter := this.matterDao.CheckByUuid(matterUuid)

	//only compute dir
	if matter.Dir {
		//compute the total size.
		size := this.matterDao.SizeByPuuidAndUserUuid(matterUuid, user.Uuid)

		//when changed, we update
		if matter.Size != size {
			db := core.CONTEXT.GetDB().Model(&Matter{}).Where("uuid = ?", matterUuid).Update("size", size)
			this.PanicError(db.Error)
		}

	}

	//update parent recursively.
	this.ComputeRouteSize(matter.Puuid, user)
}

// compute all dir's size.
func (this *MatterService) ComputeAllDirSize(user *User) {

	this.logger.Info("Compute all dir's size for user %s %s", user.Uuid, user.Username)

	rootMatter := NewRootMatter(user)
	this.ComputeDirSize(rootMatter, user)
}

// compute a dir's size.
func (this *MatterService) ComputeDirSize(dirMatter *Matter, user *User) {

	this.logger.Info("Compute dir's size %s %s", dirMatter.Uuid, dirMatter.Name)

	//update sub dir first
	childrenDirMatters := this.matterDao.FindByUserUuidAndPuuidAndDirTrue(user.Uuid, dirMatter.Uuid)
	for _, childrenDirMatter := range childrenDirMatters {
		this.ComputeDirSize(childrenDirMatter, user)
	}

	//if to root directory, then update to user's info.
	if dirMatter.Uuid == MATTER_ROOT {

		size := this.matterDao.SizeByPuuidAndUserUuid(MATTER_ROOT, user.Uuid)

		db := core.CONTEXT.GetDB().Model(&User{}).Where("uuid = ?", user.Uuid).Update("total_size", size)
		this.PanicError(db.Error)

		//update user total size info in cache.
		user.TotalSize = size
	} else {

		//compute self.
		size := this.matterDao.SizeByPuuidAndUserUuid(dirMatter.Uuid, user.Uuid)

		//when changed, we update
		if dirMatter.Size != size {
			db := core.CONTEXT.GetDB().Model(&Matter{}).Where("uuid = ?", dirMatter.Uuid).Update("size", size)
			this.PanicError(db.Error)
		}
	}

}

//inner create directory.
func (this *MatterService) createDirectory(request *http.Request, dirMatter *Matter, name string, user *User) *Matter {

	if dirMatter == nil {
		panic(result.BadRequest("dirMatter cannot be nil"))
	}

	if !dirMatter.Dir {
		panic(result.BadRequest("dirMatter must be directory"))
	}

	if dirMatter.Deleted {
		panic(result.BadRequest("Dir has been deleted. Cannot create dir under it."))
	}

	if dirMatter.UserUuid != user.Uuid {

		panic(result.BadRequest("file's user not the same"))
	}

	name = strings.TrimSpace(name)
	if name == "" {
		panic(result.BadRequest("name cannot be blank"))
	}

	if len(name) > MATTER_NAME_MAX_LENGTH {

		panic(result.BadRequestI18n(request, i18n.MatterNameLengthExceedLimit, len(name), MATTER_NAME_MAX_LENGTH))

	}

	if m, _ := regexp.MatchString(MATTER_NAME_PATTERN, name); m {
		panic(result.BadRequestI18n(request, i18n.MatterNameContainSpecialChars))
	}

	//if exist. return.
	matter := this.matterDao.FindByUserUuidAndPuuidAndDirAndName(user.Uuid, dirMatter.Uuid, TRUE, name)
	if matter != nil {
		return matter
	}

	parts := strings.Split(dirMatter.Path, "/")

	if len(parts) > MATTER_NAME_MAX_DEPTH {
		panic(result.BadRequestI18n(request, i18n.MatterDepthExceedLimit, len(parts), MATTER_NAME_MAX_DEPTH))
	}

	absolutePath := GetUserMatterRootDir(user.Username) + dirMatter.Path + "/" + name

	relativePath := dirMatter.Path + "/" + name

	//crate directory on disk.
	dirPath := util.MakeDirAll(absolutePath)
	this.logger.Info("Create Directory: %s", dirPath)

	//create in db
	matter = &Matter{
		Puuid:     dirMatter.Uuid,
		UserUuid:  user.Uuid,
		Username:  user.Username,
		Dir:       true,
		Name:      name,
		Path:      relativePath,
		VisitTime: time.Now(),
	}

	matter = this.matterDao.Create(matter)

	return matter
}

func (this *MatterService) AtomicCreateDirectory(request *http.Request, dirMatter *Matter, name string, user *User) *Matter {

	if dirMatter.Deleted {
		panic(result.BadRequest("Dir has been deleted. Cannot create sub dir under it."))
	}

	this.userService.MatterLock(user.Uuid)
	defer this.userService.MatterUnlock(user.Uuid)

	matter := this.createDirectory(request, dirMatter, name, user)

	return matter
}

//copy or move may overwrite.
func (this *MatterService) handleOverwrite(request *http.Request, user *User, destinationPath string, overwrite bool) {

	destMatter := this.matterDao.findByUserUuidAndPath(user.Uuid, destinationPath)
	if destMatter != nil {
		//if exist
		if overwrite {
			//delete.
			this.Delete(request, destMatter, user)
		} else {
			//throw precondition failed. (RFC4918:10.6)
			panic(result.CustomWebResult(result.PRECONDITION_FAILED, fmt.Sprintf("%s exists", destMatter.Path)))
		}
	}

}

//move srcMatter to destMatter. invoker must handled the overwrite and lock.
func (this *MatterService) move(request *http.Request, srcMatter *Matter, destDirMatter *Matter, user *User) {

	if srcMatter == nil {
		panic(result.BadRequest("srcMatter cannot be nil."))
	}

	if !destDirMatter.Dir {
		panic(result.BadRequestI18n(request, i18n.MatterDestinationMustDirectory))
	}

	srcPuuid := srcMatter.Puuid
	destDirUuid := destDirMatter.Uuid

	if srcMatter.Dir {

		//if src is dir.
		destAbsolutePath := destDirMatter.AbsolutePath() + "/" + srcMatter.Name
		srcAbsolutePath := srcMatter.AbsolutePath()

		//move src to dest on disk.
		err := os.Rename(srcAbsolutePath, destAbsolutePath)
		this.PanicError(err)

		//change info on db.
		srcMatter.Puuid = destDirMatter.Uuid
		srcMatter.Path = destDirMatter.Path + "/" + srcMatter.Name
		srcMatter = this.matterDao.Save(srcMatter)

		//reCompute the path.
		matters := this.matterDao.FindByPuuidAndUserUuid(srcMatter.Uuid, srcMatter.UserUuid, nil)
		for _, m := range matters {
			this.adjustPath(m, srcMatter)
		}

	} else {

		//if src is NOT dir.
		destAbsolutePath := destDirMatter.AbsolutePath() + "/" + srcMatter.Name
		srcAbsolutePath := srcMatter.AbsolutePath()

		//move src to dest on disk.
		err := os.Rename(srcAbsolutePath, destAbsolutePath)
		this.PanicError(err)

		//delete caches.
		this.imageCacheDao.DeleteByMatterUuid(srcMatter.Uuid)

		//change info in db.
		srcMatter.Puuid = destDirMatter.Uuid
		srcMatter.Path = destDirMatter.Path + "/" + srcMatter.Name
		srcMatter = this.matterDao.Save(srcMatter)

	}

	//reCompute the size of src and dest.
	this.ComputeRouteSize(srcPuuid, user)
	this.ComputeRouteSize(destDirUuid, user)

}

//move srcMatter to destMatter(must be dir)
func (this *MatterService) AtomicMove(request *http.Request, srcMatter *Matter, destDirMatter *Matter, overwrite bool, user *User) {

	if srcMatter == nil {
		panic(result.BadRequest("srcMatter cannot be nil."))
	}

	this.userService.MatterLock(srcMatter.UserUuid)
	defer this.userService.MatterUnlock(srcMatter.UserUuid)

	if destDirMatter == nil {
		panic(result.BadRequest("destDirMatter cannot be nil."))
	}
	if !destDirMatter.Dir {
		panic(result.BadRequestI18n(request, i18n.MatterDestinationMustDirectory))
	}

	//neither move to itself, nor move to its children.
	destDirMatter = this.WrapParentDetail(request, destDirMatter)
	tmpMatter := destDirMatter
	for tmpMatter != nil {
		if srcMatter.Uuid == tmpMatter.Uuid {
			panic(result.BadRequestI18n(request, i18n.MatterMoveRecursive))
		}
		tmpMatter = tmpMatter.Parent
	}

	//handle the overwrite
	destinationPath := destDirMatter.Path + "/" + srcMatter.Name
	this.handleOverwrite(request, user, destinationPath, overwrite)

	//do the move operation.
	this.move(request, srcMatter, destDirMatter, user)
}

//move srcMatters to destMatter(must be dir)
func (this *MatterService) AtomicMoveBatch(request *http.Request, srcMatters []*Matter, destDirMatter *Matter, user *User) {

	if destDirMatter == nil {
		panic(result.BadRequest("destDirMatter cannot be nil."))
	}

	this.userService.MatterLock(destDirMatter.UserUuid)
	defer this.userService.MatterUnlock(destDirMatter.UserUuid)

	if srcMatters == nil {
		panic(result.BadRequest("srcMatters cannot be nil."))
	}

	if !destDirMatter.Dir {
		panic(result.BadRequestI18n(request, i18n.MatterDestinationMustDirectory))
	}

	//neither move to itself, nor move to its children.
	destDirMatter = this.WrapParentDetail(request, destDirMatter)
	for _, srcMatter := range srcMatters {

		tmpMatter := destDirMatter
		for tmpMatter != nil {
			if srcMatter.Uuid == tmpMatter.Uuid {
				panic(result.BadRequestI18n(request, i18n.MatterMoveRecursive))
			}
			tmpMatter = tmpMatter.Parent
		}
	}

	for _, srcMatter := range srcMatters {
		this.move(request, srcMatter, destDirMatter, user)
	}

}

//copy srcMatter to destMatter. invoker must handled the overwrite and lock.
func (this *MatterService) copy(request *http.Request, srcMatter *Matter, destDirMatter *Matter, name string) {

	this.logger.Info("copy srcPath = %s destPath = %s/%s", srcMatter.Path, destDirMatter.Path, name)

	if srcMatter.Dir {

		newMatter := &Matter{
			Puuid:     destDirMatter.Uuid,
			UserUuid:  srcMatter.UserUuid,
			Username:  srcMatter.Username,
			Dir:       srcMatter.Dir,
			Name:      name,
			Md5:       "",
			Size:      srcMatter.Size,
			Privacy:   srcMatter.Privacy,
			Path:      destDirMatter.Path + "/" + name,
			Prop:      EMPTY_JSON_MAP,
			VisitTime: time.Now(),
		}

		newMatter = this.matterDao.Create(newMatter)

		//make the dir
		util.MakeDirAll(newMatter.AbsolutePath())

		//copy children
		matters := this.matterDao.FindByPuuidAndUserUuid(srcMatter.Uuid, srcMatter.UserUuid, nil)
		for _, m := range matters {
			this.copy(request, m, newMatter, m.Name)
		}

	} else {

		destAbsolutePath := destDirMatter.AbsolutePath() + "/" + name
		srcAbsolutePath := srcMatter.AbsolutePath()

		//copy file on disk.
		util.CopyFile(srcAbsolutePath, destAbsolutePath)

		newMatter := &Matter{
			Puuid:     destDirMatter.Uuid,
			UserUuid:  srcMatter.UserUuid,
			Username:  srcMatter.Username,
			Dir:       srcMatter.Dir,
			Name:      name,
			Md5:       "",
			Size:      srcMatter.Size,
			Privacy:   srcMatter.Privacy,
			Path:      destDirMatter.Path + "/" + name,
			Prop:      EMPTY_JSON_MAP,
			VisitTime: time.Now(),
		}
		newMatter = this.matterDao.Create(newMatter)

	}
}

//copy srcMatter to destMatter.
func (this *MatterService) AtomicCopy(request *http.Request, srcMatter *Matter, destDirMatter *Matter, name string, overwrite bool, user *User) {

	if srcMatter == nil {
		panic(result.BadRequest("srcMatter cannot be nil."))
	}

	this.userService.MatterLock(srcMatter.UserUuid)
	defer this.userService.MatterUnlock(srcMatter.UserUuid)

	if !destDirMatter.Dir {
		panic(result.BadRequestI18n(request, i18n.MatterDestinationMustDirectory))
	}

	destinationPath := destDirMatter.Path + "/" + name
	this.handleOverwrite(request, user, destinationPath, overwrite)

	this.copy(request, srcMatter, destDirMatter, name)
}

//rename matter to name
func (this *MatterService) AtomicRename(request *http.Request, matter *Matter, name string, overwrite bool, user *User) {

	this.logger.Info("Try to rename srcPath = %s to name = %s", matter.Path, name)

	if user == nil {
		panic(result.BadRequest("user cannot be nil"))
	}

	if matter.Deleted {
		panic(result.BadRequest("matter has been deleted. Cannot rename."))
	}

	this.userService.MatterLock(user.Uuid)
	defer this.userService.MatterUnlock(user.Uuid)

	name = CheckMatterName(request, name)

	if name == matter.Name {
		panic(result.BadRequestI18n(request, i18n.MatterNameNoChange))
	}

	//check whether the name used by another matter.
	oldMatter := this.matterDao.FindByUserUuidAndPuuidAndDirAndName(user.Uuid, matter.Puuid, "", name)
	if oldMatter != nil {
		if overwrite {
			//delete this one.
			this.Delete(request, oldMatter, user)
		} else {
			panic(result.CustomWebResult(result.PRECONDITION_FAILED, fmt.Sprintf("%s already exists", name)))
		}

	}

	if matter.Dir {
		//如果源是文件夹

		oldAbsolutePath := matter.AbsolutePath()
		absoluteDirPath := util.GetDirOfPath(oldAbsolutePath)
		relativeDirPath := util.GetDirOfPath(matter.Path)
		newAbsolutePath := absoluteDirPath + "/" + name

		//物理文件一口气移动
		err := os.Rename(oldAbsolutePath, newAbsolutePath)
		this.PanicError(err)

		//修改数据库中信息
		matter.Name = name
		matter.Path = relativeDirPath + "/" + name
		matter = this.matterDao.Save(matter)

		//调整该文件夹下文件的Path.
		matters := this.matterDao.FindByPuuidAndUserUuid(matter.Uuid, matter.UserUuid, nil)
		for _, m := range matters {
			this.adjustPath(m, matter)
		}

	} else {
		//如果源是普通文件

		oldAbsolutePath := matter.AbsolutePath()
		absoluteDirPath := util.GetDirOfPath(oldAbsolutePath)
		relativeDirPath := util.GetDirOfPath(matter.Path)
		newAbsolutePath := absoluteDirPath + "/" + name

		//物理文件进行移动
		err := os.Rename(oldAbsolutePath, newAbsolutePath)
		this.PanicError(err)

		//删除对应的缓存。
		this.imageCacheDao.DeleteByMatterUuid(matter.Uuid)

		//修改数据库中信息
		matter.Name = name
		matter.Path = relativeDirPath + "/" + name
		matter = this.matterDao.Save(matter)

	}

	return
}

//将本地文件映射到蓝眼云盘中去。
func (this *MatterService) AtomicMirror(request *http.Request, srcPath string, destPath string, overwrite bool, user *User) {

	if user == nil {
		panic(result.BadRequest("user cannot be nil"))
	}

	//操作锁
	this.userService.MatterLock(user.Uuid)
	defer this.userService.MatterUnlock(user.Uuid)

	//验证参数。
	if destPath == "" {
		panic(result.BadRequest("dest cannot be null"))
	}

	destDirMatter := this.CreateDirectories(request, user, destPath)

	if destDirMatter.Deleted {
		panic(result.BadRequest("dest matter has been deleted. Cannot mirror."))
	}

	this.mirror(request, srcPath, destDirMatter, overwrite, user)
}

//将本地文件/文件夹映射到蓝眼云盘中去。
func (this *MatterService) mirror(request *http.Request, srcPath string, destDirMatter *Matter, overwrite bool, user *User) {

	if user == nil {
		panic(result.BadRequest("user cannot be nil"))
	}

	fileStat, err := os.Stat(srcPath)
	if err != nil {

		if os.IsNotExist(err) {
			panic(result.BadRequest("srcPath %s not exist", srcPath))
		} else {

			panic(result.BadRequest("srcPath err %s %s", srcPath, err.Error()))
		}

	}

	this.logger.Info("mirror srcPath = %s destPath = %s", srcPath, destDirMatter.Path)

	if fileStat.IsDir() {

		//判断当前文件夹下，文件是否已经存在了。
		srcDirMatter := this.matterDao.FindByUserUuidAndPuuidAndDirAndName(user.Uuid, destDirMatter.Uuid, TRUE, fileStat.Name())

		if srcDirMatter == nil {
			srcDirMatter = this.createDirectory(request, destDirMatter, fileStat.Name(), user)
		}

		fileInfos, err := ioutil.ReadDir(srcPath)
		this.PanicError(err)

		//递归处理本文件夹下的文件或文件夹
		for _, fileInfo := range fileInfos {

			path := fmt.Sprintf("%s/%s", srcPath, fileInfo.Name())
			this.mirror(request, path, srcDirMatter, overwrite, user)
		}

	} else {

		//判断当前文件夹下，文件是否已经存在了。
		matter := this.matterDao.FindByUserUuidAndPuuidAndDirAndName(user.Uuid, destDirMatter.Uuid, FALSE, fileStat.Name())
		if matter != nil {
			//如果是覆盖，那么删除之前的文件
			if overwrite {
				this.Delete(request, matter, user)
			} else {
				//直接完成。
				return
			}
		}

		//准备直接从本地上传了。
		file, err := os.Open(srcPath)
		this.PanicError(err)
		defer func() {
			err := file.Close()
			this.PanicError(err)
		}()

		this.Upload(request, file, user, destDirMatter, fileStat.Name(), true)

	}

}

//根据一个文件夹路径，依次创建，找到最后一个文件夹的matter，如果中途出错，返回err. 如果存在了那就直接返回即可。
func (this *MatterService) CreateDirectories(request *http.Request, user *User, dirPath string) *Matter {

	dirPath = path.Clean(dirPath)

	if dirPath == "/" {
		return NewRootMatter(user)
	}

	//ignore the last slash.
	dirPath = strings.TrimSuffix(dirPath, "/")

	folders := strings.Split(dirPath, "/")

	if len(folders) > MATTER_NAME_MAX_DEPTH {
		panic(result.BadRequestI18n(request, i18n.MatterDepthExceedLimit, len(folders), MATTER_NAME_MAX_DEPTH))
	}

	//validate every matter name.
	for k, name := range folders {
		//first element is ""
		if k != 0 {
			CheckMatterName(request, name)
		}
	}

	var dirMatter *Matter
	for k, name := range folders {

		//ignore the first element.
		if k == 0 {
			dirMatter = NewRootMatter(user)
			continue
		}

		dirMatter = this.createDirectory(request, dirMatter, name, user)
	}

	return dirMatter
}

//wrap a matter. put its parent.
func (this *MatterService) WrapParentDetail(request *http.Request, matter *Matter) *Matter {

	if matter == nil {
		panic(result.BadRequest("matter cannot be nil."))
	}

	//when self not root.
	if matter.Uuid != MATTER_ROOT {

		puuid := matter.Puuid
		tmpMatter := matter
		for puuid != MATTER_ROOT {
			pFile := this.matterDao.CheckByUuid(puuid)
			tmpMatter.Parent = pFile
			tmpMatter = pFile
			puuid = pFile.Puuid
		}
	}

	return matter
}

//wrap a matter ,put its children
func (this *MatterService) WrapChildrenDetail(request *http.Request, matter *Matter) {

	if matter == nil {
		panic(result.BadRequest("matter cannot be nil."))
	}

	if matter.Dir {

		children := this.matterDao.FindByPuuidAndUserUuid(matter.Uuid, matter.UserUuid, nil)
		matter.Children = children

		for _, child := range matter.Children {
			this.WrapChildrenDetail(request, child)
		}
	}

}

//fetch a matter's detail with parent info.
func (this *MatterService) Detail(request *http.Request, uuid string) *Matter {
	matter := this.matterDao.CheckByUuid(uuid)
	return this.WrapParentDetail(request, matter)
}

//crawl a url to dirMatter
func (this *MatterService) AtomicCrawl(request *http.Request, url string, filename string, user *User, dirMatter *Matter, privacy bool) *Matter {

	if user == nil {
		panic(result.BadRequest("user cannot be nil."))
	}

	if dirMatter.Deleted {
		panic(result.BadRequest("Dir has been deleted. Cannot crawl under it."))
	}

	this.userService.MatterLock(user.Uuid)
	defer this.userService.MatterUnlock(user.Uuid)

	if url == "" || (!strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://")) {
		panic(`url must start with http:// or https://`)
	}

	if filename == "" {
		panic(result.BadRequest("filename cannot be null."))
	}

	//download from url.
	resp, err := http.Get(url)
	this.PanicError(err)

	return this.Upload(request, resp.Body, user, dirMatter, filename, privacy)
}

//adjust a matter's path.
func (this *MatterService) adjustPath(matter *Matter, parentMatter *Matter) {

	if matter.Dir {

		matter.Path = parentMatter.Path + "/" + matter.Name
		matter = this.matterDao.Save(matter)

		//adjust children.
		matters := this.matterDao.FindByPuuidAndUserUuid(matter.Uuid, matter.UserUuid, nil)
		for _, m := range matters {
			this.adjustPath(m, matter)
		}

	} else {
		//delete caches.
		this.imageCacheDao.DeleteByMatterUuid(matter.Uuid)

		matter.Path = parentMatter.Path + "/" + matter.Name
		matter = this.matterDao.Save(matter)
	}

}

//delete someone's EyeblueTank files according to physics files.
func (this *MatterService) DeleteByPhysics(request *http.Request, user *User) {

	if user == nil {
		panic(result.BadRequest("user cannot be nil."))
	}

	//scan user's file. scan level by level.
	rootMatter := NewRootMatter(user)
	this.deleteFolderByPhysics(request, rootMatter, user)

}

func (this *MatterService) deleteFolderByPhysics(request *http.Request, dirMatter *Matter, user *User) {

	//scan user's file. scan level by level.
	this.matterDao.PageHandle(dirMatter.Uuid, user.Uuid, "", "", "", nil, nil, func(matter *Matter) {

		if matter.Dir {
			//delete children first.
			this.deleteFolderByPhysics(request, matter, user)
		}

		if !util.PathExists(matter.AbsolutePath()) {
			this.logger.Info("physics file not exist. delete from tank. %s", matter.Name)
			this.AtomicDelete(nil, matter, user)
		}

	})
}

//scan someone's physics files to EyeblueTank
func (this *MatterService) ScanPhysics(request *http.Request, user *User) {

	if user == nil {
		panic(result.BadRequest("user cannot be nil."))
	}

	rootDirPath := GetUserMatterRootDir(user.Username)
	this.logger.Info("scan %s's root dir %s", user.Username, rootDirPath)

	rootExists := util.PathExists(rootDirPath)
	if !rootExists {
		util.MakeDirAll(rootDirPath)
	}
	rootFileInfo, err := os.Lstat(rootDirPath)
	if err != nil {
		panic(result.BadRequest("cannot get root file info."))
	}

	rootMatter := NewRootMatter(user)
	this.scanPhysicsFolder(request, rootFileInfo, rootMatter, user)
}

func (this *MatterService) scanPhysicsFolder(request *http.Request, dirInfo os.FileInfo, dirMatter *Matter, user *User) {
	if !dirInfo.IsDir() {
		return
	}

	//fetch all matters under this folder.
	_, matters := this.matterDao.PlainPage(0, 1000, dirMatter.Uuid, user.Uuid, "", "", "", nil, nil, nil)
	nameMatterMap := make(map[string]*Matter)
	for _, m := range matters {
		nameMatterMap[m.Name] = m
	}

	dirPath := dirMatter.AbsolutePath()
	names, err := util.ReadDirNames(dirPath)
	if err != nil {
		this.logger.Error("occur error when ReadDirNames %s %s", dirPath, err.Error())
		return
	}
	for _, name := range names {
		fileFullPath := filepath.Join(dirPath, name)
		fileInfo, err := os.Lstat(fileFullPath)
		if err != nil {
			this.logger.Error("occur error when Lstat %s %s", name, err.Error())
			continue
		}

		//find ther matter
		var matter *Matter
		_, ok := nameMatterMap[name]
		if ok {
			//exits. check the basic info.
			matter = nameMatterMap[name]
			//only check the fileSize.
			if !matter.Dir {
				if matter.Size != fileInfo.Size() {
					this.logger.Info("update matter: %s size:%d -> %d", name, matter.Size, fileInfo.Size())
					this.updateNonDirMatter(matter, fileInfo.Size(), user)
				}
			} else {

				//recursive scan this folder.
				this.scanPhysicsFolder(request, fileInfo, matter, user)

			}

		} else {

			if fileInfo.IsDir() {

				//create folder.
				matter = this.createDirectory(request, dirMatter, name, user)

				//recursive scan this folder.
				this.scanPhysicsFolder(request, fileInfo, matter, user)

			} else {

				//not exist. add basic info.
				this.logger.Info("Create matter: %s size:%d", name, fileInfo.Size())
				matter = this.createNonDirMatter(dirMatter, name, fileInfo.Size(), true, user)

			}

		}
	}
}

//clean all the expired deleted matters
func (this *MatterService) CleanExpiredDeletedMatters() {
	//mock a request.
	request := &http.Request{}

	preference := this.preferenceService.Fetch()

	this.userDao.PageHandle("", "", func(user *User) {

		this.logger.Info("Clean %s 's deleted matters", user.Username)

		thenDate := time.Now()
		thenDate = thenDate.AddDate(0, 0, int(-preference.DeletedKeepDays))
		if preference.DeletedKeepDays != 0 {
			thenDate = util.FirstSecondOfDay(thenDate)
		}

		//first remove all the matter(not dir).
		this.matterDao.PageHandle("", "", "", FALSE, TRUE, &thenDate, nil, func(matter *Matter) {
			this.Delete(request, matter, user)
		})

		sortArray := []builder.OrderPair{
			{
				Key:   "path",
				Value: DIRECTION_DESC,
			},
		}

		//remove all the deleted directories. sort by path.
		this.matterDao.PageHandle("", "", "", TRUE, TRUE, &thenDate, sortArray, func(matter *Matter) {
			this.Delete(request, matter, user)
		})

	})

}
