package store

import (
	"bytes"
	"compress/gzip"
	"crypto/md5"
	"fmt"
	"go/build"
	"html/template"
	"io"
	"io/ioutil"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"sort"
	"strings"
	"sync"

	"github.com/gobuffalo/packr/v2/internal"
	"github.com/karrick/godirwalk"

	"github.com/gobuffalo/packr/v2/file/resolver/encoding/hex"
	"github.com/gobuffalo/packr/v2/plog"
	"github.com/rogpeppe/go-internal/modfile"

	"github.com/gobuffalo/packr/v2/jam/parser"
	"golang.org/x/sync/errgroup"
)

var _ Store = &Disk{}

const DISK_GLOBAL_KEY = "__packr_global__"

type Disk struct {
	DBPath    string
	DBPackage string
	global    map[string]string
	boxes     map[string]*parser.Box
	moot      *sync.RWMutex
}

func NewDisk(path string, pkg string) *Disk {
	if len(path) == 0 {
		path = "packrd"
	}
	if len(pkg) == 0 {
		pkg = "packrd"
	}
	if !filepath.IsAbs(path) {
		path, _ = filepath.Abs(path)
	}
	return &Disk{
		DBPath:    path,
		DBPackage: pkg,
		global:    map[string]string{},
		boxes:     map[string]*parser.Box{},
		moot:      &sync.RWMutex{},
	}
}

func (d *Disk) FileNames(box *parser.Box) ([]string, error) {
	path := box.AbsPath
	if len(box.AbsPath) == 0 {
		path = box.Path
	}
	var names []string
	if _, err := os.Stat(path); err != nil {
		return names, nil
	}
	err := godirwalk.Walk(path, &godirwalk.Options{
		FollowSymbolicLinks: true,
		Callback: func(path string, de *godirwalk.Dirent) error {
			if !de.IsRegular() {
				return nil
			}
			names = append(names, path)
			return nil
		},
	})
	return names, err
}

func (d *Disk) Files(box *parser.Box) ([]*parser.File, error) {
	var files []*parser.File
	names, err := d.FileNames(box)
	if err != nil {
		return files, err
	}
	for _, n := range names {
		b, err := ioutil.ReadFile(n)
		if err != nil {
			return files, err
		}
		f := parser.NewFile(n, bytes.NewReader(b))
		files = append(files, f)
	}
	return files, nil
}

func (d *Disk) Pack(box *parser.Box) error {
	plog.Debug(d, "Pack", "box", box.Name)
	d.boxes[box.Name] = box
	names, err := d.FileNames(box)
	if err != nil {
		return err
	}
	for _, n := range names {
		_, ok := d.global[n]
		if ok {
			continue
		}
		k := makeKey(box, n)
		// not in the global, so add it!
		d.global[n] = k
	}
	return nil
}

func (d *Disk) Clean(box *parser.Box) error {
	root := box.PackageDir
	if len(root) == 0 {
		return fmt.Errorf("can't clean an empty box.PackageDir")
	}
	plog.Debug(d, "Clean", "box", box.Name, "root", root)
	return clean(root)
}

type options struct {
	Package     string
	GlobalFiles map[string]string
	Boxes       []optsBox
	GK          string
}

type optsBox struct {
	Name string
	Path string
}

