// Copyright (c) 2013, Vastech SA (PTY) LTD. All rights reserved.
// http://github.com/gogo/protobuf
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
//     * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

/*
The stringer plugin generates a String method for each message.

It is enabled by the following extensions:

  - stringer
  - stringer_all

The stringer plugin also generates a test given it is enabled using one of the following extensions:

  - testgen
  - testgen_all

Let us look at:

  github.com/gogo/protobuf/test/example/example.proto

Btw all the output can be seen at:

  github.com/gogo/protobuf/test/example/*

The following message:

  option (gogoproto.goproto_stringer_all) = false;
  option (gogoproto.stringer_all) =  true;

  message A {
	optional string Description = 1 [(gogoproto.nullable) = false];
	optional int64 Number = 2 [(gogoproto.nullable) = false];
	optional bytes Id = 3 [(gogoproto.customtype) = "github.com/gogo/protobuf/test/custom.Uuid", (gogoproto.nullable) = false];
  }

given to the stringer stringer, will generate the following code:

  func (this *A) String() string {
	if this == nil {
		return "nil"
	}
	s := strings.Join([]string{`&A{`,
		`Description:` + fmt.Sprintf("%v", this.Description) + `,`,
		`Number:` + fmt.Sprintf("%v", this.Number) + `,`,
		`Id:` + fmt.Sprintf("%v", this.Id) + `,`,
		`XXX_unrecognized:` + fmt.Sprintf("%v", this.XXX_unrecognized) + `,`,
		`}`,
	}, "")
	return s
  }

and the following test code:

	func TestAStringer(t *testing4.T) {
		popr := math_rand4.New(math_rand4.NewSource(time4.Now().UnixNano()))
		p := NewPopulatedA(popr, false)
		s1 := p.String()
		s2 := fmt1.Sprintf("%v", p)
		if s1 != s2 {
			t.Fatalf("String want %v got %v", s1, s2)
		}
	}

Typically fmt.Printf("%v") will stop to print when it reaches a pointer and
not print their values, while the generated String method will always print all values, recursively.

*/
package stringer

import (
	"github.com/gogo/protobuf/gogoproto"
	"github.com/gogo/protobuf/protoc-gen-gogo/generator"
	"strings"
)

type stringer struct {
	*generator.Generator
	generator.PluginImports
	atleastOne bool
	localName  string
}

func NewStringer() *stringer {
	return &stringer{}
}

func (p *stringer) Name() string {
	return "stringer"
}

func (p *stringer) Init(g *generator.Generator) {
	p.Generator = g
}

