package console

import (
	"fmt"
	"strings"
	"sync"

	"github.com/reeflective/readline"
	"github.com/reeflective/readline/inputrc"
)

// Console is an integrated console application instance.
type Console struct {
	// Application
	name          string           // Used in the prompt, and for readline `.inputrc` application-specific settings.
	shell         *readline.Shell  // Provides readline functionality (inputs, completions, hints, history)
	printLogo     func(c *Console) // Simple logo printer.
	cmdHighlight  string           // Ansi code for highlighting of command in default highlighter. Green by default.
	flagHighlight string           // Ansi code for highlighting of flag in default highlighter. Grey by default.
	menus         map[string]*Menu // Different command trees, prompt engines, etc.
	filters       []string         // Hide commands based on their attributes and current context.
	isExecuting   bool             // Used by log functions, which need to adapt behavior (print the prompt, etc.)
	printed       bool             // Used to adjust asynchronous messages too.
	mutex         *sync.RWMutex    // Concurrency management.

	// Execution

	// Leave an empty line before executing the command.
	NewlineBefore bool

	// Leave an empty line after executing the command.
	// Note that if you also want this newline to be used when logging messages
	// with TransientPrintf(), Printf() calls, you should leave this to false,
	// and add a leading newline to your prompt instead: the readline shell will
	// know how to handle it in all situations.
	NewlineAfter bool

	// Leave empty lines with NewlineBefore and NewlineAfter, even if the provided input was empty.
	// Empty characters are defined as any number of spaces and tabs. The 'empty' character set
	// can be changed by modifying Console.EmptyChars
	// This field is false by default.
	NewlineWhenEmpty bool

	// Characters that are used to determine whether an input line was empty. If a line is not entirely
	// made up by any of these characters, then it is not considered empty. The default characters
	// are ' ' and '\t'.
	EmptyChars []rune

	// PreReadlineHooks - All the functions in this list will be executed,
	// in their respective orders, before the console starts reading
	// any user input (ie, before redrawing the prompt).
	PreReadlineHooks []func() error

	// PreCmdRunLineHooks - Same as PreCmdRunHooks, but will have an effect on the
	// input line being ultimately provided to the command parser. This might
	// be used by people who want to apply supplemental, specific processing
	// on the command input line.
	PreCmdRunLineHooks []func(args []string) ([]string, error)

	// PreCmdRunHooks - Once the user has entered a command, but before executing
	// the target command, the console will execute every function in this list.
	// These hooks are distinct from the cobra.PreRun() or OnInitialize hooks,
	// and might be used in combination with them.
	PreCmdRunHooks []func() error

	// PostCmdRunHooks are run after the target cobra command has been executed.
	// These hooks are distinct from the cobra.PreRun() or OnFinalize hooks,
	// and might be used in combination with them.
	PostCmdRunHooks []func() error
}

// New - Instantiates a new console application, with sane but powerful defaults.
// This instance can then be passed around and used to bind commands, setup additional
// things, print asynchronous messages, or modify various operating parameters on the fly.
// The app parameter is an optional name of the application using this console.
func New(app string) *Console {
	console := &Console{
		name:  app,
		shell: readline.NewShell(inputrc.WithApp(strings.ToLower(app))),
		menus: make(map[string]*Menu),
		mutex: &sync.RWMutex{},
	}

	// Quality of life improvements.
	console.setupShell()

	// Make a default menu and make it current.
	// Each menu is created with a default prompt engine.
	defaultMenu := console.NewMenu("")
	defaultMenu.active = true

	// Set the history for this menu
	for _, name := range defaultMenu.historyNames {
		console.shell.History.Add(name, defaultMenu.histories[name])
	}

	// Syntax highlighting, multiline callbacks, etc.
	console.cmdHighlight = seqFgGreen
	console.flagHighlight = seqBrightWigth
	console.shell.AcceptMultiline = console.acceptMultiline
	console.shell.SyntaxHighlighter = console.highlightSyntax

	// Completion
	console.shell.Completer = console.complete
	console.defaultStyleConfig()

	// Defaults
	console.EmptyChars = []rune{' ', '\t'}
	return console
}

// Shell returns the console readline shell instance, so that the user can
// further configure it or use some of its API for lower-level stuff.
func (c *Console) Shell() *readline.Shell {
	return c.shell
}

