//go:build plan9
// +build plan9

// Copyright 2025 The TCell Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package tty

import (
	"bufio"
	"errors"
	"fmt"
	"io"
	"os"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
)

// p9Tty implements tcell.Tty using Plan 9's /dev/cons and /dev/consctl.
// Raw mode is enabled by writing "rawon" to /dev/consctl while the fd stays open.
// Resize notifications are read from /dev/wctl: the first read returns geometry,
// subsequent reads block until the window changes (rio(4)).
//
// References:
// - kbdfs(8): cons/consctl rawon|rawoff semantics
// - rio(4): wctl geometry and blocking-on-change behavior
// - vt(1): VT100 emulator typically used for TUI programs on Plan 9
//
// Limitations:
//   - We assume VT100-level capabilities (often no colors, no mouse).
//   - Window size is conservative: we return 80x24 unless overridden.
//     Set LINES/COLUMNS (or TCELL_LINES/TCELL_COLS) to refine.
//   - Mouse and bracketed paste are not wired; terminfo/xterm queries
//     are not attempted because vt(1) may not support them.
type p9Tty struct {
	cons    *os.File // /dev/cons (read+write)
	consctl *os.File // /dev/consctl (write "rawon"/"rawoff")
	wctl    *os.File // /dev/wctl (resize notifications)

	closed  atomic.Bool
	started bool

	onResize atomic.Value // resize channel
	wg       sync.WaitGroup
	stopCh   chan struct{}
}

func NewDevTty() (Tty, error) { // tcell signature
	return newPlan9TTY()
}

func NewStdIoTty() (Tty, error) { // also required by tcell
	// On Plan 9 there is no POSIX tty discipline on stdin/stdout;
	// use /dev/cons explicitly for robustness.
	return newPlan9TTY()
}

func NewDevTtyFromDev(_ string) (Tty, error) { // required by tcell
	// Plan 9 does not have multiple "ttys" in the POSIX sense;
	// always bind to /dev/cons and /dev/consctl.
	return newPlan9TTY()
}

func newPlan9TTY() (Tty, error) {
	cons, err := os.OpenFile("/dev/cons", os.O_RDWR, 0)
	if err != nil {
		return nil, fmt.Errorf("open /dev/cons: %w", err)
	}
	consctl, err := os.OpenFile("/dev/consctl", os.O_WRONLY, 0)
	if err != nil {
		_ = cons.Close()
		return nil, fmt.Errorf("open /dev/consctl: %w", err)
	}
	// /dev/wctl may not exist (console without rio); best-effort.
	wctl, _ := os.OpenFile("/dev/wctl", os.O_RDWR, 0)

	t := &p9Tty{
		cons:    cons,
		consctl: consctl,
		wctl:    wctl,
		stopCh:  make(chan struct{}),
	}
	return t, nil
}

func (t *p9Tty) Start() error {

	if t.started {
		return nil
	}
	if t.closed.Load() {
		return errors.New("tty closed")
	}

	// Recreate stop channel if absent or closed (supports resume).
	if t.stopCh == nil || isClosed(t.stopCh) {
		t.stopCh = make(chan struct{})
	}

	// Put console into raw mode; remains active while consctl is open.
	if _, err := t.consctl.Write([]byte("rawon")); err != nil {
		return fmt.Errorf("enable raw mode: %w", err)
	}

	// Reopen /dev/wctl on resume; best-effort (system console may lack it).
	if t.wctl == nil {
		if f, err := os.OpenFile("/dev/wctl", os.O_RDWR, 0); err == nil {
			t.wctl = f
		}
	}

	if t.wctl != nil {
		t.wg.Add(1)
		go t.watchResize()
	}
	t.started = true
	return nil
}

func (t *p9Tty) Drain() error {
	// Per tcell docs, this may reasonably be a no-op on non-POSIX ttys.
	// Read deadlines are not available on plan9 os.File; we rely on Stop().
	return nil
}

func (t *p9Tty) Stop() error {

	// Signal watcher to stop (if not already).
	if t.stopCh != nil && !isClosed(t.stopCh) {
		close(t.stopCh)
	}

	// Exit raw mode first.
	_, _ = t.consctl.Write([]byte("rawoff"))

	// Closing wctl unblocks watchResize; nil it so Start() can reopen later.
	if t.wctl != nil {
		_ = t.wctl.Close()
		t.wctl = nil
	}

	// Ensure watcher goroutine has exited before returning.
	t.wg.Wait()
	t.started = false
	return nil
}

func (t *p9Tty) Close() error {

	if t.closed.Swap(true) {
		return nil
	}

	if t.stopCh != nil && !isClosed(t.stopCh) {
		close(t.stopCh)
	}
	_, _ = t.consctl.Write([]byte("rawoff"))

	_ = t.cons.Close()
	_ = t.consctl.Close()
	if t.wctl != nil {
		_ = t.wctl.Close()
		t.wctl = nil
	}

	t.wg.Wait()
	return nil
}

func (t *p9Tty) Read(p []byte) (int, error) {
	return t.cons.Read(p)
}

func (t *p9Tty) Write(p []byte) (int, error) {
	return t.cons.Write(p)
}

func (t *p9Tty) NotifyResize(resizeQ chan<- bool) {
	t.onResize.Store(resizeQ)
}

func (t *p9Tty) WindowSize() (WindowSize, error) {
	// Strategy:
	// 1) honor explicit overrides (TCELL_LINES/TCELL_COLS, LINES/COLUMNS),
	// 2) otherwise return conservative 80x24.
	// Reading /dev/wctl gives pixel geometry, but char cell metrics are
	// not generally available to non-draw clients; vt(1) is fixed-cell.
	lines, cols := envInt("TCELL_LINES"), envInt("TCELL_COLS")
	if lines == 0 {
		lines = envInt("LINES")
	}
	if cols == 0 {
		cols = envInt("COLUMNS")
	}
	if lines <= 0 {
		lines = 24
	}
	if cols <= 0 {
		cols = 80
	}
	return WindowSize{Width: cols, Height: lines}, nil
}

// watchResize blocks on /dev/wctl reads; each read returns when the window
// changes size/position/state, per rio(4). We ignore the parsed geometry and
// just notify tcell to re-query WindowSize().
func (t *p9Tty) watchResize() {
	defer t.wg.Done()

	r := bufio.NewReader(t.wctl)
	for {
		select {
		case <-t.stopCh:
			return
		default:
		}
		// Each read delivers something like:
		// "   minx        miny        maxx        maxy   visible current\n"
		// We don't need to parse here; just signal.
		_, err := r.ReadString('\n')
		if err != nil {
			if errors.Is(err, io.EOF) {
				return
			}
			// transient errors: continue
		}
		if rq, ok := t.onResize.Load().(chan<- bool); ok && rq != nil {
			select {
			case rq <- true:
			default:
			}
		}
	}
}

func envInt(name string) int {
	if s := strings.TrimSpace(os.Getenv(name)); s != "" {
		if v, err := strconv.Atoi(s); err == nil {
			return v
		}
	}
	return 0
}

// helper: safe check if a channel is closed
func isClosed(ch <-chan struct{}) bool {
	select {
	case <-ch:
		return true
	default:
		return false
	}
}
