package readline

import (
	"bufio"
	"bytes"
	"encoding/binary"
	"fmt"
	"io"
	"net"
	"os"
	"sync"
	"sync/atomic"
)

type MsgType int16

const (
	T_DATA = MsgType(iota)
	T_WIDTH
	T_WIDTH_REPORT
	T_ISTTY_REPORT
	T_RAW
	T_ERAW // exit raw
	T_EOF
)

type RemoteSvr struct {
	eof           int32
	closed        int32
	width         int32
	reciveChan    chan struct{}
	writeChan     chan *writeCtx
	conn          net.Conn
	isTerminal    bool
	funcWidthChan func()
	stopChan      chan struct{}

	dataBufM sync.Mutex
	dataBuf  bytes.Buffer
}

type writeReply struct {
	n   int
	err error
}

type writeCtx struct {
	msg   *Message
	reply chan *writeReply
}

func newWriteCtx(msg *Message) *writeCtx {
	return &writeCtx{
		msg:   msg,
		reply: make(chan *writeReply),
	}
}

func NewRemoteSvr(conn net.Conn) (*RemoteSvr, error) {
	rs := &RemoteSvr{
		width:      -1,
		conn:       conn,
		writeChan:  make(chan *writeCtx),
		reciveChan: make(chan struct{}),
		stopChan:   make(chan struct{}),
	}
	buf := bufio.NewReader(rs.conn)

	if err := rs.init(buf); err != nil {
		return nil, err
	}

	go rs.readLoop(buf)
	go rs.writeLoop()
	return rs, nil
}

func (r *RemoteSvr) init(buf *bufio.Reader) error {
	m, err := ReadMessage(buf)
	if err != nil {
		return err
	}
	// receive isTerminal
	if m.Type != T_ISTTY_REPORT {
		return fmt.Errorf("unexpected init message")
	}
	r.GotIsTerminal(m.Data)

	// receive width
	m, err = ReadMessage(buf)
	if err != nil {
		return err
	}
	if m.Type != T_WIDTH_REPORT {
		return fmt.Errorf("unexpected init message")
	}
	r.GotReportWidth(m.Data)

	return nil
}

func (r *RemoteSvr) HandleConfig(cfg *Config) {
	cfg.Stderr = r
	cfg.Stdout = r
	cfg.Stdin = r
	cfg.FuncExitRaw = r.ExitRawMode
	cfg.FuncIsTerminal = r.IsTerminal
	cfg.FuncMakeRaw = r.EnterRawMode
	cfg.FuncExitRaw = r.ExitRawMode
	cfg.FuncGetWidth = r.GetWidth
	cfg.FuncOnWidthChanged = func(f func()) {
		r.funcWidthChan = f
	}
}

func (r *RemoteSvr) IsTerminal() bool {
	return r.isTerminal
}

func (r *RemoteSvr) checkEOF() error {
	if atomic.LoadInt32(&r.eof) == 1 {
		return io.EOF
	}
	return nil
}

func (r *RemoteSvr) Read(b []byte) (int, error) {
	r.dataBufM.Lock()
	n, err := r.dataBuf.Read(b)
	r.dataBufM.Unlock()
	if n == 0 {
		if err := r.checkEOF(); err != nil {
			return 0, err
		}
	}

	if n == 0 && err == io.EOF {
		<-r.reciveChan
		r.dataBufM.Lock()
		n, err = r.dataBuf.Read(b)
		r.dataBufM.Unlock()
	}
	if n == 0 {
		if err := r.checkEOF(); err != nil {
			return 0, err
		}
	}

	return n, err
}

func (r *RemoteSvr) writeMsg(m *Message) error {
	ctx := newWriteCtx(m)
	r.writeChan <- ctx
	reply := <-ctx.reply
	return reply.err
}

