package main

import (
	"bytes"
	"crypto/sha1"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"time"

	wruntime "github.com/wailsapp/wails/v2/pkg/runtime"
	"github.com/zhyee/zipstream"
)

type McVersionManifestEntry struct {
	Id              string
	Type            string
	Url             string
	Time            time.Time
	ReleaseTime     time.Time
	Sha1            string
	ComplianceLevel int
}

type McLatestEntry struct {
	Release  string
	Snapshot string
}

type McVersionManifest struct {
	Latest   McLatestEntry
	Versions []McVersionManifestEntry
}

type McArguments struct {
	Game []string
	Jvm  []string
}

type McAssetIndex struct {
	Id        string
	Sha1      string
	Size      int
	TotalSize int
	Url       string
}

type McJavaVersion struct {
	Component    string
	MajorVersion int
}

type McLibraryArtifact struct {
	Path string
	Sha1 string
	Size int
	Url  string
}

type McLibraryDownload struct {
	Artifact    McLibraryArtifact
	Classifiers map[string]interface{}
}

type McRuleOs struct {
	Name    string
	Version string
	ARch    string
}

type McRule struct {
	Action   string
	Features map[string]bool
	Os       McRuleOs
}

type McLibrary struct {
	Downloads McLibraryDownload
	Name      string
	Rules     []McRule
	Natives   map[string]string
}

type McDownload struct {
	Sha1 string
	Size int
	Url  string
}

type McLogging struct {
	Client McLoggingClient
}

type McLoggingClient struct {
	Argument string
	File     McDownload
}

type McMetadata struct {
	Arguments              McArguments
	AssetIndex             McAssetIndex
	Assets                 string
	ComplianceLevel        int
	Downloads              map[string]McDownload
	Id                     string
	JavaVersion            McJavaVersion
	Libraries              []McLibrary
	MainClass              string
	MinimumLauncherVersion int
	ReleaseTime            time.Time
	Time                   time.Time
	Type                   string
	MinecraftArguments     string
	Logging                McLogging
}

func GetVersionManifest() (McVersionManifest, error) {
	resp, err := http.Get("https://piston-meta.mojang.com/mc/game/version_manifest_v2.json")
	var returnError error = nil
	if err == nil {
		defer resp.Body.Close()
		data, err := io.ReadAll(resp.Body)
		if err == nil {
			versionManifest := McVersionManifest{}
			err = json.Unmarshal(data, &versionManifest)
			if err == nil {
				dir, err := os.UserConfigDir()
				if err != nil {
					return versionManifest, nil
				}
				err = os.MkdirAll(filepath.Join(dir, "FCLauncher", "cache"), 0755)
				f, err := os.OpenFile(filepath.Join(dir, "FCLauncher", "cache", "versionManifest.json"), os.O_CREATE|os.O_RDWR, 0755)
				defer f.Close()
				f.Write(data)
				return versionManifest, nil
			} else {
				returnError = fmt.Errorf("Unable to parse Json: %e\n", err)
			}
		} else {
			returnError = fmt.Errorf("Unable to read version manifest: %e\n", err)
		}
	} else {
		returnError = fmt.Errorf("Unable to download version manifest: %e\n", err)
	}

	dir, err := os.UserConfigDir()
	if err != nil {
		return McVersionManifest{}, returnError
	}
	path := filepath.Join(dir, "FCLauncher", "cache", "versionManifest.json")
	if _, err = os.Stat(path); err != nil {
		return McVersionManifest{}, returnError
	}
	f, err := os.OpenFile(path, os.O_RDONLY, 0755)
	if err != nil {
		return McVersionManifest{}, returnError
	}
	data, _ := io.ReadAll(f)
	if err != nil {
		return McVersionManifest{}, returnError
	}
	versionManifest := McVersionManifest{}
	err = json.Unmarshal(data, &versionManifest)
	if err != nil {
		return McVersionManifest{}, returnError
	}
	return versionManifest, nil

}

