/*
ArOZ Online System - fscov File Naming Format Conversion Tool

This micro-servie is designed to convert filename from and to umfilename (ArOZ Online System File Explorer Custom Format)
and standard UTF-8 File System filename.

Usage:
./fsconv
(Convert all the files, folder and sub-directories from and to umfilename to UTF8 filename.)

./fsconv {target} {mode (um / utf)}
(Convert all the files, folder and sub-directories to filename in given mode, target can be file or directory.)

*/

package main

import (
	"fmt"
	"io/ioutil"
	"os"
	"path"
	"path/filepath"
	"reflect"
	"sort"
	"strings"
	"bytes"
	"encoding/hex"
	"flag"
)

//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 scan_recursive(dir_path string, ignore []string) ([]string, []string) {

	folders := []string{}
	files := []string{}

	// Scan
	filepath.Walk(dir_path, func(path string, f os.FileInfo, err error) error {

		_continue := false

		// Loop : Ignore Files & Folders
		for _, i := range ignore {

			// If ignored path
			if strings.Index(path, i) != -1 {

				// Continue
				_continue = true
			}
		}

		if _continue == false {

			f, err = os.Stat(path)

			// If no error
			if err != nil {
				panic("ERROR. " + err.Error())
			}

			// File & Folder Mode
			f_mode := f.Mode()

			// Is folder
			if f_mode.IsDir() {

				// Append to Folders Array
				folders = append(folders, path)

				// Is file
			} else if f_mode.IsRegular() {

				// Append to Files Array
				files = append(files, path)
			}
		}

		return nil
	})

	return folders, files
}

func hex2bin(s string) ([]byte, error) {
	ret, err := hex.DecodeString(s)
	return ret, err
}

func bin2hex(s string) (string) {
	src := []byte(s)
	encodedStr := hex.EncodeToString(src)
	return encodedStr;
}

type ByLen []string
 
func (a ByLen) Len() int {
   return len(a)
}
 
func (a ByLen) Less(i, j int) bool {
   return len(a[i]) > len(a[j])
}
 
func (a ByLen) Swap(i, j int) {
   a[i], a[j] = a[j], a[i]
}

func recursiveUTF8Conv(dir string, rf bool, rd bool){
	folders, files := scan_recursive(dir, []string{os.Args[0]})
	if (rf){
		for i := 0; i < len(files); i++ {
			thisFilepath := strings.ReplaceAll(files[i], "\\", "/")
			filename := path.Base(thisFilepath)
			extension := filepath.Ext(filename)
			name := filename[0 : len(filename)-len(extension)]
			//Check if it has the inith prefix
			if len(name) > 5 && name[0:5] == "inith"{
				//This is hex filename. Translate its name to normal filename
				originalName,_ := hex2bin(name[5:])
				safeName := getSafeFilename(string(originalName))
				fmt.Println(filename + " -> " + safeName + extension)
				err := os.Rename(files[i], path.Dir(thisFilepath) + "/" + safeName + extension)
				if (err != nil){
					panic(err)
				}
			}
			
		}
	}

	if (rd){
		//Sort the array of folders by string length, hence deeper folder will be renamed first
		sort.Sort(ByLen(folders))
		applicationStartupPath, _ := Realpath(path.Dir(os.Args[0]))
		for j := 0; j < len(folders); j++{
			foldername := path.Base(folders[j])
			if (foldername != applicationStartupPath && len(filepath.Base(foldername)) > 2){
				//All subdirectories except the root
				if (isHex(filepath.Base(foldername))){
					//This folder use hex foldername. Convert it to UTF8 filename
					convFoldername, _ := hex2bin(filepath.Base(foldername));
					safeName := getSafeFilename(string(convFoldername))
					fmt.Println(filepath.Base(foldername) + " -> " + string(safeName))
					os.Rename(foldername,filepath.Dir(foldername) + "/" + string(safeName))
				}
			}
			
		}
	}

}

