//usr/bin/go run $0 $@ ; exit
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"math"
	"os"
	"path/filepath"
	"sort"
	"strings"
)

const (
	inputDir   = "configs/coins"
	outputFile = "docs/ports.md"
)

// PortInfo contains backend and blockbook ports
type PortInfo struct {
	CoinName              string
	BlockbookInternalPort uint16
	BlockbookPublicPort   uint16
	BackendRPCPort        uint16
	BackendServicePorts   map[string]uint16
}

// PortInfoSlice is self describing
type PortInfoSlice []*PortInfo

// Config contains coin configuration
type Config struct {
	Coin struct {
		Name  string `json:"name"`
		Label string `json:"label"`
	}
	Ports map[string]uint16 `json:"ports"`
}

func checkPorts() int {
	ports := make(map[uint16][]string)
	status := 0

	files, err := ioutil.ReadDir(inputDir)
	if err != nil {
		panic(err)
	}

	for _, fi := range files {
		if fi.IsDir() || fi.Name()[0] == '.' {
			continue
		}

		path := filepath.Join(inputDir, fi.Name())
		f, err := os.Open(path)
		if err != nil {
			panic(fmt.Errorf("%s: %s", path, err))
		}
		defer f.Close()

		v := Config{}
		d := json.NewDecoder(f)
		err = d.Decode(&v)
		if err != nil {
			panic(fmt.Errorf("%s: json: %s", path, err))
		}

		if _, ok := v.Ports["blockbook_internal"]; !ok {
			fmt.Printf("%s: missing blockbook_internal port\n", v.Coin.Name)
			status = 1
		}
		if _, ok := v.Ports["blockbook_public"]; !ok {
			fmt.Printf("%s: missing blockbook_public port\n", v.Coin.Name)
			status = 1
		}
		if _, ok := v.Ports["backend_rpc"]; !ok {
			fmt.Printf("%s: missing backend_rpc port\n", v.Coin.Name)
			status = 1
		}

		for _, port := range v.Ports {
			if port > 0 {
				ports[port] = append(ports[port], v.Coin.Name)
			}
		}
	}

	for port, coins := range ports {
		if len(coins) > 1 {
			fmt.Printf("port %d: registered by %q\n", port, coins)
			status = 1
		}
	}

	if status != 0 {
		fmt.Println("Got some errors")
	}
	return status
}

func main() {
	output := "stdout"
	if len(os.Args) > 1 {
		if len(os.Args) == 2 && os.Args[1] == "-w" {
			output = outputFile
		} else {
			fmt.Fprintf(os.Stderr, "Usage: %s [-w]\n", filepath.Base(os.Args[0]))
			fmt.Fprintf(os.Stderr, "    -w    write output to %s instead of stdout\n", outputFile)
			os.Exit(1)
		}
	}

	status := checkPorts()
	if status != 0 {
		os.Exit(status)
	}

	slice, err := loadPortInfo(inputDir)
	if err != nil {
		panic(err)
	}

	sortPortInfo(slice)

	err = writeMarkdown(output, slice)
	if err != nil {
		panic(err)
	}
}

func loadPortInfo(dir string) (PortInfoSlice, error) {
	files, err := ioutil.ReadDir(dir)
	if err != nil {
		return nil, err
	}

	items := make(PortInfoSlice, 0, len(files))

	for _, fi := range files {
		if fi.IsDir() || fi.Name()[0] == '.' {
			continue
		}

		path := filepath.Join(dir, fi.Name())
		f, err := os.Open(path)
		if err != nil {
			return nil, fmt.Errorf("%s: %s", path, err)
		}
		defer f.Close()

		v := Config{}
		d := json.NewDecoder(f)
		err = d.Decode(&v)
		if err != nil {
			return nil, fmt.Errorf("%s: json: %s", path, err)
		}

		name := v.Coin.Label
		if len(name) == 0 {
			name = v.Coin.Name
		}
		item := &PortInfo{CoinName: name, BackendServicePorts: map[string]uint16{}}
		for k, v := range v.Ports {
			if v == 0 {
				continue
			}

			switch k {
			case "blockbook_internal":
				item.BlockbookInternalPort = v
			case "blockbook_public":
				item.BlockbookPublicPort = v
			case "backend_rpc":
				item.BackendRPCPort = v
			default:
				if len(k) > 8 && k[:8] == "backend_" {
					item.BackendServicePorts[k[8:]] = v
				}
			}
		}

		items = append(items, item)
	}

	return items, nil
}