func GetVersionMetadata(wantedVersion string) (McMetadata, error) {
	manifest, err := GetVersionManifest()
	if err != nil {
		return McMetadata{}, fmt.Errorf("GetVersionMetadata: %e\n", err)
	}

	for _, version := range manifest.Versions {
		if wantedVersion == version.Id {
			//found it
			dir, err := os.UserConfigDir()
			if err == nil {
				path := filepath.Join(dir, "FCLauncher", "cache", "versionMetadata", wantedVersion+".json")
				if _, err := os.Stat(path); err == nil {
					if f, err := os.OpenFile(path, os.O_RDONLY, 0755); err == nil {
						defer f.Close()
						if data, err := io.ReadAll(f); err == nil {
							sha := sha1.Sum(data)
							if hex.EncodeToString(sha[:20]) == version.Sha1 {
								metadata := McMetadata{}
								json.Unmarshal(data, &metadata)
								return metadata, nil
							}
						}
					}
				}
			}
			resp, err := http.Get(version.Url)
			if err != nil {
				return McMetadata{}, fmt.Errorf("Unable to download metadata: %e\n", err)
			}
			defer resp.Body.Close()
			data, err := io.ReadAll(resp.Body)
			if err != nil {
				return McMetadata{}, fmt.Errorf("Unable to download metadata: %e\n", err)
			}
			sha := sha1.Sum(data)
			if hex.EncodeToString(sha[:20]) != version.Sha1 {
				return McMetadata{}, fmt.Errorf("GetMetadata: Sha1 hash does not match\n")
			}
			metadata := McMetadata{}
			err = json.Unmarshal(data, &metadata)
			dir, err = os.UserConfigDir()
			if err == nil {
				path := filepath.Join(dir, "FCLauncher", "cache", "versionMetadata")
				if os.MkdirAll(path, 0755) == nil {
					if f, err := os.OpenFile(filepath.Join(path, wantedVersion+".json"), os.O_CREATE|os.O_RDWR, 0755); err == nil {
						defer f.Close()
						f.Write(data)
					}
				}
			}
			return metadata, nil

		}
	}
	return McMetadata{}, fmt.Errorf("Unable to find version %s\n", wantedVersion)
}

func GetAssetIndex(mcVersion string) ([]string, error) {
	found := false
	var data []byte
	metadata, err := GetVersionMetadata(mcVersion)
	if err != nil {
		return []string{}, fmt.Errorf("Unable to pull manifest: %e\n", err)
	}
	dir, _ := os.UserConfigDir()
	path := filepath.Join(dir, "FCLauncher", "assets", "indexes")
	if _, err := os.Stat(filepath.Join(path, metadata.Assets+".json")); err == nil {
		//cache file exists
		if f, err := os.OpenFile(filepath.Join(path, metadata.Assets+".json"), os.O_RDONLY, 0755); err == nil {
			defer f.Close()
			if data, err = io.ReadAll(f); err == nil {
				sha := sha1.Sum(data)
				if hex.EncodeToString(sha[:20]) == metadata.AssetIndex.Sha1 {
					found = true
				}
			}
		}
	}
	if !found {
		//no cache file
		data = []byte{}
		resp, err := http.Get(metadata.AssetIndex.Url)
		if err != nil {
			return []string{}, fmt.Errorf("Unable to pull asset index: %e\n", err)
		}
		defer resp.Body.Close()
		data, err = io.ReadAll(resp.Body)
		if err != nil {
			return []string{}, fmt.Errorf("Unable to pull asset index: %e\n", err)
		}
		sha := sha1.Sum(data)
		if hex.EncodeToString(sha[:20]) != metadata.AssetIndex.Sha1 {
			return []string{}, fmt.Errorf("Sha mismatch!\n")
		}
		os.MkdirAll(path, 0755)
		f, _ := os.OpenFile(filepath.Join(path, metadata.Assets+".json"), os.O_CREATE|os.O_RDWR, 0755)
		defer f.Close()
		f.Write(data)
	}

	//at this point data is populated
	var index map[string]interface{}
	json.Unmarshal(data, &index)
	index = index["objects"].(map[string]interface{})
	hashes := []string{}
	for _, val := range index {
		index_map := val.(map[string]interface{})
		hashes = append(hashes, index_map["hash"].(string))
	}
	return hashes, nil
}

