// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package repl

import (
	"errors"
	"fmt"
	"strings"

	antlr "github.com/antlr4-go/antlr/v4"

	"github.com/google/cel-go/common/env"
	"github.com/google/cel-go/repl/parser"
)

var (
	compileUsage = `Compile emits a textproto representation of the compiled expression.
%compile <expr>`

	parseUsage = `Parse emits a textproto representation of the parsed expression.
%parse <expr>`

	declareUsage = `Declare introduces a variable or function for type checking, but
doesn't define a value for it:
%declare <identifier> : <type>
%declare <identifier> (<param_identifier> : <param_type>, ...) : <result-type>`

	deleteUsage = `Delete removes a variable or function declaration from the evaluation context.
%delete <identifier>`

	letUsage = `Let introduces a variable or function defined by a sub-CEL expression.
%let <identifier> (: <type>)? = <expr>
%let <identifier> (<param_identifier> : <param_type>, ...) : <result-type> -> <expr>`

	optionUsage = `Option enables a CEL environment option which enables configuration and
optional language features.
%option --container 'google.protobuf'
%option --extension 'all'`

	loadDescriptorsUsage = `LoadDescriptors loads a protobuf descriptor file (google.protobuf.FileDescriptorSet)
from disk or from a predefined package. Supported packages are "cel-spec-test-types"
(TestAllTypes) and "google-rpc" (AttributeContext).
%load_descriptors 'path/to/descriptor_set.binarypb'
%load_descriptors --pkg 'cel-spec-test-types'`

	exitUsage = `Exit terminates the REPL.
%exit`

	helpUsage = `Help prints usage information for the commands supported by the REPL.
%help`

	statusUsage = `Status prints the current state of the REPL session
%status
%status --yaml`

	configUsage = `Config loads a canned REPL state from a config file
%configure """%let foo : int = 42"""
%configure --yaml --file 'path/to/env.yaml'`
)

type letVarCmd struct {
	identifier string
	typeHint   *env.TypeDesc
	src        string
}

type letFnCmd struct {
	identifier string
	resultType *env.TypeDesc
	params     []letFunctionParam
	src        string
}

type delCmd struct {
	identifier string
}

type simpleCmd struct {
	cmd  string
	args []string
}

type compileCmd struct {
	expr string
}

type parseCmd struct {
	expr string
}

type evalCmd struct {
	parseOnly bool
	expr      string
}

// Cmder interface provides normalized command name from a repl command.
// Command specifics are available via checked type casting to the specific
// command type.
type Cmder interface {
	// Cmd returns the normalized name for the command.
	Cmd() string
}

func (c *letVarCmd) Cmd() string {
	if c.src == "" {
		return "declare"
	}
	return "let"
}

func (c *letFnCmd) Cmd() string {
	if c.src == "" {
		return "declare"
	}
	return "let"
}

func (c *delCmd) Cmd() string {
	return "delete"
}

func (c *simpleCmd) Cmd() string {
	return c.cmd
}

func (c *compileCmd) Cmd() string {
	return "compile"
}

func (c *parseCmd) Cmd() string {
	return "parse"
}

func (c *evalCmd) Cmd() string {
	return "eval"
}

type commandParseListener struct {
	antlr.DefaultErrorListener
	parser.BaseCommandsListener

	errs  []error
	cmd   Cmder
	usage string
}

func (c *commandParseListener) reportIssue(e error) {
	c.errs = append(c.errs, e)
}

// extractSourceText extracts original text from a parse rule match.
// Preserves original whitespace if possible.
func extractSourceText(ctx antlr.ParserRuleContext) string {
	if ctx.GetStart() == nil || ctx.GetStop() == nil ||
		ctx.GetStart().GetStart() < 0 || ctx.GetStop().GetStop() < 0 {
		// fallback to the normalized parse
		return ctx.GetText()
	}
	s, e := ctx.GetStart().GetStart(), ctx.GetStop().GetStop()
	return ctx.GetStart().GetInputStream().GetText(s, e)
}

