// Copyright 2022 Molecula Corp. All rights reserved.

package planner

import (
	"context"
	"fmt"

	"github.com/featurebasedb/featurebase/v3/sql3"
	"github.com/featurebasedb/featurebase/v3/sql3/planner/types"
)

// PlanOpProjection handles row projection and expression evaluation
type PlanOpProjection struct {
	ChildOp     types.PlanOperator
	Projections []types.PlanExpression
	warnings    []string
}

func NewPlanOpProjection(expressions []types.PlanExpression, child types.PlanOperator) *PlanOpProjection {
	return &PlanOpProjection{
		ChildOp:     child,
		Projections: expressions,
		warnings:    make([]string, 0),
	}
}

func (p *PlanOpProjection) Schema() types.Schema {
	var s = make(types.Schema, len(p.Projections))
	for i, e := range p.Projections {
		s[i] = ExpressionToColumn(e)
	}
	return s
}

func (p *PlanOpProjection) Iterator(ctx context.Context, row types.Row) (types.RowIterator, error) {
	i, err := p.ChildOp.Iterator(ctx, row)
	if err != nil {
		return nil, err
	}
	return &iter{
		p:         p,
		childIter: i,
		row:       row,
	}, nil
}

func (p *PlanOpProjection) Children() []types.PlanOperator {
	return []types.PlanOperator{
		p.ChildOp,
	}
}

func (p *PlanOpProjection) WithChildren(children ...types.PlanOperator) (types.PlanOperator, error) {
	if len(children) != 1 {
		return nil, sql3.NewErrInternalf("unexpected number of children '%d'", len(children))
	}
	return NewPlanOpProjection(p.Projections, children[0]), nil
}

func (p *PlanOpProjection) Plan() map[string]interface{} {
	result := make(map[string]interface{})
	result["_op"] = fmt.Sprintf("%T", p)
	result["_schema"] = p.Schema().Plan()

	result["child"] = p.ChildOp.Plan()

	ps := make([]interface{}, 0)
	for _, e := range p.Projections {
		ps = append(ps, e.Plan())
	}
	result["projections"] = ps

	return result
}

func (p *PlanOpProjection) String() string {
	return ""
}

func (p *PlanOpProjection) AddWarning(warning string) {
	p.warnings = append(p.warnings, warning)
}

func (p *PlanOpProjection) Warnings() []string {
	var w []string
	w = append(w, p.warnings...)
	if p.ChildOp != nil {
		w = append(w, p.ChildOp.Warnings()...)
	}
	return w
}

func (p *PlanOpProjection) Expressions() []types.PlanExpression {
	return p.Projections
}

func (p *PlanOpProjection) WithUpdatedExpressions(exprs ...types.PlanExpression) (types.PlanOperator, error) {
	if len(exprs) != len(p.Projections) {
		return nil, sql3.NewErrInternalf("unexpected number of exprs '%d'", len(exprs))
	}
	return NewPlanOpProjection(exprs, p.ChildOp), nil
}

func ExpressionToColumn(e types.PlanExpression) *types.PlannerColumn {
	name := ""
	relationName := ""

	switch thisExpr := e.(type) {
	case *qualifiedRefPlanExpression:
		name = thisExpr.columnName
		relationName = thisExpr.tableName

	case *aliasPlanExpression:
		name = thisExpr.aliasName
	}

	return &types.PlannerColumn{
		ColumnName:   name,
		RelationName: relationName,
		Type:         e.Type(),
	}
}

type iter struct {
	p         *PlanOpProjection
	childIter types.RowIterator
	row       types.Row
}

func (i *iter) Next(ctx context.Context) (types.Row, error) {
	childRow, err := i.childIter.Next(ctx)
	if err != nil {
		return nil, err
	}

	return ProjectRow(ctx, i.p.Projections, childRow)
}

// ProjectRow evaluates a set of projections.
func ProjectRow(ctx context.Context, projections []types.PlanExpression, row types.Row) (types.Row, error) {
	var fields types.Row
	for _, expr := range projections {
		f, fErr := expr.Evaluate(row)
		if fErr != nil {
			return nil, fErr
		}
		fields = append(fields, f)
	}
	return fields, nil
}