func DownloadAssets(mcVersion string, assetPath string, a App) error {
	a.Status("Downloading Minecraft Assets")
	assets, err := GetAssetIndex(mcVersion)
	if err != nil {
		return fmt.Errorf("Unable to get asset index: %e\n", err)
	}
	metadata, err := GetVersionMetadata(mcVersion)
	if err != nil {
		return fmt.Errorf("Unable to get version metadata: %e\n", err)
	}
	total := metadata.AssetIndex.TotalSize
	downloaded := 0
	wruntime.EventsEmit(a.Ctx, "download", downloaded, total)
	for _, hash := range assets {
		if _, err := os.Stat(filepath.Join(assetPath, "objects", hash[:2], hash)); err == nil {
			f, _ := os.OpenFile(filepath.Join(assetPath, "objects", hash[:2], hash), os.O_RDONLY, 0755)
			defer f.Close()
			data, _ := io.ReadAll(f)
			sha := sha1.Sum(data)
			if hex.EncodeToString(sha[:20]) == hash {
				downloaded += len(data)
				wruntime.EventsEmit(a.Ctx, "download", downloaded, total)
				continue
			}
		}
		resp, err := http.Get(fmt.Sprintf("https://resources.download.minecraft.net/%s/%s", hash[:2], hash))
		if err != nil {
			return fmt.Errorf("unable to download assets: %e\n", err)
		}
		defer resp.Body.Close()
		buff := new(bytes.Buffer)
		for {
			count, err := io.CopyN(buff, resp.Body, BlockSize)
			if err == io.EOF {
				downloaded += int(count)
				break
			}
			if err != nil {
				return fmt.Errorf("Error Downloading assets: %e\n", err)
			}
			downloaded += int(count)
			wruntime.EventsEmit(a.Ctx, "download", downloaded, total)
		}
		data := buff.Bytes()
		sha := sha1.Sum(data)
		if hex.EncodeToString(sha[:20]) != hash {
			return fmt.Errorf("unable to download assets: Sha1 Mismatch\n")
		}
		err = os.MkdirAll(filepath.Join(assetPath, "objects", hash[:2]), 0755)
		if err != nil {
			return fmt.Errorf("unable to download assets: Unable to create directory\n")
		}
		f, err := os.OpenFile(filepath.Join(assetPath, "objects", hash[:2], hash), os.O_CREATE|os.O_RDWR, 0755)
		if err != nil {
			return fmt.Errorf("unable to download assets: Unable to open file\n")
		}
		defer f.Close()
		f.Write(data)
		wruntime.EventsEmit(a.Ctx, "download", downloaded, total)
	}
	wruntime.EventsEmit(a.Ctx, "download_complete")
	return nil
}

func DownloadLibraries(mcVersion string, libPath string, a App) error {
	metadata, err := GetVersionMetadata(mcVersion)
	if err != nil {
		return fmt.Errorf("unable to pull version metadata: %e\n", err)
	}
	for _, lib := range metadata.Libraries {
		a.Status(fmt.Sprintf("Checking %s\n", lib.Name))
		if _, err := os.Stat(filepath.Join(libPath, lib.Downloads.Artifact.Path)); err == nil {
			f, _ := os.OpenFile(filepath.Join(libPath, lib.Downloads.Artifact.Path), os.O_CREATE|os.O_RDWR, 0755)
			defer f.Close()
			data, _ := io.ReadAll(f)
			sha := sha1.Sum(data)
			if hex.EncodeToString(sha[:20]) == lib.Downloads.Artifact.Sha1 {
				continue
			}
		}

		a.Status(fmt.Sprintf("Downloading %s\n", lib.Name))
		resp, err := http.Get(lib.Downloads.Artifact.Url)
		if err != nil {
			return fmt.Errorf("unable to download libs: %e\n", err)
		}
		defer resp.Body.Close()
		buff := new(bytes.Buffer)
		downloaded := 0
		for {
			count, err := io.CopyN(buff, resp.Body, BlockSize)
			if err == io.EOF {
				downloaded += int(count)
				break
			}
			if err != nil {
				return fmt.Errorf("Error Downloading libs: %e\n", err)
			}
			downloaded += int(count)
			wruntime.EventsEmit(a.Ctx, "download", downloaded, resp.ContentLength)
		}
		data := buff.Bytes()
		sha := sha1.Sum(data)
		if hex.EncodeToString(sha[:20]) != lib.Downloads.Artifact.Sha1 {
			return fmt.Errorf("unable to download libs: Sha1 Mismatch\n")
		}
		path := ""
		tokens := strings.Split(lib.Downloads.Artifact.Path, "/")
		for ind, token := range tokens {
			if ind != len(tokens)-1 {
				path = filepath.Join(path, token)
			}
		}
		err = os.MkdirAll(filepath.Join(libPath, path), 0755)
		if err != nil {
			return fmt.Errorf("unable to download libs: Unable to create directory\n")
		}
		f, err := os.OpenFile(filepath.Join(libPath, lib.Downloads.Artifact.Path), os.O_CREATE|os.O_RDWR, 0755)
		if err != nil {
			return fmt.Errorf("unable to download libs: Unable to open file\n")
		}
		defer f.Close()
		f.Write(data)
		wruntime.EventsEmit(a.Ctx, "download_complete")
	}
	return nil
}

