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

// +build ignore

package main

import (
	"bytes"
	"go/format"
	"log"
	"os"
	"strings"
	"text/template"

	"gonum.org/v1/gonum/unit"
)

type Unit struct {
	DimensionName string
	Receiver      string
	PowerOffset   int    // from normal (for example, mass base unit is kg, not g)
	PrintString   string // print string for the unit (kg for mass)
	ExtraConstant []Constant
	Name          string
	TypeComment   string // text to comment the type
	Dimensions    []Dimension
	ErForm        string // for Xxxer interface
}

type Dimension struct {
	Name  string
	Power int
}

func (u Unit) Units() string {
	dims := make(unit.Dimensions)
	for _, d := range u.Dimensions {
		dims[dimOf[d.Name]] = d.Power
	}
	return dims.String()
}

const (
	AngleName             string = "AngleDim"
	CurrentName           string = "CurrentDim"
	LengthName            string = "LengthDim"
	LuminousIntensityName string = "LuminousIntensityDim"
	MassName              string = "MassDim"
	MoleName              string = "MoleDim"
	TemperatureName       string = "TemperatureDim"
	TimeName              string = "TimeDim"
)

var dimOf = map[string]unit.Dimension{
	"AngleDim":             unit.AngleDim,
	"CurrentDim":           unit.CurrentDim,
	"LengthDim":            unit.LengthDim,
	"LuminousIntensityDim": unit.LuminousIntensityDim,
	"MassDim":              unit.MassDim,
	"MoleDim":              unit.MoleDim,
	"TemperatureDim":       unit.TemperatureDim,
	"TimeDim":              unit.TimeDim,
}

type Constant struct {
	Name  string
	Value string
}

type Prefix struct {
	Name  string
	Power int
}