// Parse parses a repl command line into a command object. This provides
// the normalized command name plus any parsed parameters (e.g. variable names
// in let statements).
//
// An error is returned if the statement isn't well formed. See the parser
// pacakage for details on the antlr grammar.
func Parse(line string) (Cmder, error) {
	line = strings.TrimSpace(line)
	listener := &commandParseListener{}
	is := antlr.NewInputStream(line)
	lexer := parser.NewCommandsLexer(is)
	lexer.RemoveErrorListeners()
	lexer.AddErrorListener(listener)
	p := parser.NewCommandsParser(antlr.NewCommonTokenStream(lexer, antlr.TokenDefaultChannel))
	p.RemoveErrorListeners()
	p.AddErrorListener(listener)

	antlr.ParseTreeWalkerDefault.Walk(listener, p.StartCommand())

	if len(listener.errs) > 0 {
		errFmt := make([]string, len(listener.errs))
		for i, err := range listener.errs {
			errFmt[i] = err.Error()
		}

		if listener.usage != "" {
			errFmt = append(errFmt, "", "Usage:", listener.usage)
		}
		return nil, fmt.Errorf("invalid command: %v", strings.Join(errFmt, "\n"))
	}
	if listener.cmd.Cmd() == "help" {
		return nil, errors.New(strings.Join([]string{
			compileUsage,
			parseUsage,
			declareUsage,
			deleteUsage,
			letUsage,
			statusUsage,
			configUsage,
			optionUsage,
			loadDescriptorsUsage,
			helpUsage,
			exitUsage,
		}, "\n\n"))
	}
	return listener.cmd, nil
}

// ANTLR interface implementations

// Implement antlr ErrorListener interface for syntax errors.
func (c *commandParseListener) SyntaxError(recognizer antlr.Recognizer, offendingSymbol any, line, column int, msg string, e antlr.RecognitionException) {
	c.errs = append(c.errs, fmt.Errorf("(%d:%d) %s", line, column, msg))
}

// Implement ANTLR interface for commands listener.
func (c *commandParseListener) EnterSimple(ctx *parser.SimpleContext) {
	cmd := "undefined"
	if ctx.GetCmd() != nil {
		cmd = ctx.GetCmd().GetText()[1:]
	}
	var args []string
	for _, arg := range ctx.GetArgs() {
		a := arg.GetText()
		if strings.HasPrefix(a, "-") {
			a = "--" + strings.ToLower(strings.TrimLeft(a, "-"))
		} else {
			a = strings.Trim(a, "\"'")
		}
		args = append(args, a)

	}
	c.cmd = &simpleCmd{cmd: cmd, args: args}
}

func (c *commandParseListener) EnterHelp(ctx *parser.HelpContext) {
	c.cmd = &simpleCmd{cmd: "help"}
}

func (c *commandParseListener) EnterEmpty(ctx *parser.EmptyContext) {
	c.cmd = &simpleCmd{cmd: "null"}
}

func (c *commandParseListener) EnterLet(ctx *parser.LetContext) {
	c.usage = letUsage
	if ctx.GetFn() != nil {
		c.cmd = &letFnCmd{}
	} else if ctx.GetVar_() != nil {
		c.cmd = &letVarCmd{}
	} else {
		c.errs = append(c.errs, fmt.Errorf("missing declaration in let"))
	}
}

func (c *commandParseListener) EnterDeclare(ctx *parser.DeclareContext) {
	c.usage = declareUsage
	if ctx.GetFn() != nil {
		c.cmd = &letFnCmd{}
	} else if ctx.GetVar_() != nil {
		c.cmd = &letVarCmd{}
	} else {
		c.errs = append(c.errs, fmt.Errorf("missing declaration in declare"))
	}
}

func (c *commandParseListener) ExitDeclare(ctx *parser.DeclareContext) {
	var typeHint *env.TypeDesc
	switch cmd := c.cmd.(type) {
	case *letVarCmd:
		typeHint = cmd.typeHint
	case *letFnCmd:
		typeHint = cmd.resultType
	}
	if typeHint == nil {
		c.reportIssue(errors.New("result type required for declare"))
	}
}

