// The following directive is necessary to make the package coherent:

//go:build ignore
// +build ignore

// This program generates extended_key_usage.go. It can be invoked by running
// `$ go generate`
package main

import (
	"bytes"
	"encoding/csv"
	"io"
	"net/http"
	"os"
	"sort"
	"strings"
)

const (
	COLUMN_IDX_OID        = 0
	COLUMN_IDX_SHORT_NAME = 2
)

const (
	GO_PREFIX    = "oidExtKeyUsage"
	CONST_PREFIX = "OID_EKU"
)

type OID struct {
	OID       string
	ShortName string
}

func (o *OID) OIDDecl() string {
	parts := strings.Split(o.OID, ".")
	buffer := bytes.Buffer{}
	buffer.WriteString("asn1.ObjectIdentifier{")
	for idx, p := range parts {
		buffer.WriteString(p)
		if idx != len(parts)-1 {
			buffer.WriteString(", ")
		}
	}
	buffer.WriteString("}")
	return buffer.String()
}

func (o *OID) GoName(prefix string) string {
	parts := strings.Split(o.ShortName, "-")
	for idx, p := range parts {
		if prefix == "" && idx == 0 {
			continue
		}
		parts[idx] = strings.Title(p)
	}
	return prefix + strings.Join(parts, "")
}

func (o *OID) GoConstant(prefix string) string {
	parts := strings.Split(o.ShortName, "-")
	buffer := bytes.Buffer{}
	if prefix != "" {
		buffer.WriteString(strings.ToUpper(prefix))
		buffer.WriteString("_")
	}
	for idx, p := range parts {
		buffer.WriteString(strings.ToUpper(p))
		if idx != len(parts)-1 {
			buffer.WriteString("_")
		}
	}
	return buffer.String()
}

func (o *OID) JSONName(prefix string) string {
	parts := strings.Split(o.ShortName, "-")
	buffer := bytes.Buffer{}
	if prefix != "" {
		buffer.WriteString(strings.ToLower(prefix))
		buffer.WriteString("_")
	}
	for idx, p := range parts {
		buffer.WriteString(strings.ToLower(p))
		if idx != len(parts)-1 {
			buffer.WriteString("_")
		}
	}
	return buffer.String()
}

func (o *OID) StructFieldName() string {
	parts := strings.Split(o.ShortName, "-")
	buffer := bytes.Buffer{}
	for _, p := range parts {
		buffer.WriteString(strings.Title(p))
	}
	return buffer.String()
}

func writeHeader(out io.Writer) {
	s := `// Created by extended_key_usage_gen; DO NOT EDIT

// Copyright 2017 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.

package x509

import (
	"encoding/asn1"
)

`
	out.Write([]byte(s))
}

