// Copyright 2018 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.

package sqlbuilder

import (
	"fmt"
	"reflect"

	"github.com/huandu/go-clone"
)

const (
	unionDistinct = " UNION " // Default union type is DISTINCT.
	unionAll      = " UNION ALL "
)

const (
	unionMarkerInit injectionMarker = iota
	unionMarkerAfterUnion
	unionMarkerAfterOrderBy
	unionMarkerAfterLimit
)

// NewUnionBuilder creates a new UNION builder.
func NewUnionBuilder() *UnionBuilder {
	return DefaultFlavor.NewUnionBuilder()
}

func newUnionBuilder() *UnionBuilder {
	return &UnionBuilder{
		args:      &Args{},
		injection: newInjection(),
	}
}

// Clone returns a deep copy of UnionBuilder.
// It's useful when you want to create a base builder and clone it to build similar queries.
func (ub *UnionBuilder) Clone() *UnionBuilder {
	return clone.Clone(ub).(*UnionBuilder)
}

func init() {
	t := reflect.TypeOf(UnionBuilder{})
	clone.SetCustomFunc(t, func(allocator *clone.Allocator, old, new reflect.Value) {
		cloned := allocator.CloneSlowly(old)
		new.Set(cloned)

		ub := cloned.Addr().Interface().(*UnionBuilder)
		for i, b := range ub.builders {
			ub.args.Replace(ub.builderVars[i], b)
		}
	})
}

// UnionBuilder is a builder to build UNION.
type UnionBuilder struct {
	opt         string
	orderByCols []string
	order       string
	limitVar    string
	offsetVar   string

	builders    []Builder
	builderVars []string

	args *Args

	injection *injection
	marker    injectionMarker
}

var _ Builder = new(UnionBuilder)

// Union unions all builders together using UNION operator.
func Union(builders ...Builder) *UnionBuilder {
	return DefaultFlavor.NewUnionBuilder().Union(builders...)
}

// Union unions all builders together using UNION operator.
func (ub *UnionBuilder) Union(builders ...Builder) *UnionBuilder {
	return ub.union(unionDistinct, builders...)
}

// UnionAll unions all builders together using UNION ALL operator.
func UnionAll(builders ...Builder) *UnionBuilder {
	return DefaultFlavor.NewUnionBuilder().UnionAll(builders...)
}

// UnionAll unions all builders together using UNION ALL operator.
func (ub *UnionBuilder) UnionAll(builders ...Builder) *UnionBuilder {
	return ub.union(unionAll, builders...)
}

func (ub *UnionBuilder) union(opt string, builders ...Builder) *UnionBuilder {
	builderVars := make([]string, 0, len(builders))

	for _, b := range builders {
		builderVars = append(builderVars, ub.Var(b))
	}

	ub.opt = opt
	ub.builders = append([]Builder(nil), builders...)
	ub.builderVars = builderVars
	ub.marker = unionMarkerAfterUnion
	return ub
}

// OrderBy sets columns of ORDER BY in SELECT.
//
// It's recommended to use OrderByAsc or OrderByDesc instead for better support of multiple ORDER BY columns with different directions.
// OrderBy combined with Asc/Desc only supports a single direction for all columns.
func (ub *UnionBuilder) OrderBy(col ...string) *UnionBuilder {
	ub.orderByCols = col
	ub.marker = unionMarkerAfterOrderBy
	return ub
}

// OrderByAsc sets a column of ORDER BY in SELECT with ASC order.
// It supports chaining multiple calls to add multiple ORDER BY columns with different directions.
//
//	ub.OrderByAsc("name").OrderByDesc("id")
//	// Generates: ORDER BY name ASC, id DESC
func (ub *UnionBuilder) OrderByAsc(col string) *UnionBuilder {
	ub.orderByCols = append(ub.orderByCols, col+" ASC")
	ub.marker = unionMarkerAfterOrderBy
	return ub
}

// OrderByDesc sets a column of ORDER BY in SELECT with DESC order.
// It supports chaining multiple calls to add multiple ORDER BY columns with different directions.
//
//	ub.OrderByDesc("id").OrderByAsc("name")
//	// Generates: ORDER BY id DESC, name ASC
func (ub *UnionBuilder) OrderByDesc(col string) *UnionBuilder {
	ub.orderByCols = append(ub.orderByCols, col+" DESC")
	ub.marker = unionMarkerAfterOrderBy
	return ub
}

