// Copyright 2021 Molecula Corp. All rights reserved.
package sql

import (
	"fmt"
	"reflect"
	"strconv"
	"strings"
	"time"

	"github.com/molecula/featurebase/v3"
	"github.com/molecula/featurebase/v3/pql"
	"github.com/pkg/errors"
	"vitess.io/vitess/go/vt/sqlparser"
)

// parseColumn is a column parsed from a sql query. Its qualifier
// value should map to either the name or alias of a parseTable.
type parseColumn struct {
	name      string
	qualifier string
}

// parseTable is a table parsed from a sql query. It includes
// its name and alias, along with a boolean indicating whether
// the table is the primary side of a join statement (i.e. it
// refers to the Pilosa column _id).
type parseTable struct {
	name    string
	alias   string
	primary bool          // indicates the side of the join representing the column _id
	column  *parseColumn  // contains the related column from the ON clause
	index   *pilosa.Index // the pilosa index related to this table
}

// parseTables is a slice of parseTable parsed from a single sql query.
type parseTables []*parseTable

// byName returns the parseTable from the slice which matches on name.
// If there is no match it returns nil.
func (j parseTables) byName(n string) *parseTable {
	for i := range j {
		if j[i].name == n {
			return j[i]
		}
	}
	return nil
}

// byName returns the parseTable from the slice which matches on alias.
// If there is no match it returns nil.
func (j parseTables) byAlias(a string) *parseTable {
	for i := range j {
		if j[i].alias == a {
			return j[i]
		}
	}
	return nil
}

// primary returns the primary parseTable from the slice.
// In order for this to be useful, it is assumed that
// joinTables contains exactly two joinTable pointers
// (a primary and a secondary).
func (j parseTables) primary() *parseTable {
	for i := range j {
		if j[i].primary {
			return j[i]
		}
	}
	return nil
}

// secondary returns the secondary parseTable from the slice.
func (j parseTables) secondary() *parseTable {
	for i := range j {
		if !j[i].primary {
			return j[i]
		}
	}
	return nil
}

// tableWhere represents a parseTable from a sql query along
// with the portion of the where clause that relates to
// that table. For example, if a sql query had:
//     from tbl1, tbl2
//     where tbl1.field1=1 and tbl2.field2=2
// then each table would have a separate tableWhere object
// with the where made up of only the field with matching qualifier.
type tableWhere struct {
	table *parseTable
	where string
}

// tableWheres is a slice of tableWhere.
type tableWheres []*tableWhere

// extractParseTable returns a parseTable for the sqlparser.TableExpr.
func extractParseTable(tableExpr sqlparser.TableExpr) (*parseTable, error) {
	switch tbl := tableExpr.(type) {
	case *sqlparser.AliasedTableExpr:
		tableName := tbl.Expr.(sqlparser.TableName).ToViewName().Name.String()
		alias := tbl.As.String()
		if alias == "" {
			alias = tableName
		}
		return &parseTable{
			name:  tableName,
			alias: alias,
		}, nil
	}

	return nil, errors.New("unsupported table expression")
}

func extractSelectFields(index *pilosa.Index, stmt *sqlparser.Select) ([]Column, selectFeatures, error) {
	columns := []Column{}
	features := selectFeatures{}
	for _, item := range stmt.SelectExprs {
		switch expr := item.(type) {
		case *sqlparser.AliasedExpr:
			var column Column
			var alias string = expr.As.String()
			switch colExpr := expr.Expr.(type) {
			case *sqlparser.ColName:
				fieldName := colExpr.Name.String()

				if fieldName == ColID {
					if index.Options().Keys {
						column = NewKeyIndexColumn(index, alias)
					} else {
						column = NewIDIndexColumn(index, alias)
					}
				} else {
					field := index.Field(fieldName)
					if field == nil {
						return nil, features, errors.Wrapf(pilosa.ErrFieldNotFound, "field %s", fieldName)
					}
					column = NewFieldColumn(field, alias)
				}
			case *sqlparser.FuncExpr:
				funcName := FuncName(strings.ToLower(colExpr.Name.String()))
				var field *pilosa.Field

				if len(colExpr.Exprs) != 1 {
					return nil, features, errors.New("function should have a single argument")
				}

				switch expr := colExpr.Exprs[0].(type) {
				case *sqlparser.AliasedExpr:
					if colExpr, ok := expr.Expr.(*sqlparser.ColName); ok {
						fieldName := colExpr.Name.String()
						field = index.Field(fieldName)
						if field == nil {
							return nil, features, errors.Wrapf(pilosa.ErrFieldNotFound, "field %s", fieldName)
						}
					} else {
						return nil, features, errors.New("table name is required")
					}
				case *sqlparser.StarExpr:
					// We don't currently track this; it either has a field or doesn't.
				default:
					return nil, features, errors.New("table name is required")
				}

				switch funcName {
				case FuncCount, FuncMin, FuncMax, FuncSum, FuncAvg:
					column = NewFuncColumn(funcName, field, alias)
				default:
					return nil, features, fmt.Errorf("unknown function: %s", funcName)
				}
				features.funcs = append(features.funcs, selectFunc{
					funcName: funcName,
					field:    field,
				})
			default:
				return nil, features, errors.New("table name is required")
			}
			columns = append(columns, column)
		case *sqlparser.StarExpr:
			columns = append(columns, NewStarColumn())
		default:
			return nil, features, errors.New("only column names or * are supported in select")
		}
	}
	return columns, features, nil
}