func (p *stringer) Generate(file *generator.FileDescriptor) {
	proto3 := gogoproto.IsProto3(file.FileDescriptorProto)
	p.PluginImports = generator.NewPluginImports(p.Generator)
	p.atleastOne = false

	p.localName = generator.FileName(file)

	fmtPkg := p.NewImport("fmt")
	stringsPkg := p.NewImport("strings")
	reflectPkg := p.NewImport("reflect")
	sortKeysPkg := p.NewImport("github.com/gogo/protobuf/sortkeys")
	for _, message := range file.Messages() {
		if !gogoproto.IsStringer(file.FileDescriptorProto, message.DescriptorProto) {
			continue
		}
		if gogoproto.EnabledGoStringer(file.FileDescriptorProto, message.DescriptorProto) {
			panic("old string method needs to be disabled, please use gogoproto.goproto_stringer or gogoproto.goproto_stringer_all and set it to false")
		}
		if message.DescriptorProto.GetOptions().GetMapEntry() {
			continue
		}
		p.atleastOne = true
		ccTypeName := generator.CamelCaseSlice(message.TypeName())
		p.P(`func (this *`, ccTypeName, `) String() string {`)
		p.In()
		p.P(`if this == nil {`)
		p.In()
		p.P(`return "nil"`)
		p.Out()
		p.P(`}`)
		for _, field := range message.Field {
			if !generator.IsMap(file.FileDescriptorProto, field) {
				continue
			}
			fieldname := p.GetFieldName(message, field)
			mapMsg := generator.GetMap(file.FileDescriptorProto, field)
			keyField, valueField := mapMsg.GetMapFields()
			keysName := `keysFor` + fieldname
			keygoTyp, _ := p.GoType(nil, keyField)
			keygoTyp = strings.Replace(keygoTyp, "*", "", 1)
			keyCapTyp := generator.CamelCase(keygoTyp)
			valuegoTyp, _ := p.GoType(nil, valueField)
			p.P(keysName, ` := make([]`, keygoTyp, `, 0, len(this.`, fieldname, `))`)
			p.P(`for k, _ := range this.`, fieldname, ` {`)
			p.In()
			p.P(keysName, ` = append(`, keysName, `, k)`)
			p.Out()
			p.P(`}`)
			p.P(sortKeysPkg.Use(), `.`, keyCapTyp, `s(`, keysName, `)`)
			mapName := `mapStringFor` + fieldname
			p.P(mapName, ` := "map[`, keygoTyp, `]`, valuegoTyp, `{"`)
			p.P(`for _, k := range `, keysName, ` {`)
			p.In()
			p.P(mapName, ` += fmt.Sprintf("%v: %v,", k, this.`, fieldname, `[k])`)
			p.Out()
			p.P(`}`)
			p.P(mapName, ` += "}"`)
		}
		p.P("s := ", stringsPkg.Use(), ".Join([]string{`&", ccTypeName, "{`,")
		oneofs := make(map[string]struct{})
		for _, field := range message.Field {
			nullable := gogoproto.IsNullable(field)
			repeated := field.IsRepeated()
			fieldname := p.GetFieldName(message, field)
			oneof := field.OneofIndex != nil
			if oneof {
				if _, ok := oneofs[fieldname]; ok {
					continue
				} else {
					oneofs[fieldname] = struct{}{}
				}
				p.P("`", fieldname, ":`", ` + `, fmtPkg.Use(), `.Sprintf("%v", this.`, fieldname, ") + `,", "`,")
			} else if generator.IsMap(file.FileDescriptorProto, field) {
				mapName := `mapStringFor` + fieldname
				p.P("`", fieldname, ":`", ` + `, mapName, " + `,", "`,")
			} else if field.IsMessage() || p.IsGroup(field) {
				desc := p.ObjectNamed(field.GetTypeName())
				msgname := p.TypeName(desc)
				msgnames := strings.Split(msgname, ".")
				typeName := msgnames[len(msgnames)-1]
				if nullable {
					p.P("`", fieldname, ":`", ` + `, stringsPkg.Use(), `.Replace(`, fmtPkg.Use(), `.Sprintf("%v", this.`, fieldname, `), "`, typeName, `","`, msgname, `"`, ", 1) + `,", "`,")
				} else if repeated {
					p.P("`", fieldname, ":`", ` + `, stringsPkg.Use(), `.Replace(`, stringsPkg.Use(), `.Replace(`, fmtPkg.Use(), `.Sprintf("%v", this.`, fieldname, `), "`, typeName, `","`, msgname, `"`, ", 1),`&`,``,1) + `,", "`,")
				} else {
					p.P("`", fieldname, ":`", ` + `, stringsPkg.Use(), `.Replace(`, stringsPkg.Use(), `.Replace(this.`, fieldname, `.String(), "`, typeName, `","`, msgname, `"`, ", 1),`&`,``,1) + `,", "`,")
				}
			} else {
				if nullable && !repeated && !proto3 {
					p.P("`", fieldname, ":`", ` + valueToString`, p.localName, `(this.`, fieldname, ") + `,", "`,")
				} else {
					p.P("`", fieldname, ":`", ` + `, fmtPkg.Use(), `.Sprintf("%v", this.`, fieldname, ") + `,", "`,")
				}
			}
		}
		if message.DescriptorProto.HasExtension() {
			if gogoproto.HasExtensionsMap(file.FileDescriptorProto, message.DescriptorProto) {
				p.P("`XXX_extensions:` + proto.StringFromExtensionsMap(this.XXX_extensions) + `,`,")
			} else {
				p.P("`XXX_extensions:` + proto.StringFromExtensionsBytes(this.XXX_extensions) + `,`,")
			}
		}
		if gogoproto.HasUnrecognized(file.FileDescriptorProto, message.DescriptorProto) {
			p.P("`XXX_unrecognized:` + ", fmtPkg.Use(), `.Sprintf("%v", this.XXX_unrecognized) + `, "`,`,")
		}
		p.P("`}`,")
		p.P(`}`, `,""`, ")")
		p.P(`return s`)
		p.Out()
		p.P(`}`)

		//Generate String methods for oneof fields
		for _, field := range message.Field {
			oneof := field.OneofIndex != nil
			if !oneof {
				continue
			}
			ccTypeName := p.OneOfTypeName(message, field)
			p.P(`func (this *`, ccTypeName, `) String() string {`)
			p.In()
			p.P(`if this == nil {`)
			p.In()
			p.P(`return "nil"`)
			p.Out()
			p.P(`}`)
			p.P("s := ", stringsPkg.Use(), ".Join([]string{`&", ccTypeName, "{`,")
			fieldname := p.GetOneOfFieldName(message, field)
			if field.IsMessage() || p.IsGroup(field) {
				desc := p.ObjectNamed(field.GetTypeName())
				msgname := p.TypeName(desc)
				msgnames := strings.Split(msgname, ".")
				typeName := msgnames[len(msgnames)-1]
				p.P("`", fieldname, ":`", ` + `, stringsPkg.Use(), `.Replace(`, fmtPkg.Use(), `.Sprintf("%v", this.`, fieldname, `), "`, typeName, `","`, msgname, `"`, ", 1) + `,", "`,")
			} else {
				p.P("`", fieldname, ":`", ` + `, fmtPkg.Use(), `.Sprintf("%v", this.`, fieldname, ") + `,", "`,")
			}
			p.P("`}`,")
			p.P(`}`, `,""`, ")")
			p.P(`return s`)
			p.Out()
			p.P(`}`)
		}
	}

	if !p.atleastOne {
		return
	}

	p.P(`func valueToString`, p.localName, `(v interface{}) string {`)
	p.In()
	p.P(`rv := `, reflectPkg.Use(), `.ValueOf(v)`)
	p.P(`if rv.IsNil() {`)
	p.In()
	p.P(`return "nil"`)
	p.Out()
	p.P(`}`)
	p.P(`pv := `, reflectPkg.Use(), `.Indirect(rv).Interface()`)
	p.P(`return `, fmtPkg.Use(), `.Sprintf("*%v", pv)`)
	p.Out()
	p.P(`}`)

}

func init() {
	generator.RegisterPlugin(NewStringer())
}