var Units = []Unit{
	// Base units.
	{
		DimensionName: "Dimless",
		Receiver:      "d",
		TypeComment:   "Dimless represents a dimensionless constant",
		Dimensions:    []Dimension{},
	},
	{
		DimensionName: "Angle",
		Receiver:      "a",
		PrintString:   "rad",
		Name:          "Rad",
		TypeComment:   "Angle represents an angle in radians",
		Dimensions: []Dimension{
			{Name: AngleName, Power: 1},
		},
	},
	{
		DimensionName: "Current",
		Receiver:      "i",
		PrintString:   "A",
		Name:          "Ampere",
		TypeComment:   "Current represents a current in Amperes",
		Dimensions: []Dimension{
			{Name: CurrentName, Power: 1},
		},
	},
	{
		DimensionName: "Length",
		Receiver:      "l",
		PrintString:   "m",
		Name:          "Metre",
		TypeComment:   "Length represents a length in metres",
		Dimensions: []Dimension{
			{Name: LengthName, Power: 1},
		},
	},
	{
		DimensionName: "LuminousIntensity",
		Receiver:      "j",
		PrintString:   "cd",
		Name:          "Candela",
		TypeComment:   "Candela represents a luminous intensity in candela",
		Dimensions: []Dimension{
			{Name: LuminousIntensityName, Power: 1},
		},
	},
	{
		DimensionName: "Mass",
		Receiver:      "m",
		PowerOffset:   -3,
		PrintString:   "kg",
		Name:          "Gram",
		TypeComment:   "Mass represents a mass in kilograms",
		ExtraConstant: []Constant{
			{Name: "Kilogram", Value: "Kilo * Gram"},
		},
		Dimensions: []Dimension{
			{Name: MassName, Power: 1},
		},
	},
	{
		DimensionName: "Mole",
		Receiver:      "n",
		PrintString:   "mol",
		Name:          "Mol",
		TypeComment:   "Mole represents an amount in moles",
		Dimensions: []Dimension{
			{Name: MoleName, Power: 1},
		},
	},
	{
		DimensionName: "Temperature",
		Receiver:      "t",
		PrintString:   "K",
		Name:          "Kelvin",
		TypeComment:   "Temperature represents a temperature in Kelvin",
		Dimensions: []Dimension{
			{Name: TemperatureName, Power: 1},
		},
		ErForm: "Temperaturer",
	},
	{
		DimensionName: "Time",
		Receiver:      "t",
		PrintString:   "s",
		Name:          "Second",
		TypeComment:   "Time represents a duration in seconds",
		ExtraConstant: []Constant{
			{Name: "Minute", Value: "60 * Second"},
			{Name: "Hour", Value: "60 * Minute"},
		},
		Dimensions: []Dimension{
			{Name: TimeName, Power: 1},
		},
		ErForm: "Timer",
	},

	// Derived units.
	{
		DimensionName: "AbsorbedRadioactiveDose",
		Receiver:      "a",
		PrintString:   "Gy",
		Name:          "Gray",
		TypeComment:   "AbsorbedRadioactiveDose is a measure of absorbed dose of ionizing radiation in grays",
		Dimensions: []Dimension{
			{Name: LengthName, Power: 2},
			{Name: TimeName, Power: -2},
		},
	},
	{
		DimensionName: "Acceleration",
		Receiver:      "a",
		PrintString:   "m s^-2",
		TypeComment:   "Acceleration represents an acceleration in metres per second squared",
		Dimensions: []Dimension{
			{Name: LengthName, Power: 1},
			{Name: TimeName, Power: -2},
		},
	},
	{
		DimensionName: "Area",
		Receiver:      "a",
		PrintString:   "m^2",
		TypeComment:   "Area represents an area in square metres",
		Dimensions: []Dimension{
			{Name: LengthName, Power: 2},
		},
	},
	{
		DimensionName: "Radioactivity",
		Receiver:      "r",
		PrintString:   "Bq",
		Name:          "Becquerel",
		TypeComment:   "Radioactivity represents a rate of radioactive decay in becquerels",
		Dimensions: []Dimension{
			{Name: TimeName, Power: -1},
		},
	},
	{
		DimensionName: "Capacitance",
		Receiver:      "cp",
		PrintString:   "F",
		Name:          "Farad",
		TypeComment:   "Capacitance represents an electrical capacitance in Farads",
		Dimensions: []Dimension{
			{Name: CurrentName, Power: 2},
			{Name: LengthName, Power: -2},
			{Name: MassName, Power: -1},
			{Name: TimeName, Power: 4},
		},
		ErForm: "Capacitancer",
	},
	{
		DimensionName: "Charge",
		Receiver:      "ch",
		PrintString:   "C",
		Name:          "Coulomb",
		TypeComment:   "Charge represents an electric charge in Coulombs",
		Dimensions: []Dimension{
			{Name: CurrentName, Power: 1},
			{Name: TimeName, Power: 1},
		},
		ErForm: "Charger",
	},
	{
		DimensionName: "Conductance",
		Receiver:      "co",
		PrintString:   "S",
		Name:          "Siemens",
		TypeComment:   "Conductance represents an electrical conductance in Siemens",
		Dimensions: []Dimension{
			{Name: CurrentName, Power: 2},
			{Name: LengthName, Power: -2},
			{Name: MassName, Power: -1},
			{Name: TimeName, Power: 3},
		},
		ErForm: "Conductancer",
	},
	{
		DimensionName: "EquivalentRadioactiveDose",
		Receiver:      "a",
		PrintString:   "Sy",
		Name:          "Sievert",
		TypeComment:   "EquivalentRadioactiveDose is a measure of equivalent dose of ionizing radiation in sieverts",
		Dimensions: []Dimension{
			{Name: LengthName, Power: 2},
			{Name: TimeName, Power: -2},
		},
	},
	{
		DimensionName: "Energy",
		Receiver:      "e",
		PrintString:   "J",
		Name:          "Joule",
		TypeComment:   "Energy represents a quantity of energy in Joules",
		Dimensions: []Dimension{
			{Name: LengthName, Power: 2},
			{Name: MassName, Power: 1},
			{Name: TimeName, Power: -2},
		},
	},
	{
		DimensionName: "Frequency",
		Receiver:      "f",
		PrintString:   "Hz",
		Name:          "Hertz",
		TypeComment:   "Frequency represents a frequency in Hertz",
		Dimensions: []Dimension{
			{Name: TimeName, Power: -1},
		},
	},
	{
		DimensionName: "Force",
		Receiver:      "f",
		PrintString:   "N",
		Name:          "Newton",
		TypeComment:   "Force represents a force in Newtons",
		Dimensions: []Dimension{
			{Name: LengthName, Power: 1},
			{Name: MassName, Power: 1},
			{Name: TimeName, Power: -2},
		},
		ErForm: "Forcer",
	},
	{
		DimensionName: "Inductance",
		Receiver:      "i",
		PrintString:   "H",
		Name:          "Henry",
		TypeComment:   "Inductance represents an electrical inductance in Henry",
		Dimensions: []Dimension{
			{Name: CurrentName, Power: -2},
			{Name: LengthName, Power: 2},
			{Name: MassName, Power: 1},
			{Name: TimeName, Power: -2},
		},
		ErForm: "Inductancer",
	},
	{
		DimensionName: "Power",
		Receiver:      "pw",
		PrintString:   "W",
		Name:          "Watt",
		TypeComment:   "Power represents a power in Watts",
		Dimensions: []Dimension{
			{Name: LengthName, Power: 2},
			{Name: MassName, Power: 1},
			{Name: TimeName, Power: -3},
		},
	},
	{
		DimensionName: "Resistance",
		Receiver:      "r",
		PrintString:   "Ω",
		Name:          "Ohm",
		TypeComment:   "Resistance represents an electrical resistance, impedance or reactance in Ohms",
		Dimensions: []Dimension{
			{Name: CurrentName, Power: -2},
			{Name: LengthName, Power: 2},
			{Name: MassName, Power: 1},
			{Name: TimeName, Power: -3},
		},
		ErForm: "Resistancer",
	},
	{
		DimensionName: "MagneticFlux",
		Receiver:      "m",
		PrintString:   "Wb",
		Name:          "Weber",
		TypeComment:   "MagneticFlux represents a magnetic flux in Weber",
		Dimensions: []Dimension{
			{Name: CurrentName, Power: -1},
			{Name: LengthName, Power: 2},
			{Name: MassName, Power: 1},
			{Name: TimeName, Power: -2},
		},
	},
	{
		DimensionName: "MagneticFluxDensity",
		Receiver:      "m",
		PrintString:   "T",
		Name:          "Tesla",
		TypeComment:   "MagneticFluxDensity represents a magnetic flux density in Tesla",
		Dimensions: []Dimension{
			{Name: CurrentName, Power: -1},
			{Name: MassName, Power: 1},
			{Name: TimeName, Power: -2},
		},
	},
	{
		DimensionName: "Pressure",
		Receiver:      "pr",
		PrintString:   "Pa",
		Name:          "Pascal",
		TypeComment:   "Pressure represents a pressure in Pascals",
		Dimensions: []Dimension{
			{Name: LengthName, Power: -1},
			{Name: MassName, Power: 1},
			{Name: TimeName, Power: -2},
		},
		ErForm: "Pressurer",
	},
	{
		DimensionName: "Torque",
		Receiver:      "t",
		PrintString:   "N m",
		Name:          "Newtonmetre",
		TypeComment:   "Torque represents a torque in Newton metres",
		Dimensions: []Dimension{
			{Name: LengthName, Power: 2},
			{Name: MassName, Power: 1},
			{Name: TimeName, Power: -2},
		},
		ErForm: "Torquer",
	},
	{
		DimensionName: "Velocity",
		Receiver:      "v",
		PrintString:   "m s^-1",
		TypeComment:   "Velocity represents a velocity in metres per second",
		Dimensions: []Dimension{
			{Name: LengthName, Power: 1},
			{Name: TimeName, Power: -1},
		},
	},
	{
		DimensionName: "Voltage",
		Receiver:      "v",
		PrintString:   "V",
		Name:          "Volt",
		TypeComment:   "Voltage represents a voltage in Volts",
		Dimensions: []Dimension{
			{Name: CurrentName, Power: -1},
			{Name: LengthName, Power: 2},
			{Name: MassName, Power: 1},
			{Name: TimeName, Power: -3},
		},
		ErForm: "Voltager",
	},
	{
		DimensionName: "Volume",
		Receiver:      "v",
		PowerOffset:   -3,
		PrintString:   "m^3",
		Name:          "Litre",
		TypeComment:   "Volume represents a volume in cubic metres",
		Dimensions: []Dimension{
			{Name: LengthName, Power: 3},
		},
	},
}

