// Copyright 2014 The Go Authors.  All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package main

import (
	"bytes"
	"fmt"
	"go/ast"
	"go/token"
	"go/types"
	"io"
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"
	"unicode"
	"unicode/utf8"

	"golang.org/x/mobile/bind"
	"golang.org/x/mobile/internal/importers"
	"golang.org/x/mobile/internal/importers/java"
	"golang.org/x/mobile/internal/importers/objc"
	"golang.org/x/tools/go/packages"
)

func genPkg(lang string, p *types.Package, astFiles []*ast.File, allPkg []*types.Package, classes []*java.Class, otypes []*objc.Named) {
	fname := defaultFileName(lang, p)
	conf := &bind.GeneratorConfig{
		Fset:   fset,
		Pkg:    p,
		AllPkg: allPkg,
	}
	var pname string
	if p != nil {
		pname = p.Name()
	} else {
		pname = "universe"
	}
	var buf bytes.Buffer
	generator := &bind.Generator{
		Printer: &bind.Printer{Buf: &buf, IndentEach: []byte("\t")},
		Fset:    conf.Fset,
		AllPkg:  conf.AllPkg,
		Pkg:     conf.Pkg,
		Files:   astFiles,
	}
	switch lang {
	case "java":
		g := &bind.JavaGen{
			JavaPkg:   *javaPkg,
			Generator: generator,
		}
		g.Init(classes)

		pkgname := bind.JavaPkgName(*javaPkg, p)
		pkgDir := strings.Replace(pkgname, ".", "/", -1)
		buf.Reset()
		w, closer := writer(filepath.Join("java", pkgDir, fname))
		processErr(g.GenJava())
		io.Copy(w, &buf)
		closer()
		for i, name := range g.ClassNames() {
			buf.Reset()
			w, closer := writer(filepath.Join("java", pkgDir, name+".java"))
			processErr(g.GenClass(i))
			io.Copy(w, &buf)
			closer()
		}
		buf.Reset()
		w, closer = writer(filepath.Join("src", "gobind", pname+"_android.c"))
		processErr(g.GenC())
		io.Copy(w, &buf)
		closer()
		buf.Reset()
		w, closer = writer(filepath.Join("src", "gobind", pname+"_android.h"))
		processErr(g.GenH())
		io.Copy(w, &buf)
		closer()
		// Generate support files along with the universe package
		if p == nil {
			dir, err := packageDir("golang.org/x/mobile/bind")
			if err != nil {
				errorf(`"golang.org/x/mobile/bind" is not found; run go get golang.org/x/mobile/bind: %v`, err)
				return
			}
			repo := filepath.Clean(filepath.Join(dir, "..")) // golang.org/x/mobile directory.
			for _, javaFile := range []string{"Seq.java"} {
				src := filepath.Join(repo, "bind/java/"+javaFile)
				in, err := os.Open(src)
				if err != nil {
					errorf("failed to open Java support file: %v", err)
				}
				defer in.Close()
				w, closer := writer(filepath.Join("java", "go", javaFile))
				defer closer()
				if _, err := io.Copy(w, in); err != nil {
					errorf("failed to copy Java support file: %v", err)
					return
				}
			}
			// Copy support files
			if err != nil {
				errorf("unable to import bind/java: %v", err)
				return
			}
			javaDir, err := packageDir("golang.org/x/mobile/bind/java")
			if err != nil {
				errorf("unable to import bind/java: %v", err)
				return
			}
			copyFile(filepath.Join("src", "gobind", "seq_android.c"), filepath.Join(javaDir, "seq_android.c.support"))
			copyFile(filepath.Join("src", "gobind", "seq_android.go"), filepath.Join(javaDir, "seq_android.go.support"))
			copyFile(filepath.Join("src", "gobind", "seq_android.h"), filepath.Join(javaDir, "seq_android.h"))
		}
	case "go":
		w, closer := writer(filepath.Join("src", "gobind", fname))
		conf.Writer = w
		processErr(bind.GenGo(conf))
		closer()
		w, closer = writer(filepath.Join("src", "gobind", pname+".h"))
		genPkgH(w, pname)
		io.Copy(w, &buf)
		closer()
		w, closer = writer(filepath.Join("src", "gobind", "seq.h"))
		genPkgH(w, "seq")
		io.Copy(w, &buf)
		closer()
		dir, err := packageDir("golang.org/x/mobile/bind")
		if err != nil {
			errorf("unable to import bind: %v", err)
			return
		}
		copyFile(filepath.Join("src", "gobind", "seq.go"), filepath.Join(dir, "seq.go.support"))
	case "objc":
		g := &bind.ObjcGen{
			Generator: generator,
			Prefix:    *prefix,
		}
		g.Init(otypes)
		w, closer := writer(filepath.Join("src", "gobind", pname+"_darwin.h"))
		processErr(g.GenGoH())
		io.Copy(w, &buf)
		closer()
		hname := strings.Title(fname[:len(fname)-2]) + ".objc.h"
		w, closer = writer(filepath.Join("src", "gobind", hname))
		processErr(g.GenH())
		io.Copy(w, &buf)
		closer()
		mname := strings.Title(fname[:len(fname)-2]) + "_darwin.m"
		w, closer = writer(filepath.Join("src", "gobind", mname))
		conf.Writer = w
		processErr(g.GenM())
		io.Copy(w, &buf)
		closer()
		if p == nil {
			// Copy support files
			dir, err := packageDir("golang.org/x/mobile/bind/objc")
			if err != nil {
				errorf("unable to import bind/objc: %v", err)
				return
			}
			copyFile(filepath.Join("src", "gobind", "seq_darwin.m"), filepath.Join(dir, "seq_darwin.m.support"))
			copyFile(filepath.Join("src", "gobind", "seq_darwin.go"), filepath.Join(dir, "seq_darwin.go.support"))
			copyFile(filepath.Join("src", "gobind", "ref.h"), filepath.Join(dir, "ref.h"))
			copyFile(filepath.Join("src", "gobind", "seq_darwin.h"), filepath.Join(dir, "seq_darwin.h"))
		}
	default:
		errorf("unknown target language: %q", lang)
	}
}