func extractIndexName(stmt *sqlparser.Select) (string, error) {
	if len(stmt.From) != 1 {
		return "", errors.New("selecting from multiple tables is not supported")
	}

	fromExpr := stmt.From[0]
	switch from := fromExpr.(type) {
	case *sqlparser.AliasedTableExpr:
		indexName := from.Expr.(sqlparser.TableName).ToViewName().Name.String()
		return indexName, nil
	}

	return "", errors.New("unsupported from clause")
}

// extractParseColumn returns a parseColumn for the sqlparser.ColName.
func extractParseColumn(col *sqlparser.ColName) (*parseColumn, error) {
	colName := col.Name.String()
	qualifier := col.Qualifier.ToViewName().Name.String()

	return &parseColumn{
		name:      colName,
		qualifier: qualifier,
	}, nil
}

func extractWhere(index *pilosa.Index, expr sqlparser.Expr) (string, error) {
	switch e := expr.(type) {
	case *sqlparser.ComparisonExpr:
		parseCol, op, val, err := extractComparison(e)
		if err != nil {
			return "", err
		}

		if parseCol.name == "_id" {
			switch op {
			case "=":
				return ConstRow(val), nil
			case "in":
				switch valExpr := val.(type) {
				case []interface{}:
					return ConstRow(valExpr...), nil
				}
			}
		}

		field := index.Field(parseCol.name)
		if field == nil {
			return "", errors.Wrap(pilosa.ErrFieldNotFound, parseCol.name)
		}

		switch field.Type() {
		case pilosa.FieldTypeInt, pilosa.FieldTypeDecimal, pilosa.FieldTypeTimestamp:
			switch op {
			case "=":
				return Equals(field.Name(), val), nil
			case "<":
				return LT(field.Name(), val), nil
			case "<=":
				return LTE(field.Name(), val), nil
			case ">":
				return GT(field.Name(), val), nil
			case ">=":
				return GTE(field.Name(), val), nil
			case "<>", "!=":
				return NotEquals(field.Name(), val), nil
			}
		default:
			switch op {
			case "=":
				return Row(field.Name(), val)
			case "in":
				var qs []string
				switch valExpr := val.(type) {
				case []interface{}:
					for _, v := range valExpr {
						q, err := Row(field.Name(), v)
						if err != nil {
							return "", err
						}
						qs = append(qs, q)
					}
					return Union(qs...), nil
				default:
					return "", fmt.Errorf("in operator expects `[]interface{}` but got: %T", valExpr)
				}
			case "like":
				sval, ok := val.(string)
				if !ok {
					return "", fmt.Errorf("like operator expects `string` but got: %T", val)
				}
				return Like(field.Name(), sval), nil
			}
		}
	case *sqlparser.AndExpr:
		pql, err := extractWhereDateRange(index, e.Left, e.Right)
		if err == nil {
			return pql, err
		}
		left, err := extractWhere(index, e.Left)
		if err != nil {
			return "", err
		}
		right, err := extractWhere(index, e.Right)
		if err != nil {
			return "", err
		}
		return Intersect(left, right), nil
	case *sqlparser.OrExpr:
		left, err := extractWhere(index, e.Left)
		if err != nil {
			return "", err
		}
		right, err := extractWhere(index, e.Right)
		if err != nil {
			return "", err
		}
		return Union(left, right), nil
	case *sqlparser.NotExpr:
		expr, err := extractWhere(index, e.Expr)
		if err != nil {
			return "", err
		}
		return Not(expr), nil
	case *sqlparser.ParenExpr:
		expr, err := extractWhere(index, e.Expr)
		if err != nil {
			return "", err
		}
		return expr, nil
	case *sqlparser.RangeCond:
		if e.Operator != "between" {
			return "", errors.New("only between is supported")
		}
		left, ok := e.Left.(*sqlparser.ColName)
		if !ok {
			return "", errors.New("left operand must be a column name")
		}
		columnName := left.Name.String()
		fieldName, isSpecial := ExtractFieldName(columnName)
		if isSpecial {
			return "", errors.New("special fields are not allowed here")
		}
		field := index.Field(fieldName)

		switch field.Type() {
		case pilosa.FieldTypeInt:
			fromNum, err := extractInt(e.From)
			if err != nil {
				return "", err
			}
			toNum, err := extractInt(e.To)
			if err != nil {
				return "", err
			}
			return Between(field.Name(), fromNum, toNum), nil
		case pilosa.FieldTypeDecimal:
			fromNum, err := extractFloat(e.From)
			if err != nil {
				return "", err
			}
			toNum, err := extractFloat(e.To)
			if err != nil {
				return "", err
			}
			return Between(field.Name(), fromNum, toNum), nil
		case pilosa.FieldTypeTimestamp:
			fromTime, err := extractTimestamp(e.From)
			if err != nil {
				return "", err
			}
			toTime, err := extractTimestamp(e.To)
			if err != nil {
				return "", err
			}
			return Between(field.Name(), fromTime, toTime), nil
		default:
			return "", errors.New("only int and float64 fields are supported")
		}
	case *sqlparser.IsExpr:
		left, ok := e.Expr.(*sqlparser.ColName)
		if !ok {
			return "", errors.New("left operand must be a column name")
		}
		field := index.Field(left.Name.String())
		if field.Type() == pilosa.FieldTypeInt || field.Type() == pilosa.FieldTypeTimestamp {
			if e.Operator == "is not null" {
				return NotNull(field.Name()), nil
			}
			return "", fmt.Errorf("only `is not null` is supported for %s fields", field.Type())
		}
		return "", fmt.Errorf("`is` expression is supported only for %s fields", field.Type())
	}
	return "", errors.New("cannot extract where")
}