// Asc sets order of ORDER BY to ASC.
//
// Deprecated: Use OrderByAsc instead. Asc only supports a single direction for all ORDER BY columns.
func (ub *UnionBuilder) Asc() *UnionBuilder {
	ub.order = "ASC"
	ub.marker = unionMarkerAfterOrderBy
	return ub
}

// Desc sets order of ORDER BY to DESC.
//
// Deprecated: Use OrderByDesc instead. Desc only supports a single direction for all ORDER BY columns.
func (ub *UnionBuilder) Desc() *UnionBuilder {
	ub.order = "DESC"
	ub.marker = unionMarkerAfterOrderBy
	return ub
}

// Limit sets the LIMIT in SELECT.
func (ub *UnionBuilder) Limit(limit int) *UnionBuilder {
	if limit < 0 {
		ub.limitVar = ""
		return ub
	}

	ub.limitVar = ub.Var(limit)
	ub.marker = unionMarkerAfterLimit
	return ub
}

// Offset sets the LIMIT offset in SELECT.
func (ub *UnionBuilder) Offset(offset int) *UnionBuilder {
	if offset < 0 {
		ub.offsetVar = ""
		return ub
	}

	ub.offsetVar = ub.Var(offset)
	ub.marker = unionMarkerAfterLimit
	return ub
}

// String returns the compiled SELECT string.
func (ub *UnionBuilder) String() string {
	s, _ := ub.Build()
	return s
}

// Build returns compiled SELECT string and args.
// They can be used in `DB#Query` of package `database/sql` directly.
func (ub *UnionBuilder) Build() (sql string, args []interface{}) {
	return ub.BuildWithFlavor(ub.args.Flavor)
}

