package arn

import (
	"errors"
	"fmt"
	"sort"

	"github.com/aerogo/nano"
)

// Person represents a person in real life.
type Person struct {
	Name  PersonName `json:"name" editable:"true"`
	Image Image      `json:"image"`

	hasID
	hasPosts
	hasCreator
	hasEditor
	hasLikes
	hasDraft
}

// NewPerson creates a new person.
func NewPerson() *Person {
	return &Person{
		hasID: hasID{
			ID: GenerateID("Person"),
		},
		hasCreator: hasCreator{
			Created: DateTimeUTC(),
		},
	}
}

// Link returns the path to the person.
func (person *Person) Link() string {
	return "/person/" + person.ID
}

// TitleByUser returns the preferred title for the given user.
func (person *Person) TitleByUser(user *User) string {
	return person.Name.ByUser(user)
}

// String returns the default display name for the person.
func (person *Person) String() string {
	return person.Name.ByUser(nil)
}

// TypeName returns the type name.
func (person *Person) TypeName() string {
	return "Person"
}

// Self returns the object itself.
func (person *Person) Self() Loggable {
	return person
}

// ImageLink ...
func (person *Person) ImageLink(size string) string {
	extension := ".jpg"

	if size == "original" {
		extension = person.Image.Extension
	}

	return fmt.Sprintf("//%s/images/persons/%s/%s%s?%v", MediaHost, size, person.ID, extension, person.Image.LastModified)
}

// Publish publishes the person draft.
func (person *Person) Publish() error {
	// No name
	if person.Name.ByUser(nil) == "" {
		return errors.New("No person name")
	}

	// No image
	if !person.HasImage() {
		return errors.New("No person image")
	}

	return publish(person)
}

// Unpublish turns the person into a draft.
func (person *Person) Unpublish() error {
	return unpublish(person)
}

// HasImage returns true if the person has an image.
func (person *Person) HasImage() bool {
	return person.Image.Extension != "" && person.Image.Width > 0
}

// GetPerson ...
func GetPerson(id ID) (*Person, error) {
	obj, err := DB.Get("Person", id)

	if err != nil {
		return nil, err
	}

	return obj.(*Person), nil
}

// DeleteImages deletes all images for the person.
func (person *Person) DeleteImages() {
	deleteImages("persons", person.ID, person.Image.Extension)
}

// SortPersonsByLikes sorts the given slice of persons by the amount of likes.
func SortPersonsByLikes(persons []*Person) {
	sort.Slice(persons, func(i, j int) bool {
		aLikes := len(persons[i].Likes)
		bLikes := len(persons[j].Likes)

		if aLikes == bLikes {
			return persons[i].Name.English.First < persons[j].Name.English.First
		}

		return aLikes > bLikes
	})
}

// StreamPersons returns a stream of all persons.
func StreamPersons() <-chan *Person {
	channel := make(chan *Person, nano.ChannelBufferSize)

	go func() {
		for obj := range DB.All("Person") {
			channel <- obj.(*Person)
		}

		close(channel)
	}()

	return channel
}

// FilterPersons filters all persons by a custom function.
func FilterPersons(filter func(*Person) bool) []*Person {
	var filtered []*Person

	channel := DB.All("Person")

	for obj := range channel {
		realObject := obj.(*Person)

		if filter(realObject) {
			filtered = append(filtered, realObject)
		}
	}

	return filtered
}

// AllPersons returns a slice of all persons.
func AllPersons() []*Person {
	all := make([]*Person, 0, DB.Collection("Person").Count())

	stream := StreamPersons()

	for obj := range stream {
		all = append(all, obj)
	}

	return all
}