// Copyright (C) 2015 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.

package nat

import (
	"fmt"
	"log/slog"
	"net"
	"strconv"
	"sync"
	"time"
)

type MappingChangeSubscriber func()

type Mapping struct {
	protocol  Protocol
	ipVersion IPVersion
	address   Address

	extAddresses map[string][]Address // NAT ID -> Address
	expires      time.Time
	subscribers  []MappingChangeSubscriber
	mut          sync.RWMutex
}

func (m *Mapping) setAddressLocked(id string, addresses []Address) {
	slog.Info("New external port opened", "protocol", m.protocol, "external", addresses, "local", m.address, "gateway", id)
	m.extAddresses[id] = addresses
}

func (m *Mapping) removeAddressLocked(id string) {
	addresses, ok := m.extAddresses[id]
	if ok {
		slog.Info("Removing external open port", "protocol", m.protocol, "external", addresses, "gateway", id)
		delete(m.extAddresses, id)
	}
}

func (m *Mapping) clearAddresses() {
	m.mut.Lock()
	change := len(m.extAddresses) > 0
	for id, addr := range m.extAddresses {
		l.Debugf("Clearing mapping %s: ID: %s Address: %s", m, id, addr)
		delete(m.extAddresses, id)
	}
	m.expires = time.Time{}
	m.mut.Unlock()
	if change {
		m.notify()
	}
}

func (m *Mapping) notify() {
	m.mut.RLock()
	for _, subscriber := range m.subscribers {
		subscriber()
	}
	m.mut.RUnlock()
}

func (m *Mapping) Protocol() Protocol {
	return m.protocol
}

func (m *Mapping) Address() Address {
	return m.address
}

func (m *Mapping) ExternalAddresses() []Address {
	m.mut.RLock()
	addrs := make([]Address, 0, len(m.extAddresses))
	for _, addr := range m.extAddresses {
		addrs = append(addrs, addr...)
	}
	m.mut.RUnlock()
	return addrs
}

func (m *Mapping) OnChanged(subscribed MappingChangeSubscriber) {
	m.mut.Lock()
	m.subscribers = append(m.subscribers, subscribed)
	m.mut.Unlock()
}

func (m *Mapping) String() string {
	return fmt.Sprintf("%s/%s", m.address, m.protocol)
}

func (m *Mapping) GoString() string {
	return m.String()
}

// Checks if the mappings local IP address matches the IP address of the gateway
// For example, if we are explicitly listening on 192.168.0.12, there is no
// point trying to acquire a mapping on a gateway to which the local IP is
// 10.0.0.1. Fallback to true if any of the IPs is not there.
func (m *Mapping) validGateway(ip net.IP) bool {
	if m.address.IP == nil || ip == nil || m.address.IP.IsUnspecified() || ip.IsUnspecified() {
		return true
	}
	return m.address.IP.Equal(ip)
}

// Address is essentially net.TCPAddr yet is more general, and has a few helper
// methods which reduce boilerplate code.
type Address struct {
	IP   net.IP
	Port int
}

func (a Address) Equal(b Address) bool {
	return a.Port == b.Port && a.IP.Equal(b.IP)
}

func (a Address) String() string {
	var ipStr string
	if a.IP == nil {
		ipStr = net.IPv4zero.String()
	} else {
		ipStr = a.IP.String()
	}
	return net.JoinHostPort(ipStr, strconv.Itoa(a.Port))
}

func (a Address) GoString() string {
	return a.String()
}
