package nat

import (
	"fmt"
	"net"
	"sync"
	"time"

	"github.com/jbenet/goprocess"
)

// Mapping represents a port mapping in a NAT.
type Mapping interface {
	// NAT returns the NAT object this Mapping belongs to.
	NAT() *NAT

	// Protocol returns the protocol of this port mapping. This is either
	// "tcp" or "udp" as no other protocols are likely to be NAT-supported.
	Protocol() string

	// InternalPort returns the internal device port. Mapping will continue to
	// try to map InternalPort() to an external facing port.
	InternalPort() int

	// ExternalPort returns the external facing port. If the mapping is not
	// established, port will be 0
	ExternalPort() int

	// ExternalAddr returns the external facing address. If the mapping is not
	// established, addr will be nil, and and ErrNoMapping will be returned.
	ExternalAddr() (addr net.Addr, err error)

	// Close closes the port mapping
	Close() error
}

// keeps republishing
type mapping struct {
	sync.Mutex // guards all fields

	nat       *NAT
	proto     string
	intport   int
	extport   int
	permanent bool
	proc      goprocess.Process

	cached    net.IP
	cacheTime time.Time
	cacheLk   sync.Mutex
}

func (m *mapping) NAT() *NAT {
	m.Lock()
	defer m.Unlock()
	return m.nat
}

func (m *mapping) Protocol() string {
	m.Lock()
	defer m.Unlock()
	return m.proto
}

func (m *mapping) InternalPort() int {
	m.Lock()
	defer m.Unlock()
	return m.intport
}

func (m *mapping) ExternalPort() int {
	m.Lock()
	defer m.Unlock()
	return m.extport
}

func (m *mapping) setExternalPort(p int) {
	m.Lock()
	defer m.Unlock()
	m.extport = p
}

func (m *mapping) ExternalAddr() (net.Addr, error) {
	m.cacheLk.Lock()
	defer m.cacheLk.Unlock()
	oport := m.ExternalPort()
	if oport == 0 {
		// dont even try right now.
		return nil, ErrNoMapping
	}

	if time.Since(m.cacheTime) >= CacheTime {
		m.nat.natmu.Lock()
		cval, err := m.nat.nat.GetExternalAddress()
		m.nat.natmu.Unlock()

		if err != nil {
			return nil, err
		}

		m.cached = cval
		m.cacheTime = time.Now()
	}
	switch m.Protocol() {
	case "tcp":
		return &net.TCPAddr{
			IP:   m.cached,
			Port: oport,
		}, nil
	case "udp":
		return &net.UDPAddr{
			IP:   m.cached,
			Port: oport,
		}, nil
	default:
		panic(fmt.Sprintf("invalid protocol %q", m.Protocol()))
	}
}

func (m *mapping) Close() error {
	return m.proc.Close()
}
