// Copyright 2015 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package network

import (
	"fmt"
	"net"
	"strconv"
	"strings"
)

type stanza interface{}

type stanzaAuto struct {
	interfaces []string
}

type stanzaInterface struct {
	name         string
	kind         interfaceKind
	auto         bool
	configMethod configMethod
	options      map[string][]string
}

type interfaceKind int

const (
	interfaceBond = interfaceKind(iota)
	interfacePhysical
	interfaceVLAN
)

type route struct {
	destination net.IPNet
	gateway     net.IP
}

type configMethod interface{}

type configMethodStatic struct {
	addresses   []net.IPNet
	nameservers []net.IP
	domains     []string
	routes      []route
	hwaddress   net.HardwareAddr
}

type configMethodLoopback struct{}

type configMethodManual struct{}

type configMethodDHCP struct {
	hwaddress net.HardwareAddr
}

func parseStanzas(lines []string) (stanzas []stanza, err error) {
	rawStanzas, err := splitStanzas(lines)
	if err != nil {
		return nil, err
	}

	stanzas = make([]stanza, 0, len(rawStanzas))
	for _, rawStanza := range rawStanzas {
		if stanza, err := parseStanza(rawStanza); err == nil {
			stanzas = append(stanzas, stanza)
		} else {
			return nil, err
		}
	}

	autos := make([]string, 0)
	interfaceMap := make(map[string]*stanzaInterface)
	for _, stanza := range stanzas {
		switch c := stanza.(type) {
		case *stanzaAuto:
			autos = append(autos, c.interfaces...)
		case *stanzaInterface:
			interfaceMap[c.name] = c
		}
	}

	// Apply the auto attribute
	for _, auto := range autos {
		if iface, ok := interfaceMap[auto]; ok {
			iface.auto = true
		}
	}

	return stanzas, nil
}

func splitStanzas(lines []string) ([][]string, error) {
	var curStanza []string
	stanzas := make([][]string, 0)
	for _, line := range lines {
		if isStanzaStart(line) {
			if curStanza != nil {
				stanzas = append(stanzas, curStanza)
			}
			curStanza = []string{line}
		} else if curStanza != nil {
			curStanza = append(curStanza, line)
		} else {
			return nil, fmt.Errorf("missing stanza start %q", line)
		}
	}

	if curStanza != nil {
		stanzas = append(stanzas, curStanza)
	}

	return stanzas, nil
}

func isStanzaStart(line string) bool {
	switch strings.Split(line, " ")[0] {
	case "auto":
		fallthrough
	case "iface":
		fallthrough
	case "mapping":
		return true
	}

	if strings.HasPrefix(line, "allow-") {
		return true
	}

	return false
}

func parseStanza(rawStanza []string) (stanza, error) {
	if len(rawStanza) == 0 {
		panic("empty stanza")
	}
	tokens := strings.Fields(rawStanza[0])
	if len(tokens) < 2 {
		return nil, fmt.Errorf("malformed stanza start %q", rawStanza[0])
	}

	kind := tokens[0]
	attributes := tokens[1:]

	switch kind {
	case "auto":
		return parseAutoStanza(attributes, rawStanza[1:])
	case "iface":
		return parseInterfaceStanza(attributes, rawStanza[1:])
	default:
		return nil, fmt.Errorf("unknown stanza %q", kind)
	}
}

func parseAutoStanza(attributes []string, options []string) (*stanzaAuto, error) {
	return &stanzaAuto{interfaces: attributes}, nil
}

