/*
ArOZ Online System - fsexec File System Asynchronize File Opertation Execution Instance

This is a stand alone application design to replace the synchronize file operation designed since the start
of the AOB project. By replacing the original file operation php script with this new script, the file explorer
in AOB should be "freeze-free" while moving / copying large files.

This application provide the following functions

copy {from} {target}
copy_folder {from} {target}
delete {filename}
move {from} {target}
move_folder {from} {target}

All the command should be stringify with JSON and encoded into base64, then pass into the command parameter of the
application launching paramters.
*/

package main

import (
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"os"
	"path"
	"path/filepath"
	"reflect"
	"time"
)

//Commonly used functions
func in_array(val interface{}, array interface{}) (exists bool, index int) {
	exists = false
	index = -1

	switch reflect.TypeOf(array).Kind() {
	case reflect.Slice:
		s := reflect.ValueOf(array)

		for i := 0; i < s.Len(); i++ {
			if reflect.DeepEqual(val, s.Index(i).Interface()) == true {
				index = i
				exists = true
				return
			}
		}
	}

	return
}

func file_exists(path string) bool {
	if _, err := os.Stat(path); os.IsNotExist(err) {
		return false
	}
	return true
}

func file_get_contents(path string) string {
	dat, err := ioutil.ReadFile(path)
	if err != nil {
		panic("Unable to read file: " + path)
	}
	return (string(dat))

}

func writeLog(filepath string, prefix string, message string) bool {
	f, err := os.OpenFile(filepath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		log.Println(err)
	}
	defer f.Close()

	logger := log.New(f, prefix, log.LstdFlags)
	logger.Println(message)
	return true
}

func showHelp() {
	fmt.Println("<<ArOZ Online System File System Freeze Script Executer>>")
	fmt.Println("Usage: ./fsexec {uuid} {command_in_base64}")
}

func decodeBase64(base64string string) string {
	l, err := base64.StdEncoding.DecodeString(base64string)
	if err != nil {
		panic(err)
	}
	return string(l)
}

func validateCopySourceAndTarget(from string, target string) bool {
	if !file_exists(from) {
		return false
	}
	if file_exists(target) {
		return false
	}
	return true
}

func getTimestamp() string {
	t := time.Now()
	return (t.Format("2006-01-02 150405"))
}

func moveLogFile(logfile string, target string, uuid string) {
	//If then else is used to prevent invalid move target
	if target == "done" {
		os.Rename(logfile, "log/done/"+uuid+".log")
	} else if target == "error" {
		os.Rename(logfile, "log/error/"+uuid+".log")
	} else {
		//What happened?o
		panic("ERROR. Undefined log file sattle location.")
	}

}

func finishFileOperation(logfile string, uuid string) {
	//Finishing the file operation.
	writeLog(logfile, "[done] ", "Task finished successfully")
	moveLogFile(logfile, "done", uuid)
	fmt.Println("Done")
}

func initChk() {
	//Initiation Checking for all the required directories
	if !file_exists("log/") {
		os.MkdirAll("log/", 0777)
	}
	if !file_exists("log/done/") {
		os.MkdirAll("log/done/", 0777)
	}
	if !file_exists("log/error/") {
		os.MkdirAll("log/error/", 0777)
	}
}

