package Core

import (
	"encoding/json"
	"io/ioutil"
	"log"
	"math"
	"os"
	"path"
	"strings"
	"sync"

	"github.com/essial/OpenDiablo2/PaletteDefs"

	"github.com/essial/OpenDiablo2/Sound"

	"github.com/essial/OpenDiablo2/Common"
	"github.com/essial/OpenDiablo2/MPQ"
	"github.com/essial/OpenDiablo2/ResourcePaths"
	"github.com/essial/OpenDiablo2/Scenes"
	"github.com/essial/OpenDiablo2/UI"

	"github.com/hajimehoshi/ebiten"
	"github.com/mitchellh/go-homedir"
)

// EngineConfig defines the configuration for the engine, loaded from config.json
type EngineConfig struct {
	Language        string
	FullScreen      bool
	Scale           float64
	RunInBackground bool
	TicksPerSecond  int
	FpsCap          int
	VsyncEnabled    bool
	MpqPath         string
	MpqLoadOrder    []string
	SfxVolume       float64
	BgmVolume       float64
}

// Engine is the core OpenDiablo2 engine
type Engine struct {
	Settings        *EngineConfig                    // Engine configuration settings from json file
	Files           map[string]*Common.MpqFileRecord // Map that defines which files are in which MPQs
	LoadingSprite   *Common.Sprite                   // The sprite shown when loading stuff
	loadingProgress float64                          // LoadingProcess is a range between 0.0 and 1.0. If set, loading screen displays.
	stepLoadingSize float64                          // The size for each loading step
	CurrentScene    Scenes.Scene                     // The current scene being rendered
	UIManager       *UI.Manager                      // The UI manager
	SoundManager    *Sound.Manager                   // The sound manager
	nextScene       Scenes.Scene                     // The next scene to be loaded at the end of the game loop
	fullscreenKey   bool                             // When true, the fullscreen toggle is still being pressed
}

// CreateEngine creates and instance of the OpenDiablo2 engine
func CreateEngine() *Engine {
	result := &Engine{
		CurrentScene: nil,
		nextScene:    nil,
	}
	result.loadConfigurationFile()
	ResourcePaths.LanguageCode = result.Settings.Language
	result.mapMpqFiles()
	Common.LoadPalettes(result.Files, result)
	Common.LoadTextDictionary(result)
	Common.LoadLevelTypes(result)
	Common.LoadLevelPresets(result)
	Common.LoadSounds(result)
	result.SoundManager = Sound.CreateManager(result)
	result.SoundManager.SetVolumes(result.Settings.BgmVolume, result.Settings.SfxVolume)
	result.UIManager = UI.CreateManager(result, *result.SoundManager)
	result.LoadingSprite = result.LoadSprite(ResourcePaths.LoadingScreen, PaletteDefs.Loading)
	loadingSpriteSizeX, loadingSpriteSizeY := result.LoadingSprite.GetSize()
	result.LoadingSprite.MoveTo(int(400-(loadingSpriteSizeX/2)), int(300+(loadingSpriteSizeY/2)))
	result.SetNextScene(Scenes.CreateMainMenu(result, result, result.UIManager, result.SoundManager))
	//result.SetNextScene(Scenes.CreateBlizzardIntro(result, result))
	return result
}