func (r *RemoteSvr) Write(b []byte) (int, error) {
	ctx := newWriteCtx(NewMessage(T_DATA, b))
	r.writeChan <- ctx
	reply := <-ctx.reply
	return reply.n, reply.err
}

func (r *RemoteSvr) EnterRawMode() error {
	return r.writeMsg(NewMessage(T_RAW, nil))
}

func (r *RemoteSvr) ExitRawMode() error {
	return r.writeMsg(NewMessage(T_ERAW, nil))
}

func (r *RemoteSvr) writeLoop() {
	defer r.Close()

loop:
	for {
		select {
		case ctx, ok := <-r.writeChan:
			if !ok {
				break
			}
			n, err := ctx.msg.WriteTo(r.conn)
			ctx.reply <- &writeReply{n, err}
		case <-r.stopChan:
			break loop
		}
	}
}

func (r *RemoteSvr) Close() {
	if atomic.CompareAndSwapInt32(&r.closed, 0, 1) {
		close(r.stopChan)
		r.conn.Close()
	}
}

func (r *RemoteSvr) readLoop(buf *bufio.Reader) {
	defer r.Close()
	for {
		m, err := ReadMessage(buf)
		if err != nil {
			break
		}
		switch m.Type {
		case T_EOF:
			atomic.StoreInt32(&r.eof, 1)
			select {
			case r.reciveChan <- struct{}{}:
			default:
			}
		case T_DATA:
			r.dataBufM.Lock()
			r.dataBuf.Write(m.Data)
			r.dataBufM.Unlock()
			select {
			case r.reciveChan <- struct{}{}:
			default:
			}
		case T_WIDTH_REPORT:
			r.GotReportWidth(m.Data)
		case T_ISTTY_REPORT:
			r.GotIsTerminal(m.Data)
		}
	}
}

func (r *RemoteSvr) GotIsTerminal(data []byte) {
	if binary.BigEndian.Uint16(data) == 0 {
		r.isTerminal = false
	} else {
		r.isTerminal = true
	}
}

func (r *RemoteSvr) GotReportWidth(data []byte) {
	atomic.StoreInt32(&r.width, int32(binary.BigEndian.Uint16(data)))
	if r.funcWidthChan != nil {
		r.funcWidthChan()
	}
}

func (r *RemoteSvr) GetWidth() int {
	return int(atomic.LoadInt32(&r.width))
}

// -----------------------------------------------------------------------------

type Message struct {
	Type MsgType
	Data []byte
}

func ReadMessage(r io.Reader) (*Message, error) {
	m := new(Message)
	var length int32
	if err := binary.Read(r, binary.BigEndian, &length); err != nil {
		return nil, err
	}
	if err := binary.Read(r, binary.BigEndian, &m.Type); err != nil {
		return nil, err
	}
	m.Data = make([]byte, int(length)-2)
	if _, err := io.ReadFull(r, m.Data); err != nil {
		return nil, err
	}
	return m, nil
}

func NewMessage(t MsgType, data []byte) *Message {
	return &Message{t, data}
}

func (m *Message) WriteTo(w io.Writer) (int, error) {
	buf := bytes.NewBuffer(make([]byte, 0, len(m.Data)+2+4))
	binary.Write(buf, binary.BigEndian, int32(len(m.Data)+2))
	binary.Write(buf, binary.BigEndian, m.Type)
	buf.Write(m.Data)
	n, err := buf.WriteTo(w)
	return int(n), err
}

// -----------------------------------------------------------------------------

type RemoteCli struct {
	conn        net.Conn
	raw         RawMode
	receiveChan chan struct{}
	inited      int32
	isTerminal  *bool

	data  bytes.Buffer
	dataM sync.Mutex
}

func NewRemoteCli(conn net.Conn) (*RemoteCli, error) {
	r := &RemoteCli{
		conn:        conn,
		receiveChan: make(chan struct{}),
	}
	return r, nil
}

func (r *RemoteCli) MarkIsTerminal(is bool) {
	r.isTerminal = &is
}

