package database

import (
	"strings"
	"time"
)

// Book metadata
type Book struct {
	ID          string
	Title       string
	Authors     []string `sql:"authors" pg:",array"`
	Contributor string
	Publisher   string
	Description string
	Tags        []string `sql:"tags" pg:",array"`
	Date        string
	Lang        string
	Isbn        string
	FileSize    int
	Cover       bool `sql:",notnull"`
	Active      bool `sql:",notnull"`
	UploadDate  time.Time
	Tsv         string
}

// 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(?))")
		rankParams = append(rankParams, textQuery)
		searchCondition += " AND to_tsquery(?) @@ 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).
		SelectAndCountEstimate(100)
	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 + " = ?"
		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) {
	words := strings.Split(query, " ")
	for _, w := range words {
		if w == "" {
			continue
		}
		tag := strings.SplitN(w, ":", 2)
		if len(tag) > 1 && 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":
				// TODO how do we do trigram on arrays??
				trigramQuerys = append(trigramQuerys, columnq{"array_to_string(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{"array_to_string(tags, ' ')", value})
			case "tag":
				trigramQuerys = append(trigramQuerys, columnq{"array_to_string(tags, ' ')", value})
			default:
				if len(textQuery) != 0 {
					textQuery += " | "
				}
				textQuery += w
			}
		} else {
			if len(textQuery) != 0 {
				lastChar := textQuery[len(textQuery)-1:]
				if w != "&" && w != "|" && lastChar != "&" && lastChar != "|" {
					textQuery += " | "
				} else {
					textQuery += " "
				}
			}
			textQuery += w
		}
	}
	return
}