// Copyright 2016 Qiang Xue. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package dbx

import (
	"bytes"
	"fmt"
	"regexp"
	"strings"
)

// QueryBuilder builds different clauses for a SELECT SQL statement.
type QueryBuilder interface {
	// BuildSelect generates a SELECT clause from the given selected column names.
	BuildSelect(cols []string, distinct bool, option string) string
	// BuildFrom generates a FROM clause from the given tables.
	BuildFrom(tables []string) string
	// BuildGroupBy generates a GROUP BY clause from the given group-by columns.
	BuildGroupBy(cols []string) string
	// BuildJoin generates a JOIN clause from the given join information.
	BuildJoin([]JoinInfo, Params) string
	// BuildWhere generates a WHERE clause from the given expression.
	BuildWhere(Expression, Params) string
	// BuildHaving generates a HAVING clause from the given expression.
	BuildHaving(Expression, Params) string
	// BuildOrderByAndLimit generates the ORDER BY and LIMIT clauses.
	BuildOrderByAndLimit(string, []string, int64, int64) string
	// BuildUnion generates a UNION clause from the given union information.
	BuildUnion([]UnionInfo, Params) string

	// CombineUnion combines the nonempty unionClause with the provided sql string.
	//
	// The unionClause is expected to be the result of BuildUnion.
	// If the unionClause is an empty string it returns the sql argument unmodified.
	//
	// This method exists as a workaround to minimize breaking changes
	// and to allow different SQL builders to specify for example whether
	// they support parenthesis around the UNION SQL queries
	// (SQLite for example will throw a SyntaxError if the UNION parts are wrapped in parenthesis).
	CombineUnion(sql string, unionClause string) string
}

// BaseQueryBuilder provides a basic implementation of QueryBuilder.
type BaseQueryBuilder struct {
	db *DB
}

var _ QueryBuilder = &BaseQueryBuilder{}

// NewBaseQueryBuilder creates a new BaseQueryBuilder instance.
func NewBaseQueryBuilder(db *DB) *BaseQueryBuilder {
	return &BaseQueryBuilder{db}
}

// DB returns the DB instance associated with the query builder.
func (q *BaseQueryBuilder) DB() *DB {
	return q.db
}

// the regexp for columns and tables.
var selectRegex = regexp.MustCompile(`(?i:\s+as\s+|\s+)([\w\-_\.]+)$`)

// BuildSelect generates a SELECT clause from the given selected column names.
func (q *BaseQueryBuilder) BuildSelect(cols []string, distinct bool, option string) string {
	var s bytes.Buffer
	s.WriteString("SELECT ")
	if distinct {
		s.WriteString("DISTINCT ")
	}
	if option != "" {
		s.WriteString(option)
		s.WriteString(" ")
	}
	if len(cols) == 0 {
		s.WriteString("*")
		return s.String()
	}

	for i, col := range cols {
		if i > 0 {
			s.WriteString(", ")
		}
		matches := selectRegex.FindStringSubmatch(col)
		if len(matches) == 0 {
			s.WriteString(q.db.QuoteColumnName(col))
		} else {
			col := col[:len(col)-len(matches[0])]
			alias := matches[1]
			s.WriteString(q.db.QuoteColumnName(col) + " AS " + q.db.QuoteSimpleColumnName(alias))
		}
	}

	return s.String()
}

// BuildFrom generates a FROM clause from the given tables.
func (q *BaseQueryBuilder) BuildFrom(tables []string) string {
	if len(tables) == 0 {
		return ""
	}
	s := ""
	for _, table := range tables {
		table = q.quoteTableNameAndAlias(table)
		if s == "" {
			s = table
		} else {
			s += ", " + table
		}
	}
	return "FROM " + s
}

// BuildJoin generates a JOIN clause from the given join information.
func (q *BaseQueryBuilder) BuildJoin(joins []JoinInfo, params Params) string {
	if len(joins) == 0 {
		return ""
	}
	parts := []string{}
	for _, join := range joins {
		sql := join.Join + " " + q.quoteTableNameAndAlias(join.Table)
		on := ""
		if join.On != nil {
			on = join.On.Build(q.db, params)
		}
		if on != "" {
			sql += " ON " + on
		}
		parts = append(parts, sql)
	}
	return strings.Join(parts, " ")
}

