package gengo

import (
	"bytes"
	"fmt"
	"go/ast"
	"go/token"
	"io"
	"os"
	"path/filepath"
	"strconv"

	"github.com/can1357/gengo/clang"
	"github.com/dave/dst"
	"github.com/dave/dst/decorator"
)

type Package struct {
	Name     string
	Restorer *decorator.Restorer
	Files    map[string]*dst.File
	Provider
}

func NewPackageWithProvider(name string, prov Provider) *Package {
	return &Package{
		Name:     name,
		Restorer: decorator.NewRestorer(),
		Files:    make(map[string]*dst.File),
		Provider: prov,
	}
}

func NewPackage(name string, opts ...BaseProviderOption) *Package {
	return &Package{
		Name:     name,
		Restorer: decorator.NewRestorer(),
		Files:    make(map[string]*dst.File),
		Provider: NewBaseProvider(opts...),
	}
}

func (p *Package) Transform(module string, opt *clang.Options) error {
	ast, layouts, err := clang.Parse(opt)
	if err != nil {
		return fmt.Errorf("failed to transform %+v: %w", opt.Sources, err)
	}
	main := p.Upsert(module)
	main.EmitFrom(ast, layouts)
	return nil
}

func (p *Package) Fprint(fn func(path string) (io.WriteCloser, error)) error {
	p.Restorer.Extras = true
	for k, f := range p.Files {
		file, err := fn(k + ".go")
		if err != nil {
			return err
		}
		p.Restorer.Fprint(file, f)
		if err := file.Close(); err != nil {
			return err
		}
	}
	return nil
}
func (p *Package) Upsert(module string) Module {
	f, ok := p.Files[module]
	if !ok {
		f = &dst.File{
			Name:  dst.NewIdent(p.Name),
			Scope: dst.NewScope(nil),
		}
		f.Decs.Start.Append("// Code generated by gengo. DO NOT EDIT.\n")
		p.Files[module] = f

		// Import from unsafe & gengort.
		f.Decls = append(f.Decls, &dst.GenDecl{
			Tok: token.IMPORT,
			Specs: []dst.Spec{
				&dst.ImportSpec{
					Path: &dst.BasicLit{
						Kind:  token.STRING,
						Value: `"unsafe"`,
					},
				},
				&dst.ImportSpec{
					Path: &dst.BasicLit{
						Kind:  token.STRING,
						Value: `"github.com/can1357/gengo/gengort"`,
					},
				},
			},
		})

		// If this is the first module, add some runtime details.
		if len(p.Files) == 1 {
			// const GengoLibraryName = "module"
			f.Decls = append(f.Decls, &dst.GenDecl{
				Tok: token.CONST,
				Specs: []dst.Spec{
					&dst.ValueSpec{
						Names: []*dst.Ident{
							dst.NewIdent("GengoLibraryName"),
						},
						Values: []dst.Expr{
							&dst.BasicLit{Kind: token.STRING, Value: strconv.Quote(p.Name)},
						},
					},
				},
			})
			// var GengoLibrary = gengort.NewLibrary(GengoLibraryName)
			f.Decls = append(f.Decls, &dst.GenDecl{
				Tok: token.VAR,
				Specs: []dst.Spec{
					&dst.ValueSpec{
						Names: []*dst.Ident{
							dst.NewIdent("GengoLibrary"),
						},
						Values: []dst.Expr{
							&dst.CallExpr{
								Fun: &dst.SelectorExpr{
									X:   dst.NewIdent("gengort"),
									Sel: dst.NewIdent("NewLibrary"),
								},
								Args: []dst.Expr{
									dst.NewIdent("GengoLibraryName"),
								},
							},
						},
					},
				},
			})
		}
	}
	return Module{p, f}
}

type stdoutCloser struct{ io.Writer }

func (stdoutCloser) Close() error {
	fmt.Println()
	return nil
}
func (p *Package) Print() {
	p.Fprint(func(path string) (io.WriteCloser, error) {
		fmt.Println("//// ", path)
		return stdoutCloser{os.Stdout}, nil
	})
}
func (p *Package) WriteToDir(dir string) error {
	os.Mkdir(dir, 0755)
	return p.Fprint(func(path string) (io.WriteCloser, error) {
		return os.Create(filepath.Join(dir, path))
	})
}

type Module struct {
	Parent *Package
	*dst.File
}

func (m Module) AddType(tc TypeClass, name string, decl dst.Expr) TypeRef {
	ref := m.Parent.AddType(tc, name, decl)
	m.Decls = append(m.Decls, ref.Decl)
	return ref
}

func (m Module) Go() (*ast.File, error) {
	return m.Parent.Restorer.RestoreFile(m.File)
}
func (m Module) Fprint(w io.Writer) {
	m.Parent.Restorer.Fprint(w, m.File)
}
func (m Module) String() string {
	buf := &bytes.Buffer{}
	m.Fprint(buf)
	return buf.String()
}
func (m Module) Print() {
	m.Fprint(os.Stdout)
}
func (m Module) GetInitFunc() *dst.FuncDecl {
	for _, decl := range m.Decls {
		if f, ok := decl.(*dst.FuncDecl); ok && f.Name.Name == "init" {
			return f
		}
	}
	return nil
}
func (m Module) AddInitFunc() *dst.FuncDecl {
	i := m.GetInitFunc()
	if i == nil {
		i = &dst.FuncDecl{
			Name: dst.NewIdent("init"),
			Type: &dst.FuncType{},
			Body: &dst.BlockStmt{},
			Decs: dst.FuncDeclDecorations{
				NodeDecs: dst.NodeDecs{
					Before: dst.NewLine,
					After:  dst.NewLine,
					Start: []string{
						"//  Gengo init function.",
					},
				},
			},
		}
		m.Decls = append(m.Decls, i)
	}
	return i
}
func (m Module) OnInit(stmts ...dst.Stmt) {
	f := m.AddInitFunc()
	f.Body.List = append(f.Body.List, stmts...)
}