// SetPrintLogo - Sets the function that will be called to print the logo.
func (c *Console) SetPrintLogo(f func(c *Console)) {
	c.printLogo = f
}

// NewMenu - Create a new command menu, to which the user
// can attach any number of commands (with any nesting), as
// well as some specific items like history sources, prompt
// configurations, sets of expanded variables, and others.
func (c *Console) NewMenu(name string) *Menu {
	c.mutex.RLock()
	defer c.mutex.RUnlock()
	menu := newMenu(name, c)
	c.menus[name] = menu

	return menu
}

// ActiveMenu - Return the currently used console menu.
func (c *Console) ActiveMenu() *Menu {
	c.mutex.Lock()
	defer c.mutex.Unlock()

	return c.activeMenu()
}

// Menu returns one of the console menus by name, or nil if no menu is found.
func (c *Console) Menu(name string) *Menu {
	c.mutex.Lock()
	defer c.mutex.Unlock()

	return c.menus[name]
}

// SwitchMenu - Given a name, the console switches its command menu:
// The next time the console rebinds all of its commands, it will only bind those
// that belong to this new menu. If the menu is invalid, i.e that no commands
// are bound to this menu name, the current menu is kept.
func (c *Console) SwitchMenu(menu string) {
	c.mutex.Lock()
	target, found := c.menus[menu]
	c.mutex.Unlock()

	if found && target != nil {
		// Only switch if the target menu was found.
		current := c.activeMenu()
		if current != nil && target == current {
			return
		}

		if current != nil {
			current.active = false
		}

		target.active = true

		// Remove the currently bound history sources
		// (old menu) and bind the ones peculiar to this one.
		c.shell.History.Delete()

		for _, name := range target.historyNames {
			c.shell.History.Add(name, target.histories[name])
		}

		// Regenerate the commands, outputs and everything related.
		target.resetPreRun()
	}
}

// TransientPrintf prints a string message (a log, or more broadly, an asynchronous event)
// without bothering the user, displaying the message and "pushing" the prompt below it.
// The message is printed regardless of the current menu.
//
// If this function is called while a command is running, the console will simply print the log
// below the line, and will not print the prompt. In any other case this function works normally.
func (c *Console) TransientPrintf(msg string, args ...any) (n int, err error) {
	if c.isExecuting {
		return fmt.Printf(msg, args...)
	}

	// If the last message we printed asynchronously
	// immediately precedes this new message, move up
	// another row, so we don't waste too much space.
	if c.printed && c.NewlineAfter {
		fmt.Print("\x1b[1A")
	}

	if c.NewlineAfter {
		msg += "\n"
	}

	c.printed = true

	return c.shell.PrintTransientf(msg, args...)
}

// Printf prints a string message (a log, or more broadly, an asynchronous event)
// below the current prompt. The message is printed regardless of the current menu.
//
// If this function is called while a command is running, the console will simply print the log
// below the line, and will not print the prompt. In any other case this function works normally.
func (c *Console) Printf(msg string, args ...any) (n int, err error) {
	if c.isExecuting {
		return fmt.Printf(msg, args...)
	}

	return c.shell.Printf(msg, args...)
}

// SystemEditor - This function is a renamed-reexport of the underlying readline.StartEditorWithBuffer
// function, which enables you to conveniently edit files/buffers from within the console application.
// Naturally, the function will block until the editor is exited, and the updated buffer is returned.
// The filename parameter can be used to pass a specific filename.ext pattern, which might be useful
// if the editor has builtin filetype plugin functionality.
func (c *Console) SystemEditor(buffer []byte, filetype string) ([]byte, error) {
	emacs := c.shell.Config.GetString("editing-mode") == "emacs"

	edited, err := c.shell.Buffers.EditBuffer([]rune(string(buffer)), "", filetype, emacs)

	return []byte(string(edited)), err
}

func (c *Console) setupShell() {
	cfg := c.shell.Config

	// Some options should be set to on because they
	// are quite neceessary for efficient console use.
	cfg.Set("skip-completed-text", true)
	cfg.Set("menu-complete-display-prefix", true)
}

func (c *Console) activeMenu() *Menu {
	for _, menu := range c.menus {
		if menu.active {
			return menu
		}
	}

	// Else return the default menu.
	return c.menus[""]
}