// Generate generates a file for each of the units
func main() {
	for _, unit := range Units {
		generate(unit)
		generateTest(unit)
	}
}

const headerTemplate = `// Code generated by "go generate gonum.org/v1/gonum/unit”; DO NOT EDIT.

// Copyright ©2014 The Gonum 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 unit

import (
	"errors"
	"fmt"
	"math"
	{{if .PrintString}}"unicode/utf8"{{end}}
)

// {{.TypeComment}}.
type {{.DimensionName}} float64
`

var header = template.Must(template.New("header").Parse(headerTemplate))

const constTemplate = `
const {{if .ExtraConstant}}({{end}}
	{{.Name}} {{.DimensionName}} = {{if .PowerOffset}} 1e{{.PowerOffset}} {{else}} 1 {{end}}
	{{$name := .Name}}
	{{range .ExtraConstant}} {{.Name}} = {{.Value}}
	{{end}}
{{if .ExtraConstant}}){{end}}
`

var prefix = template.Must(template.New("prefix").Parse(constTemplate))

const methodTemplate = `
// Unit converts the {{.DimensionName}} to a *Unit.
func ({{.Receiver}} {{.DimensionName}}) Unit() *Unit {
	return New(float64({{.Receiver}}), Dimensions{
		{{range .Dimensions}} {{.Name}}: {{.Power}},
		{{end}}
		})
}

// {{.DimensionName}} allows {{.DimensionName}} to implement a {{if .ErForm}}{{.ErForm}}{{else}}{{.DimensionName}}er{{end}} interface.
func ({{.Receiver}} {{.DimensionName}}) {{.DimensionName}}() {{.DimensionName}} {
	return {{.Receiver}}
}

// From converts the unit into the receiver. From returns an
// error if there is a mismatch in dimension.
func ({{.Receiver}} *{{.DimensionName}}) From(u Uniter) error {
	if !DimensionsMatch(u, {{if .Name}}{{.Name}}{{else}}{{.DimensionName}}(0){{end}}){
		*{{.Receiver}} = {{.DimensionName}}(math.NaN())
		return errors.New("unit: dimension mismatch")
	}
	*{{.Receiver}} = {{.DimensionName}}(u.Unit().Value())
	return nil
}
`

