// Copyright (c) 2017 Ernest Micklei
//
// MIT License
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

package proto

// Proto represents a .proto definition
type Proto struct {
	Filename string
	Elements []Visitee
	Imports  []Import
	Enums    []Enum
	Package  []Package
	Options  []Option
	Messages []Message
	Services []Service
	Extends  []Extensions
}

// Accept dispatches the call to the visitor.
func (proto *Proto) Accept(v Visitor) {
	// As Proto is not (yet) a Visitee, we enumerate its elements instead
	//v.VisitProto(proto)
	for _, each := range proto.Elements {
		each.Accept(v)
	}
}

// addElement is part of elementContainer
func (proto *Proto) addElement(v Visitee) {
	v.parent(proto)
	proto.Elements = append(proto.Elements, v)
}

// elements is part of elementContainer
func (proto *Proto) elements() []Visitee {
	return proto.Elements
}

// takeLastComment is part of elementContainer
// removes and returns the last element of the list if it is a Comment.
func (proto *Proto) takeLastComment(expectedOnLine int) (last *Comment) {
	last, proto.Elements = takeLastCommentIfEndsOnLine(proto.Elements, expectedOnLine)
	return
}

// parse parsers a complete .proto definition source.
func (proto *Proto) parse(p *Parser) (*Proto, error) {
	pro := new(Proto)
	for {
		pos, tok, lit := p.next()
		switch {
		case isComment(lit):
			if com := mergeOrReturnComment(proto.Elements, lit, pos); com != nil { // not merged?
				proto.Elements = append(proto.Elements, com)
			}
		case tOPTION == tok:
			o := new(Option)
			o.Position = pos
			o.Comment, proto.Elements = takeLastCommentIfEndsOnLine(proto.Elements, pos.Line-1)
			if err := o.parse(p); err != nil {
				return pro, err
			}
			pro.Options = append(pro.Options, *o)
			proto.addElement(o)
		case tSYNTAX == tok:
			s := new(Syntax)
			s.Position = pos
			s.Comment, proto.Elements = takeLastCommentIfEndsOnLine(proto.Elements, pos.Line-1)
			if err := s.parse(p); err != nil {
				return pro, err
			}
			proto.addElement(s)
		case tIMPORT == tok:
			im := new(Import)
			im.Position = pos
			im.Comment, proto.Elements = takeLastCommentIfEndsOnLine(proto.Elements, pos.Line-1)
			if err := im.parse(p); err != nil {
				return pro, err
			}
			pro.Imports = append(pro.Imports, *im)
			proto.addElement(im)
		case tENUM == tok:
			enum := new(Enum)
			enum.Position = pos
			enum.Comment, proto.Elements = takeLastCommentIfEndsOnLine(proto.Elements, pos.Line-1)
			if err := enum.parse(p); err != nil {
				return pro, err
			}
			pro.Enums = append(pro.Enums, *enum)
			proto.addElement(enum)
		case tSERVICE == tok:
			service := new(Service)
			service.Position = pos
			service.Comment, proto.Elements = takeLastCommentIfEndsOnLine(proto.Elements, pos.Line-1)
			err := service.parse(p)
			if err != nil {
				return pro, err
			}
			pro.Services = append(pro.Services, *service)
			proto.addElement(service)
		case tPACKAGE == tok:
			pkg := new(Package)
			pkg.Position = pos
			pkg.Comment, proto.Elements = takeLastCommentIfEndsOnLine(proto.Elements, pos.Line-1)
			if err := pkg.parse(p); err != nil {
				return pro, err
			}
			pro.Package = append(pro.Package, *pkg)
			proto.addElement(pkg)
		case tMESSAGE == tok:
			msg := new(Message)
			msg.Position = pos
			msg.Comment, proto.Elements = takeLastCommentIfEndsOnLine(proto.Elements, pos.Line-1)
			if err := msg.parse(p); err != nil {
				return pro, err
			}
			pro.Messages = append(pro.Messages, *msg)
			proto.addElement(msg)
			// BEGIN proto2
		case tEXTEND == tok:
			msg := new(Message)
			msg.Position = pos
			msg.Comment, proto.Elements = takeLastCommentIfEndsOnLine(proto.Elements, pos.Line-1)
			msg.IsExtend = true
			if err := msg.parse(p); err != nil {
				return pro, err
			}
			proto.addElement(msg)
			// END proto2
		case tSEMICOLON == tok:
			maybeScanInlineComment(p, proto)
			// continue
		case tEOF == tok:
			goto done
		default:
			return pro, p.unexpected(lit, ".proto element {comment|option|import|syntax|enum|service|package|message}", p)
		}
	}
done:
	return pro, nil
}

func (proto *Proto) parent(v Visitee) {}

// elementContainer unifies types that have elements.
type elementContainer interface {
	addElement(v Visitee)
	elements() []Visitee
	takeLastComment(expectedOnLine int) *Comment
}
