package inproc

import (
	"errors"
	"io"
	"math"
	"net"
	"strconv"
	"sync"
	"time"

	"github.com/anacrolix/missinggo"
)

var (
	mu       sync.Mutex
	cond         = sync.Cond{L: &mu}
	nextPort int = 1
	conns        = map[int]*packetConn{}
)

type Addr struct {
	Port int
}

func (Addr) Network() string {
	return "inproc"
}

func (me Addr) String() string {
	return ":" + strconv.FormatInt(int64(me.Port), 10)
}

func getPort() (port int) {
	mu.Lock()
	defer mu.Unlock()
	port = nextPort
	nextPort++
	return
}

func ResolveAddr(network, str string) (net.Addr, error) {
	return ResolveInprocAddr(network, str)
}

func ResolveInprocAddr(network, str string) (addr Addr, err error) {
	if str == "" {
		addr.Port = getPort()
		return
	}
	_, p, err := net.SplitHostPort(str)
	if err != nil {
		return
	}
	i64, err := strconv.ParseInt(p, 10, 0)
	if err != nil {
		return
	}
	addr.Port = int(i64)
	if addr.Port == 0 {
		addr.Port = getPort()
	}
	return
}

func ListenPacket(network, addrStr string) (nc net.PacketConn, err error) {
	addr, err := ResolveInprocAddr(network, addrStr)
	if err != nil {
		return
	}
	mu.Lock()
	defer mu.Unlock()
	if _, ok := conns[addr.Port]; ok {
		err = errors.New("address in use")
		return
	}
	pc := &packetConn{
		addr:          addr,
		readDeadline:  newCondDeadline(&cond),
		writeDeadline: newCondDeadline(&cond),
	}
	conns[addr.Port] = pc
	nc = pc
	return
}

type packet struct {
	data []byte
	addr Addr
}

type packetConn struct {
	closed        bool
	addr          Addr
	reads         []packet
	readDeadline  *condDeadline
	writeDeadline *condDeadline
}

func (me *packetConn) Close() error {
	mu.Lock()
	defer mu.Unlock()
	me.closed = true
	delete(conns, me.addr.Port)
	cond.Broadcast()
	return nil
}

func (me *packetConn) LocalAddr() net.Addr {
	return me.addr
}

type errTimeout struct{}

func (errTimeout) Error() string {
	return "i/o timeout"
}

func (errTimeout) Temporary() bool {
	return false
}

func (errTimeout) Timeout() bool {
	return true
}

var _ net.Error = errTimeout{}

func (me *packetConn) WriteTo(b []byte, na net.Addr) (n int, err error) {
	mu.Lock()
	defer mu.Unlock()
	if me.closed {
		err = errors.New("closed")
		return
	}
	if me.writeDeadline.exceeded() {
		err = errTimeout{}
		return
	}
	n = len(b)
	port := missinggo.AddrPort(na)
	c, ok := conns[port]
	if !ok {
		// log.Printf("no conn for port %d", port)
		return
	}
	c.reads = append(c.reads, packet{append([]byte(nil), b...), me.addr})
	cond.Broadcast()
	return
}

func (me *packetConn) ReadFrom(b []byte) (n int, addr net.Addr, err error) {
	mu.Lock()
	defer mu.Unlock()
	for {
		if len(me.reads) != 0 {
			r := me.reads[0]
			me.reads = me.reads[1:]
			n = copy(b, r.data)
			addr = r.addr
			// log.Println(addr)
			return
		}
		if me.closed {
			err = io.EOF
			return
		}
		if me.readDeadline.exceeded() {
			err = errTimeout{}
			return
		}
		cond.Wait()
	}
}

func (me *packetConn) SetDeadline(t time.Time) error {
	me.writeDeadline.setDeadline(t)
	me.readDeadline.setDeadline(t)
	return nil
}

func (me *packetConn) SetReadDeadline(t time.Time) error {
	me.readDeadline.setDeadline(t)
	return nil
}

func (me *packetConn) SetWriteDeadline(t time.Time) error {
	me.writeDeadline.setDeadline(t)
	return nil
}

func newCondDeadline(cond *sync.Cond) (ret *condDeadline) {
	ret = &condDeadline{
		timer: time.AfterFunc(math.MaxInt64, func() {
			mu.Lock()
			ret._exceeded = true
			mu.Unlock()
			cond.Broadcast()
		}),
	}
	ret.setDeadline(time.Time{})
	return
}

type condDeadline struct {
	mu        sync.Mutex
	_exceeded bool
	timer     *time.Timer
}

func (me *condDeadline) setDeadline(t time.Time) {
	me.mu.Lock()
	defer me.mu.Unlock()
	me._exceeded = false
	if t.IsZero() {
		me.timer.Stop()
		return
	}
	me.timer.Reset(t.Sub(time.Now()))
}

func (me *condDeadline) exceeded() bool {
	me.mu.Lock()
	defer me.mu.Unlock()
	return me._exceeded
}