func generateASN1(rawToOID map[string]OID) []byte {
	buffer := bytes.Buffer{}
	// Create sorted slice of keys to ensure deterministic output
	var keys = make([]string, 0, len(rawToOID))
	for k := range rawToOID {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	for _, k := range keys {
		oid := rawToOID[k]
		goName := oid.GoName(GO_PREFIX)
		oidDecl := oid.OIDDecl()
		buffer.WriteString(goName)
		buffer.WriteString(" = ")
		buffer.WriteString(oidDecl)
		buffer.WriteString("\n")
	}
	return buffer.Bytes()
}

func generateIntegerConstants(rawToOID map[string]OID) []byte {
	buffer := bytes.Buffer{}
	buffer.WriteString("const (\n")
	first := true
	// Create sorted slice of keys to ensure deterministic output
	var keys = make([]string, 0, len(rawToOID))
	for k := range rawToOID {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	for _, k := range keys {
		oid := rawToOID[k]
		goName := oid.GoName("ExtKeyUsage")
		buffer.WriteString(goName)
		if first {
			buffer.WriteString(" ExtKeyUsage = iota")
			first = false
		}
		buffer.WriteString("\n")
	}
	buffer.WriteString(")\n")
	return buffer.Bytes()
}

func generateNameConstants(rawToOID map[string]OID) []byte {
	buffer := bytes.Buffer{}
	// Create sorted slice of keys to ensure deterministic output
	var keys = make([]string, 0, len(rawToOID))
	for k := range rawToOID {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	for _, k := range keys {
		oid := rawToOID[k]
		constantName := oid.GoConstant(CONST_PREFIX)
		buffer.WriteString(constantName)
		buffer.WriteString(" = \"")
		buffer.WriteString(oid.OID)
		buffer.WriteString("\"\n")
	}
	return buffer.Bytes()
}

func generateOIDMap(rawToOID map[string]OID, mapName string) []byte {
	buffer := bytes.Buffer{}
	buffer.WriteString(mapName)
	buffer.WriteString(" = make(map[string]asn1.ObjectIdentifier)\n")

	// Create sorted slice of keys to ensure deterministic output
	var keys = make([]string, 0, len(rawToOID))
	for k := range rawToOID {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	for _, k := range keys {
		oid := rawToOID[k]
		constantName := oid.GoConstant(CONST_PREFIX)
		goName := oid.GoName(GO_PREFIX)
		buffer.WriteString(mapName)
		buffer.WriteString("[")
		buffer.WriteString(constantName)
		buffer.WriteString("] = ")
		buffer.WriteString(goName)
		buffer.WriteString("\n")
	}
	return buffer.Bytes()
}

func generateIntegerMap(rawToOID map[string]OID, mapName string) []byte {
	buffer := bytes.Buffer{}
	buffer.WriteString(mapName)
	buffer.WriteString(" = make(map[string]ExtKeyUsage)\n")

	// Create sorted slice of keys to ensure deterministic output
	var keys = make([]string, 0, len(rawToOID))
	for k := range rawToOID {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	for _, k := range keys {
		oid := rawToOID[k]
		constantName := oid.GoConstant(CONST_PREFIX)
		goName := oid.GoName("ExtKeyUsage")
		buffer.WriteString(mapName)
		buffer.WriteString("[")
		buffer.WriteString(constantName)
		buffer.WriteString("] = ")
		buffer.WriteString(goName)
		buffer.WriteString("\n")
	}
	return buffer.Bytes()
}

func generateEKUJSONStruct(rawToOID map[string]OID) []byte {
	buffer := bytes.Buffer{}
	buffer.WriteString("type auxExtendedKeyUsage struct {\n")

	// Create sorted slice of keys to ensure deterministic output
	var keys = make([]string, 0, len(rawToOID))
	for k := range rawToOID {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	for _, k := range keys {
		oid := rawToOID[k]
		buffer.WriteString(oid.StructFieldName())
		buffer.WriteString(" bool `json:\"")
		buffer.WriteString(oid.JSONName(""))
		buffer.WriteString(",omitempty\" oid:\"")
		buffer.WriteString(oid.OID)
		buffer.WriteString("\"`\n")
	}
	buffer.WriteString("Unknown []string `json:\"unknown,omitempty\"`")
	buffer.WriteString("}\n\n")
	buffer.WriteString("func (aux *auxExtendedKeyUsage) populateFromASN1(oid asn1.ObjectIdentifier) {\n")
	buffer.WriteString("s := oid.String()\n")
	buffer.WriteString("switch s {\n")
	for _, k := range keys {
		oid := rawToOID[k]
		buffer.WriteString("case ")
		constantName := oid.GoConstant(CONST_PREFIX)
		buffer.WriteString(constantName)
		buffer.WriteString(":\n")
		buffer.WriteString("aux.")
		buffer.WriteString(oid.StructFieldName())
		buffer.WriteString(" = true\n")
	}
	buffer.WriteString("default:\n")
	buffer.WriteString("}\n")
	buffer.WriteString("return")
	buffer.WriteString("}\n\n")

	buffer.WriteString("func (aux *auxExtendedKeyUsage) populateFromExtKeyUsage(eku ExtKeyUsage) {\n")
	buffer.WriteString("switch eku {\n")
	for _, k := range keys {
		oid := rawToOID[k]
		buffer.WriteString("case ")
		ekuName := oid.GoName("ExtKeyUsage")
		buffer.WriteString(ekuName)
		buffer.WriteString(":\n")
		buffer.WriteString("aux.")
		buffer.WriteString(oid.StructFieldName())
		buffer.WriteString(" = true\n")
	}
	buffer.WriteString("default:\n")
	buffer.WriteString("}\n")
	buffer.WriteString("return")
	buffer.WriteString("}\n\n")

	return buffer.Bytes()
}

func main() {
	out, err := os.Create("extended_key_usage.go")
	if err != nil {
		panic(err.Error())
	}
	defer out.Close()
	writeHeader(out)

	resp, err := http.Get("https://raw.githubusercontent.com/zmap/constants/master/x509/extended_key_usage.csv")
	if err != nil {
		panic(err.Error())
	}
	defer resp.Body.Close()

	rawToOID := make(map[string]OID)
	r := csv.NewReader(resp.Body)
	for lines := 0; ; lines++ {
		record, err := r.Read()
		if err == io.EOF {
			break
		}
		if err != nil {
			panic(err.Error())
		}
		if lines == 0 {
			// Header row
			continue
		}
		oid := record[COLUMN_IDX_OID]
		shortName := record[COLUMN_IDX_SHORT_NAME]
		rawToOID[oid] = OID{
			OID:       oid,
			ShortName: shortName,
		}
	}

	out.Write([]byte("const (\n"))
	constants := generateNameConstants(rawToOID)
	out.Write(constants)
	out.Write([]byte(")\n"))

	out.Write([]byte("var (\n"))
	oidDecls := generateASN1(rawToOID)
	out.Write(oidDecls)
	out.Write([]byte(")\n"))

	integersConstants := generateIntegerConstants(rawToOID)
	out.Write(integersConstants)

	out.Write(generateEKUJSONStruct(rawToOID))

	out.Write([]byte("\nvar ekuOIDs map[string]asn1.ObjectIdentifier\n\n"))
	out.Write([]byte("\nvar ekuConstants map[string]ExtKeyUsage\n\n"))

	out.Write([]byte("func init() {\n"))
	mapEntries := generateOIDMap(rawToOID, "ekuOIDs")
	out.Write(mapEntries)
	out.Write([]byte("\n"))
	intMapEntries := generateIntegerMap(rawToOID, "ekuConstants")
	out.Write(intMapEntries)
	out.Write([]byte("}\n"))
}
