package elmclient

import (
	"fmt"
	"io"
	"strings"

	"github.com/apex/rpc/internal/format"
	"github.com/apex/rpc/internal/schemautil"
	"github.com/apex/rpc/schema"
)

// TODO: imports, exports, encoders, decoders, request functions, formatting, optional fields with Maybe

var module = `
-- Do not edit, this file was generated by github.com/apex/rpc.

`

// Generate writes the Elm client implementations to w.
func Generate(w io.Writer, s *schema.Schema) error {
	fmt.Fprintf(w, module)
	generateTypes(w, s)
	generateMethodTypes(w, s)
	generateMethodFuncs(w, s)
	generateDecoderFuncs(w, s)
	return nil
}

// generateTypes writes types to w.
func generateTypes(w io.Writer, s *schema.Schema) {
	out := fmt.Fprintf
	out(w, "-- TYPES\n\n")
	for _, t := range s.TypesSlice() {
		name := format.GoName(t.Name)
		out(w, "{-| %s %s -}\n", name, t.Description)
		out(w, "type alias %s =\n", name)
		writeFields(w, s, t.Properties)
		out(w, "\n\n")
	}
}

// generateMethodTypes writes method types to w.
func generateMethodTypes(w io.Writer, s *schema.Schema) {
	out := fmt.Fprintf
	out(w, "-- METHOD PARAMS\n\n")
	for _, m := range s.Methods {
		name := format.GoName(m.Name)

		if len(m.Inputs) > 0 {
			out(w, "{-| %sInput params. -}\n", name)
			out(w, "type alias %sInput =\n", name)
			writeFields(w, s, m.Inputs)
			out(w, "\n\n")
		}

		if len(m.Outputs) > 0 {
			out(w, "{-| %sOutput params. -}\n", name)
			out(w, "type alias %sOutput =\n", name)
			writeFields(w, s, m.Outputs)
			out(w, "\n\n")
		}
	}
}

// generateMethodFuncs writes method functions to w.
func generateMethodFuncs(w io.Writer, s *schema.Schema) {
	out := fmt.Fprintf
	out(w, "-- METHODS\n\n")
	for _, m := range s.Methods {
		name := format.JsName(m.Name)
		out(w, "%s : %sInput \n", name, format.GoName(m.Name))
		out(w, "%s = \n   ...", name)
		out(w, "\n\n")
	}
}

// generateDecoderFuncs writes json decoder functions to w.
func generateDecoderFuncs(w io.Writer, s *schema.Schema) {
	out := fmt.Fprintf
	out(w, "-- DECODERS\n\n")
	for _, t := range s.TypesSlice() {
		fname := format.JsName(t.Name) + "Decoder"
		tname := format.GoName(t.Name)
		writeDecoderFunc(w, s, fname, tname, t.Properties)
	}

	for _, m := range s.Methods {
		if len(m.Inputs) > 0 {
			fname := format.JsName(m.Name) + "InputDecoder"
			tname := format.GoName(m.Name) + "Input"
			writeDecoderFunc(w, s, fname, tname, m.Inputs)
		}

		if len(m.Outputs) > 0 {
			fname := format.JsName(m.Name) + "OutputDecoder"
			tname := format.GoName(m.Name) + "Output"
			writeDecoderFunc(w, s, fname, tname, m.Outputs)
		}
	}
}

// writeDecoderFunc to writer.
func writeDecoderFunc(w io.Writer, s *schema.Schema, funcName, typeName string, fields []schema.Field) {
	out := fmt.Fprintf
	out(w, "%s : Decoder %s\n", funcName, typeName)
	out(w, "%s =\n", funcName)
	out(w, "    Decode.success %s\n", typeName)
	writeDecoderFields(w, s, fields)
	out(w, "\n\n")
}

// writeDecoderFields to writer.
func writeDecoderFields(w io.Writer, s *schema.Schema, fields []schema.Field) {
	for _, f := range fields {
		// TODO: handle optional
		fmt.Fprintf(w, "      |> required %q %s\n", f.Name, elmDecoderType(s, f))
	}
}

// writeFields to writer.
func writeFields(w io.Writer, s *schema.Schema, fields []schema.Field) {
	out := fmt.Fprintf
	out(w, "  {")
	for i, f := range fields {
		out(w, " ")
		if i > 0 {
			out(w, " , ")
		}
		writeField(w, s, f)
	}
	out(w, "  }")
}

// writeField to writer.
func writeField(w io.Writer, s *schema.Schema, f schema.Field) {
	fmt.Fprintf(w, "%s : %s\n", format.JsName(f.Name), elmType(s, f))
}

// capitalize returns a capitalized string.
func capitalize(s string) string {
	return strings.ToUpper(string(s[0])) + string(s[1:])
}

// elmDecoderType returns an Elm decoder for field f.
func elmDecoderType(s *schema.Schema, f schema.Field) string {
	// ref
	if ref := f.Type.Ref.Value; ref != "" {
		t := schemautil.ResolveRef(s, f.Type.Ref)
		return format.JsName(t.Name) + "Decoder"
	}

	// TODO: decide on import, prefix these

	// type
	switch f.Type.Type {
	case schema.String:
		return "string"
	case schema.Int:
		return "int"
	case schema.Float:
		return "float"
	case schema.Bool:
		return "bool"
	case schema.Timestamp:
		return "string" // TODO: handle dates
	case schema.Object:
		return "object" // TODO: handle Dicts
	case schema.Array:
		return "(list " + elmDecoderType(s, schema.Field{
			Type: schema.TypeObject(f.Items),
		}) + ")"
	default:
		panic("unhandled type")
	}
}

// elmType returns a Elm equivalent type for field f.
func elmType(s *schema.Schema, f schema.Field) string {
	// ref
	if ref := f.Type.Ref.Value; ref != "" {
		t := schemautil.ResolveRef(s, f.Type.Ref)
		return format.GoName(t.Name)
	}

	// type
	switch f.Type.Type {
	case schema.String:
		return "String"
	case schema.Int:
		return "Int"
	case schema.Float:
		return "Float"
	case schema.Bool:
		return "Bool"
	case schema.Timestamp:
		return "String" // TODO: handle dates
	case schema.Object:
		return "object" // TODO: handle Dicts
	case schema.Array:
		return "List " + elmType(s, schema.Field{
			Type: schema.TypeObject(f.Items),
		})
	default:
		panic("unhandled type")
	}
}
