// Copyright 2018 Google 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 pcapreplayer provides a net.PacketConn which replays a pcap file, and
// optionally records a new golden file.
package pcapreplayer

import (
	"fmt"
	"net"
	"os"
	"path/filepath"
	"time"

	"github.com/google/gopacket"
	"github.com/google/gopacket/layers"
	"github.com/google/gopacket/pcapgo"
)

func pcapopen(input, output string) (*pcapgo.Reader, *pcapgo.Writer, error) {
	f, err := os.Open(input)
	if err != nil {
		return nil, nil, err
	}
	pcapr, err := pcapgo.NewReader(f)
	if err != nil {
		return nil, nil, err
	}

	var pcapw *pcapgo.Writer
	if output != "" {
		if err := os.MkdirAll(filepath.Dir(output), 0755); err != nil {
			return nil, nil, err
		}
		of, err := os.Create(output)
		if err != nil {
			return nil, nil, err
		}
		pcapw = pcapgo.NewWriter(of)
		if err := pcapw.WriteFileHeader(1600, layers.LinkTypeEthernet); err != nil {
			return nil, nil, err
		}
	}
	return pcapr, pcapw, nil
}

func readFrom(r *pcapgo.Reader, buf []byte) (int, net.IP, error) {
	data, _, err := r.ReadPacketData()
	if err != nil {
		return 0, nil, err
	}
	pkt := gopacket.NewPacket(data, layers.LayerTypeEthernet, gopacket.DecodeOptions{})
	// TODO: get source IP
	udp := pkt.Layer(layers.LayerTypeUDP)
	if udp == nil {
		return 0, nil, fmt.Errorf("pcap contained unexpected non-UDP packet")
	}

	//log.Printf("ReadFrom(): %x, %v, pkt = %+v", udp.LayerPayload(), err, pkt)
	copy(buf, udp.LayerPayload())
	return len(udp.LayerPayload()), net.ParseIP("192.168.23.1"), err
}

func mustParseMAC(s string) net.HardwareAddr {
	hw, err := net.ParseMAC(s)
	if err != nil {
		panic(err)
	}
	return hw
}

type layer interface {
	gopacket.NetworkLayer

	// Cannot embed gopacket.SerializableLayer because it includes a conflicting
	// definition of LayerType():
	SerializeTo(b gopacket.SerializeBuffer, opts gopacket.SerializeOptions) error
}

func pcapwrite(w *pcapgo.Writer, ip layer, udp *layers.UDP, b []byte) error {
	buf := gopacket.NewSerializeBuffer()
	udp.SetNetworkLayerForChecksum(ip)
	var ethernetType layers.EthernetType
	switch ip.LayerType() {
	case layers.LayerTypeIPv4:
		ethernetType = layers.EthernetTypeIPv4
	case layers.LayerTypeIPv6:
		ethernetType = layers.EthernetTypeIPv6
	}
	gopacket.SerializeLayers(buf,
		gopacket.SerializeOptions{
			FixLengths:       true,
			ComputeChecksums: true,
		},
		&layers.Ethernet{
			SrcMAC:       mustParseMAC("00:00:5E:00:53:00"), // RFC7042
			DstMAC:       mustParseMAC("33:33:00:01:00:02"), // IPv6mcast_01:00:02
			EthernetType: ethernetType,
		},
		ip,
		udp,
		gopacket.Payload(b),
	)
	got := buf.Bytes()

	ci := gopacket.CaptureInfo{
		CaptureLength: len(got),
		Length:        len(got),
	}
	if err := w.WritePacket(ci, got); err != nil {
		return fmt.Errorf("pcap.WritePacket(): %v", err)
	}
	return nil
}

// packetConn is a net.PacketConn which replays a pcap file.
type packetConn struct {
	pcapr *pcapgo.Reader
	pcapw *pcapgo.Writer
}