func sortPortInfo(slice PortInfoSlice) {
	// normalizes values in order to sort zero values at the bottom of the slice
	normalize := func(a, b uint16) (uint16, uint16) {
		if a == 0 {
			a = math.MaxUint16
		}
		if b == 0 {
			b = math.MaxUint16
		}
		return a, b
	}

	// sort values by BlockbookPublicPort, then by BackendRPCPort and finally by
	// CoinName; zero values are sorted at the bottom of the slice
	sort.Slice(slice, func(i, j int) bool {
		a, b := normalize(slice[i].BlockbookPublicPort, slice[j].BlockbookPublicPort)

		if a < b {
			return true
		}
		if a > b {
			return false
		}

		a, b = normalize(slice[i].BackendRPCPort, slice[j].BackendRPCPort)

		if a < b {
			return true
		}
		if a > b {
			return false
		}

		return strings.Compare(slice[i].CoinName, slice[j].CoinName) == -1
	})
}

func writeMarkdown(output string, slice PortInfoSlice) error {
	var (
		buf bytes.Buffer
		err error
	)

	fmt.Fprintf(&buf, "# Registry of ports\n\n")

	header := []string{"coin", "blockbook internal port", "blockbook public port", "backend rpc port", "backend service ports (zmq)"}
	writeTable(&buf, header, slice)

	fmt.Fprintf(&buf, "\n> NOTE: This document is generated from coin definitions in `configs/coins`.\n")

	out := os.Stdout
	if output != "stdout" {
		out, err = os.OpenFile(output, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
		if err != nil {
			return err
		}
		defer out.Close()
	}

	n, err := out.Write(buf.Bytes())
	if err != nil {
		return err
	}
	if n < len(buf.Bytes()) {
		return io.ErrShortWrite
	}

	return nil
}

func writeTable(w io.Writer, header []string, slice PortInfoSlice) {
	rows := make([][]string, len(slice))
	for i, item := range slice {
		row := make([]string, len(header))
		row[0] = item.CoinName
		if item.BlockbookInternalPort > 0 {
			row[1] = fmt.Sprintf("%d", item.BlockbookInternalPort)
		}
		if item.BlockbookPublicPort > 0 {
			row[2] = fmt.Sprintf("%d", item.BlockbookPublicPort)
		}
		if item.BackendRPCPort > 0 {
			row[3] = fmt.Sprintf("%d", item.BackendRPCPort)
		}

		svcPorts := make([]string, 0, len(item.BackendServicePorts))
		for k, v := range item.BackendServicePorts {
			var s string
			if k == "message_queue" {
				s = fmt.Sprintf("%d", v)
			} else {
				s = fmt.Sprintf("%d %s", v, k)
			}
			svcPorts = append(svcPorts, s)
		}

		row[4] = strings.Join(svcPorts, ", ")

		rows[i] = row
	}

	padding := make([]int, len(header))
	for column := range header {
		padding[column] = len(header[column])

		for _, row := range rows {
			padding[column] = maxInt(padding[column], len(row[column]))
		}
	}

	content := make([][]string, 0, len(rows)+2)

	content = append(content, paddedRow(header, padding))
	content = append(content, delim("-", padding))

	for _, row := range rows {
		content = append(content, paddedRow(row, padding))
	}

	for _, row := range content {
		fmt.Fprintf(w, "|%s|\n", strings.Join(row, "|"))
	}
}

func maxInt(a, b int) int {
	if a > b {
		return a
	}
	return b
}

func paddedRow(row []string, padding []int) []string {
	out := make([]string, len(row))
	for i := 0; i < len(row); i++ {
		format := fmt.Sprintf(" %%-%ds ", padding[i])
		out[i] = fmt.Sprintf(format, row[i])
	}
	return out
}

func delim(str string, padding []int) []string {
	out := make([]string, len(padding))
	for i := 0; i < len(padding); i++ {
		out[i] = strings.Repeat(str, padding[i]+2)
	}
	return out
}