func extractWhereDateRange(index *pilosa.Index, leftExpr sqlparser.Expr, rightExpr sqlparser.Expr) (string, error) {
	var fieldName string
	var fromExpr sqlparser.Expr
	var toExpr sqlparser.Expr
	var isSpecial bool
	var idOrKey interface{}

	// Where part is expected to be in one of the following forms:
	// where FIELD=VALUE and FIELD between DATETIME_FORMAT and DATETIME_FORMAT
	// Or:
	// where FIELD between DATETIME_FORMAT and DATETIME_FORMAT and FIELD=VALUE

	extract := func(compExpr *sqlparser.ComparisonExpr, rangeCond *sqlparser.RangeCond) error {
		parseCol, op, val, err := extractComparison(compExpr)
		if err != nil {
			return err
		}
		idOrKey = val
		if op != "=" {
			// only = operator can exist here.
			return errors.New("only = operator can exist here")
		}
		fieldName, isSpecial = ExtractFieldName(parseCol.name)
		if isSpecial {
			// column name cannot be special here.
			return errors.New("column name cannot be special here")
		}
		if rangeCond.Operator != "between" {
			// only between is accepted at this point.
			return errors.New("only between is accepted at this point")
		}
		condFieldName, _ := ExtractFieldName(rangeCond.Left.(*sqlparser.ColName).Name.String())
		if condFieldName != fieldName {
			// the field names in both sides should be the same, otherwise reject.
			return errors.New("the field names in both sides should be the same, otherwise reject")
		}
		fromExpr = rangeCond.From
		toExpr = rangeCond.To
		return nil
	}

	if left, ok := leftExpr.(*sqlparser.ComparisonExpr); ok {
		// if FIELD=VALUE part is on the left, and range part is on the right
		if right, ok := rightExpr.(*sqlparser.RangeCond); ok {
			if err := extract(left, right); err != nil {
				return "", err
			}
		} else {
			return "", errors.New("right is not a range cond")
		}
	} else if right, ok := rightExpr.(*sqlparser.ComparisonExpr); ok {
		// if FIELD=VALUE part is on the right, and left part is on the left
		if left, ok := leftExpr.(*sqlparser.RangeCond); ok {
			if err := extract(right, left); err != nil {
				return "", err
			}
		} else {
			return "", errors.New("left is not a range cond")
		}
	}

	fromStr, err := extractStr(fromExpr)
	if err != nil {
		return "", err
	}
	toStr, err := extractStr(toExpr)
	if err != nil {
		return "", err
	}
	// try to convert `from` and `to` to time
	fromTime, ok := ConvertToTime(fromStr)
	if !ok {
		return "", errors.New("from operand must be in the correct time format")
	}
	toTime, ok := ConvertToTime(toStr)
	if !ok {
		return "", errors.New("to operand must be in the correct time format")
	}

	field := index.Field(fieldName)
	return RowRange(field.Name(), idOrKey, fromTime, toTime)
}