var methods = template.Must(template.New("methods").Parse(methodTemplate))

const formatTemplate = `
func ({{.Receiver}} {{.DimensionName}}) Format(fs fmt.State, c rune) {
	switch c {
	case 'v':
		if fs.Flag('#') {
			fmt.Fprintf(fs, "%T(%v)", {{.Receiver}}, float64({{.Receiver}}))
			return
		}
		fallthrough
	case 'e', 'E', 'f', 'F', 'g', 'G':
		p, pOk := fs.Precision()
		w, wOk := fs.Width()
		{{if .PrintString}}const unit = " {{.PrintString}}"
		switch {
		case pOk && wOk:
			fmt.Fprintf(fs, "%*.*"+string(c), pos(w-utf8.RuneCount([]byte(unit))), p, float64({{.Receiver}}))
		case pOk:
			fmt.Fprintf(fs, "%.*"+string(c), p, float64({{.Receiver}}))
		case wOk:
			fmt.Fprintf(fs, "%*"+string(c), pos(w-utf8.RuneCount([]byte(unit))), float64({{.Receiver}}))
		default:
			fmt.Fprintf(fs, "%"+string(c), float64({{.Receiver}}))
		}
		fmt.Fprint(fs, unit)
	default:
		fmt.Fprintf(fs, "%%!%c(%T=%g {{.PrintString}})", c, {{.Receiver}}, float64({{.Receiver}})) {{else}} switch {
		case pOk && wOk:
			fmt.Fprintf(fs, "%*.*"+string(c), w, p, float64({{.Receiver}}))
		case pOk:
			fmt.Fprintf(fs, "%.*"+string(c), p, float64({{.Receiver}}))
		case wOk:
			fmt.Fprintf(fs, "%*"+string(c), w, float64({{.Receiver}}))
		default:
			fmt.Fprintf(fs, "%"+string(c), float64({{.Receiver}}))
		}
	default:
		fmt.Fprintf(fs, "%%!%c(%T=%g)", c, {{.Receiver}}, float64({{.Receiver}})) {{end}}
	}
}
`