// NewPCAP returns a net.PacketConn which replays packets from pcap file input,
// writing packets to pcap file output (if non-empty).
//
// See https://en.wikipedia.org/wiki/Pcap for details on pcap.
func NewPacketConn(input, output string) (net.PacketConn, error) {
	pcapr, pcapw, err := pcapopen(input, output)
	return &packetConn{pcapr, pcapw}, err
}

func (r *packetConn) LocalAddr() net.Addr                { return nil }
func (r *packetConn) Close() error                       { return nil }
func (r *packetConn) SetDeadline(t time.Time) error      { return nil }
func (r *packetConn) SetReadDeadline(t time.Time) error  { return nil }
func (r *packetConn) SetWriteDeadline(t time.Time) error { return nil }

func (r *packetConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
	if r.pcapw == nil {
		return len(b), nil
	}

	return len(b), pcapwrite(r.pcapw,
		&layers.IPv6{
			Version:      6,
			TrafficClass: 0,
			NextHeader:   layers.IPProtocolUDP,
			HopLimit:     1,
			SrcIP:        net.ParseIP("fe80::200:5eff:fe00:5300"),
			DstIP:        net.ParseIP("ff02::1:2"),
		},
		&layers.UDP{
			SrcPort: 546,
			DstPort: 547,
		},
		b)
}

func (r *packetConn) ReadFrom(buf []byte) (int, net.Addr, error) {
	l, ip, err := readFrom(r.pcapr, buf)
	return l, &net.IPAddr{IP: ip}, err
}

// dhcp4conn is a dhcp4client.ConnectionInt which replays a pcap file.
type dhcp4conn struct {
	pcapr *pcapgo.Reader
	pcapw *pcapgo.Writer
}

// NewDHCP4Conn returns a dhcp4client.ConnectionInt which replays packets from
// pcap file input, writing packets to pcap file output (if non-empty).
//
// See https://en.wikipedia.org/wiki/Pcap for details on pcap.
func NewDHCP4Conn(input, output string) (net.PacketConn, error) {
	pcapr, pcapw, err := pcapopen(input, output)
	return &dhcp4conn{pcapr: pcapr, pcapw: pcapw}, err
}

func (r *dhcp4conn) LocalAddr() net.Addr                { return nil }
func (r *dhcp4conn) Close() error                       { return nil }
func (r *dhcp4conn) SetDeadline(t time.Time) error      { return nil }
func (r *dhcp4conn) SetReadDeadline(t time.Time) error  { return nil }
func (r *dhcp4conn) SetWriteDeadline(t time.Time) error { return nil }

func (r *dhcp4conn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
	if r.pcapw == nil {
		return len(b), nil
	}

	return len(b), pcapwrite(r.pcapw,
		&layers.IPv4{
			Version:  4,
			TTL:      255,
			Protocol: layers.IPProtocolUDP,
			SrcIP:    net.ParseIP("0.0.0.0"),
			DstIP:    net.ParseIP("255.255.255.255"),
		},
		&layers.UDP{
			SrcPort: 68,
			DstPort: 67,
		},
		b)
}

func (r *dhcp4conn) ReadFrom(buf []byte) (int, net.Addr, error) {
	data, _, err := r.pcapr.ReadPacketData()
	if err != nil {
		return 0, nil, err
	}
	pkt := gopacket.NewPacket(data, layers.LayerTypeEthernet, gopacket.DecodeOptions{})
	// TODO: get source IP
	eth := pkt.Layer(layers.LayerTypeEthernet)
	if eth == nil {
		return 0, nil, fmt.Errorf("pcap contained unexpected non-IPv4 packet")
	}

	//log.Printf("ReadFrom(): %x, %v, pkt = %+v", udp.LayerPayload(), err, pkt)
	copy(buf, eth.LayerPayload())
	ip := net.ParseIP("192.168.23.1")
	return len(eth.LayerPayload()), &net.IPAddr{IP: ip}, nil
}