func extractVal(e sqlparser.Expr) (interface{}, error) {
	val, ok := e.(*sqlparser.SQLVal)
	if !ok {
		return nil, errors.New("expression must be a value")
	}
	switch val.Type {
	case sqlparser.StrVal:
		return string(val.Val), nil
	case sqlparser.IntVal:
		value, err := strconv.Atoi(string(val.Val))
		if err != nil {
			return nil, err
		}
		return value, nil
	case sqlparser.FloatVal:
		value, err := strconv.ParseFloat(string(val.Val), 64)
		if err != nil {
			return nil, err
		}
		return value, nil
	default:
		return nil, fmt.Errorf("unknown type: %d", val.Type)
	}
}

func extractInt(e sqlparser.Expr) (int, error) {
	val, err := extractVal(e)
	if err != nil {
		return 0, err
	}
	num, ok := val.(int)
	if !ok {
		return 0, errors.New("value must be an integer")
	}
	return num, nil
}

func extractFloat(e sqlparser.Expr) (float64, error) {
	val, err := extractVal(e)
	if err != nil {
		return 0, err
	}

	var num float64
	switch v := val.(type) {
	case int:
		num = float64(v)
	case float64:
		num = v
	default:
		return 0, errors.New("value must be convertable to a float64")
	}
	return num, nil
}

func extractTimestamp(e sqlparser.Expr) (time.Time, error) {
	val, err := extractVal(e)
	if err != nil {
		return time.Time{}, err
	}
	s, ok := val.(string)
	if !ok {
		return time.Time{}, errors.New("value must be an ISO 8601-formated timestamp string")
	}
	t, err := time.Parse(time.RFC3339Nano, s)
	if err != nil {
		return time.Time{}, errors.New("value must be an ISO 8601-formated timestamp string")
	}
	return t, nil
}

func extractStr(e sqlparser.Expr) (string, error) {
	val, err := extractVal(e)
	if err != nil {
		return "", err
	}
	s, ok := val.(string)
	if !ok {
		return "", errors.New("value must be a string")
	}
	return s, nil
}

func extractTuple(tuple sqlparser.ValTuple) ([]interface{}, error) {
	result := []interface{}{}
	for _, item := range tuple {
		if val, ok := item.(*sqlparser.SQLVal); ok {
			v, err := extractVal(val)
			if err != nil {
				return nil, err
			}
			result = append(result, v)
		} else {
			return nil, errors.New("tuple should contain integers or strings")
		}
	}
	return result, nil
}

func extractComparison(expr *sqlparser.ComparisonExpr) (col *parseColumn, op string, value interface{}, err error) {
	op = expr.Operator
	if colExpr, ok := expr.Left.(*sqlparser.ColName); ok {
		col, err = extractParseColumn(colExpr)
		if err != nil {
			return
		}
		if op == "in" {
			switch valExpr := expr.Right.(type) {
			case sqlparser.ValTuple:
				value, err = extractTuple(valExpr)

			default:
				err = fmt.Errorf("in operator excepts only a tuple or a query, received `%s`",
					reflect.TypeOf(valExpr).String())
				return
			}
		} else {
			value, err = extractVal(expr.Right)
		}
		if err != nil {
			return
		}
	} else {
		if colExpr, ok := expr.Right.(*sqlparser.ColName); ok {
			col, err = extractParseColumn(colExpr)
			if err != nil {
				return
			}
			if op == "in" {
				value, err = extractTuple(expr.Left.(sqlparser.ValTuple))
			} else {
				value, err = extractVal(expr.Left)
			}
			if err != nil {
				return
			}
		} else {
			err = errors.New("either left or right operand should be a column name")
		}
	}
	return
}

func extractLimitOffset(stmt *sqlparser.Select) (limit uint, offset uint, hasLimit bool, hasOffset bool, err error) {
	if stmt.Limit == nil {
		return 0, 0, false, false, nil
	}
	if offsetExpr, ok := stmt.Limit.Offset.(*sqlparser.SQLVal); ok {
		val, err := extractVal(offsetExpr)
		if err != nil {
			return 0, 0, false, false, err
		}
		if offsetVal, ok := val.(int); ok {
			offset = uint(offsetVal)
		} else {
			return 0, 0, false, false, errors.New("offset must be an integer")
		}
		hasOffset = true
	}
	if limitExpr, ok := stmt.Limit.Rowcount.(*sqlparser.SQLVal); ok {
		val, err := extractVal(limitExpr)
		if err != nil {
			return 0, 0, false, false, err
		}
		if limitVal, ok := val.(int); ok {
			limit = uint(limitVal)
		} else {
			return 0, 0, false, false, errors.New("limit must be an integer")
		}
		hasLimit = true
	}
	return limit, offset, hasLimit, hasOffset, nil
}

// extractOrderBy returns the order by fields and directions (asc/desc)
// as separate string slices.
func extractOrderBy(stmt *sqlparser.Select) ([]string, []string, error) {
	var flds []string
	var dirs []string

	for _, item := range stmt.OrderBy {
		switch colExpr := item.Expr.(type) {
		case *sqlparser.ColName:
			colName := colExpr.Name.String()
			flds = append(flds, colName)
			dirs = append(dirs, item.Direction)
		}
	}

	return flds, dirs, nil
}