func (v *Engine) loadConfigurationFile() {
	log.Println("Loading configuration file")
	configJSON, err := ioutil.ReadFile("config.json")
	if err != nil {
		log.Fatal(err)
	}
	var config EngineConfig

	err = json.Unmarshal(configJSON, &config)
	if err != nil {
		log.Fatal(err)
	}
	v.Settings = &config
	// Path fixup for wine-installed diablo 2 in linux
	if v.Settings.MpqPath[0] != '/' {
		if _, err := os.Stat(v.Settings.MpqPath); os.IsNotExist(err) {
			homeDir, _ := homedir.Dir()
			newPath := strings.ReplaceAll(v.Settings.MpqPath, `C:\`, homeDir+"/.wine/drive_c/")
			newPath = strings.ReplaceAll(newPath, "C:/", homeDir+"/.wine/drive_c/")
			newPath = strings.ReplaceAll(newPath, `\`, "/")
			if _, err := os.Stat(newPath); !os.IsNotExist(err) {
				log.Printf("Detected linux wine installation, path updated to wine prefix path.")
				v.Settings.MpqPath = newPath
			}
		}
	}
}

func (v *Engine) mapMpqFiles() {
	log.Println("mapping mpq file structure")
	v.Files = make(map[string]*Common.MpqFileRecord)
	for _, mpqFileName := range v.Settings.MpqLoadOrder {
		mpqPath := path.Join(v.Settings.MpqPath, mpqFileName)
		mpq, err := MPQ.Load(mpqPath)
		if err != nil {
			log.Fatal(err)
		}
		fileListText, err := mpq.ReadFile("(listfile)")
		if err != nil || fileListText == nil {
			// Super secret patch file activate!
			continue
		}
		fileList := strings.Split(string(fileListText), "\r\n")

		for _, filePath := range fileList {
			if _, exists := v.Files[strings.ToLower(filePath)]; exists {
				if v.Files[strings.ToLower(filePath)].IsPatch {
					v.Files[strings.ToLower(filePath)].UnpatchedMpqFile = mpqPath
				}
				continue
			}
			v.Files[`/`+strings.ReplaceAll(strings.ToLower(filePath), `\`, `/`)] = &Common.MpqFileRecord{
				mpqPath, false, ""}
		}
	}
}

var mutex sync.Mutex

// LoadFile loads a file from the specified mpq and returns the data as a byte array
func (v *Engine) LoadFile(fileName string) []byte {
	fileName = strings.ReplaceAll(fileName, "{LANG}", ResourcePaths.LanguageCode)
	mutex.Lock()
	// TODO: May want to cache some things if performance becomes an issue
	mpqFile := v.Files[strings.ToLower(fileName)]
	var mpq MPQ.MPQ
	var err error
	if mpqFile == nil {
		// Super secret non-listed file?
		for _, mpqFile := range v.Settings.MpqLoadOrder {
			mpqFilePath := path.Join(v.Settings.MpqPath, mpqFile)
			mpq, err = MPQ.Load(mpqFilePath)
			newFileName := strings.ReplaceAll(fileName, `/`, `\`)[1:]
			if err != nil {
				continue
			}
			if !mpq.FileExists(newFileName) {
				continue
			}
			// We found the super-secret file!
			v.Files[strings.ToLower(fileName)] = &Common.MpqFileRecord{mpqFilePath, false, ""}
			break
		}
	} else if mpqFile.IsPatch {
		log.Fatal("Tried to load a patchfile")
	} else {
		mpq, err = MPQ.Load(mpqFile.MpqFile)
		if err != nil {
			log.Printf("Error loading file '%s'", fileName)
			log.Fatal(err)
		}
	}

	fileName = strings.ReplaceAll(fileName, `/`, `\`)[1:]
	blockTableEntry, err := mpq.GetFileBlockData(fileName)
	if err != nil {
		log.Printf("Error locating block data entry for '%s' in mpq file '%s'", fileName, mpq.FileName)
		log.Fatal(err)
	}
	mpqStream := MPQ.CreateStream(mpq, blockTableEntry, fileName)
	result := make([]byte, blockTableEntry.UncompressedFileSize)
	mpqStream.Read(result, 0, blockTableEntry.UncompressedFileSize)
	mutex.Unlock()
	return result
}

// IsLoading returns true if the engine is currently in a loading state
func (v *Engine) IsLoading() bool {
	return v.loadingProgress < 1.0
}

// LoadSprite loads a sprite from the game's data files
func (v *Engine) LoadSprite(fileName string, palette PaletteDefs.PaletteType) *Common.Sprite {
	data := v.LoadFile(fileName)
	sprite := Common.CreateSprite(data, Common.Palettes[palette])
	return sprite
}

// updateScene handles the scene maintenance for the engine
func (v *Engine) updateScene() {
	if v.nextScene == nil {
		return
	}
	if v.CurrentScene != nil {
		v.CurrentScene.Unload()
	}
	v.CurrentScene = v.nextScene
	v.nextScene = nil
	v.UIManager.Reset()
	thingsToLoad := v.CurrentScene.Load()
	v.SetLoadingStepSize(1.0 / float64(len(thingsToLoad)))
	v.ResetLoading()
	go func() {
		for _, f := range thingsToLoad {
			f()
			v.StepLoading()
		}
		v.FinishLoading()
	}()
}

// Update updates the internal state of the engine
func (v *Engine) Update() {
	if ebiten.IsKeyPressed(ebiten.KeyAlt) && ebiten.IsKeyPressed(ebiten.KeyEnter) {
		if !v.fullscreenKey {
			ebiten.SetFullscreen(!ebiten.IsFullscreen())
		}
		v.fullscreenKey = true
	} else {
		v.fullscreenKey = false
	}

	v.updateScene()
	if v.CurrentScene == nil {
		log.Fatal("no scene loaded")
	}

	if v.IsLoading() {
		return
	}

	v.CurrentScene.Update(float64(1) / ebiten.CurrentTPS())
	v.UIManager.Update()
}

// Draw draws the game
func (v *Engine) Draw(screen *ebiten.Image) {
	if v.loadingProgress < 1.0 {
		v.LoadingSprite.Frame = uint8(Common.Max(0, Common.Min(uint32(len(v.LoadingSprite.Frames)-1), uint32(float64(len(v.LoadingSprite.Frames)-1)*v.loadingProgress))))
		v.LoadingSprite.Draw(screen)
	} else {
		if v.CurrentScene == nil {
			log.Fatal("no scene loaded")
		}
		v.CurrentScene.Render(screen)
		v.UIManager.Draw(screen)
	}
}

// SetNextScene tells the engine what scene to load on the next update cycle
func (v *Engine) SetNextScene(nextScene Scenes.Scene) {
	v.nextScene = nextScene
}

// SetLoadingStepSize sets the size of the loading step
func (v *Engine) SetLoadingStepSize(size float64) {
	v.stepLoadingSize = size
}

// ResetLoading resets the loading progress
func (v *Engine) ResetLoading() {
	v.loadingProgress = 0.0
}

// StepLoading increments the loading progress
func (v *Engine) StepLoading() {
	v.loadingProgress = math.Min(1.0, v.loadingProgress+v.stepLoadingSize)
}

// FinishLoading terminates the loading phase
func (v *Engine) FinishLoading() {
	v.loadingProgress = 1.0
}