package database

import (
	"strings"
	"time"

	"github.com/go-pg/pg"
)

// Book metadata
type Book struct {
	ID          string `sql:"type:varchar(16)"`
	Title       string
	Authors     []string `sql:"authors" pg:",array"`
	Contributor string
	Publisher   string
	Description string
	Tags        []string `sql:"tags" pg:",array"`
	Date        string
	Lang        string    `sql:"type:varchar(3)"`
	Isbn        string    `sql:"type:varchar(13)"`
	FileSize    int       `sql:"type:integer"`
	Cover       bool      `sql:",notnull"`
	Active      bool      `sql:",notnull"`
	UploadDate  time.Time `sql:"type:timestamp"`
	Tsv         string    `sql:"type:tsvector"`
	Visit       *Visit
}

// AddBook to the database
func (db *pgDB) AddBook(book Book) error {
	emptyTime := time.Time{}
	if book.UploadDate == emptyTime {
		book.UploadDate = time.Now()
	}

	return db.sql.Insert(&book)
}

// GetBooks matching query
func (db *pgDB) GetBooks(query string, length int, start int) (books []Book, num int, err error) {
	return db.getBooks(true, query, length, start)
}

// GetNewBooks returns a list of books in the incoming queue and the number of books
// in the queue
func (db *pgDB) GetNewBooks(query string, length int, start int) (books []Book, num int, err error) {
	return db.getBooks(false, query, length, start)
}

func (db *pgDB) getBooks(active bool, query string, length int, start int) (books []Book, num int, err error) {
	rank := []string{}
	rankParams := []interface{}{}
	searchCondition := ""
	if active {
		searchCondition += "active is true"
	} else {
		searchCondition += "active is not true"
	}
	searchParams := []interface{}{}

	textQuery, columnQuerys, trigramQuerys := buildQuery(query)
	for _, c := range columnQuerys {
		searchCondition += " AND " + c.column + " = ?"
		searchParams = append(searchParams, c.value)
	}
	for _, c := range trigramQuerys {
		rank = append(rank, "word_similarity(?, "+c.column+")")
		rankParams = append(rankParams, c.value)
		searchCondition += " AND " + c.column + " %> ?"
		searchParams = append(searchParams, c.value)
	}
	if textQuery != "" {
		rank = append(rank, "ts_rank(tsv, to_tsquery_multilingual(?))")
		rankParams = append(rankParams, textQuery)
		searchCondition += " AND to_tsquery_multilingual(?) @@ tsv"
		searchParams = append(searchParams, textQuery)
	}

	order := "upload_date DESC"
	if len(rank) > 0 {
		order = strings.Join(rank, "+") + " DESC, upload_date DESC"
	}

	num, err = db.sql.Model(&books).
		Where(searchCondition, searchParams...).
		OrderExpr(order, rankParams...).
		Offset(start).
		Limit(length).
		SelectAndCount()
	return books, num, err
}

// GetBookID returns a the book with the specified id
func (db *pgDB) GetBookID(id string) (Book, error) {
	var book Book
	err := db.sql.Model(&book).
		Where("id = ?", id).
		Select()
	return book, err
}

// DeleteBook removes the book with id from the database
func (db *pgDB) DeleteBook(id string) error {
	_, err := db.sql.Model(&Book{}).
		Where("id = ?", id).
		Delete()
	return err
}

// UpdateBook metadata
func (db *pgDB) UpdateBook(id string, data map[string]interface{}) error {
	setCondition := ""
	params := []interface{}{}
	for col, val := range data {
		colValid := false
		for _, name := range []string{"title", "authors", "contributor", "publisher",
			"description", "tags", "date", "lang", "isbn"} {
			if col == name {
				colValid = true
				break
			}
		}
		if !colValid {
			continue
		}

		if len(setCondition) != 0 {
			setCondition += ", "
		}
		setCondition += col + " = ?"
		if col == "authors" || col == "tags" {
			params = append(params, pg.Array(val))
		} else {
			params = append(params, val)
		}
	}
	_, err := db.sql.Model(&Book{}).
		Set(setCondition, params...).
		Where("id = ?", id).
		Update()
	return err
}

// ActiveBook activates the book
func (db *pgDB) ActiveBook(id string) error {
	uploadDate := time.Now()
	_, err := db.sql.Model(&Book{}).
		Set("active = true, upload_date = ? ", uploadDate).
		Where("id = ?", id).
		Update()
	return err
}

// IsBookActive checks if the book is active
func (db *pgDB) IsBookActive(id string) bool {
	var active []bool
	err := db.sql.Model(&Book{}).
		Column("active").
		Where("id = ?", id).
		Select(&active)
	if err != nil || len(active) != 1 {
		return false
	}
	return active[0]
}

type columnq struct {
	column string
	value  string
}

func buildQuery(query string) (textQuery string, columnQuerys []columnq, trigramQuerys []columnq) {
	tokens := extractTokens(query)
	for _, token := range tokens {
		if token == "" {
			continue
		}
		tag := strings.SplitN(token, ":", 2)
		value := ""
		if len(tag) > 1 {
			value = strings.Replace(tag[1], "%", "\\%", 0)
			value = strings.Replace(value, "_", "\\_", 0)
		}

		switch tag[0] {
		case "lang":
			columnQuerys = append(columnQuerys, columnq{"lang", value})
		case "isbn":
			columnQuerys = append(columnQuerys, columnq{"isbn", value})
		case "author":
			trigramQuerys = append(trigramQuerys, columnq{"text(authors)", value})
		case "title":
			trigramQuerys = append(trigramQuerys, columnq{"title", value})
		case "contributor":
			trigramQuerys = append(trigramQuerys, columnq{"contributor", value})
		case "publisher":
			trigramQuerys = append(trigramQuerys, columnq{"publisher", value})
		case "subject":
			trigramQuerys = append(trigramQuerys, columnq{"text(tags)", value})
		case "tag":
			trigramQuerys = append(trigramQuerys, columnq{"text(tags)", value})
		default:
			if len(textQuery) != 0 {
				lastChar := textQuery[len(textQuery)-1:]
				if token[:1] != "&" && token[:1] != "|" && lastChar != "&" && lastChar != "|" {
					textQuery += " | "
				} else {
					textQuery += " "
				}
			}
			textQuery += strings.Join(strings.Fields(token), " <-> ")
		}
	}
	return
}

func extractTokens(query string) []string {
	tokens := []string{}
	quoted := strings.Split(query, "\"")
	for i, s := range quoted {
		if i%2 == 0 {
			tokens = append(tokens, strings.Fields(s)...)
		} else {
			// quoted string
			if len(tokens) > 0 {
				lastToken := tokens[len(tokens)-1]
				if len(lastToken) > 0 && lastToken[len(lastToken)-1] == ':' {
					tokens[len(tokens)-1] += s
					continue
				}
			}
			tokens = append(tokens, s)
		}
	}
	return tokens
}