var form = template.Must(template.New("format").Parse(formatTemplate))

func generate(unit Unit) {
	lowerName := strings.ToLower(unit.DimensionName)
	filename := lowerName + ".go"
	f, err := os.Create(filename)
	if err != nil {
		log.Fatal(err)
	}
	defer f.Close()

	var buf bytes.Buffer

	err = header.Execute(&buf, unit)
	if err != nil {
		log.Fatal(err)
	}

	if unit.Name != "" {
		err = prefix.Execute(&buf, unit)
		if err != nil {
			log.Fatal(err)
		}
	}

	err = methods.Execute(&buf, unit)
	if err != nil {
		log.Fatal(err)
	}

	err = form.Execute(&buf, unit)
	if err != nil {
		log.Fatal(err)
	}

	b, err := format.Source(buf.Bytes())
	if err != nil {
		f.Write(buf.Bytes()) // This is here to debug bad format
		log.Fatalf("error formatting %q: %s", unit.DimensionName, err)
	}

	f.Write(b)
}

const testTemplate = `// Code generated by "go generate gonum.org/v1/gonum/unit; DO NOT EDIT.

// Copyright ©2019 The Gonum 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 unit

import (
	"fmt"
	"testing"
)

func Test{{.DimensionName}}(t *testing.T) {
	t.Parallel()
	for _, value := range []float64{-1, 0, 1} {
		var got {{.DimensionName}}
		err := got.From({{.DimensionName}}(value).Unit())
		if err != nil {
			t.Errorf("unexpected error for %T conversion: %v", got, err)
		}
		if got != {{.DimensionName}}(value) {
			t.Errorf("unexpected result from round trip of %T(%v): got: %v want: %v", got, float64(value), got, value)
		}
		if got != got.{{.DimensionName}}() {
			t.Errorf("unexpected result from self interface method call: got: %#v want: %#v", got, value)
		}
		err = got.From(ether(1))
		if err == nil {
			t.Errorf("expected error for ether to %T conversion", got)
		}
	}
}

func Test{{.DimensionName}}Format(t *testing.T) {
	t.Parallel()
	for _, test := range []struct{
		value  {{.DimensionName}}
		format string
		want   string
	}{
		{1.23456789, "%v", "1.23456789{{with .PrintString}} {{.}}{{end}}"},
		{1.23456789, "%.1v", "1{{with .PrintString}} {{.}}{{end}}"},
		{1.23456789, "%20.1v", "{{if .PrintString}}{{$s := printf "1 %s" .PrintString}}{{printf "%20s" $s}}{{else}}{{printf "%20s" "1"}}{{end}}"},
		{1.23456789, "%20v", "{{if .PrintString}}{{$s := printf "1.23456789 %s" .PrintString}}{{printf "%20s" $s}}{{else}}{{printf "%20s" "1.23456789"}}{{end}}"},
		{1.23456789, "%1v", "1.23456789{{with .PrintString}} {{.}}{{end}}"},
		{1.23456789, "%#v", "unit.{{.DimensionName}}(1.23456789)"},
		{1.23456789, "%s", "%!s(unit.{{.DimensionName}}=1.23456789{{with .PrintString}} {{.}}{{end}})"},
	} {
		got := fmt.Sprintf(test.format, test.value)
		if got != test.want {
			t.Errorf("Format %q %v: got: %q want: %q", test.format, float64(test.value), got, test.want)
		}
	}
}
`

var tests = template.Must(template.New("test").Parse(testTemplate))

func generateTest(unit Unit) {
	lowerName := strings.ToLower(unit.DimensionName)
	filename := lowerName + "_test.go"
	f, err := os.Create(filename)
	if err != nil {
		log.Fatal(err)
	}
	defer f.Close()

	var buf bytes.Buffer
	err = tests.Execute(&buf, unit)
	if err != nil {
		log.Fatal(err)
	}

	b, err := format.Source(buf.Bytes())
	if err != nil {
		f.Write(buf.Bytes()) // This is here to debug bad format.
		log.Fatalf("error formatting test for %q: %s", unit.DimensionName, err)
	}

	f.Write(b)
}
