package cmd

import (
	"bufio"
	"bytes"
	"encoding/binary"
	"fmt"
	"image"
	"image/png"
	"os"
	"path"
	"path/filepath"
	"runtime"
	"strings"
	"text/template"
	"time"

	"github.com/jackmordaunt/icns"
	"golang.org/x/image/draw"
)

// PackageHelper helps with the 'wails package' command
type PackageHelper struct {
	platform string
	fs       *FSHelper
	log      *Logger
	system   *SystemHelper
}

// NewPackageHelper creates a new PackageHelper!
func NewPackageHelper(platform string) *PackageHelper {
	return &PackageHelper{
		platform: platform,
		fs:       NewFSHelper(),
		log:      NewLogger(),
		system:   NewSystemHelper(),
	}
}

type plistData struct {
	Title     string
	Exe       string
	PackageID string
	Version   string
	Author    string
	Date      string
}

func newPlistData(title, exe, packageID, version, author string) *plistData {
	now := time.Now().Format(time.RFC822)
	return &plistData{
		Title:     title,
		Exe:       exe,
		Version:   version,
		PackageID: packageID,
		Author:    author,
		Date:      now,
	}
}

type windowsIcoHeader struct {
	_          uint16
	imageType  uint16
	imageCount uint16
}

type windowsIcoDescriptor struct {
	width   uint8
	height  uint8
	colours uint8
	_       uint8
	planes  uint16
	bpp     uint16
	size    uint32
	offset  uint32
}

type windowsIcoContainer struct {
	Header windowsIcoDescriptor
	Data   []byte
}

func generateWindowsIcon(pngFilename string, iconfile string) error {
	sizes := []int{256, 128, 64, 48, 32, 16}

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

	pngdata, err := png.Decode(pngfile)
	if err != nil {
		return err
	}

	icons := []windowsIcoContainer{}

	for _, size := range sizes {
		rect := image.Rect(0, 0, int(size), int(size))
		rawdata := image.NewRGBA(rect)
		scale := draw.CatmullRom
		scale.Scale(rawdata, rect, pngdata, pngdata.Bounds(), draw.Over, nil)

		icondata := new(bytes.Buffer)
		writer := bufio.NewWriter(icondata)
		err = png.Encode(writer, rawdata)
		if err != nil {
			return err
		}
		writer.Flush()

		imgSize := size
		if imgSize >= 256 {
			imgSize = 0
		}

		data := icondata.Bytes()

		icn := windowsIcoContainer{
			Header: windowsIcoDescriptor{
				width:  uint8(imgSize),
				height: uint8(imgSize),
				planes: 1,
				bpp:    32,
				size:   uint32(len(data)),
			},
			Data: data,
		}
		icons = append(icons, icn)
	}

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

	ico := windowsIcoHeader{
		imageType:  1,
		imageCount: uint16(len(sizes)),
	}
	err = binary.Write(outfile, binary.LittleEndian, ico)
	if err != nil {
		return err
	}

	offset := uint32(6 + 16*len(sizes))
	for _, icon := range icons {
		icon.Header.offset = offset
		err = binary.Write(outfile, binary.LittleEndian, icon.Header)
		if err != nil {
			return err
		}
		offset += icon.Header.size
	}
	for _, icon := range icons {
		_, err = outfile.Write(icon.Data)
		if err != nil {
			return err
		}
	}
	return nil
}

func defaultString(val string, defaultVal string) string {
	if val != "" {
		return val
	}
	return defaultVal
}

func (b *PackageHelper) getPackageFileBaseDir() string {
	// Calculate template base dir
	_, filename, _, _ := runtime.Caller(1)
	return filepath.Join(path.Dir(filename), "packages", b.platform)
}

// Package the application into a platform specific package
func (b *PackageHelper) Package(po *ProjectOptions) error {
	switch b.platform {
	case "darwin":
		return b.packageOSX(po)
	case "windows":
		return b.PackageWindows(po, true)
	case "linux":
		return b.packageLinux(po)
	default:
		return fmt.Errorf("platform '%s' not supported for bundling yet", b.platform)
	}
}

func (b *PackageHelper) packageLinux(po *ProjectOptions) error {
	return nil
}

// Package the application for OSX
func (b *PackageHelper) packageOSX(po *ProjectOptions) error {
	build := path.Join(b.fs.Cwd(), "build")

	system := NewSystemHelper()
	config, err := system.LoadConfig()
	if err != nil {
		return err
	}

	name := defaultString(po.Name, "WailsTest")
	exe := defaultString(po.BinaryName, name)
	version := defaultString(po.Version, "0.1.0")
	author := defaultString(config.Name, "Anonymous")
	packageID := strings.Join([]string{"wails", name, version}, ".")
	plistData := newPlistData(name, exe, packageID, version, author)
	appname := po.Name + ".app"
	plistFilename := path.Join(build, appname, "Contents", "Info.plist")
	customPlist := path.Join(b.fs.Cwd(), "info.plist")

	// Check binary exists
	source := path.Join(build, exe)
	if po.CrossCompile == true {
		file, err := b.fs.FindFile(build, "darwin")
		if err != nil {
			return err
		}
		source = path.Join(build, file)
	}

	if !b.fs.FileExists(source) {
		// We need to build!
		return fmt.Errorf("Target '%s' not available. Has it been compiled yet?", source)
	}
	// Remove the existing package
	os.RemoveAll(appname)

	// Create directories
	exeDir := path.Join(build, appname, "/Contents/MacOS")
	b.fs.MkDirs(exeDir, 0755)
	resourceDir := path.Join(build, appname, "/Contents/Resources")
	b.fs.MkDirs(resourceDir, 0755)

	// Do we have a custom plist in the project directory?
	if !fs.FileExists(customPlist) {

		// No - create a new plist from our defaults
		tmpl := template.New("infoPlist")
		plistFile := filepath.Join(b.getPackageFileBaseDir(), "info.plist")
		infoPlist, err := os.ReadFile(plistFile)
		if err != nil {
			return err
		}
		tmpl.Parse(string(infoPlist))

		// Write the template to a buffer
		var tpl bytes.Buffer
		err = tmpl.Execute(&tpl, plistData)
		if err != nil {
			return err
		}

		// Save to the package
		err = os.WriteFile(plistFilename, tpl.Bytes(), 0644)
		if err != nil {
			return err
		}

		// Also write to project directory for customisation
		err = os.WriteFile(customPlist, tpl.Bytes(), 0644)
		if err != nil {
			return err
		}
	} else {
		// Yes - we have a plist. Copy it to the package verbatim
		err = fs.CopyFile(customPlist, plistFilename)
		if err != nil {
			return err
		}
	}

	// Copy executable
	target := path.Join(exeDir, exe)
	err = b.fs.CopyFile(source, target)
	if err != nil {
		return err
	}

	err = os.Chmod(target, 0755)
	if err != nil {
		return err
	}
	err = b.packageIconOSX(resourceDir)
	return err
}