func extractTableNames(stmt *sqlparser.Select) ([]string, error) {
	if len(stmt.From) == 0 {
		return []string{}, nil
	}

	switch from := stmt.From[0].(type) {
	case *sqlparser.AliasedTableExpr:
		tableName := from.Expr.(sqlparser.TableName).ToViewName().Name.String()
		return []string{tableName}, nil
	case *sqlparser.JoinTableExpr:
		ret := []string{}
		switch left := from.LeftExpr.(type) {
		case *sqlparser.AliasedTableExpr:
			leftTableName := left.Expr.(sqlparser.TableName).ToViewName().Name.String()
			ret = append(ret, leftTableName)
		}
		switch right := from.RightExpr.(type) {
		case *sqlparser.AliasedTableExpr:
			rightTableName := right.Expr.(sqlparser.TableName).ToViewName().Name.String()
			ret = append(ret, rightTableName)
		}
		return ret, nil
	}

	return []string{}, nil
}

func extractGroupByFieldNames(stmt sqlparser.GroupBy) ([]string, error) {
	fields := make([]string, len(stmt))
	for i, item := range stmt {
		col, ok := item.(*sqlparser.ColName)
		if !ok {
			return nil, errors.New("group by accepts columns")
		}
		fields[i] = col.Name.String()
	}
	return fields, nil
}

func extractHavingClause(stmt *sqlparser.Where) (*HavingClause, error) {
	if stmt == nil {
		return nil, nil
	}
	if stmt.Type != "having" {
		return nil, fmt.Errorf("invalid having type: %s", stmt.Type)
	}

	hc := &HavingClause{}

	switch having := stmt.Expr.(type) {
	case *sqlparser.RangeCond:
		if having.Operator != "between" {
			return nil, errors.New("only between is supported")
		}
		hc.Subj = having.Left.(*sqlparser.ColName).Name.String()
		hc.Cond.Op = pql.BETWEEN
		fromPred, err := extractInt(having.From)
		if err != nil {
			return nil, err
		}
		toPred, err := extractInt(having.To)
		if err != nil {
			return nil, err
		}
		vals := make([]interface{}, 2)
		switch hc.Subj {
		case "count":
			vals[0] = uint64(fromPred)
			vals[1] = uint64(toPred)
		case "sum":
			vals[0] = int64(fromPred)
			vals[1] = int64(toPred)
		}
		hc.Cond.Value = vals
		return hc, nil
	case *sqlparser.AndExpr:
		left := having.Left.(*sqlparser.ComparisonExpr)
		right := having.Right.(*sqlparser.ComparisonExpr)
		leftName := left.Left.(*sqlparser.ColName).Name.String()
		rightName := right.Left.(*sqlparser.ColName).Name.String()
		if leftName != rightName {
			return nil, fmt.Errorf("having comparitors do not match: %s, %s", leftName, rightName)
		}
		hc.Subj = leftName

		leftOp := extractComparisonOp(left)
		rightOp := extractComparisonOp(right)

		leftPred, err := extractInt(left.Right)
		if err != nil {
			return nil, err
		}
		rightPred, err := extractInt(right.Right)
		if err != nil {
			return nil, err
		}

		intVals := make([]int, 2)
		if leftOp == pql.GT && rightOp == pql.LT {
			intVals[0] = leftPred
			intVals[1] = rightPred
			hc.Cond.Op = pql.BTWN_LT_LT
		} else if leftOp == pql.GT && rightOp == pql.LTE {
			intVals[0] = leftPred
			intVals[1] = rightPred
			hc.Cond.Op = pql.BTWN_LT_LTE
		} else if leftOp == pql.GTE && rightOp == pql.LT {
			intVals[0] = leftPred
			intVals[1] = rightPred
			hc.Cond.Op = pql.BTWN_LTE_LT
		} else if leftOp == pql.GTE && rightOp == pql.LTE {
			intVals[0] = leftPred
			intVals[1] = rightPred
			hc.Cond.Op = pql.BETWEEN
		} else if leftOp == pql.LT && rightOp == pql.GT {
			intVals[0] = rightPred
			intVals[1] = leftPred
			hc.Cond.Op = pql.BTWN_LT_LT
		} else if leftOp == pql.LT && rightOp == pql.GTE {
			intVals[0] = rightPred
			intVals[1] = leftPred
			hc.Cond.Op = pql.BTWN_LTE_LT
		} else if leftOp == pql.LTE && rightOp == pql.GT {
			intVals[0] = rightPred
			intVals[1] = leftPred
			hc.Cond.Op = pql.BTWN_LT_LTE
		} else if leftOp == pql.LTE && rightOp == pql.GTE {
			intVals[0] = rightPred
			intVals[1] = leftPred
			hc.Cond.Op = pql.BETWEEN
		}

		vals := make([]interface{}, 2)
		switch hc.Subj {
		case "count":
			vals[0] = uint64(intVals[0])
			vals[1] = uint64(intVals[1])
		case "sum":
			vals[0] = int64(intVals[0])
			vals[1] = int64(intVals[1])
		}
		hc.Cond.Value = vals

		return hc, nil
	case *sqlparser.ComparisonExpr:
		hc.Subj = having.Left.(*sqlparser.ColName).Name.String()
		hc.Cond.Op = extractComparisonOp(having)
		switch hc.Subj {
		case "count":
			pred, err := extractInt(having.Right)
			if err != nil {
				return nil, err
			}
			hc.Cond.Value = uint64(pred)
		case "sum":
			pred, err := extractInt(having.Right)
			if err != nil {
				return nil, err
			}
			hc.Cond.Value = int64(pred)
		}
		return hc, nil
	}

	return nil, errors.New("unsupported having clause")
}

