{{>header}}

import { Express, Request, Response, NextFunction } from 'express'
import { Api } from './models'
import { ApiOptions } from './types'

type ToFromRequestFunction<T> = (name: string, value: any) => T
type MapOf<T> = { [name: string]: T }

let __options: ApiOptions | undefined

export function setValidationOptions(options: ApiOptions | undefined) {
	__options = options
}

/**
 * A conditional type to convert an interface model to the equivalent serialized model.
 * We may represent dates as Date objects in our object model, but we must translate
 * them to strings for the serialized model.
 */
type ToResponse<T> =
	T extends Date ? string
	: T extends object ? {
		[P in keyof T]: ToResponse<T[P]>
	}
	: T extends (infer R)[] ? ToResponse<R>[]
	: T

export function arrayFromRequest<T>(next: ToFromRequestFunction<T>): ToFromRequestFunction<T[]> {
	return function(name: string, value: any) {
		if (typeof value !== 'object' || typeof value.length !== 'number') {
			throw `Invalid type for ${name}: expected array got ${typeof value}`
		}
	
		const result: T[] = []
		for (const el of value) {
			result.push(next(name, el))
		}
		return result
	}
}

export function arrayFromRequestWithNullable<T>(next: ToFromRequestFunction<T | null>): ToFromRequestFunction<(T | null)[]> {
	return function(name: string, value: any) {
		if (typeof value !== 'object' || typeof value.length !== 'number') {
			throw `Invalid type for ${name}: expected array got ${typeof value}`
		}
	
		const result: (T | null)[] = []
		for (const el of value) {
			if (value === null) {
				result.push(null)
			} else {
				result.push(next(name, el))
			}
		}
		return result
	}
}

export function arrayToResponse<T>(next: ToFromRequestFunction<T>): ToFromRequestFunction<T[]> {
	return arrayFromRequest(next)
}

export function arrayToResponseWithNullable<T>(next: ToFromRequestFunction<T | null>): ToFromRequestFunction<(T | null)[]> {
	return arrayFromRequestWithNullable(next)
}

export function mapFromRequest<T>(next: ToFromRequestFunction<T>): ToFromRequestFunction<MapOf<T>> {
	return function(name: string, value: any) {
		if (typeof value !== 'object') {
			throw `Invalid type for ${name}: expected object got ${typeof value}`
		}
	
		const result: MapOf<T> = {}
		for (const key in value) {
			if (value.hasOwnProperty(key)) {
				result[key] = next(name, value[key])
			}
		}
		return result
	}
}

export function mapToResponse<T>(next: ToFromRequestFunction<T>): ToFromRequestFunction<MapOf<T>> {
	return mapFromRequest(next)
}

export function allowNull<T>(next: ToFromRequestFunction<T>): ToFromRequestFunction<T | null> {
	return function(name: string, value: any) {
		if (value === null) {
			return null
		}
		return next(name, value)
	}
}

export function allowUndefined<T>(next: ToFromRequestFunction<T>): ToFromRequestFunction<T | undefined> {
	return function(name: string, value: any) {
		if (value === undefined) {
			return undefined
		}
		return next(name, value)
	}
}

export function allowNullOrUndefined<T>(next: ToFromRequestFunction<T>): ToFromRequestFunction<T | null | undefined> {
	return function(name: string, value: any) {
		if (value === null) {
			return null
		}
		if (value === undefined) {
			return undefined
		}
		return next(name, value)
	}
}

export function unsupportedFromRequest(name: string, value: any): unknown {
	if (value === undefined) {
		throw `Invalid type for ${name}: expected unknown got undefined`
	}
	return value
}

export function unsupportedToResponse(name: string, value: unknown): any {
	return unsupportedFromRequest(name, value)
}

export function parseUnsupported(name: string, value: any): unknown {
	if (value === undefined) {
		throw `Invalid value for ${name}: expected unknown got undefined`
	}
	return value
}

export function booleanFromRequest(name: string, value: any): boolean {
	if (typeof value !== 'boolean') {
		throw `Invalid type for ${name}: expected boolean got ${typeof value}`
	}
	return value
}

export function booleanToResponse(name: string, value: boolean): any {
	return booleanFromRequest(name, value)
}

export function parseBoolean(name: string, value: any): boolean {
	if (value === 'true') {
		return true
	} else if (value === 'false') {
		return false
	} else {
		throw `Invalid value for ${name}: expected boolean got "${value}"`
	}
}

export function stringFromRequest(name: string, value: any): string {
	if (typeof value !== 'string') {
		throw `Invalid type for ${name}: expected string got ${typeof value}`
	}
	return value
}

export function stringToResponse(name: string, value: string): any {
	return stringFromRequest(name, value)
}

export function binaryFromRequest(name: string, value: any): Buffer {
	if (typeof value !== 'string') {
		throw `Invalid type for ${name}: expected string got ${typeof value}`
	}
	return Buffer.from(value, 'base64')
}

export function binaryToResponse(name: string, value: string | Buffer): string {
	if (typeof value === 'string') {
		return value
	} else {
		return value.toString('base64')
	}
}

export function parseString(name: string, value: any): string {
	if (value === undefined) {
		throw `Invalid value for ${name}: expected string got undefined`
	}
	if (typeof value === 'string') {
		return value
	}
	if (typeof value === 'object' && typeof value.length === 'number') {
		if (value.length > 0) {
			return value[0]
		}
	}

	throw `Invalid value for ${name}: expected string got ${typeof value}`
}