func InstallNatives(mcVersion string, nativesDir string) {
	metadata, _ := GetVersionMetadata(mcVersion)
	for _, lib := range metadata.Libraries {
		if lib.Natives != nil {
			glob := lib.Natives[runtime.GOOS]
			fmt.Printf("glob is: %s\n", glob)
			if lib.Downloads.Classifiers[glob] != nil {
				artifact := lib.Downloads.Classifiers[glob].(map[string]interface{})
				resp, _ := http.Get(artifact["url"].(string))
				defer resp.Body.Close()
				zr := zipstream.NewReader(resp.Body)
				for {
					e, err := zr.GetNextEntry()
					if err == io.EOF {
						break
					}
					if e.IsDir() {
						os.MkdirAll(filepath.Join(nativesDir, e.Name), 0755)
					} else {
						zc, _ := e.Open()
						f, _ := os.OpenFile(filepath.Join(nativesDir, e.Name), os.O_CREATE|os.O_RDWR, 0755)
						defer zc.Close()
						defer f.Close()
						io.Copy(f, zc)
					}
				}
			}
		}
	}
}

func DownloadLoggingConfig(mcVersion string, gameDir string) {
	metadata, _ := GetVersionMetadata(mcVersion)
	resp, err := http.Get(metadata.Logging.Client.File.Url)
	if err != nil {
		fmt.Printf("Error downloading logging config: %s\n", err)
		return
	}
	defer resp.Body.Close()
	os.MkdirAll(gameDir, 0755)
	f, _ := os.OpenFile(filepath.Join(gameDir, "log4j2.xml"), os.O_CREATE|os.O_RDWR, 0755)
	defer f.Close()
	io.Copy(f, resp.Body)
}

func DownloadExecutable(mcVersion string, binDir string, a App) error {
	metadata, err := GetVersionMetadata(mcVersion)
	if err != nil {
		return fmt.Errorf("unable to pull version metadata: %e\n", err)
	}
	a.Status(fmt.Sprintf("Checking Minecraft %s Executable\n", mcVersion))
	if _, err := os.Stat(filepath.Join(binDir, mcVersion, "client.jar")); err == nil {
		f, _ := os.OpenFile(filepath.Join(binDir, mcVersion, "client.jar"), os.O_CREATE|os.O_RDWR, 0755)
		defer f.Close()
		data, _ := io.ReadAll(f)
		sha := sha1.Sum(data)
		if hex.EncodeToString(sha[:20]) == metadata.Downloads["client"].Sha1 {
			return nil
		}
	}

	a.Status(fmt.Sprintf("Downloading Minecraft %s Executable\n", mcVersion))
	resp, err := http.Get(metadata.Downloads["client"].Url)
	if err != nil {
		return fmt.Errorf("unable to download executable: %e\n", err)
	}
	defer resp.Body.Close()
	buff := new(bytes.Buffer)
	downloaded := 0
	for {
		count, err := io.CopyN(buff, resp.Body, BlockSize)
		if err == io.EOF {
			downloaded += int(count)
			break
		}
		if err != nil {
			return fmt.Errorf("Error Downloading executable: %e\n", err)
		}
		downloaded += int(count)
		wruntime.EventsEmit(a.Ctx, "download", downloaded, resp.ContentLength)
	}
	data := buff.Bytes()
	sha := sha1.Sum(data)
	if hex.EncodeToString(sha[:20]) != metadata.Downloads["client"].Sha1 {
		return fmt.Errorf("unable to download executable: Sha1 Mismatch\n")
	}
	err = os.MkdirAll(filepath.Join(binDir, mcVersion), 0755)
	if err != nil {
		return fmt.Errorf("unable to download executable: Unable to create directory\n")
	}
	f, err := os.OpenFile(filepath.Join(binDir, mcVersion, "client.jar"), os.O_CREATE|os.O_RDWR, 0755)
	if err != nil {
		return fmt.Errorf("unable to download executable: Unable to open file\n")
	}
	defer f.Close()
	f.Write(data)
	wruntime.EventsEmit(a.Ctx, "download_complete")
	return nil

}

