package main

import (
	"fmt"
	"io/ioutil"
	"math"
	"os"
	"path"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/FossoresLP/go-uuid-v4"
)

func check(e error) {
	if e != nil {
		panic(e)
	}
}

//ArOZ PHP-Golang Bridge
//The following sections remap the PHP functions to golang for the ease of development
func file_exists(filepath string) bool {
	if _, err := os.Stat(filepath); !os.IsNotExist(err) {
		return true
	}
	return false
}

func mkdir(filepath string) {
	os.MkdirAll(filepath, os.ModePerm)
}

func file_put_contents(file string, data string) bool {
	f, err := os.Create(file)
	check(err)
	_, err = f.WriteString(data)
	defer f.Close()
	if err != nil {
		return false
	}
	return true
}

func file_get_contents(file string) string {
	b, err := ioutil.ReadFile(file)
	check(err)
	return string(b)
}

func strtolower(word string) string {
	return strings.ToLower(word)
}

func strtoupper(word string) string {
	return strings.ToUpper(word)
}

func trim(word string) string {
	return strings.Trim(word, " ")
}

func strlen(word string) int {
	return len(word)
}

func count(array []string) int {
	return len(array)
}

func explode(key string, word string) []string {
	return strings.Split(word, key)
}

func implode(key string, array []string) string {
	return strings.Join(array[:], key)
}

func str_replace(target string, result string, word string) string {
	return strings.Replace(word, target, result, -1)
}

func in_array(a string, list []string) bool {
	for _, b := range list {
		if b == a {
			return true
		}
	}
	return false
}

func strpos(word string, target string) int {
	return strings.Index(word, target)
}

func dirname(filepath string) string {
	return path.Dir(filepath)
}

func basename(filepath string) string {
	return path.Base(filepath)
}

//End of mapping functions
//Utilities functions
func genUUIDv4() string {
	uuid, err := uuid.NewString()
	check(err)
	return uuid
}

func padZeros(thisInt string, maxval int) string {
	targetLength := len(strconv.Itoa(maxval))
	result := thisInt
	if len(thisInt) < targetLength {
		padzeros := targetLength - len(thisInt)
		for i := 0; i < padzeros; i++ {
			result = "0" + result
		}
	}
	return result

}

//End of utilities functions

func init() {
	//Check if the required directory exists. If not, create it.
	if !file_exists("setting.config") {
		setting, err := os.Create("setting.config")
		check(err)
		defer setting.Close()
		setting.WriteString("")
		setting.Sync()
	}
	if !file_exists("chunks/") {
		mkdir("chunks/")
	}
	if !file_exists("uploads/") {
		mkdir("uploads/")
	}
	if !file_exists("index/") {
		mkdir("index/")
	}
}

func main() {
	//arozdfs implementation in Golang
	/*
		Supported commands:
		help --> show all the help information

		[Uploading to arozdfs commands]
		slice
		-infile <filename> --> declare the input file
		-storepath <pathname> --> Relative path from the arozdfs root
		-slice <filesize> --> declare the slicing filesize

		upload
		-push <clusterlist.config> --> push to a list of clusters and sync file index to other clusters

		[Download from arozdfs commands]
		download
		-outfile <file.index> --> rebuild a file from cluster storage to local drive

		open
		-storepath <local path> --> the file chunks tmp folder
		-uuid <file uuid> --> the uuid which the file is stored
		-outfile <filename> --> filepath for the exported and merged file

		[File Operations]
		remove <file.index> --> remove all chunks related to thie file index
		rename <file.index> <newfile.index> --> rename all records related to this file
		move <filepath/file.index> <newpath/file.index> --> move the file to a new path in index directory


		[System checking commands]
		checkfile <file.index> --> check if a file contains all chunks which has at least two copies of each chunks
		rebuild --> Check all files on the system and fix all chunks which has corrupted
		migrate <host-uuid>

	*/

	//For each argument, start the processing
	switch functgroup := os.Args[1]; functgroup {
	case "help":
		showHelp()
	case "slice":
		startSlicingProc()
	case "upload":
		startUploadProc()
	case "download":
		startDownloadProc()
	case "open":
		openChunkedFile()
	case "debug":
		fmt.Println(padZeros("1", 32)) //Debug function. Change this line for unit testing
	default:
		showNotFound()
	}

	/*
		//Examples for using the Go-PHP bridge functions
		file_put_contents("Hello World.txt", "This is the content of the file.")
		fmt.Println(file_get_contents("Hello World.txt"))

		array := explode(",", "Apple,Orange,Pizza")
		fmt.Println(array)
		newstring := implode(",", array)
		fmt.Println(newstring)
		fmt.Println(in_array("Pizza", array))
		fmt.Println(strpos(newstring, "Pizza"))
		fmt.Println(strtoupper("hello world"))
		fmt.Println(str_replace("Pizza", "Ramen", newstring))
	*/
}