// BuildWhere generates a WHERE clause from the given expression.
func (q *BaseQueryBuilder) BuildWhere(e Expression, params Params) string {
	if e != nil {
		if c := e.Build(q.db, params); c != "" {
			return "WHERE " + c
		}
	}
	return ""
}

// BuildHaving generates a HAVING clause from the given expression.
func (q *BaseQueryBuilder) BuildHaving(e Expression, params Params) string {
	if e != nil {
		if c := e.Build(q.db, params); c != "" {
			return "HAVING " + c
		}
	}
	return ""
}

// BuildGroupBy generates a GROUP BY clause from the given group-by columns.
func (q *BaseQueryBuilder) BuildGroupBy(cols []string) string {
	if len(cols) == 0 {
		return ""
	}
	s := ""
	for i, col := range cols {
		if i == 0 {
			s = q.db.QuoteColumnName(col)
		} else {
			s += ", " + q.db.QuoteColumnName(col)
		}
	}
	return "GROUP BY " + s
}

// BuildOrderByAndLimit generates the ORDER BY and LIMIT clauses.
func (q *BaseQueryBuilder) BuildOrderByAndLimit(sql string, cols []string, limit int64, offset int64) string {
	if orderBy := q.BuildOrderBy(cols); orderBy != "" {
		sql += " " + orderBy
	}
	if limit := q.BuildLimit(limit, offset); limit != "" {
		return sql + " " + limit
	}
	return sql
}

// BuildUnion generates a UNION clause from the given union information.
func (q *BaseQueryBuilder) BuildUnion(unions []UnionInfo, params Params) string {
	if len(unions) == 0 {
		return ""
	}
	sql := ""
	for i, union := range unions {
		if i > 0 {
			sql += " "
		}
		for k, v := range union.Query.params {
			params[k] = v
		}
		u := "UNION"
		if union.All {
			u = "UNION ALL"
		}
		sql += fmt.Sprintf("%v (%v)", u, union.Query.sql)
	}
	return sql
}

// CombineUnion combines the nonempty unionClause with the provided sql string.
//
// The unionClause is expected to be the result of BuildUnion.
// If the unionClause is an empty string it returns the sql argument unmodified.
//
// This method exists as a workaround to minimize breaking changes
// and to allow different SQL builders to specify for example whether
// they support parenthesis around the UNION SQL queries
// (SQLite for example will throw a SyntaxError if the UNION parts are wrapped in parenthesis).
func (q *BaseQueryBuilder) CombineUnion(sql string, unionClause string) string {
	if unionClause == "" {
		return sql
	}

	return fmt.Sprintf("(%v) %v", sql, unionClause)
}

var orderRegex = regexp.MustCompile(`\s+((?i)ASC|DESC)$`)

// BuildOrderBy generates the ORDER BY clause.
func (q *BaseQueryBuilder) BuildOrderBy(cols []string) string {
	if len(cols) == 0 {
		return ""
	}
	s := ""
	for i, col := range cols {
		if i > 0 {
			s += ", "
		}
		matches := orderRegex.FindStringSubmatch(col)
		if len(matches) == 0 {
			s += q.db.QuoteColumnName(col)
		} else {
			col := col[:len(col)-len(matches[0])]
			dir := matches[1]
			s += q.db.QuoteColumnName(col) + " " + dir
		}
	}
	return "ORDER BY " + s
}

// BuildLimit generates the LIMIT clause.
func (q *BaseQueryBuilder) BuildLimit(limit int64, offset int64) string {
	if limit < 0 && offset > 0 {
		// most DBMS requires LIMIT when OFFSET is present
		limit = 9223372036854775807 // 2^63 - 1
	}

	sql := ""
	if limit >= 0 {
		sql = fmt.Sprintf("LIMIT %v", limit)
	}
	if offset <= 0 {
		return sql
	}
	if sql != "" {
		sql += " "
	}
	return sql + fmt.Sprintf("OFFSET %v", offset)
}

func (q *BaseQueryBuilder) quoteTableNameAndAlias(table string) string {
	matches := selectRegex.FindStringSubmatch(table)
	if len(matches) == 0 {
		return q.db.QuoteTableName(table)
	}
	table = table[:len(table)-len(matches[0])]
	return q.db.QuoteTableName(table) + " " + q.db.QuoteSimpleTableName(matches[1])
}