// BuildWithFlavor returns compiled SELECT string and args with flavor and initial args.
// They can be used in `DB#Query` of package `database/sql` directly.
func (ub *UnionBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{}) (sql string, args []interface{}) {
	buf := newStringBuilder()
	ub.injection.WriteTo(buf, unionMarkerInit)

	nestedSelect := (flavor == Oracle && (len(ub.limitVar) > 0 || len(ub.offsetVar) > 0)) ||
		(flavor == Informix && len(ub.limitVar) > 0)

	if len(ub.builderVars) > 0 {
		needParen := flavor != SQLite

		if nestedSelect {
			buf.WriteLeadingString("SELECT * FROM (")
		}

		if needParen {
			buf.WriteLeadingString("(")
			buf.WriteString(ub.builderVars[0])
			buf.WriteRune(')')
		} else {
			buf.WriteLeadingString(ub.builderVars[0])
		}

		for _, b := range ub.builderVars[1:] {
			buf.WriteString(ub.opt)

			if needParen {
				buf.WriteRune('(')
			}

			buf.WriteString(b)

			if needParen {
				buf.WriteRune(')')
			}
		}

		if nestedSelect {
			buf.WriteLeadingString(")")
		}
	}

	ub.injection.WriteTo(buf, unionMarkerAfterUnion)

	if len(ub.orderByCols) > 0 {
		buf.WriteLeadingString("ORDER BY ")
		buf.WriteStrings(ub.orderByCols, ", ")

		if ub.order != "" {
			buf.WriteRune(' ')
			buf.WriteString(ub.order)
		}

		ub.injection.WriteTo(buf, unionMarkerAfterOrderBy)
	}

	switch flavor {
	case MySQL, SQLite, ClickHouse:
		if len(ub.limitVar) > 0 {
			buf.WriteLeadingString("LIMIT ")
			buf.WriteString(ub.limitVar)

			if len(ub.offsetVar) > 0 {
				buf.WriteLeadingString("OFFSET ")
				buf.WriteString(ub.offsetVar)
			}
		}

	case CQL:
		if len(ub.limitVar) > 0 {
			buf.WriteLeadingString("LIMIT ")
			buf.WriteString(ub.limitVar)
		}

	case PostgreSQL:
		if len(ub.limitVar) > 0 {
			buf.WriteLeadingString("LIMIT ")
			buf.WriteString(ub.limitVar)
		}

		if len(ub.offsetVar) > 0 {
			buf.WriteLeadingString("OFFSET ")
			buf.WriteString(ub.offsetVar)
		}

	case Presto:
		// There might be a hidden constraint in Presto requiring offset to be set before limit.
		// The select statement documentation (https://prestodb.io/docs/current/sql/select.html)
		// puts offset before limit, and Trino, which is based on Presto, seems
		// to require this specific order.
		if len(ub.offsetVar) > 0 {
			buf.WriteLeadingString("OFFSET ")
			buf.WriteString(ub.offsetVar)
		}

		if len(ub.limitVar) > 0 {
			buf.WriteLeadingString("LIMIT ")
			buf.WriteString(ub.limitVar)
		}

	case SQLServer:
		// If ORDER BY is not set, sort column #1 by default.
		// It's required to make OFFSET...FETCH work.
		if len(ub.orderByCols) == 0 && (len(ub.limitVar) > 0 || len(ub.offsetVar) > 0) {
			buf.WriteLeadingString("ORDER BY 1")
		}

		if len(ub.offsetVar) > 0 {
			buf.WriteLeadingString("OFFSET ")
			buf.WriteString(ub.offsetVar)
			buf.WriteString(" ROWS")
		}

		if len(ub.limitVar) > 0 {
			if len(ub.offsetVar) == 0 {
				buf.WriteLeadingString("OFFSET 0 ROWS")
			}

			buf.WriteLeadingString("FETCH NEXT ")
			buf.WriteString(ub.limitVar)
			buf.WriteString(" ROWS ONLY")
		}

	case Oracle:
		// It's required to make OFFSET...FETCH work.
		if len(ub.offsetVar) > 0 {
			buf.WriteLeadingString("OFFSET ")
			buf.WriteString(ub.offsetVar)
			buf.WriteString(" ROWS")
		}

		if len(ub.limitVar) > 0 {
			if len(ub.offsetVar) == 0 {
				buf.WriteLeadingString("OFFSET 0 ROWS")
			}

			buf.WriteLeadingString("FETCH NEXT ")
			buf.WriteString(ub.limitVar)
			buf.WriteString(" ROWS ONLY")
		}

	case Informix:
		// [SKIP N] FIRST M
		// M must be greater than 0
		if len(ub.limitVar) > 0 {
			if len(ub.offsetVar) > 0 {
				buf.WriteLeadingString("SKIP ")
				buf.WriteString(ub.offsetVar)
			}

			buf.WriteLeadingString("FIRST ")
			buf.WriteString(ub.limitVar)
		}

	case Doris:
		// #192: Doris doesn't support ? in OFFSET and LIMIT.
		if len(ub.limitVar) > 0 {
			buf.WriteLeadingString("LIMIT ")
			buf.WriteString(fmt.Sprint(ub.args.Value(ub.limitVar)))

			if len(ub.offsetVar) > 0 {
				buf.WriteLeadingString("OFFSET ")
				buf.WriteString(fmt.Sprint(ub.args.Value(ub.offsetVar)))
			}
		}
	}

	if len(ub.limitVar) > 0 {
		ub.injection.WriteTo(buf, unionMarkerAfterLimit)
	}

	return ub.args.CompileWithFlavor(buf.String(), flavor, initialArg...)
}

// SetFlavor sets the flavor of compiled sql.
func (ub *UnionBuilder) SetFlavor(flavor Flavor) (old Flavor) {
	old = ub.args.Flavor
	ub.args.Flavor = flavor
	return
}

// Flavor returns flavor of builder
func (ub *UnionBuilder) Flavor() Flavor {
	return ub.args.Flavor
}

// Var returns a placeholder for value.
func (ub *UnionBuilder) Var(arg interface{}) string {
	return ub.args.Add(arg)
}

// SQL adds an arbitrary sql to current position.
func (ub *UnionBuilder) SQL(sql string) *UnionBuilder {
	ub.injection.SQL(ub.marker, sql)
	return ub
}