func parseInterfaceStanza(attributes []string, options []string) (*stanzaInterface, error) {
	if len(attributes) != 3 {
		return nil, fmt.Errorf("incorrect number of attributes")
	}

	iface := attributes[0]
	confMethod := attributes[2]

	optionMap := make(map[string][]string, 0)
	for _, option := range options {
		if strings.HasPrefix(option, "post-up") {
			tokens := strings.SplitAfterN(option, " ", 2)
			if len(tokens) != 2 {
				continue
			}
			if v, ok := optionMap["post-up"]; ok {
				optionMap["post-up"] = append(v, tokens[1])
			} else {
				optionMap["post-up"] = []string{tokens[1]}
			}
		} else if strings.HasPrefix(option, "pre-down") {
			tokens := strings.SplitAfterN(option, " ", 2)
			if len(tokens) != 2 {
				continue
			}
			if v, ok := optionMap["pre-down"]; ok {
				optionMap["pre-down"] = append(v, tokens[1])
			} else {
				optionMap["pre-down"] = []string{tokens[1]}
			}
		} else {
			tokens := strings.Fields(option)
			optionMap[tokens[0]] = tokens[1:]
		}
	}

	var conf configMethod
	switch confMethod {
	case "static":
		config := configMethodStatic{
			addresses:   make([]net.IPNet, 1),
			routes:      make([]route, 0),
			nameservers: make([]net.IP, 0),
		}
		if addresses, ok := optionMap["address"]; ok {
			if len(addresses) == 1 {
				config.addresses[0].IP = net.ParseIP(addresses[0])
			}
		}
		if netmasks, ok := optionMap["netmask"]; ok {
			if len(netmasks) == 1 {
				config.addresses[0].Mask = net.IPMask(net.ParseIP(netmasks[0]).To4())
			}
		}
		if config.addresses[0].IP == nil || config.addresses[0].Mask == nil {
			return nil, fmt.Errorf("malformed static network config for %q", iface)
		}
		if gateways, ok := optionMap["gateway"]; ok {
			if len(gateways) == 1 {
				config.routes = append(config.routes, route{
					destination: net.IPNet{
						IP:   net.IPv4(0, 0, 0, 0),
						Mask: net.IPv4Mask(0, 0, 0, 0),
					},
					gateway: net.ParseIP(gateways[0]),
				})
			}
		}
		if hwaddress, err := parseHwaddress(optionMap, iface); err == nil {
			config.hwaddress = hwaddress
		} else {
			return nil, err
		}
		for _, nameserver := range optionMap["dns-nameservers"] {
			config.nameservers = append(config.nameservers, net.ParseIP(nameserver))
		}
		for _, postup := range optionMap["post-up"] {
			if strings.HasPrefix(postup, "route add") {
				route := route{}
				fields := strings.Fields(postup)
				for i, field := range fields[:len(fields)-1] {
					switch field {
					case "-net":
						if _, dst, err := net.ParseCIDR(fields[i+1]); err == nil {
							route.destination = *dst
						} else {
							route.destination.IP = net.ParseIP(fields[i+1])
						}
					case "netmask":
						route.destination.Mask = net.IPMask(net.ParseIP(fields[i+1]).To4())
					case "gw":
						route.gateway = net.ParseIP(fields[i+1])
					}
				}
				if route.destination.IP != nil && route.destination.Mask != nil && route.gateway != nil {
					config.routes = append(config.routes, route)
				}
			}
		}
		conf = config
	case "loopback":
		conf = configMethodLoopback{}
	case "manual":
		conf = configMethodManual{}
	case "dhcp":
		config := configMethodDHCP{}
		if hwaddress, err := parseHwaddress(optionMap, iface); err == nil {
			config.hwaddress = hwaddress
		} else {
			return nil, err
		}
		conf = config
	default:
		return nil, fmt.Errorf("invalid config method %q", confMethod)
	}

	if _, ok := optionMap["vlan_raw_device"]; ok {
		return parseVLANStanza(iface, conf, attributes, optionMap)
	}

	if strings.Contains(iface, ".") {
		return parseVLANStanza(iface, conf, attributes, optionMap)
	}

	if _, ok := optionMap["bond-slaves"]; ok {
		return parseBondStanza(iface, conf, attributes, optionMap)
	}

	return parsePhysicalStanza(iface, conf, attributes, optionMap)
}

func parseHwaddress(options map[string][]string, iface string) (net.HardwareAddr, error) {
	if hwaddress, ok := options["hwaddress"]; ok && len(hwaddress) == 2 {
		switch hwaddress[0] {
		case "ether":
			if address, err := net.ParseMAC(hwaddress[1]); err == nil {
				return address, nil
			}
			return nil, fmt.Errorf("malformed hwaddress option for %q", iface)
		}
	}
	return nil, nil
}

func parseBondStanza(iface string, conf configMethod, attributes []string, options map[string][]string) (*stanzaInterface, error) {
	return &stanzaInterface{name: iface, kind: interfaceBond, configMethod: conf, options: options}, nil
}

func parsePhysicalStanza(iface string, conf configMethod, attributes []string, options map[string][]string) (*stanzaInterface, error) {
	return &stanzaInterface{name: iface, kind: interfacePhysical, configMethod: conf, options: options}, nil
}

func parseVLANStanza(iface string, conf configMethod, attributes []string, options map[string][]string) (*stanzaInterface, error) {
	var id string
	if strings.Contains(iface, ".") {
		tokens := strings.Split(iface, ".")
		id = tokens[len(tokens)-1]
	} else if strings.HasPrefix(iface, "vlan") {
		id = strings.TrimPrefix(iface, "vlan")
	} else {
		return nil, fmt.Errorf("malformed vlan name %q", iface)
	}

	if _, err := strconv.Atoi(id); err != nil {
		return nil, fmt.Errorf("malformed vlan name %q", iface)
	}
	options["id"] = []string{id}
	options["raw_device"] = options["vlan_raw_device"]

	return &stanzaInterface{name: iface, kind: interfaceVLAN, configMethod: conf, options: options}, nil
}