func startDownloadProc() {

}

func startSlicingProc() {
	storepath := ""
	infile := ""
	slice := 64 //Default 64MB per file chunk
	fileUUID := genUUIDv4()
	for i, arg := range os.Args {
		if strpos(arg, "-") == 0 {
			//This is a parameter defining keyword
			if arg == "-infile" {
				infile = os.Args[i+1]
			} else if arg == "-storepath" {
				storepath = os.Args[i+1]
			} else if arg == "-slice" {
				sliceSize, err := strconv.Atoi(os.Args[i+1])
				check(err)
				slice = sliceSize
			}
		}
	}
	if storepath != "" && infile != "" {
		fmt.Println(storepath + " " + infile + " " + strconv.Itoa(slice) + " " + fileUUID)
		splitFileChunks(infile, "chunks/"+storepath+"/", fileUUID, slice)
	} else {
		fmt.Println("ERROR. Undefined storepath or infile.")
	}

}

func splitFileChunks(rawfile string, outputdir string, outfilename string, chunksize int) bool {
	if !file_exists(outputdir) {
		mkdir(outputdir)
	}
	fileToBeChunked := rawfile
	file, err := os.Open(fileToBeChunked)

	if err != nil {
		return false
	}

	defer file.Close()

	fileInfo, _ := file.Stat()

	var fileSize int64 = fileInfo.Size()
	var fileChunk = float64(chunksize * 1000 * 1000) // chunksize in MB

	// calculate total number of parts the file will be chunked into

	totalPartsNum := uint64(math.Ceil(float64(fileSize) / float64(fileChunk)))
	fmt.Printf("Splitting to %d pieces.\n", totalPartsNum)
	for i := uint64(0); i < totalPartsNum; i++ {
		partSize := int(math.Min(fileChunk, float64(fileSize-int64(i*uint64(fileChunk)))))
		partBuffer := make([]byte, partSize)
		file.Read(partBuffer)
		// write to disk
		fileName := outputdir + outfilename + "_" + padZeros(strconv.FormatUint(i, 10), int(totalPartsNum))
		_, err := os.Create(fileName)
		if err != nil {
			return false
		}
		// write/save buffer to disk
		ioutil.WriteFile(fileName, partBuffer, os.ModeAppend)
		fmt.Println("Split to : ", fileName)
	}
	return true
}

func openChunkedFile() {
	storepath := ""
	uuid := ""
	outfile := ""
	for i, arg := range os.Args {
		if strpos(arg, "-") == 0 {
			//This is a parameter defining keyword
			if arg == "-uuid" {
				uuid = os.Args[i+1]
			} else if arg == "-storepath" {
				storepath = os.Args[i+1]
			} else if arg == "-outfile" {
				outfile = os.Args[i+1]
			}
		}
	}
	if storepath != "" && uuid != "" && outfile != "" {
		fmt.Println(storepath + " " + uuid + " " + outfile)
		joinFileChunks(storepath+"/"+uuid, outfile)
	} else {
		fmt.Println("ERROR. Undefined storepath, outfile or uuid.")
	}
}

func joinFileChunks(fileuuid string, outfilename string) bool {
	matches, _ := filepath.Glob(fileuuid + "_*")
	if len(matches) == 0 {
		fmt.Println("ERROR. No filechunk file for this uuid.")
		return false
	}
	outfile, err := os.Create(outfilename)
	if err != nil {
		return false
	}
	//For each file chunk, merge them into the output file
	for j := 0; j < len(matches); j++ {
		b, _ := ioutil.ReadFile(matches[j])
		outfile.Write(b)
	}
	return true
}

func startUploadProc() {
	for i, arg := range os.Args {
		// print index and value
		fmt.Println("item", i, "is", arg)
	}
}

func showHelp() {

}

func showNotFound() {
	fmt.Println("ERROR. Command not found for function group: " + os.Args[1])
}
