// Copyright 2019 CUE Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cue

import (
	"bytes"
	"compress/gzip"
	"encoding/gob"
	"path/filepath"
	"strings"

	"cuelang.org/go/cue/ast"
	"cuelang.org/go/cue/ast/astutil"
	"cuelang.org/go/cue/build"
	"cuelang.org/go/cue/errors"
	"cuelang.org/go/cue/format"
	"cuelang.org/go/cue/token"
	"cuelang.org/go/internal"
	"cuelang.org/go/internal/core/export"
)

// root.
type instanceData struct {
	Root  bool
	Path  string
	Files []fileData
}

type fileData struct {
	Name string
	Data []byte
}

const version = 1

type unmarshaller struct {
	ctxt    *build.Context
	imports map[string]*instanceData
}

func (b *unmarshaller) load(pos token.Pos, path string) *build.Instance {
	bi := b.imports[path]
	if bi == nil {
		return nil
	}
	return b.build(bi)
}

func (b *unmarshaller) build(bi *instanceData) *build.Instance {
	p := b.ctxt.NewInstance(bi.Path, b.load)
	p.ImportPath = bi.Path
	for _, f := range bi.Files {
		_ = p.AddFile(f.Name, f.Data)
	}
	p.Complete()
	return p
}

func compileInstances(r *Runtime, data []*instanceData) (instances []*Instance, err error) {
	b := unmarshaller{
		ctxt:    build.NewContext(),
		imports: map[string]*instanceData{},
	}
	for _, i := range data {
		if i.Path == "" {
			if !i.Root {
				return nil, errors.Newf(token.NoPos,
					"data contains non-root package without import path")
			}
			continue
		}
		b.imports[i.Path] = i
	}

	builds := []*build.Instance{}
	for _, i := range data {
		if !i.Root {
			continue
		}
		builds = append(builds, b.build(i))
	}

	return r.BuildInstances(builds)
}

// Unmarshal returns a slice of instances from bytes generated by
// [Runtime.Marshal].
func (r *Runtime) Unmarshal(b []byte) ([]*Instance, error) {
	if len(b) == 0 {
		return nil, errors.Newf(token.NoPos, "unmarshal failed: empty buffer")
	}

	switch b[0] {
	case version:
	default:
		return nil, errors.Newf(token.NoPos,
			"unmarshal failed: unsupported version %d, regenerate data", b[0])
	}

	reader, err := gzip.NewReader(bytes.NewReader(b[1:]))
	if err != nil {
		return nil, errors.Newf(token.NoPos, "unmarshal failed: %v", err)
	}

	data := []*instanceData{}
	err = gob.NewDecoder(reader).Decode(&data)
	if err != nil {
		return nil, errors.Newf(token.NoPos, "unmarshal failed: %v", err)
	}

	return compileInstances(r, data)
}

// Marshal creates bytes from a group of instances. Imported instances will
// be included in the emission.
//
// The stored instances are functionally the same, but preserving of file
// information is only done on a best-effort basis.
func (r *Runtime) Marshal(values ...InstanceOrValue) (b []byte, err error) {
	staged := []instanceData{}
	done := map[string]int{}

	var errs errors.Error

	var stageInstance func(i Value) (pos int)
	stageInstance = func(i Value) (pos int) {
		inst := i.BuildInstance()
		if p, ok := done[inst.ImportPath]; ok {
			return p
		}
		// TODO: support exporting instance
		file, _ := export.Def(r.runtime(), inst.ID(), i.instance().root)
		imports := []string{}
		file.VisitImports(func(i *ast.ImportDecl) {
			for _, spec := range i.Specs {
				info, _ := astutil.ParseImportSpec(spec)
				imports = append(imports, info.ID)
			}
		})

		if inst.PkgName != "" {
			if pkg := internal.Package(file); pkg == nil {
				pkg := &ast.Package{Name: ast.NewIdent(inst.PkgName)}
				file.Decls = append([]ast.Decl{pkg}, file.Decls...)
			} else if pkg.Name.Name != inst.PkgName {
				// pi is guaranteed to be generated by Def, so it is "safe" to modify.
				pkg.Name = ast.NewIdent(inst.PkgName)
			}
		}

		b, err := format.Node(file)
		errs = errors.Append(errs, errors.Promote(err, "marshal"))

		filename := "unmarshal"
		if len(inst.Files) == 1 {
			filename = inst.Files[0].Filename

			dir := inst.Dir
			if inst.Root != "" {
				dir = inst.Root
			}
			if dir != "" {
				filename = filepath.FromSlash(filename)
				filename, _ = filepath.Rel(dir, filename)
				filename = filepath.ToSlash(filename)
			}
		}
		// TODO: this should probably be changed upstream, but as the path
		// is for reference purposes only, this is safe.
		importPath := filepath.ToSlash(i.instance().ImportPath)

		staged = append(staged, instanceData{
			Path:  importPath,
			Files: []fileData{{filename, b}},
		})

		p := len(staged) - 1

		for _, imp := range imports {
			i := getImportFromPath(r.runtime(), imp)
			if i == nil || !strings.Contains(imp, ".") {
				continue // a builtin package.
			}
			stageInstance(i.Value())
		}

		return p
	}

	for _, val := range values {
		staged[stageInstance(val.Value())].Root = true
	}

	buf := &bytes.Buffer{}
	buf.WriteByte(version)

	zw := gzip.NewWriter(buf)
	if err := gob.NewEncoder(zw).Encode(staged); err != nil {
		return nil, err
	}

	if err := zw.Close(); err != nil {
		return nil, err
	}

	return buf.Bytes(), nil

}
