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

package sqlbuilder

import (
	"reflect"

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

const (
	deleteMarkerInit injectionMarker = iota
	deleteMarkerAfterWith
	deleteMarkerAfterDeleteFrom
	deleteMarkerAfterUsing
	deleteMarkerAfterWhere
	deleteMarkerAfterOrderBy
	deleteMarkerAfterLimit
	deleteMarkerAfterReturning
)

// NewDeleteBuilder creates a new DELETE builder.
func NewDeleteBuilder() *DeleteBuilder {
	return DefaultFlavor.NewDeleteBuilder()
}

func newDeleteBuilder() *DeleteBuilder {
	args := &Args{}
	proxy := &whereClauseProxy{}
	return &DeleteBuilder{
		whereClauseProxy: proxy,
		whereClauseExpr:  args.Add(proxy),

		Cond: Cond{
			Args: args,
		},
		args:      args,
		injection: newInjection(),
	}
}

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

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

		db := cloned.Addr().Interface().(*DeleteBuilder)
		db.args.Replace(db.whereClauseExpr, db.whereClauseProxy)
		db.args.Replace(db.cteBuilderVar, db.cteBuilder)
	})
}

// DeleteBuilder is a builder to build DELETE.
type DeleteBuilder struct {
	*WhereClause
	Cond

	whereClauseProxy *whereClauseProxy
	whereClauseExpr  string

	cteBuilderVar string
	cteBuilder    *CTEBuilder

	tables      []string
	usingTables []string
	orderByCols []string
	order       string
	limitVar    string
	returning   []string

	args *Args

	injection *injection
	marker    injectionMarker
}

var _ Builder = new(DeleteBuilder)

// DeleteFrom sets table name in DELETE.
func DeleteFrom(table ...string) *DeleteBuilder {
	return DefaultFlavor.NewDeleteBuilder().DeleteFrom(table...)
}

// With sets WITH clause (the Common Table Expression) before DELETE.
func (db *DeleteBuilder) With(builder *CTEBuilder) *DeleteBuilder {
	db.marker = deleteMarkerAfterWith
	db.cteBuilderVar = db.Var(builder)
	db.cteBuilder = builder
	return db
}

// DeleteFrom sets table name in DELETE.
func (db *DeleteBuilder) DeleteFrom(table ...string) *DeleteBuilder {
	db.tables = table
	db.marker = deleteMarkerAfterDeleteFrom
	return db
}

// Using sets the USING clause in DELETE.
// It's a PostgreSQL extension that allows referencing other tables in the WHERE clause,
// similar to a JOIN.
//
//	db.DeleteFrom("orders").Using("customers").Where("orders.customer_id = customers.id", "customers.status = 'inactive'")
//	// Generates: DELETE FROM orders USING customers WHERE orders.customer_id = customers.id AND customers.status = 'inactive'
func (db *DeleteBuilder) Using(table ...string) *DeleteBuilder {
	db.usingTables = table
	db.marker = deleteMarkerAfterUsing
	return db
}

// TableNames returns all table names in this DELETE statement.
func (db *DeleteBuilder) TableNames() []string {
	var additionalTableNames []string

	if db.cteBuilder != nil {
		additionalTableNames = db.cteBuilder.tableNamesForFrom()
	}

	var tableNames []string

	if len(db.tables) > 0 && len(additionalTableNames) > 0 {
		tableNames = make([]string, len(db.tables)+len(additionalTableNames))
		copy(tableNames, db.tables)
		copy(tableNames[len(db.tables):], additionalTableNames)
	} else if len(db.tables) > 0 {
		tableNames = db.tables
	} else if len(additionalTableNames) > 0 {
		tableNames = additionalTableNames
	}

	return tableNames
}

// Where adds expressions to the WHERE clause in DELETE.
//
// Multiple calls to Where will join expressions with AND.
// To reset the WHERE clause, set the WhereClause field to nil.
func (db *DeleteBuilder) Where(andExpr ...string) *DeleteBuilder {
	if len(andExpr) == 0 || estimateStringsBytes(andExpr) == 0 {
		return db
	}

	if db.WhereClause == nil {
		db.WhereClause = NewWhereClause()
	}

	db.WhereClause.AddWhereExpr(db.args, andExpr...)
	db.marker = deleteMarkerAfterWhere
	return db
}

// AddWhereClause adds all clauses in the whereClause to SELECT.
func (db *DeleteBuilder) AddWhereClause(whereClause *WhereClause) *DeleteBuilder {
	if db.WhereClause == nil {
		db.WhereClause = NewWhereClause()
	}

	db.WhereClause.AddWhereClause(whereClause)
	return db
}

// OrderBy sets columns of ORDER BY in DELETE.
//
// 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 (db *DeleteBuilder) OrderBy(col ...string) *DeleteBuilder {
	db.orderByCols = col
	db.marker = deleteMarkerAfterOrderBy
	return db
}

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

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

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

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

// Limit sets the LIMIT in DELETE.
func (db *DeleteBuilder) Limit(limit int) *DeleteBuilder {
	if limit < 0 {
		db.limitVar = ""
		return db
	}

	db.limitVar = db.Var(limit)
	db.marker = deleteMarkerAfterLimit
	return db
}

// Returning sets returning columns.
// For DBMS that doesn't support RETURNING, e.g. MySQL, it will be ignored.
func (db *DeleteBuilder) Returning(col ...string) *DeleteBuilder {
	db.returning = col
	db.marker = deleteMarkerAfterReturning
	return db
}

// String returns the compiled DELETE string.
func (db *DeleteBuilder) String() string {
	s, _ := db.Build()
	return s
}

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

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

	if db.cteBuilder != nil {
		buf.WriteLeadingString(db.cteBuilderVar)
		db.injection.WriteTo(buf, deleteMarkerAfterWith)
	}

	tableNames := db.TableNames()

	if len(tableNames) > 0 {
		buf.WriteLeadingString("DELETE FROM ")
		buf.WriteStrings(tableNames, ", ")
	}

	db.injection.WriteTo(buf, deleteMarkerAfterDeleteFrom)

	if flavor == SQLServer {
		if len(db.returning) > 0 {
			buf.WriteLeadingString("OUTPUT ")
			buf.WriteStringsPrefixed("DELETED.", db.returning, ", ")
		}

		db.injection.WriteTo(buf, insertMarkerAfterReturning)
	}

	if len(db.usingTables) > 0 {
		buf.WriteLeadingString("USING ")
		buf.WriteStrings(db.usingTables, ", ")
	}

	db.injection.WriteTo(buf, deleteMarkerAfterUsing)

	if db.WhereClause != nil {
		db.whereClauseProxy.WhereClause = db.WhereClause
		defer func() {
			db.whereClauseProxy.WhereClause = nil
		}()

		buf.WriteLeadingString(db.whereClauseExpr)
		db.injection.WriteTo(buf, deleteMarkerAfterWhere)
	}

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

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

		db.injection.WriteTo(buf, deleteMarkerAfterOrderBy)
	}

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

		db.injection.WriteTo(buf, deleteMarkerAfterLimit)
	}

	if flavor == PostgreSQL || flavor == SQLite {
		if len(db.returning) > 0 {
			buf.WriteLeadingString("RETURNING ")
			buf.WriteStrings(db.returning, ", ")
		}

		db.injection.WriteTo(buf, deleteMarkerAfterReturning)
	}

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

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

// Flavor returns flavor of builder
func (db *DeleteBuilder) Flavor() Flavor {
	return db.args.Flavor
}

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