func GetBaseLaunchArgs(mcVersion string, instance Instance, libDir string, binDir string, assetDir string, gameDir string) ([]string, error) {
	args := []string{}
	metadata, err := GetVersionMetadata(mcVersion)
	if err != nil {
		return args, fmt.Errorf("GetLaunchArgs: %e\n", err)
	}

	searchArgs := []string{}

	if metadata.MinecraftArguments != "" {
		searchArgs = strings.Split(metadata.MinecraftArguments, " ")
	} else {
		searchArgs = metadata.Arguments.Game
	}

	args = append(args, "-Djava.library.path="+filepath.Join(gameDir, "natives"))
	args = append(args, "-Djna.tmpdir="+filepath.Join(gameDir, "natives"))
	args = append(args, "-Dorg.lwjgl.system.SharedLibraryExtractpath="+filepath.Join(gameDir, "natives"))
	args = append(args, "-Dio.netty.native.workdir="+filepath.Join(gameDir, "natives"))
	loggingArg := strings.ReplaceAll(metadata.Logging.Client.Argument, "${path}", filepath.Join(gameDir, "log4j2.xml"))
	if loggingArg != "" {
		args = append(args, loggingArg)
	}
	args = append(args, "-Xms512m")
	args = append(args, "-Xmx4096m")
	args = append(args, "-cp")
	arg := ""
	separater := ":"
	if runtime.GOOS == "windows" {
		separater = ";"
	}
	for _, lib := range instance.Libraries {
		arg += filepath.Join(libDir, lib) + separater
		if _, err := os.Stat(filepath.Join(libDir, lib)); err != nil {
			fmt.Printf("Error: missing library: %s\n", lib)
		}
	}
	arg += filepath.Join(binDir, mcVersion, "client.jar")
	args = append(args, arg)

	args = append(args, instance.MainClass)

	for _, val := range searchArgs {
		switch val {
		case "${version_name}":
			args = append(args, mcVersion)
		case "${game_directory}":
			args = append(args, gameDir)
		case "${assets_root}":
			args = append(args, assetDir)
		case "${clientid}":
			args = append(args, client_id)
		case "${version_type}":
			args = append(args, metadata.Type)
		case "${user_type}":
			args = append(args, "mojang")
		case "${assets_index_name}":
			args = append(args, metadata.Assets)
		case "${user_properties}":
			args = append(args, "{}")
		default:
			args = append(args, val)
		}
	}

	return args, nil
}

func GetOfflineLaunchArgs(mcVersion string, instance Instance, libDir string, binDir string, assetDir string, gameDir string, playerName string) ([]string, error) {
	args, err := GetBaseLaunchArgs(mcVersion, instance, libDir, binDir, assetDir, gameDir)
	if err != nil {
		return []string{}, fmt.Errorf("GatOfflineLaunchArgs: %e\n", err)
	}

	for ind, val := range args {
		switch val {
		case "${auth_player_name}":
			args[ind] = playerName
		case "${auth_uuid}":
			args[ind] = "null"
		case "${auth_access_token}":
			args[ind] = "null"
		case "${auth_xuid}":
			args[ind] = "null"
		default:
		}
	}

	return args, nil
}

func GetOnlineLaunchArgs(mcVersion string, instance Instance, libDir string, binDir string, assetDir string, gameDir string, auth LauncherAuth) ([]string, error) {
	args, err := GetBaseLaunchArgs(mcVersion, instance, libDir, binDir, assetDir, gameDir)
	if err != nil {
		return []string{}, fmt.Errorf("GatOfflineLaunchArgs: %e\n", err)
	}

	for ind, val := range args {
		switch val {
		case "${auth_player_name}":
			args[ind] = auth.Name
		case "${auth_uuid}":
			args[ind] = auth.Id
		case "${auth_access_token}":
			args[ind] = auth.Token
		case "${auth_xuid}":
			args[ind] = auth.Id
		default:
		}
	}

	return args, nil
}