func genPkgH(w io.Writer, pname string) {
	fmt.Fprintf(w, `// Code generated by gobind. DO NOT EDIT.

#ifdef __GOBIND_ANDROID__
#include "%[1]s_android.h"
#endif
#ifdef __GOBIND_DARWIN__
#include "%[1]s_darwin.h"
#endif`, pname)
}

func genObjcPackages(dir string, types []*objc.Named, embedders []importers.Struct) error {
	var buf bytes.Buffer
	cg := &bind.ObjcWrapper{
		Printer: &bind.Printer{
			IndentEach: []byte("\t"),
			Buf:        &buf,
		},
	}
	var genNames []string
	for _, emb := range embedders {
		genNames = append(genNames, emb.Name)
	}
	cg.Init(types, genNames)
	for i, opkg := range cg.Packages() {
		pkgDir := filepath.Join(dir, "src", "ObjC", opkg)
		if err := os.MkdirAll(pkgDir, 0700); err != nil {
			return err
		}
		pkgFile := filepath.Join(pkgDir, "package.go")
		buf.Reset()
		cg.GenPackage(i)
		if err := ioutil.WriteFile(pkgFile, buf.Bytes(), 0600); err != nil {
			return err
		}
	}
	buf.Reset()
	cg.GenInterfaces()
	objcBase := filepath.Join(dir, "src", "ObjC")
	if err := os.MkdirAll(objcBase, 0700); err != nil {
		return err
	}
	if err := ioutil.WriteFile(filepath.Join(objcBase, "interfaces.go"), buf.Bytes(), 0600); err != nil {
		return err
	}
	goBase := filepath.Join(dir, "src", "gobind")
	if err := os.MkdirAll(goBase, 0700); err != nil {
		return err
	}
	buf.Reset()
	cg.GenGo()
	if err := ioutil.WriteFile(filepath.Join(goBase, "interfaces_darwin.go"), buf.Bytes(), 0600); err != nil {
		return err
	}
	buf.Reset()
	cg.GenH()
	if err := ioutil.WriteFile(filepath.Join(goBase, "interfaces.h"), buf.Bytes(), 0600); err != nil {
		return err
	}
	buf.Reset()
	cg.GenM()
	if err := ioutil.WriteFile(filepath.Join(goBase, "interfaces_darwin.m"), buf.Bytes(), 0600); err != nil {
		return err
	}
	return nil
}

