// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// This file is a modified version of net/hosts.go from the golang repo

package hosts

import (
	"bufio"
	"bytes"
	"io"
	"net"
	"os"
	"strings"
	"sync"
	"time"

	"github.com/coredns/coredns/plugin"
)

const cacheMaxAge = 5 * time.Second

func parseLiteralIP(addr string) net.IP {
	if i := strings.Index(addr, "%"); i >= 0 {
		// discard ipv6 zone
		addr = addr[0:i]
	}

	return net.ParseIP(addr)
}

func absDomainName(b string) string {
	return plugin.Name(b).Normalize()
}

// Hostsfile contains known host entries.
type Hostsfile struct {
	sync.RWMutex

	// list of zones we are authoritive for
	Origins []string

	// Key for the list of literal IP addresses must be a host
	// name. It would be part of DNS labels, a FQDN or an absolute
	// FQDN.
	// For now the key is converted to lower case for convenience.
	byNameV4 map[string][]net.IP
	byNameV6 map[string][]net.IP

	// Key for the list of host names must be a literal IP address
	// including IPv6 address with zone identifier.
	// We don't support old-classful IP address notation.
	byAddr map[string][]string

	// inline saves the hosts file is inlined in Corefile
	// We need a copy here as we want to use inline to override
	// the default /etc/hosts
	inline []string

	expire time.Time
	path   string
	mtime  time.Time
	size   int64
}

// ReadHosts determines if the cached data needs to be updated based on the size and modification time of the hostsfile.
func (h *Hostsfile) ReadHosts() {
	now := time.Now()

	// Not deferring h.RUnlock() because we may need to remove the read lock and aquire a write lock
	h.RLock()
	if now.Before(h.expire) && len(h.byAddr) > 0 {
		h.RUnlock()
		return
	}
	stat, err := os.Stat(h.path)
	if err == nil && h.mtime.Equal(stat.ModTime()) && h.size == stat.Size() {
		h.RUnlock()
		h.Lock()
		h.expire = now.Add(cacheMaxAge)
		h.Unlock()
		return
	}

	var file *os.File
	if file, _ = os.Open(h.path); file == nil {
		// If this is the first time then we will try to parse inline
		if len(h.byAddr) == 0 && len(h.inline) > 0 {
			h.Parse(nil)
		}
		h.RUnlock()
		return
	}
	defer file.Close()

	h.RUnlock()
	h.Lock()
	defer h.Unlock()
	h.Parse(file)

	// Update the data cache.
	h.expire = now.Add(cacheMaxAge)
	h.mtime = stat.ModTime()
	h.size = stat.Size()
}

// Parse reads the hostsfile and populates the byName and byAddr maps.
func (h *Hostsfile) Parse(file io.Reader) {
	hsv4 := make(map[string][]net.IP)
	hsv6 := make(map[string][]net.IP)
	is := make(map[string][]string)

	var readers []io.Reader
	if file != nil {
		readers = append(readers, file)
	}
	readers = append(readers, strings.NewReader(strings.Join(h.inline, "\n")))
	scanner := bufio.NewScanner(io.MultiReader(readers...))
	for scanner.Scan() {
		line := scanner.Bytes()
		if i := bytes.Index(line, []byte{'#'}); i >= 0 {
			// Discard comments.
			line = line[0:i]
		}
		f := bytes.Fields(line)
		if len(f) < 2 {
			continue
		}
		addr := parseLiteralIP(string(f[0]))
		if addr == nil {
			continue
		}
		ver := ipVersion(string(f[0]))
		for i := 1; i < len(f); i++ {
			name := absDomainName(string(f[i]))
			if plugin.Zones(h.Origins).Matches(name) == "" {
				// name is not in Origins
				continue
			}
			switch ver {
			case 4:
				hsv4[name] = append(hsv4[name], addr)
			case 6:
				hsv6[name] = append(hsv6[name], addr)
			default:
				continue
			}
			is[addr.String()] = append(is[addr.String()], name)
		}
	}
	h.byNameV4 = hsv4
	h.byNameV6 = hsv6
	h.byAddr = is
}

// ipVersion returns what IP version was used textually
func ipVersion(s string) int {
	for i := 0; i < len(s); i++ {
		switch s[i] {
		case '.':
			return 4
		case ':':
			return 6
		}
	}
	return 0
}

// LookupStaticHostV4 looks up the IPv4 addresses for the given host from the hosts file.
func (h *Hostsfile) LookupStaticHostV4(host string) []net.IP {
	h.ReadHosts()
	h.RLock()
	defer h.RUnlock()
	if len(h.byNameV4) != 0 {
		if ips, ok := h.byNameV4[absDomainName(host)]; ok {
			ipsCp := make([]net.IP, len(ips))
			copy(ipsCp, ips)
			return ipsCp
		}
	}
	return nil
}

// LookupStaticHostV6 looks up the IPv6 addresses for the given host from the hosts file.
func (h *Hostsfile) LookupStaticHostV6(host string) []net.IP {
	h.ReadHosts()
	h.RLock()
	defer h.RUnlock()
	if len(h.byNameV6) != 0 {
		if ips, ok := h.byNameV6[absDomainName(host)]; ok {
			ipsCp := make([]net.IP, len(ips))
			copy(ipsCp, ips)
			return ipsCp
		}
	}
	return nil
}

// LookupStaticAddr looks up the hosts for the given address from the hosts file.
func (h *Hostsfile) LookupStaticAddr(addr string) []string {
	h.ReadHosts()
	h.RLock()
	defer h.RUnlock()
	addr = parseLiteralIP(addr).String()
	if addr == "" {
		return nil
	}
	if len(h.byAddr) != 0 {
		if hosts, ok := h.byAddr[addr]; ok {
			hostsCp := make([]string, len(hosts))
			copy(hostsCp, hosts)
			return hostsCp
		}
	}
	return nil
}