func (r *RemoteCli) init() error {
	if !atomic.CompareAndSwapInt32(&r.inited, 0, 1) {
		return nil
	}

	if err := r.reportIsTerminal(); err != nil {
		return err
	}

	if err := r.reportWidth(); err != nil {
		return err
	}

	// register sig for width changed
	DefaultOnWidthChanged(func() {
		r.reportWidth()
	})
	return nil
}

func (r *RemoteCli) writeMsg(m *Message) error {
	r.dataM.Lock()
	_, err := m.WriteTo(r.conn)
	r.dataM.Unlock()
	return err
}

func (r *RemoteCli) Write(b []byte) (int, error) {
	m := NewMessage(T_DATA, b)
	r.dataM.Lock()
	_, err := m.WriteTo(r.conn)
	r.dataM.Unlock()
	return len(b), err
}

func (r *RemoteCli) reportWidth() error {
	screenWidth := GetScreenWidth()
	data := make([]byte, 2)
	binary.BigEndian.PutUint16(data, uint16(screenWidth))
	msg := NewMessage(T_WIDTH_REPORT, data)

	if err := r.writeMsg(msg); err != nil {
		return err
	}
	return nil
}

func (r *RemoteCli) reportIsTerminal() error {
	var isTerminal bool
	if r.isTerminal != nil {
		isTerminal = *r.isTerminal
	} else {
		isTerminal = DefaultIsTerminal()
	}
	data := make([]byte, 2)
	if isTerminal {
		binary.BigEndian.PutUint16(data, 1)
	} else {
		binary.BigEndian.PutUint16(data, 0)
	}
	msg := NewMessage(T_ISTTY_REPORT, data)
	if err := r.writeMsg(msg); err != nil {
		return err
	}
	return nil
}

func (r *RemoteCli) readLoop() {
	buf := bufio.NewReader(r.conn)
	for {
		msg, err := ReadMessage(buf)
		if err != nil {
			break
		}
		switch msg.Type {
		case T_ERAW:
			r.raw.Exit()
		case T_RAW:
			r.raw.Enter()
		case T_DATA:
			os.Stdout.Write(msg.Data)
		}
	}
}

func (r *RemoteCli) ServeBy(source io.Reader) error {
	if err := r.init(); err != nil {
		return err
	}

	go func() {
		defer r.Close()
		for {
			n, _ := io.Copy(r, source)
			if n == 0 {
				break
			}
		}
	}()
	defer r.raw.Exit()
	r.readLoop()
	return nil
}

func (r *RemoteCli) Close() {
	r.writeMsg(NewMessage(T_EOF, nil))
}

func (r *RemoteCli) Serve() error {
	return r.ServeBy(os.Stdin)
}

func ListenRemote(n, addr string, cfg *Config, h func(*Instance), onListen ...func(net.Listener) error) error {
	ln, err := net.Listen(n, addr)
	if err != nil {
		return err
	}
	if len(onListen) > 0 {
		if err := onListen[0](ln); err != nil {
			return err
		}
	}
	for {
		conn, err := ln.Accept()
		if err != nil {
			break
		}
		go func() {
			defer conn.Close()
			rl, err := HandleConn(*cfg, conn)
			if err != nil {
				return
			}
			h(rl)
		}()
	}
	return nil
}

func HandleConn(cfg Config, conn net.Conn) (*Instance, error) {
	r, err := NewRemoteSvr(conn)
	if err != nil {
		return nil, err
	}
	r.HandleConfig(&cfg)

	rl, err := NewEx(&cfg)
	if err != nil {
		return nil, err
	}
	return rl, nil
}

func DialRemote(n, addr string) error {
	conn, err := net.Dial(n, addr)
	if err != nil {
		return err
	}
	defer conn.Close()

	cli, err := NewRemoteCli(conn)
	if err != nil {
		return err
	}
	return cli.Serve()
}