export function integerFromRequest(name: string, value: any): number {
	if (typeof value !== 'number') {
		throw `Invalid type for ${name}: expected number got ${typeof value}`
	}
	if (isNaN(value)) {
		throw `Invalid NaN for ${name}`
	}
	if (Math.floor(value) !== value) {
		throw `Invalid value for ${name}: expected integer got "${value}"`
	}
	return value
}

export function integerToResponse(name: string, value: number): any {
	return integerFromRequest(name, value)
}

export function parseInteger(name: string, value: any): number {
	if (typeof value === 'object' && typeof value.length === 'number' && value.length > 0) {
		value = value[0]
	}
	if (typeof value === 'string') {
		if (value.indexOf('.') !== -1) {
			throw `Invalid value for ${name}: expected integer got "${value}"`
		}

		const result = parseInt(value, 10)
		if (isNaN(result)) {
			throw `Invalid value for ${name}: expected integer got "${value}"`
		}
		return result
	}
	throw `Invalid value for ${name}: expected integer got ${typeof value}`
}

export function numberFromRequest(name: string, value: any): number {
	if (typeof value !== 'number') {
		throw `Invalid type for ${name}: expected number got ${typeof value}`
	}
	if (isNaN(value)) {
		throw `Invalid NaN for ${name}`
	}
	return value
}

export function numberToResponse(name: string, value: number): any {
	return numberFromRequest(name, value)
}

export function parseNumber(name: string, value: any): number {
	if (typeof value === 'object' && typeof value.length === 'number' && value.length > 0) {
		value = value[0]
	}
	if (typeof value === 'string') {
		const result = parseFloat(value)
		if (isNaN(result)) {
			throw `Invalid value for ${name}: expected float got "${value}"`
		}
		return result
	}
	throw `Invalid value for ${name}: expected number got ${typeof value}`
}

export function dateFromRequest(name: string, value: any): string {
	if (typeof value !== 'string') {
		throw `Invalid type for ${name}: expected string got ${typeof value}`
	}
	if (!value.match(/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/)) {
		throw `Invalid value for ${name}: expected valid date string got "${value}"`
	}
	return value
}

export function parseDate(name: string, value: any): string {
	return dateFromRequest(name, value)
}

export function dateToResponse(name: string, value: string): string {
	return dateFromRequest(name, value)
}

export function dateTimeFromRequest(name: string, value: any): {{#ifeq dateApproach 'native'}}Date{{else}}string{{/ifeq}} {
	if (typeof value !== 'string') {
		throw `Invalid type for ${name}: expected string got ${typeof value}`
	}
	if (!value.match(/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}(:[0-9]{2}(\.[0-9]+)?)?(Z|(\+|-)[0-9]{2}(:?[0-9]{2})?)$/)) {
		throw `Invalid value for ${name}: expected valid datetime string got "${value}"`
	}
	{{#ifeq dateApproach 'native'}}
	return new Date(value)
	{{else}}
	return value
	{{/ifeq}}
}

export function parseDateTime(name: string, value: any): {{#ifeq dateApproach 'native'}}Date{{else}}string{{/ifeq}} {
	return dateTimeFromRequest(name, value)
}

export function dateTimeToResponse(name: string, value: {{#ifeq dateApproach 'native'}}Date{{else}}string{{/ifeq}}): string {
	{{#ifeq dateApproach 'native'}}
	if (typeof value !== 'object' || typeof value.toISOString !== 'function') {
		throw `Invalid type for ${name}: expected Date got ${typeof value}`
	}
	return value.toISOString()
	{{else}}
	return dateTimeFromRequest(name, value)
	{{/ifeq}}
}

export function timeFromRequest(name: string, value: any): string {
	if (typeof value !== 'string') {
		throw `Invalid type for ${name}: expected string got ${typeof value}`
	}
	if (!value.match(/^[0-9]{2}:[0-9]{2}(:[0-9]{2}(\.[0-9]+)?)?$/)) {
		throw `Invalid value for ${name}: expected valid time string got "${value}"`
	}
	return value
}

export function parseTime(name: string, value: any): string {
	return timeFromRequest(name, value)
}

export function timeToResponse(name: string, value: string): string {
	return timeFromRequest(name, value)
}

export function verifyMinimumFiles(fieldName: string, minFileCount: number): (req: Request, res: Response, next: NextFunction) => void {
	return function(req: Request, res: Response, next: NextFunction) {
		if (!req.files || Array.isArray(req.files) || (req.files[fieldName] && req.files[fieldName].length < minFileCount)) {
			res.status(400).send(`The field '${fieldName}' requires a minimum of ${minFileCount} file(s)`)
		} else {
			next()
		}
	}
}

export function fileFromRequest(name: string, value: Express.Multer.File): any {
	if (typeof value !== 'object') {
		throw `Invalid type for ${name}: expected object got ${typeof value}`
	}
	if (typeof value.fieldname !== 'string') {
		throw `Invalid type for ${name}.fieldname: expected string got ${typeof value.fieldname}`
	}
	if (typeof value.originalname !== 'string') {
		throw `Invalid type for ${name}.originalname: expected string got ${typeof value.originalname}`
	}
	if (typeof value.mimetype !== 'string') {
		throw `Invalid type for ${name}.mimetype: expected string got ${typeof value.mimetype}`
	}
	if (typeof value.size !== 'number') {
		throw `Invalid type for ${name}.size: expected number got ${typeof value.size}`
	}
	return value
}

/* Model conversion functions */
{{>nestedValidation}}