func extractComparisonOp(expr *sqlparser.ComparisonExpr) pql.Token {
	switch expr.Operator {
	case "==":
		return pql.EQ
	case "!=":
		return pql.NEQ
	case "<":
		return pql.LT
	case "<=":
		return pql.LTE
	case ">":
		return pql.GT
	case ">=":
		return pql.GTE
	}
	return pql.ILLEGAL
}

// extractJoinTables returns a slice of parseTable containing two
// items, the primary and secondary join tables. This function does
// not extract join tables of the form:
//    from tbl1, tbl2
// The from clause must be of the form:
//    from tbl1 INNER JOIN tbl2 ON ...
//
func extractJoinTables(stmt *sqlparser.Select) (parseTables, error) {
	if len(stmt.From) != 1 {
		return nil, errors.New("selecting from multiple tables is not supported")
	}

	tbls := make([]*parseTable, 2)

	from, ok := stmt.From[0].(*sqlparser.JoinTableExpr)
	if !ok {
		return nil, errors.New("unsupported join clause")
	}

	leftTable, err := extractParseTable(from.LeftExpr)
	if err != nil {
		return nil, errors.Wrap(err, "extracting left join table")
	}
	rightTable, err := extractParseTable(from.RightExpr)
	if err != nil {
		return nil, errors.Wrap(err, "extracting right join table")
	}

	// It is not important which table goes in which tbls position;
	// the primary/secondary table will be determined later.
	tbls[0] = leftTable
	tbls[1] = rightTable

	// Get the ON condition and determine which table is primary.
	switch onCond := from.Condition.On.(type) {
	case *sqlparser.ComparisonExpr:
		if onCond.Operator != "=" {
			return nil, errors.Errorf("unsupported on condition comparison type: %s", onCond.Operator)
		}
		// get left ColName
		left, ok := onCond.Left.(*sqlparser.ColName)
		if !ok {
			return nil, errors.New("left join operand must be a column name")
		}
		leftJoinCol, err := extractParseColumn(left)
		if err != nil {
			return nil, errors.Wrap(err, "extracting left join column")
		}
		// get right ColName
		right, ok := onCond.Right.(*sqlparser.ColName)
		if !ok {
			return nil, errors.New("right join operand must be a column name")
		}
		rightJoinCol, err := extractParseColumn(right)
		if err != nil {
			return nil, errors.Wrap(err, "extracting right join column")
		}

		// The primary column is set as the column referencing the "_id" field.
		var primaryColumn *parseColumn
		if leftJoinCol.name == ColID && rightJoinCol.name != ColID {
			primaryColumn = leftJoinCol
		} else if leftJoinCol.name != ColID && rightJoinCol.name == ColID {
			primaryColumn = rightJoinCol
		} else {
			return nil, errors.Errorf("exactly one join column must be %s, have: %s, %s", ColID, leftJoinCol.name, rightJoinCol.name)
		}

		// populate the joinTable column and primary fields
		for _, jc := range []*parseColumn{leftJoinCol, rightJoinCol} {
			var found bool
			for i := range tbls {
				if tbls[i].alias == jc.qualifier {
					tbls[i].column = jc
					if jc == primaryColumn {
						tbls[i].primary = true
					}
					found = true
				}
			}
			if !found {
				return nil, errors.Errorf("no tables match qualifier: %s", jc.qualifier)
			}
		}

	default:
		return nil, errors.Errorf("unsupported on condition type: %T", onCond)
	}

	return tbls, nil
}