func genJavaPackages(dir string, classes []*java.Class, embedders []importers.Struct) error {
	var buf bytes.Buffer
	cg := &bind.ClassGen{
		JavaPkg: *javaPkg,
		Printer: &bind.Printer{
			IndentEach: []byte("\t"),
			Buf:        &buf,
		},
	}
	cg.Init(classes, embedders)
	for i, jpkg := range cg.Packages() {
		pkgDir := filepath.Join(dir, "src", "Java", jpkg)
		if err := os.MkdirAll(pkgDir, 0700); err != nil {
			return err
		}
		pkgFile := filepath.Join(pkgDir, "package.go")
		buf.Reset()
		cg.GenPackage(i)
		if err := ioutil.WriteFile(pkgFile, buf.Bytes(), 0600); err != nil {
			return err
		}
	}
	buf.Reset()
	cg.GenInterfaces()
	javaBase := filepath.Join(dir, "src", "Java")
	if err := os.MkdirAll(javaBase, 0700); err != nil {
		return err
	}
	if err := ioutil.WriteFile(filepath.Join(javaBase, "interfaces.go"), buf.Bytes(), 0600); err != nil {
		return err
	}
	goBase := filepath.Join(dir, "src", "gobind")
	if err := os.MkdirAll(goBase, 0700); err != nil {
		return err
	}
	buf.Reset()
	cg.GenGo()
	if err := ioutil.WriteFile(filepath.Join(goBase, "classes_android.go"), buf.Bytes(), 0600); err != nil {
		return err
	}
	buf.Reset()
	cg.GenH()
	if err := ioutil.WriteFile(filepath.Join(goBase, "classes.h"), buf.Bytes(), 0600); err != nil {
		return err
	}
	buf.Reset()
	cg.GenC()
	if err := ioutil.WriteFile(filepath.Join(goBase, "classes_android.c"), buf.Bytes(), 0600); err != nil {
		return err
	}
	return nil
}

func processErr(err error) {
	if err != nil {
		if list, _ := err.(bind.ErrorList); len(list) > 0 {
			for _, err := range list {
				errorf("%v", err)
			}
		} else {
			errorf("%v", err)
		}
	}
}

var fset = token.NewFileSet()

func writer(fname string) (w io.Writer, closer func()) {
	if *outdir == "" {
		return os.Stdout, func() { return }
	}

	name := filepath.Join(*outdir, fname)
	dir := filepath.Dir(name)
	if err := os.MkdirAll(dir, 0755); err != nil {
		errorf("invalid output dir: %v", err)
		os.Exit(exitStatus)
	}

	f, err := os.Create(name)
	if err != nil {
		errorf("invalid output dir: %v", err)
		os.Exit(exitStatus)
	}
	closer = func() {
		if err := f.Close(); err != nil {
			errorf("error in closing output file: %v", err)
		}
	}
	return f, closer
}

func copyFile(dst, src string) {
	w, closer := writer(dst)
	f, err := os.Open(src)
	if err != nil {
		errorf("unable to open file: %v", err)
		closer()
		os.Exit(exitStatus)
	}
	if _, err := io.Copy(w, f); err != nil {
		errorf("unable to copy file: %v", err)
		f.Close()
		closer()
		os.Exit(exitStatus)
	}
	f.Close()
	closer()
}

func defaultFileName(lang string, pkg *types.Package) string {
	switch lang {
	case "java":
		if pkg == nil {
			return "Universe.java"
		}
		firstRune, size := utf8.DecodeRuneInString(pkg.Name())
		className := string(unicode.ToUpper(firstRune)) + pkg.Name()[size:]
		return className + ".java"
	case "go":
		if pkg == nil {
			return "go_main.go"
		}
		return "go_" + pkg.Name() + "main.go"
	case "objc":
		if pkg == nil {
			return "Universe.m"
		}
		firstRune, size := utf8.DecodeRuneInString(pkg.Name())
		className := string(unicode.ToUpper(firstRune)) + pkg.Name()[size:]
		return *prefix + className + ".m"
	}
	errorf("unknown target language: %q", lang)
	os.Exit(exitStatus)
	return ""
}

func packageDir(path string) (string, error) {
	mode := packages.NeedFiles
	pkgs, err := packages.Load(&packages.Config{Mode: mode}, path)
	if err != nil {
		return "", err
	}
	if len(pkgs) == 0 || len(pkgs[0].GoFiles) == 0 {
		return "", fmt.Errorf("no Go package in %v", path)
	}
	pkg := pkgs[0]
	if len(pkg.Errors) > 0 {
		return "", fmt.Errorf("%v", pkg.Errors)
	}
	return filepath.Dir(pkg.GoFiles[0]), nil
}
