// Copyright 2011 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"
	"strings"
)

func init() {
	register(timefileinfoFix)
}

var timefileinfoFix = fix{
	"time+fileinfo",
	"2011-11-29",
	timefileinfo,
	`Rewrite for new time and os.FileInfo APIs.

This fix applies some of the more mechanical changes,
but most code will still need manual cleanup.

http://codereview.appspot.com/5392041
http://codereview.appspot.com/5416060
`,
}

var timefileinfoTypeConfig = &TypeConfig{
	Type: map[string]*Type{
		"os.File": {
			Method: map[string]string{
				"Readdir": "func() []*os.FileInfo",
				"Stat":    "func() (*os.FileInfo, error)",
			},
		},
		"time.Time": {
			Method: map[string]string{
				"Seconds":     "time.raw",
				"Nanoseconds": "time.raw",
			},
		},
	},
	Func: map[string]string{
		"ioutil.ReadDir":              "([]*os.FileInfo, error)",
		"os.Stat":                     "(*os.FileInfo, error)",
		"os.Lstat":                    "(*os.FileInfo, error)",
		"time.LocalTime":              "*time.Time",
		"time.UTC":                    "*time.Time",
		"time.SecondsToLocalTime":     "*time.Time",
		"time.SecondsToUTC":           "*time.Time",
		"time.NanosecondsToLocalTime": "*time.Time",
		"time.NanosecondsToUTC":       "*time.Time",
		"time.Parse":                  "(*time.Time, error)",
		"time.Nanoseconds":            "time.raw",
		"time.Seconds":                "time.raw",
	},
}

// timefileinfoIsOld reports whether f has evidence of being
// "old code", from before the API changes.  Evidence means:
//
//	a mention of *os.FileInfo (the pointer)
//	a mention of *time.Time (the pointer)
//	a mention of old functions from package time
//	an attempt to call time.UTC
//
func timefileinfoIsOld(f *ast.File, typeof map[interface{}]string) bool {
	old := false

	// called records the expressions that appear as
	// the function part of a function call, so that
	// we can distinguish a ref to the possibly new time.UTC
	// from the definitely old time.UTC() function call.
	called := make(map[interface{}]bool)

	before := func(n interface{}) {
		if old {
			return
		}
		if star, ok := n.(*ast.StarExpr); ok {
			if isPkgDot(star.X, "os", "FileInfo") || isPkgDot(star.X, "time", "Time") {
				old = true
				return
			}
		}
		if sel, ok := n.(*ast.SelectorExpr); ok {
			if isTopName(sel.X, "time") {
				if timefileinfoOldTimeFunc[sel.Sel.Name] {
					old = true
					return
				}
			}
			if typeof[sel.X] == "os.FileInfo" || typeof[sel.X] == "*os.FileInfo" {
				switch sel.Sel.Name {
				case "Mtime_ns", "IsDirectory", "IsRegular":
					old = true
					return
				case "Name", "Mode", "Size":
					if !called[sel] {
						old = true
						return
					}
				}
			}
		}
		call, ok := n.(*ast.CallExpr)
		if ok && isPkgDot(call.Fun, "time", "UTC") {
			old = true
			return
		}
		if ok {
			called[call.Fun] = true
		}
	}
	walkBeforeAfter(f, before, nop)
	return old
}

var timefileinfoOldTimeFunc = map[string]bool{
	"LocalTime":              true,
	"SecondsToLocalTime":     true,
	"SecondsToUTC":           true,
	"NanosecondsToLocalTime": true,
	"NanosecondsToUTC":       true,
	"Seconds":                true,
	"Nanoseconds":            true,
}

var isTimeNow = map[string]bool{
	"LocalTime":   true,
	"UTC":         true,
	"Seconds":     true,
	"Nanoseconds": true,
}