func recursiveUMFNConv(dir string,rf bool, rd bool){
	folders, files := scan_recursive(dir, []string{os.Args[0]})
	if (rf){
		for i := 0; i < len(files); i++ {
			thisFilepath := strings.ReplaceAll(files[i], "\\", "/")
			filename := path.Base(thisFilepath)
			extension := filepath.Ext(filename)
			name := filename[0 : len(filename)-len(extension)]
			//Check if it has the inith prefix
			if len(name) > 5 && name[0:5] != "inith"{
				//This is not hex filename. Translate its name to um-filename method
				umfilename := "inith" + bin2hex(name)
				fmt.Println(filename + " -> " + umfilename + extension)
				err := os.Rename(files[i], path.Dir(thisFilepath) + "/" + umfilename + extension)
				if (err != nil){
					panic(err)
				}
			}
		}
	}
	
	if (rd){
		//Sort the array of folders by string length, hence deeper folder will be renamed first
		sort.Sort(ByLen(folders))
		applicationStartupPath, _ := Realpath(path.Dir(os.Args[0]))
		for j := 0; j < len(folders); j++{
			foldername := path.Base(folders[j])
			if (foldername != applicationStartupPath){
				//All subdirectories except the root
				if (!isHex(filepath.Base(foldername))){
					//This folder do not hex foldername. Convert it to hex-foldername
					convFoldername := bin2hex(filepath.Base(foldername));
					fmt.Println(filepath.Base(foldername) + " -> " + string(convFoldername))
					os.Rename(foldername,filepath.Dir(foldername) + "/" + string(convFoldername))
				}
			}
			
		}
	}
	
}

func isDir(path string) bool{
    fi, err := os.Stat(path)
    if err != nil {
        fmt.Println(err)
        return false
    }
    switch mode := fi.Mode(); {
    case mode.IsDir():
        // do directory stuff
        return true
    case mode.IsRegular():
        // do file stuff
        return false
	}
	return false
}

func getSafeFilename(s string) string{
	replacer := strings.NewReplacer("/", "", "\\","", "@","-", "&","-", "*","-", "<","-", ">","-", "|","-", "?","-", ":","-")
	safeName := replacer.Replace(string(s))
	return safeName
}

func isHex(s string) bool{
	_, err := hex2bin(s)
	if err != nil {
		return false
	}
	return true
}


//Realpath from https://github.com/yookoala/realpath/blob/master/realpath.go
// Realpath returns the real path of a given file in the os
func Realpath(fpath string) (string, error) {

	if len(fpath) == 0 {
		return "", os.ErrInvalid
	}

	if !filepath.IsAbs(fpath) {
		pwd, err := os.Getwd()
		if err != nil {
			return "", err
		}
		fpath = filepath.Join(pwd, fpath)
	}

	path := []byte(fpath)
	nlinks := 0
	start := 1
	prev := 1
	for start < len(path) {
		c := nextComponent(path, start)
		cur := c[start:]

		switch {

		case len(cur) == 0:
			copy(path[start:], path[start+1:])
			path = path[0 : len(path)-1]

		case len(cur) == 1 && cur[0] == '.':
			if start+2 < len(path) {
				copy(path[start:], path[start+2:])
			}
			path = path[0 : len(path)-2]

		case len(cur) == 2 && cur[0] == '.' && cur[1] == '.':
			copy(path[prev:], path[start+2:])
			path = path[0 : len(path)+prev-(start+2)]
			prev = 1
			start = 1

		default:

			fi, err := os.Lstat(string(c))
			if err != nil {
				return "", err
			}
			if isSymlink(fi) {

				nlinks++
				if nlinks > 16 {
					return "", os.ErrInvalid
				}

				var link string
				link, err = os.Readlink(string(c))
				after := string(path[len(c):])

				// switch symlink component with its real path
				path = switchSymlinkCom(path, start, link, after)

				prev = 1
				start = 1
			} else {
				// Directories
				prev = start
				start = len(c) + 1
			}
		}
	}

	for len(path) > 1 && path[len(path)-1] == os.PathSeparator {
		path = path[0 : len(path)-1]
	}
	return string(path), nil

}

// test if a link is symbolic link
func isSymlink(fi os.FileInfo) bool {
	return fi.Mode()&os.ModeSymlink == os.ModeSymlink
}

// switch a symbolic link component to its real path
func switchSymlinkCom(path []byte, start int, link, after string) []byte {

	if link[0] == os.PathSeparator {
		// Absolute links
		return []byte(filepath.Join(link, after))
	}

	// Relative links
	return []byte(filepath.Join(string(path[0:start]), link, after))
}

// get the next component
func nextComponent(path []byte, start int) []byte {
	v := bytes.IndexByte(path[start:], os.PathSeparator)
	if v < 0 {
		return path
	}
	return path[0 : start+v]
}