// Close ...
func (d *Disk) Close() error {
	if len(d.boxes) == 0 {
		return nil
	}

	xb := &parser.Box{Name: DISK_GLOBAL_KEY}
	opts := options{
		Package:     d.DBPackage,
		GlobalFiles: map[string]string{},
		GK:          makeKey(xb, d.DBPath),
	}

	wg := errgroup.Group{}
	for k, v := range d.global {
		func(k, v string) {
			wg.Go(func() error {
				bb := &bytes.Buffer{}
				enc := hex.NewEncoder(bb)
				zw := gzip.NewWriter(enc)
				f, err := os.Open(k)
				if err != nil {
					return err
				}
				defer f.Close()
				io.Copy(zw, f)
				if err := zw.Close(); err != nil {
					return err
				}
				d.moot.Lock()
				opts.GlobalFiles[makeKey(xb, k)] = bb.String()
				d.moot.Unlock()
				return nil
			})
		}(k, v)
	}

	if err := wg.Wait(); err != nil {
		return err
	}

	for _, b := range d.boxes {
		ob := optsBox{
			Name: b.Name,
		}
		opts.Boxes = append(opts.Boxes, ob)
	}

	sort.Slice(opts.Boxes, func(a, b int) bool {
		return opts.Boxes[a].Name < opts.Boxes[b].Name
	})

	fm := template.FuncMap{
		"printBox": func(ob optsBox) (template.HTML, error) {
			box := d.boxes[ob.Name]
			if box == nil {
				return "", fmt.Errorf("could not find box %s", ob.Name)
			}
			fn, err := d.FileNames(box)
			if err != nil {
				return "", err
			}
			if len(fn) == 0 {
				return "", nil
			}

			type file struct {
				Resolver    string
				ForwardPath string
			}

			tmpl, err := template.New("box.go").Parse(diskGlobalBoxTmpl)
			if err != nil {
				return "", err
			}

			var files []file
			for _, s := range fn {
				p := strings.TrimPrefix(s, box.AbsPath)
				p = strings.TrimPrefix(p, string(filepath.Separator))
				files = append(files, file{
					Resolver:    strings.Replace(p, "\\", "/", -1),
					ForwardPath: makeKey(box, s),
				})
			}
			opts := map[string]interface{}{
				"Box":   box,
				"Files": files,
			}

			bb := &bytes.Buffer{}
			if err := tmpl.Execute(bb, opts); err != nil {
				return "", err
			}
			return template.HTML(bb.String()), nil
		},
	}

	os.MkdirAll(d.DBPath, 0755)
	fp := filepath.Join(d.DBPath, "packed-packr.go")
	global, err := os.Create(fp)
	if err != nil {
		return err
	}
	defer global.Close()

	tmpl := template.New(fp).Funcs(fm)
	tmpl, err = tmpl.Parse(diskGlobalTmpl)
	if err != nil {
		return err
	}

	if err := tmpl.Execute(global, opts); err != nil {
		return err
	}

	var ip string
	if internal.Mods() {
		// Starting in 1.12, we can rely on Go's method for
		// resolving where go.mod resides. Prior versions will
		// simply return an empty string.
		cmd := exec.Command("go", "env", "GOMOD")
		out, err := cmd.Output()
		if err != nil {
			return fmt.Errorf("go.mod cannot be read or does not exist while go module is enabled")
		}
		mp := strings.TrimSpace(string(out))
		if mp == "" {
			// We are on a prior version of Go; try and do
			// the resolution ourselves.
			mp = filepath.Join(filepath.Dir(d.DBPath), "go.mod")
			if _, err := os.Stat(mp); err != nil {
				mp = filepath.Join(d.DBPath, "go.mod")
			}
		}

		moddata, err := ioutil.ReadFile(mp)
		if err != nil {
			return fmt.Errorf("go.mod cannot be read or does not exist while go module is enabled")
		}
		ip = modfile.ModulePath(moddata)
		if ip == "" {
			return fmt.Errorf("go.mod is malformed")
		}
		ip = filepath.Join(ip, strings.TrimPrefix(filepath.Dir(d.DBPath), filepath.Dir(mp)))
		ip = strings.Replace(ip, "\\", "/", -1)
	} else {
		ip = filepath.Dir(d.DBPath)
		srcs := internal.GoPaths()
		srcs = append(srcs, build.Default.SrcDirs()...)
		for _, x := range srcs {
			ip = strings.TrimPrefix(ip, "/private")
			ip = strings.TrimPrefix(ip, x)
		}
		ip = strings.TrimPrefix(ip, string(filepath.Separator))
		ip = strings.TrimPrefix(ip, "src")
		ip = strings.TrimPrefix(ip, string(filepath.Separator))

		ip = strings.Replace(ip, "\\", "/", -1)
	}
	ip = path.Join(ip, d.DBPackage)

	for _, n := range opts.Boxes {
		b := d.boxes[n.Name]
		if b == nil {
			continue
		}
		p := filepath.Join(b.PackageDir, b.Package+"-packr.go")
		f, err := os.Create(p)
		if err != nil {
			return err
		}
		defer f.Close()

		o := struct {
			Package string
			Import  string
		}{
			Package: b.Package,
			Import:  ip,
		}

		tmpl, err := template.New(p).Parse(diskImportTmpl)
		if err != nil {
			return err
		}
		if err := tmpl.Execute(f, o); err != nil {
			return err
		}

	}

	return nil
}

// resolve file paths (only) for the boxes
// compile "global" db
// resolve files for boxes to point at global db
// write global db to disk (default internal/packr)
// write boxes db to disk (default internal/packr)
// write -packr.go files in each package (1 per package) that init the global db

func makeKey(box *parser.Box, path string) string {
	w := md5.New()
	fmt.Fprint(w, path)
	h := hex.EncodeToString(w.Sum(nil))
	return h
}
