// Copyright 2017 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 (
	"go/ast"
	"go/token"
	"reflect"
	"strings"
)

func init() {
	register(cftypeFix)
}

var cftypeFix = fix{
	name:     "cftype",
	date:     "2017-09-27",
	f:        cftypefix,
	desc:     `Fixes initializers and casts of C.*Ref and JNI types`,
	disabled: false,
}

// Old state:
//   type CFTypeRef unsafe.Pointer
// New state:
//   type CFTypeRef uintptr
// and similar for other *Ref types.
// This fix finds nils initializing these types and replaces the nils with 0s.
func cftypefix(f *ast.File) bool {
	return typefix(f, func(s string) bool {
		return strings.HasPrefix(s, "C.") && strings.HasSuffix(s, "Ref") && s != "C.CFAllocatorRef"
	})
}

// typefix replaces nil with 0 for all nils whose type, when passed to badType, returns true.
func typefix(f *ast.File, badType func(string) bool) bool {
	if !imports(f, "C") {
		return false
	}
	typeof, _ := typecheck(&TypeConfig{}, f)
	changed := false

	// step 1: Find all the nils with the offending types.
	// Compute their replacement.
	badNils := map[any]ast.Expr{}
	walk(f, func(n any) {
		if i, ok := n.(*ast.Ident); ok && i.Name == "nil" && badType(typeof[n]) {
			badNils[n] = &ast.BasicLit{ValuePos: i.NamePos, Kind: token.INT, Value: "0"}
		}
	})

	// step 2: find all uses of the bad nils, replace them with 0.
	// There's no easy way to map from an ast.Expr to all the places that use them, so
	// we use reflect to find all such references.
	if len(badNils) > 0 {
		exprType := reflect.TypeOf((*ast.Expr)(nil)).Elem()
		exprSliceType := reflect.TypeOf(([]ast.Expr)(nil))
		walk(f, func(n any) {
			if n == nil {
				return
			}
			v := reflect.ValueOf(n)
			if v.Type().Kind() != reflect.Pointer {
				return
			}
			if v.IsNil() {
				return
			}
			v = v.Elem()
			if v.Type().Kind() != reflect.Struct {
				return
			}
			for i := 0; i < v.NumField(); i++ {
				f := v.Field(i)
				if f.Type() == exprType {
					if r := badNils[f.Interface()]; r != nil {
						f.Set(reflect.ValueOf(r))
						changed = true
					}
				}
				if f.Type() == exprSliceType {
					for j := 0; j < f.Len(); j++ {
						e := f.Index(j)
						if r := badNils[e.Interface()]; r != nil {
							e.Set(reflect.ValueOf(r))
							changed = true
						}
					}
				}
			}
		})
	}

	// step 3: fix up invalid casts.
	// It used to be ok to cast between *unsafe.Pointer and *C.CFTypeRef in a single step.
	// Now we need unsafe.Pointer as an intermediate cast.
	// (*unsafe.Pointer)(x) where x is type *bad -> (*unsafe.Pointer)(unsafe.Pointer(x))
	// (*bad.type)(x) where x is type *unsafe.Pointer -> (*bad.type)(unsafe.Pointer(x))
	walk(f, func(n any) {
		if n == nil {
			return
		}
		// Find pattern like (*a.b)(x)
		c, ok := n.(*ast.CallExpr)
		if !ok {
			return
		}
		if len(c.Args) != 1 {
			return
		}
		p, ok := c.Fun.(*ast.ParenExpr)
		if !ok {
			return
		}
		s, ok := p.X.(*ast.StarExpr)
		if !ok {
			return
		}
		t, ok := s.X.(*ast.SelectorExpr)
		if !ok {
			return
		}
		pkg, ok := t.X.(*ast.Ident)
		if !ok {
			return
		}
		dst := pkg.Name + "." + t.Sel.Name
		src := typeof[c.Args[0]]
		if badType(dst) && src == "*unsafe.Pointer" ||
			dst == "unsafe.Pointer" && strings.HasPrefix(src, "*") && badType(src[1:]) {
			c.Args[0] = &ast.CallExpr{
				Fun:  &ast.SelectorExpr{X: &ast.Ident{Name: "unsafe"}, Sel: &ast.Ident{Name: "Pointer"}},
				Args: []ast.Expr{c.Args[0]},
			}
			changed = true
		}
	})

	return changed
}