func timefileinfo(f *ast.File) bool {
	if !imports(f, "os") && !imports(f, "time") && !imports(f, "io/ioutil") {
		return false
	}

	typeof, _ := typecheck(timefileinfoTypeConfig, f)

	if !timefileinfoIsOld(f, typeof) {
		return false
	}

	fixed := false
	walk(f, func(n interface{}) {
		p, ok := n.(*ast.Expr)
		if !ok {
			return
		}
		nn := *p

		// Rewrite *os.FileInfo and *time.Time to drop the pointer.
		if star, ok := nn.(*ast.StarExpr); ok {
			if isPkgDot(star.X, "os", "FileInfo") || isPkgDot(star.X, "time", "Time") {
				fixed = true
				*p = star.X
				return
			}
		}

		// Rewrite old time API calls to new calls.
		// The code will still not compile after this edit,
		// but the compiler will catch that, and the replacement
		// code will be the correct functions to use in the new API.
		if sel, ok := nn.(*ast.SelectorExpr); ok && isTopName(sel.X, "time") {
			fn := sel.Sel.Name
			if fn == "LocalTime" || fn == "Seconds" || fn == "Nanoseconds" {
				fixed = true
				sel.Sel.Name = "Now"
				return
			}
		}

		if call, ok := nn.(*ast.CallExpr); ok {
			if sel, ok := call.Fun.(*ast.SelectorExpr); ok {
				// Rewrite time.UTC but only when called (there's a new time.UTC var now).
				if isPkgDot(sel, "time", "UTC") {
					fixed = true
					sel.Sel.Name = "Now"
					// rewrite time.Now() into time.Now().UTC()
					*p = &ast.CallExpr{
						Fun: &ast.SelectorExpr{
							X:   call,
							Sel: ast.NewIdent("UTC"),
						},
					}
					return
				}

				// Rewrite conversions.
				if ok && isTopName(sel.X, "time") && len(call.Args) == 1 {
					fn := sel.Sel.Name
					switch fn {
					case "SecondsToLocalTime", "SecondsToUTC",
						"NanosecondsToLocalTime", "NanosecondsToUTC":
						fixed = true
						sel.Sel.Name = "Unix"
						call.Args = append(call.Args, nil)
						if strings.HasPrefix(fn, "Seconds") {
							// Unix(sec, 0)
							call.Args[1] = ast.NewIdent("0")
						} else {
							// Unix(0, nsec)
							call.Args[1] = call.Args[0]
							call.Args[0] = ast.NewIdent("0")
						}
						if strings.HasSuffix(fn, "ToUTC") {
							// rewrite call into call.UTC()
							*p = &ast.CallExpr{
								Fun: &ast.SelectorExpr{
									X:   call,
									Sel: ast.NewIdent("UTC"),
								},
							}
						}
						return
					}
				}

				// Rewrite method calls.
				switch typeof[sel.X] {
				case "*time.Time", "time.Time":
					switch sel.Sel.Name {
					case "Seconds":
						fixed = true
						sel.Sel.Name = "Unix"
						return
					case "Nanoseconds":
						fixed = true
						sel.Sel.Name = "UnixNano"
						return
					}

				case "*os.FileInfo", "os.FileInfo":
					switch sel.Sel.Name {
					case "IsDirectory":
						fixed = true
						sel.Sel.Name = "IsDir"
						return
					case "IsRegular":
						fixed = true
						sel.Sel.Name = "IsDir"
						*p = &ast.UnaryExpr{
							Op: token.NOT,
							X:  call,
						}
						return
					}
				}
			}
		}

		// Rewrite subtraction of two times.
		// Cannot handle +=/-=.
		if bin, ok := nn.(*ast.BinaryExpr); ok &&
			bin.Op == token.SUB &&
			(typeof[bin.X] == "time.raw" || typeof[bin.Y] == "time.raw") {
			fixed = true
			*p = &ast.CallExpr{
				Fun: &ast.SelectorExpr{
					X:   bin.X,
					Sel: ast.NewIdent("Sub"),
				},
				Args: []ast.Expr{bin.Y},
			}
		}

		// Rewrite field references for os.FileInfo.
		if sel, ok := nn.(*ast.SelectorExpr); ok {
			if typ := typeof[sel.X]; typ == "*os.FileInfo" || typ == "os.FileInfo" {
				addCall := false
				switch sel.Sel.Name {
				case "Name", "Size", "Mode":
					fixed = true
					addCall = true
				case "Mtime_ns":
					fixed = true
					sel.Sel.Name = "ModTime"
					addCall = true
				}
				if addCall {
					*p = &ast.CallExpr{
						Fun: sel,
					}
					return
				}
			}
		}
	})

	return true
}