func (c *commandParseListener) EnterDelete(ctx *parser.DeleteContext) {
	c.usage = deleteUsage
	if ctx.GetVar_() == nil && ctx.GetFn() == nil {
		c.reportIssue(errors.New("missing identifier in delete"))
		return
	}
	c.cmd = &delCmd{}
}

func (c *commandParseListener) EnterCompile(ctx *parser.CompileContext) {
	c.cmd = &compileCmd{}
}

func (c *commandParseListener) EnterParse(ctx *parser.ParseContext) {
	c.cmd = &parseCmd{}
}

func (c *commandParseListener) EnterExprCmd(ctx *parser.ExprCmdContext) {
	cmd := &evalCmd{}
	for _, f := range ctx.GetFlags() {
		ft := strings.TrimPrefix(strings.TrimPrefix(f.GetText(), "--"), "-")
		switch ft {
		case "parse-only":
			cmd.parseOnly = true
		default:
			c.reportIssue(fmt.Errorf("unknown or unsupported flag: %q", ft))
			return
		}
	}
	c.cmd = cmd
}

func (c *commandParseListener) ExitFnDecl(ctx *parser.FnDeclContext) {
	switch cmd := c.cmd.(type) {
	case *letFnCmd:
		if ctx.GetId() == nil {
			c.reportIssue(errors.New("missing identifier in function declaration"))
			return
		}
		if ctx.GetRType() == nil {
			c.reportIssue(errors.New("missing result type in function declaration"))
			return
		}
		cmd.identifier = ctx.GetId().GetText()
		ty, err := ParseType(ctx.GetRType().GetText())
		if err != nil {
			c.reportIssue(err)
		}
		for _, p := range ctx.GetParams() {
			if p.GetT() == nil {
				c.reportIssue(errors.New("missing type in function param declaration"))
				continue
			}
			if p.GetPid() == nil {
				c.reportIssue(errors.New("missing identifier in function param declaration"))
			}
			ty, err := ParseType(p.GetT().GetText())
			if err != nil {
				c.reportIssue(err)
			}
			cmd.params = append(cmd.params, letFunctionParam{
				identifier: p.GetPid().GetText(),
				typeHint:   ty,
			})

		}
		cmd.resultType = ty
	case *delCmd:
		if ctx.GetId() == nil {
			c.reportIssue(errors.New("missing identifier in delete"))
		}
		cmd.identifier = ctx.GetId().GetText()
	default:
		c.reportIssue(errors.New("unexepected function declaration"))
	}
}

func (c *commandParseListener) ExitQualId(ctx *parser.QualIdContext) {
	if ctx.GetRid() == nil {
		c.reportIssue(errors.New("missing root identifier"))
	}
}

func (c *commandParseListener) ExitVarDecl(ctx *parser.VarDeclContext) {
	switch cmd := c.cmd.(type) {
	case *letVarCmd:
		if ctx.GetId() == nil {
			c.reportIssue(errors.New("no identifier in variable declaration"))
			return
		}
		cmd.identifier = ctx.GetId().GetText()
		if ctx.GetT() != nil {
			ty, err := ParseType(ctx.GetT().GetText())
			if err != nil {
				c.reportIssue(err)
			}
			cmd.typeHint = ty
		}
	case *delCmd:
		if ctx.GetId() == nil {
			c.reportIssue(errors.New("missing identifier in delete"))
		}
		cmd.identifier = ctx.GetId().GetText()
	default:
		c.reportIssue(errors.New("unexpected var declaration"))
	}
}

func (c *commandParseListener) ExitExpr(ctx *parser.ExprContext) {
	expr := extractSourceText(ctx)
	switch cmd := c.cmd.(type) {
	case *compileCmd:
		cmd.expr = expr
	case *parseCmd:
		cmd.expr = expr
	case *evalCmd:
		cmd.expr = expr
	case *letFnCmd:
		cmd.src = expr
	case *letVarCmd:
		cmd.src = expr
	default:
		c.reportIssue(errors.New("unexpected CEL expression"))
	}
}
