// Copyright The Athenz Authors
// Licensed under the terms of the Apache version 2.0 license. See LICENSE file for terms.
// Original code based out of https://github.com/ardielle

package main

import (
	"fmt"
	"io"
	"os"
	"strings"
	"text/template"
	"unicode"

	"github.com/ardielle/ardielle-go/rdl"
)

func javaGenerationHeader(banner string) string {
	return fmt.Sprintf("//\n// This file generated by %s. Do not modify!\n//", banner)
}

func javaGenerationPackage(schema *rdl.Schema, ns string) string {
	if ns != "" {
		return ns
	}
	return string(schema.Namespace)
}

func camelSnakeToKebab(name string) string {
	s := strings.Replace(name, "_", "-", -1)
	result := make([]rune, 0)
	wasLower := false
	for _, c := range s {
		if unicode.IsUpper(c) {
			if wasLower {
				result = append(result, '-')
			}
			result = append(result, unicode.ToLower(c))
			wasLower = false
		} else {
			result = append(result, c)
			wasLower = true
		}
	}
	return string(result)
}

func javaGenerationRootPath(schema *rdl.Schema, def string) string {
	if def != "" {
		return def
	}
	if schema.Name != "" {
		n := camelSnakeToKebab(string(schema.Name))
		if schema.Version != nil {
			return fmt.Sprintf("/%s/v%d", n, *schema.Version)
		} else {
			return fmt.Sprintf("/%s", n)
		}
	}
	return "/"
}

func javaGenerationDir(outdir string, schema *rdl.Schema, ns string) (string, error) {
	dir := outdir
	if dir == "" {
		dir = "./src/main/java"
	}
	pack := javaGenerationPackage(schema, ns)
	if pack != "" {
		dir += "/" + strings.Replace(pack, ".", "/", -1)
	}
	_, err := os.Stat(dir)
	if err != nil {
		err = os.MkdirAll(dir, 0755)
	}
	return dir, err
}

func javaGenerateResourceError(banner string, schema *rdl.Schema, writer io.Writer, ns string) error {
	funcMap := template.FuncMap{
		"header": func() string { return javaGenerationHeader(banner) },
		"package": func() string {
			s := javaGenerationPackage(schema, ns)
			if s == "" {
				return s
			}
			return "package " + s + ";\n"
		},
	}
	t := template.Must(template.New("util").Funcs(funcMap).Parse(javaResourceErrorTemplate))
	return t.Execute(writer, schema)
}

const javaResourceErrorTemplate = `{{header}}
{{package}}
public class ResourceError {

    public int code;
    public String message;

    public ResourceError code(int code) {
        this.code = code;
        return this;
    }
    public ResourceError message(String message) {
        this.message = message;
        return this;
    }

    public String toString() {
        return "{code: " + code + ", message: \"" + message + "\"}";
    }

}
`

func javaGenerateResourceException(banner string, schema *rdl.Schema, writer io.Writer, ns string) error {
	funcMap := template.FuncMap{
		"header": func() string { return javaGenerationHeader(banner) },
		"package": func() string {
			s := javaGenerationPackage(schema, ns)
			if s == "" {
				return s
			}
			return "package " + s + ";\n"
		},
	}
	t := template.Must(template.New("util").Funcs(funcMap).Parse(javaResourceExceptionTemplate))
	return t.Execute(writer, schema)
}