//Main programming logic
func main() {
	initChk()
	acceptOperations := []string{"copy", "copy_folder", "delete", "move", "move_folder"}
	if len(os.Args) == 1 {
		showHelp()
		return
	}
	//Get the command in using base64 and required log file uuid
	startSettings := os.Args[1:]
	uuid := startSettings[0]
	encodedCommand := startSettings[1]
	decodedCommand := decodeBase64(encodedCommand)
	filename := uuid + ".log"
	var command []string
	//Create the log file for this task
	logfile := "log/" + filename
	//Check if the given id exists already.
	if file_exists(logfile) || file_exists("log/done/"+filename) || file_exists("log/error/"+filename) {
		panic("ERROR. Required task already exists.")
	}
	//Create the log file for this task
	writeLog(logfile, "[init] ", "Task created on "+getTimestamp())

	err := json.Unmarshal([]byte(decodedCommand), &command)
	if err != nil {
		writeLog(logfile, "[error] ", "Unable to parse JSON string.")
		moveLogFile(logfile, "error", uuid)
		panic("ERROR. Unable to parse JSON string.")
	}
	//Check if the given file operation correct or not.
	correctOpr, _ := in_array(string(command[0]), acceptOperations)
	if !correctOpr {
		fmt.Println(string(command[0]))
		writeLog(logfile, "[error] ", "Invalid file operation. "+command[0]+" given.")
		moveLogFile(logfile, "error", uuid)
		panic("ERROR. Invalid file operation. " + command[0] + " given.")
	}

	writeLog(logfile, "[info] ", decodedCommand)
	//Start reading the command for file operations
	opr := command[0]
	if opr == "copy" {
		//Copy a given file to a given location
		from := command[1]
		target := command[2]
		if !validateCopySourceAndTarget(from, target) {
			writeLog(logfile, "[error] ", "Invalid source file or target directory.")
			moveLogFile(logfile, "error", uuid)
			panic("ERROR. Invalid source file or target directory.")
		}
		//Check if the parent dir exists
		dirname := path.Dir(target)
		if !file_exists(dirname) {
			os.MkdirAll(dirname, 0777)
		}
		Copy(from, target)
		finishFileOperation(logfile, uuid)
	} else if opr == "copy_folder" {
		//Copy a folder to a given location
		from := command[1]
		target := command[2]
		if !validateCopySourceAndTarget(from, target) {
			writeLog(logfile, "[error] ", "Invalid source file or target directory.")
			moveLogFile(logfile, "error", uuid)
			panic("ERROR. Invalid source file or target directory.")
		}
		Copy(from, target)
		finishFileOperation(logfile, uuid)
	} else if opr == "move" {
		//Move a file
		from := command[1]
		target := command[2]
		if !validateCopySourceAndTarget(from, target) {
			writeLog(logfile, "[error] ", "Invalid source file or target directory.")
			moveLogFile(logfile, "error", uuid)
			panic("ERROR. Invalid source file or target directory.")
		}
		err := os.Rename(from, target)
		if err != nil {
			writeLog(logfile, "[error] ", "Unable to move file due to unknown error.")
			moveLogFile(logfile, "error", uuid)
			panic("ERROR. Unable to move file due to unknown error.")
		}
		finishFileOperation(logfile, uuid)
	} else if opr == "move_folder" {
		//Move a folder
		from := command[1]
		target := command[2]
		if !validateCopySourceAndTarget(from, target) {
			writeLog(logfile, "[error] ", "Invalid source file or target directory.")
			moveLogFile(logfile, "error", uuid)
			panic("ERROR. Invalid source file or target directory.")
		}
		err := os.Rename(from, target)
		if err != nil {
			writeLog(logfile, "[error] ", "Unable to move folder due to unknown error.")
			moveLogFile(logfile, "error", uuid)
			panic("ERROR. Unable to move folder due to unknown error.")
		}
		finishFileOperation(logfile, uuid)
	}

}

//Program required external library. Included in main to prevent download on other building platform.
//Recursive Copy Lib, not sure why it can't be imported like other module. Hence directly copy and paste in the section below.
const (
	// tmpPermissionForDirectory makes the destination directory writable,
	// so that stuff can be copied recursively even if any original directory is NOT writable.
	// See https://github.com/otiai10/copy/pull/9 for more information.
	tmpPermissionForDirectory = os.FileMode(0755)
)

// Copy copies src to dest, doesn't matter if src is a directory or a file
func Copy(src, dest string) error {
	info, err := os.Lstat(src)
	if err != nil {
		return err
	}
	return copy(src, dest, info)
}

// copy dispatches copy-funcs according to the mode.
// Because this "copy" could be called recursively,
// "info" MUST be given here, NOT nil.
func copy(src, dest string, info os.FileInfo) error {
	if info.Mode()&os.ModeSymlink != 0 {
		return lcopy(src, dest, info)
	}
	if info.IsDir() {
		return dcopy(src, dest, info)
	}
	return fcopy(src, dest, info)
}

// fcopy is for just a file,
// with considering existence of parent directory
// and file permission.
func fcopy(src, dest string, info os.FileInfo) error {

	if err := os.MkdirAll(filepath.Dir(dest), os.ModePerm); err != nil {
		return err
	}

	f, err := os.Create(dest)
	if err != nil {
		return err
	}
	defer f.Close()

	if err = os.Chmod(f.Name(), info.Mode()); err != nil {
		return err
	}

	s, err := os.Open(src)
	if err != nil {
		return err
	}
	defer s.Close()

	_, err = io.Copy(f, s)
	return err
}

// dcopy is for a directory,
// with scanning contents inside the directory
// and pass everything to "copy" recursively.
func dcopy(srcdir, destdir string, info os.FileInfo) error {

	originalMode := info.Mode()

	// Make dest dir with 0755 so that everything writable.
	if err := os.MkdirAll(destdir, tmpPermissionForDirectory); err != nil {
		return err
	}
	// Recover dir mode with original one.
	defer os.Chmod(destdir, originalMode)

	contents, err := ioutil.ReadDir(srcdir)
	if err != nil {
		return err
	}

	for _, content := range contents {
		cs, cd := filepath.Join(srcdir, content.Name()), filepath.Join(destdir, content.Name())
		if err := copy(cs, cd, content); err != nil {
			// If any error, exit immediately
			return err
		}
	}

	return nil
}

// lcopy is for a symlink,
// with just creating a new symlink by replicating src symlink.
func lcopy(src, dest string, info os.FileInfo) error {
	src, err := os.Readlink(src)
	if err != nil {
		return err
	}
	return os.Symlink(src, dest)
}
