package pio

import (
	"errors"
	"io"
	"sort"
	"sync"
)

// Registry is the Printer(s) container.
//
// It can be used as follows:
// reg := NewRegistry().
//        RegisterPrinter(NewPrinter("err", os.Stderr)).
//        RegisterPrinter(NewPrinter("default", os.Stdout)).
//        Print("something")
type Registry struct {
	// can change via `Register` or `RegisterPrinter` with mutex.
	// whenever a tool needs an `io.Writer` to do something
	// end-developers can pass this `Printer`.
	printers []*Printer
	mu       sync.Mutex
	once     sync.Once
}

// NewRegistry returns an empty printer Registry.
//
// Note that:
// Registry have a zero value, so it can be
// declared with a simple `var` keyword and without pointer.
func NewRegistry() *Registry {
	return new(Registry)
}

// RegisterPrinter registers an already-created printer to the
// registry.
//
// If `Printer#Name` is empty then it will be filled with
// "printer_$printers.len".
//
// If a printer with the same `Printer#Name` is already
// registered then it will be overridden by
// this new "printer".
//
// Returns this Registry, therefore it can be used as builder.
func (reg *Registry) RegisterPrinter(printer *Printer) *Registry {
	// if exists then remove first and then add the new one.
	if printerName := printer.Name; reg.Get(printerName) != nil {
		reg.Remove(printerName)
	}
	reg.mu.Lock()
	// no printer.Handle(s.handlers...)
	reg.printers = append(reg.printers, printer)
	reg.mu.Unlock()
	return reg
}

// Register creates and registers a new Printer
// based on a name(string) and an "output"(io.Writer).
//
// If "printerName" is empty then it will be filled with
// "printer_$printers.len".
//
// If a printer with the same `Printer#Name` is already
// registered then it will be overridden by
// this new "printer".
//
// Look `OutputFrom` too.
//
// Returns the just created Printer.
func (reg *Registry) Register(printerName string, output io.Writer) *Printer {
	p := NewPrinter(printerName, output)
	reg.RegisterPrinter(p)
	return p
}

// Get returns a Printer based on the "printerName".
// If printer with this name can't be found then
// this function will return nil, so a check for
// nil is always a good practice.
func (reg *Registry) Get(printerName string) *Printer {
	reg.mu.Lock()
	defer reg.mu.Unlock()
	for _, p := range reg.printers {
		if p.Name == printerName {
			return p
		}
	}
	return nil
}

// Remove deletes a printer item from the printers collection
// by its name.
//
// Returns this Registry, so it can be used as builder.
func (reg *Registry) Remove(printerName string) *Registry {
	reg.mu.Lock()
	for i, p := range reg.printers {
		if p.Name == printerName {
			reg.printers = append(reg.printers[:i], reg.printers[i+1:]...)
			break
		}
	}
	reg.mu.Unlock()
	return reg
}

// Print accepts a value of "v",
// tries to marshal its contents and flushes the result
// to all available printers.
func (reg *Registry) Print(v interface{}) (n int, err error) {
	return reg.printAll(v, false)
}

// Println accepts a value of "v",
// tries to marshal its contents and flushes the result
// to all available printers, it adds a new line at the ending,
// the result doesn't contain this new line, therefore result's contents kept as expected.
func (reg *Registry) Println(v interface{}) (n int, err error) {
	return reg.printAll(v, true)
}

func (reg *Registry) printAll(v interface{}, appendNewLine bool) (n int, err error) {
	// order once at first print.
	reg.once.Do(func() {
		reg.mu.Lock()
		sort.Slice(reg.printers, func(i, j int) bool {
			return reg.printers[i].priority > reg.printers[j].priority
		})
		reg.mu.Unlock()
	})

	for _, p := range reg.printers {
		prevErr := err

		printFunc := p.Print
		if appendNewLine {
			printFunc = p.Println
		}

		n, err = printFunc(v)

		if !p.Chained && n > 0 {
			break
		}
		n, err = combineOutputResult(n, err, prevErr)
	}
	return
}

func combineOutputResult(n int, err error, prevErr error) (totalN int, totalErr error) {
	if err != nil {
		if prevErr != nil {
			totalErr = errors.New(prevErr.Error() + string(NewLine) + err.Error())
		}
	}

	totalN += n
	return
}

// Scan scans everything from "r" and prints
// its new contents to the printers,
// forever or until the returning "cancel" is fired, once.
func (reg *Registry) Scan(r io.Reader, addNewLine bool) (cancel func()) {
	lp := len(reg.printers)
	if lp == 0 {
		return func() {}
	}

	cancelFuncs := make([]func(), lp, lp)
	cancel = func() {
		for _, c := range cancelFuncs {
			c()
		}
	}

	for i, p := range reg.printers {
		cancelFuncs[i] = p.Scan(r, addNewLine)
	}

	return cancel
}

func (reg *Registry) restore(b []byte) {
	for _, p := range reg.printers {
		p.restore(b)
	}
}