// extractWheres returns the slice of tableWhere for the sql query.
func extractWheres(indexes []*pilosa.Index, tbls parseTables, expr sqlparser.Expr) (tableWheres, error) {
	wheres := make([]*tableWhere, 0)

	// Set the index associated with each parseTable.
	// TODO: may be able to move this to parseTables creation?
	for _, idx := range indexes {
		tbl := tbls.byName(idx.Name())
		if tbl == nil {
			return nil, errors.Errorf("index not in parseTables: %s", idx.Name())
		}
		tbl.index = idx
	}

	// make a map of tbls alias to slice index.
	m := make(map[string]int)
	for i, tbl := range tbls {
		m[tbl.alias] = i
	}

	switch e := expr.(type) {
	case *sqlparser.ComparisonExpr:
		pCol, op, val, err := extractComparison(e)
		if err != nil {
			return nil, err
		}

		pTable := tbls.byAlias(pCol.qualifier)
		if pTable == nil {
			return nil, errors.Errorf("no index for qaulifier: %s", pCol.qualifier)
		} else if pTable.index == nil {
			return nil, errors.Errorf("parse table has no index: %s", pTable.name)
		}

		field := pTable.index.Field(pCol.name)

		tw := &tableWhere{
			table: pTable,
		}

		if field.Type() == pilosa.FieldTypeInt || field.Type() == pilosa.FieldTypeTimestamp {
			if field.Type() == pilosa.FieldTypeInt {
				if _, ok := val.(int); !ok {
					return nil, errors.New("right operand must be a number")
				}
			} else { // timestamp
				if _, ok := val.(time.Time); !ok {
					return nil, errors.New("right operand must be a timestamp")
				}
			}

			switch op {
			case "=":
				tw.where = Equals(field.Name(), val)
			case "<":
				tw.where = LT(field.Name(), val)
			case "<=":
				tw.where = LTE(field.Name(), val)
			case ">":
				tw.where = GT(field.Name(), val)
			case ">=":
				tw.where = GTE(field.Name(), val)
			case "<>":
				fallthrough
			case "!=":
				tw.where = NotEquals(field.Name(), val)
			}
			return append(wheres, tw), nil
		}
		if op == "=" {
			if tw.where, err = Row(field.Name(), val); err != nil {
				return nil, err
			}
			return append(wheres, tw), nil
		}

		if op == "in" {
			var qs []string
			switch valExpr := val.(type) {
			case []interface{}:
				for _, v := range valExpr {
					r, e := Row(field.Name(), v)
					if e != nil {
						return nil, errors.Wrap(err, "extracting where statements")
					}
					qs = append(qs, r)
				}
				tw.where = Union(qs...)
				return append(wheres, tw), nil

			default:
				return nil, fmt.Errorf("in operator expects `[]interface{}` but got: %T", valExpr)
			}
		}

	case *sqlparser.AndExpr:
		left, err := extractWheres(indexes, tbls, e.Left)
		if err != nil {
			return nil, err
		}
		right, err := extractWheres(indexes, tbls, e.Right)
		if err != nil {
			return nil, err
		}

		// The following logic is used to build the where portion of the query
		// related to each table. The goal is to return one or two tableWhere
		// objects (either 0 or 1 for each table in the join).
		if len(left) == 1 && len(right) == 1 && left[0].table == right[0].table {
			// if left(1) and right(1) are from the same alias,
			// then intersect them into left and return left(1)
			left[0].where = Intersect(left[0].where, right[0].where)
			return left, nil
		} else if len(left) == 1 && len(right) == 1 && left[0].table != right[0].table {
			// if left(1) and right(1) are NOT from the same alias,
			// then return final(2)
			return []*tableWhere{left[0], right[0]}, nil
		} else if len(left) == 1 && len(right) == 2 {
			// if left(1) and right(2)
			// then intersect the 1's and return final(2)
			if left[0].table == right[0].table {
				left[0].where = Intersect(left[0].where, right[0].where)
				return []*tableWhere{left[0], right[1]}, nil
			} else if left[0].table == right[1].table {
				left[0].where = Intersect(left[0].where, right[1].where)
				return []*tableWhere{left[0], right[0]}, nil
			}
			return nil, errors.Errorf("no matching table on right: %s", left[0].table.name)
		} else if len(left) == 2 && len(right) == 1 {
			// if left(2) and right(1),
			// then intersect the 1's and return final(2)
			if right[0].table == left[0].table {
				right[0].where = Intersect(right[0].where, left[0].where)
				return []*tableWhere{left[1], right[0]}, nil
			} else if right[0].table == left[1].table {
				right[0].where = Intersect(right[0].where, left[1].where)
				return []*tableWhere{left[0], right[0]}, nil
			}
			return nil, errors.Errorf("no matching table on left: %s", right[0].table.name)
		} else if len(left) == 2 && len(right) == 2 {
			// if left(2) and right(2)
			// then intsect both and return final(2)
			if left[0].table == right[0].table && left[1].table == right[1].table {
				left[0].where = Intersect(left[0].where, right[0].where)
				left[1].where = Intersect(left[1].where, right[1].where)
				return left, nil
			} else if left[0].table == right[1].table && left[1].table == right[0].table {
				left[0].where = Intersect(left[0].where, right[1].where)
				left[1].where = Intersect(left[1].where, right[0].where)
				return left, nil
			}
			return nil, errors.Errorf("non-matching tables: %s/%s, %s/%s", left[0].table.name, left[1].table.name, right[0].table.name, right[1].table.name)
		}
		return nil, errors.Errorf("invalid table count; expected 1 or 2, but got: %d, %d", len(left), len(right))
	case *sqlparser.OrExpr:
		left, err := extractWheres(indexes, tbls, e.Left)
		if err != nil {
			return nil, err
		}
		right, err := extractWheres(indexes, tbls, e.Right)
		if err != nil {
			return nil, err
		}

		if len(left) == 1 && len(right) == 1 && left[0].table == right[0].table {
			// if left(1) and right(1) are from the same alias,
			// then union them and return final(1)
			left[0].where = Union(left[0].where, right[0].where)
			return left, nil
		}
		return nil, errors.Errorf("invalid table count; expected 1/1, but got: %d/%d", len(left), len(right))
	case *sqlparser.NotExpr:
		expr, err := extractWheres(indexes, tbls, e.Expr)
		if err != nil {
			return nil, err
		}
		if len(expr) == 1 {
			expr[0].where = Not(expr[0].where)
			return expr, nil
		}
		return nil, errors.Errorf("not support a single expression. got: %d", len(expr))
	case *sqlparser.ParenExpr:
		expr, err := extractWheres(indexes, tbls, e.Expr)
		if err != nil {
			return nil, err
		}
		if len(expr) == 1 {
			return expr, nil
		}
		return nil, errors.Errorf("not support a single expression. got: %d", len(expr))
	case *sqlparser.RangeCond:
		if e.Operator != "between" {
			return nil, errors.New("only between is supported")
		}
		left, ok := e.Left.(*sqlparser.ColName)
		if !ok {
			return nil, errors.New("left operand must be a column name")
		}

		pCol, err := extractParseColumn(left)
		if err != nil {
			return nil, errors.Wrap(err, "extracting parse column")
		}

		pTable := tbls.byAlias(pCol.qualifier)
		if pTable == nil {
			return nil, errors.Errorf("no index for qaulifier: %s", pCol.qualifier)
		} else if pTable.index == nil {
			return nil, errors.Errorf("parse table has no index: %s", pTable.name)
		}

		fieldName, isSpecial := ExtractFieldName(pCol.name)
		if isSpecial {
			return nil, errors.New("special fields are not allowed here")
		}

		tw := &tableWhere{
			table: pTable,
		}

		field := pTable.index.Field(fieldName)
		if field.Type() == pilosa.FieldTypeInt {
			fromNum, err := extractInt(e.From)
			if err != nil {
				return nil, err
			}
			toNum, err := extractInt(e.To)
			if err != nil {
				return nil, err
			}
			tw.where = Between(field.Name(), fromNum, toNum)
			return append(wheres, tw), nil
		}
		if field.Type() == pilosa.FieldTypeTimestamp {
			fromTime, err := extractTimestamp(e.From)
			if err != nil {
				return nil, err
			}
			toTime, err := extractTimestamp(e.To)
			if err != nil {
				return nil, err
			}
			tw.where = Between(field.Name(), fromTime, toTime)
			return append(wheres, tw), nil
		}
		return nil, errors.New("only int or timestamp fields are supported")
	case *sqlparser.IsExpr:
		left, ok := e.Expr.(*sqlparser.ColName)
		if !ok {
			return nil, errors.New("left operand must be a column name")
		}

		pCol, err := extractParseColumn(left)
		if err != nil {
			return nil, errors.Wrap(err, "extracting parse column")
		}

		pTable := tbls.byAlias(pCol.qualifier)
		if pTable == nil {
			return nil, errors.Errorf("no index for qaulifier: %s", pCol.qualifier)
		} else if pTable.index == nil {
			return nil, errors.Errorf("parse table has no index: %s", pTable.name)
		}

		tw := &tableWhere{
			table: pTable,
		}

		field := pTable.index.Field(pCol.name)
		if field.Type() == pilosa.FieldTypeInt || field.Type() == pilosa.FieldTypeTimestamp {
			if e.Operator == "is not null" {
				tw.where = NotNull(field.Name())
				return append(wheres, tw), nil
			}
			return nil, fmt.Errorf("only `is not null` is supported for %s fields", field.Type())
		}
		return nil, fmt.Errorf("`is` expression is supported only for %s fields", field.Type())
	}
	return nil, errors.New("cannot extract where")
}