// CleanWindows removes any windows related files found in the directory
func (b *PackageHelper) CleanWindows(po *ProjectOptions) {
	pdir := b.fs.Cwd()
	basename := strings.TrimSuffix(po.BinaryName, ".exe")
	exts := []string{".ico", ".exe.manifest", ".rc", "-res.syso"}
	rsrcs := []string{}
	for _, ext := range exts {
		rsrcs = append(rsrcs, filepath.Join(pdir, basename+ext))
	}
	b.fs.RemoveFiles(rsrcs, true)
}

// PackageWindows packages the application for windows platforms
func (b *PackageHelper) PackageWindows(po *ProjectOptions, cleanUp bool) error {
	outputDir := b.fs.Cwd()
	basename := strings.TrimSuffix(po.BinaryName, ".exe")

	// Copy default icon if needed
	icon, err := b.copyIcon()
	if err != nil {
		return err
	}

	// Generate icon from PNG if it doesn't exist
	if !fs.FileExists(basename + ".ico") {
		err = generateWindowsIcon(icon, basename+".ico")
		if err != nil {
			return err
		}
	}

	// Copy manifest
	tgtManifestFile := filepath.Join(outputDir, basename+".exe.manifest")
	if !b.fs.FileExists(tgtManifestFile) {
		srcManifestfile := filepath.Join(b.getPackageFileBaseDir(), "wails.exe.manifest")
		err := b.fs.CopyFile(srcManifestfile, tgtManifestFile)
		if err != nil {
			return err
		}
	}

	// Copy rc file
	tgtRCFile := filepath.Join(outputDir, basename+".rc")
	if !b.fs.FileExists(tgtRCFile) {
		srcRCfile := filepath.Join(b.getPackageFileBaseDir(), "wails.rc")
		rcfilebytes, err := os.ReadFile(srcRCfile)
		if err != nil {
			return err
		}
		rcfiledata := strings.Replace(string(rcfilebytes), "$NAME$", basename, -1)
		err = os.WriteFile(tgtRCFile, []byte(rcfiledata), 0755)
		if err != nil {
			return err
		}
	}

	// Build syso
	sysofile := filepath.Join(outputDir, basename+"-res.syso")

	// cross-compile
	if b.platform != runtime.GOOS {
		args := []string{
			"docker", "run", "--rm",
			"-v", outputDir + ":/build",
			"--entrypoint", "/bin/sh",
			"wailsapp/xgo:1.16.3",
			"-c", "/usr/bin/x86_64-w64-mingw32-windres -o /build/" + basename + "-res.syso /build/" + basename + ".rc",
		}
		if err := NewProgramHelper().RunCommandArray(args); err != nil {
			return err
		}
	} else {
		batfile, err := fs.LocalDir(".")
		if err != nil {
			return err
		}

		windresBatFile := filepath.Join(batfile.fullPath, "windres.bat")
		windresCommand := []string{windresBatFile, sysofile, tgtRCFile}
		err = NewProgramHelper().RunCommandArray(windresCommand)
		if err != nil {
			return err
		}
	}
	return nil
}

func (b *PackageHelper) copyIcon() (string, error) {

	// TODO: Read this from project.json
	const appIconFilename = "appicon.png"
	srcIcon := path.Join(b.fs.Cwd(), appIconFilename)

	// Check if appicon.png exists
	if !b.fs.FileExists(srcIcon) {

		// Install default icon
		iconfile := filepath.Join(b.getPackageFileBaseDir(), "icon.png")
		iconData, err := os.ReadFile(iconfile)
		if err != nil {
			return "", err
		}
		err = os.WriteFile(srcIcon, iconData, 0644)
		if err != nil {
			return "", err
		}
	}
	return srcIcon, nil
}

func (b *PackageHelper) packageIconOSX(resourceDir string) error {

	srcIcon, err := b.copyIcon()
	if err != nil {
		return err
	}
	tgtBundle := path.Join(resourceDir, "iconfile.icns")
	imageFile, err := os.Open(srcIcon)
	if err != nil {
		return err
	}
	defer imageFile.Close()
	srcImg, _, err := image.Decode(imageFile)
	if err != nil {
		return err

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

	}
	defer dest.Close()
	return icns.Encode(dest, srcImg)
}