const javaResourceExceptionTemplate = `{{header}}
{{package}}
public class ResourceException extends RuntimeException {
    public static final int OK = 200;
    public static final int CREATED = 201;
    public static final int ACCEPTED = 202;
    public static final int NO_CONTENT = 204;
    public static final int MOVED_PERMANENTLY = 301;
    public static final int FOUND = 302;
    public static final int SEE_OTHER = 303;
    public static final int NOT_MODIFIED = 304;
    public static final int TEMPORARY_REDIRECT = 307;
    public static final int BAD_REQUEST = 400;
    public static final int UNAUTHORIZED = 401;
    public static final int FORBIDDEN = 403;
    public static final int NOT_FOUND = 404;
    public static final int CONFLICT = 409;
    public static final int GONE = 410;
    public static final int PRECONDITION_FAILED = 412;
    public static final int UNSUPPORTED_MEDIA_TYPE = 415;
    public static final int PRECONDITION_REQUIRED = 428;
    public static final int TOO_MANY_REQUESTS = 429;
    public static final int REQUEST_HEADER_FIELDS_TOO_LARGE = 431;
    public static final int INTERNAL_SERVER_ERROR = 500;
    public static final int NOT_IMPLEMENTED = 501;
    public static final int SERVICE_UNAVAILABLE = 503;
    public static final int GATEWAY_TIMEOUT = 504;
    public static final int NETWORK_AUTHENTICATION_REQUIRED = 511;

    public static String codeToString(int code) {
        switch (code) {
        case OK: return "OK";
        case CREATED: return "Created";
        case ACCEPTED: return "Accepted";
        case NO_CONTENT: return "No Content";
        case MOVED_PERMANENTLY: return "Moved Permanently";
        case FOUND: return "Found";
        case SEE_OTHER: return "See Other";
        case NOT_MODIFIED: return "Not Modified";
        case TEMPORARY_REDIRECT: return "Temporary Redirect";
        case BAD_REQUEST: return "Bad Request";
        case UNAUTHORIZED: return "Unauthorized";
        case FORBIDDEN: return "Forbidden";
        case NOT_FOUND: return "Not Found";
        case CONFLICT: return "Conflict";
        case GONE: return "Gone";
        case PRECONDITION_FAILED: return "Precondition Failed";
        case UNSUPPORTED_MEDIA_TYPE: return "Unsupported Media Type";
        case PRECONDITION_REQUIRED: return "Precondition Required";
        case TOO_MANY_REQUESTS: return "Too Many Requests";
        case REQUEST_HEADER_FIELDS_TOO_LARGE: return "Request Header Fields Too Large";
        case INTERNAL_SERVER_ERROR: return "Internal Server Error";
        case NOT_IMPLEMENTED: return "Not Implemented";
        case SERVICE_UNAVAILABLE: return "Service Unavailable";
        case GATEWAY_TIMEOUT: return "Gateway Timeout";
        case NETWORK_AUTHENTICATION_REQUIRED: return "Network Authentication Required";
        default: return "" + code;
        }
    }

    int code;
    Object data;

    public ResourceException(int code) {
        this(code, new ResourceError().code(code).message(codeToString(code)));
    }

    public ResourceException(int code, Object data) {
        super("ResourceException (" + code + "): " + data);
        this.code = code;
        this.data = data;
    }

    public int getCode() {
        return code;
    }

    public Object getData() {
        return data;
    }

    public <T> T getData(Class<T> cl) {
        return cl.cast(data);
    }

}
`

func javaType(reg rdl.TypeRegistry, rdlType rdl.TypeRef, optional bool, items rdl.TypeRef, keys rdl.TypeRef) string {
	t := reg.FindType(rdlType)
	if t == nil || t.Variant == 0 {
		panic("Cannot find type '" + rdlType + "'")
	}
	bt := reg.BaseType(t)
	switch bt {
	case rdl.BaseTypeAny:
		return "Object"
	case rdl.BaseTypeString:
		return "String"
	case rdl.BaseTypeSymbol, rdl.BaseTypeTimestamp, rdl.BaseTypeUUID:
		return string(rdlType)
	case rdl.BaseTypeBool:
		if optional {
			return "Boolean"
		}
		return "boolean"
	case rdl.BaseTypeInt8:
		if optional {
			return "Byte"
		}
		return "byte"
	case rdl.BaseTypeInt16:
		if optional {
			return "Short"
		}
		return "short"
	case rdl.BaseTypeInt32:
		if optional {
			return "Integer"
		}
		return "int"
	case rdl.BaseTypeInt64:
		if optional {
			return "Long"
		}
		return "long"
	case rdl.BaseTypeFloat32:
		if optional {
			return "Float"
		}
		return "float"
	case rdl.BaseTypeFloat64:
		if optional {
			return "Double"
		}
		return "double"
	case rdl.BaseTypeArray:
		i := rdl.TypeRef("Any")
		switch t.Variant {
		case rdl.TypeVariantArrayTypeDef:
			i = t.ArrayTypeDef.Items
		default:
			if items != "" && items != "Any" {
				i = items
			}
		}
		gitems := javaType(reg, i, true, "", "")
		return "List<" + gitems + ">"
	case rdl.BaseTypeMap:
		k := rdl.TypeRef("Any")
		i := rdl.TypeRef("Any")
		switch t.Variant {
		case rdl.TypeVariantMapTypeDef:
			k = t.MapTypeDef.Keys
			i = t.MapTypeDef.Items
		default:
			if keys != "" && keys != "Any" {
				k = keys
			}
			if items != "" && items != "Any" {
				i = items
			}
		}
		gkeys := javaType(reg, k, true, "", "")
		gitems := javaType(reg, i, true, "", "")
		return "Map<" + gkeys + ", " + gitems + ">"
	case rdl.BaseTypeStruct:
		if strings.HasPrefix(string(rdlType), "rdl.") {
			return string(rdlType)[4:]
		}
		switch t.Variant {
		case rdl.TypeVariantStructTypeDef:
			if t.StructTypeDef.Name == "Struct" {
				return "Object"
			}
		}
		return string(rdlType)
	default:
		return string(rdlType)
	}
}