func main() {
	dir, _ := filepath.Abs(filepath.Dir(os.Args[0]))
	if len(os.Args) < 2 {
		//Default options, usually double click will start this process
		recursiveUTF8Conv(dir,true,true)

	}else if (len(os.Args) == 2 && os.Args[1] == "help"){
		//Show help message
		fmt.Println("ArOZ Online UM File Naming Method Converter")
		fmt.Println("Usage: \n./fsconv						Convert all files under current directory and its sub-directories into UTF-8 file naming method.")
		fmt.Println("./fsconv -um 						Convert all files under current directory and its sub-directories into UM File Naming Method representation.")
		fmt.Println("Use ./fsconv -h for showing all usable flags for defined file / folder conversion")

	}else if (len(os.Args) == 2 && os.Args[1] == "-um"){
		//Convert all files and folder into um filenaming methods
		recursiveUMFNConv(dir,true,true);

	} else {
		//Given target directory with flags
		var recursive = flag.Bool("r",false,"Enable recursive filename translation.")
		var inputFile = flag.String("i","","Input filename.")
		var defaultFormat = flag.Bool("utf",false,"Convert to standard UTF-8 filename format")
		var umFormat = flag.Bool("um",false,"Convert to UMfilename format")
		var recursiveFileOnly = flag.Bool("rf",false,"Recursive rename file only.")
		var recursiveDirOnly = flag.Bool("rd",false,"Recursive rename directory only.")
		flag.Parse()

		//Check if the input file exists.
		if !file_exists(*inputFile){
			fmt.Println("ERROR. Input file not exists. Given: ", *inputFile);
			os.Exit(0);
		}

		//Check if setting logic correct
		if (*defaultFormat == *umFormat){
			//If not defined defaultFormat or umFormat, assume default Format
			*defaultFormat = true
		}


		fmt.Println("r:", *recursive)
		fmt.Println("i:", *inputFile)
		fmt.Println("utf:", *defaultFormat)
		fmt.Println("um:", *umFormat)
		fmt.Println("rf:", *recursiveFileOnly)
		fmt.Println("rd:", *recursiveDirOnly)

		//Perform translation process
		if (*recursive|| *recursiveFileOnly || *recursiveDirOnly){
			//Recursive mode
			if (*defaultFormat == true){
				//Convert everything in given directory into standard filename
				if (*recursiveFileOnly){
					recursiveUTF8Conv(*inputFile,true,false)
				}else if (*recursiveDirOnly){
					recursiveUTF8Conv(*inputFile,false,true)
				}else{
					recursiveUTF8Conv(*inputFile,true,true)
				}
				
			}else{
				//Convert everything in give ndirectory in umfilename
				if (*recursiveFileOnly){
					recursiveUMFNConv(*inputFile,true,false)
				}else if (*recursiveDirOnly){
					recursiveUMFNConv(*inputFile,false,true)
				}else{
					recursiveUMFNConv(*inputFile,true,true)
				}
			}
		}else{
			if (isDir(*inputFile)){
				//A folder
				if (*defaultFormat == true){
					if (isHex(filepath.Base(*inputFile))){
						//This folder use hex foldername. Convert it to UTF8 filename
						convFoldername, _ := hex2bin(filepath.Base(*inputFile));
						safeName := getSafeFilename(string(convFoldername))
						fmt.Println(filepath.Base(*inputFile) + " -> " + string(safeName))
						err := os.Rename(*inputFile,filepath.Dir(*inputFile) + "/" + string(safeName))
						if (err != nil){
							panic(err)
						}
					}
				}else{
					convFoldername := bin2hex(filepath.Base(*inputFile));
					fmt.Println(filepath.Base(*inputFile) + " -> " + string(convFoldername))
					os.Rename(*inputFile,filepath.Dir(*inputFile) + "/" + string(convFoldername))
				}
			}else{
				//A file
				if (*defaultFormat == true){
					thisFilepath := strings.ReplaceAll(*inputFile, "\\", "/")
					filename := path.Base(thisFilepath)
					extension := filepath.Ext(filename)
					name := filename[0 : len(filename)-len(extension)]
					//Check if it has the inith prefix
					if len(name) > 5 && name[0:5] == "inith"{
						//This is hex filename. Translate its name to normal filename
						originalName,_ := hex2bin(name[5:])
						safeName := getSafeFilename(string(originalName))
						fmt.Println(filename + " -> " + safeName + extension)
						err := os.Rename(*inputFile, path.Dir(thisFilepath) + "/" + safeName + extension)
						if (err != nil){
							panic(err)
						}
					}
				}else{
					thisFilepath := strings.ReplaceAll(*inputFile, "\\", "/")
					filename := path.Base(thisFilepath)
					extension := filepath.Ext(filename)
					name := filename[0 : len(filename)-len(extension)]
					//Check if it has the inith prefix
					if len(name) > 5 && name[0:5] != "inith"{
						//This is not hex filename. Translate its name to um-filename method
						umfilename := "inith" + bin2hex(name)
						fmt.Println(filename + " -> " + umfilename + extension)
						err := os.Rename(*inputFile, path.Dir(thisFilepath) + "/" + umfilename + extension)
						if (err != nil){
							panic(err)
						}
					}
				}
			}
		}

	}
}