import * as List from "../stdlib/List";

import * as Aliases from "../types";
import { TypeAlias, Property, Typeclass, TypeclassFunction, Impl } from "../types";

import * as Blocks from "../types";
import { Function, FunctionArg, FunctionArgsUnion, Const, ImportModule, Import, Export, Module } from "../types";

import * as Boolean from "../types";
import { Equality, InEquality, LessThan, LessThanOrEqual, GreaterThan, GreaterThanOrEqual, And, Or, ListPrepend } from "../types";

import * as Comments from "../types";
import { Comment, MultilineComment } from "../types";

import * as Control from "../types";
import { IfStatement, ElseIfStatement, ListDestructurePart, BranchPattern, Branch, CaseStatement, DoBlock, DoExpression } from "../types";

import * as Functions from "../types";
import { FunctionCall, Lambda, LambdaCall } from "../types";

import * as Objects from "../types";
import { ObjectLiteral, Field, ModuleReference } from "../types";

import * as Operators from "../types";
import { Addition, Subtraction, Multiplication, Division, Mod, LeftPipe, RightPipe } from "../types";

import * as Values from "../types";
import { Value, StringValue, FormatStringValue, ListValue, ListRange } from "../types";

import { Tag, UnionType, UnionUntaggedType, Type, TagArg, Block, Constructor, Expression, isSimpleValue } from "../types";

import { prefixLines } from "./Common";

export { generateDerw };
export { generateExpression };

function generateTag(tag: Tag): string {
    function generateTypeArg(arg: TagArg): string {
        return (function(type_: any) {
            return arg.name + ": " + type_ + "";
        })(generateType(arg.type));
    }
    const typeDefArgs: string = (function(y: any) {
        return y.join(",\n    ");
    })(List.map(generateTypeArg, tag.args));
    const funcDefArgsStr: string = tag.args.length > 0 ? ` { ${typeDefArgs} }` : "";
    return (function(y: any) {
        return y + funcDefArgsStr;
    })(generateType({
        kind: "FixedType",
        name: tag.name,
        args: [ ]
    }));
}

function generateUnionType(syntax: UnionType): string {
    const tags: string = (function(y: any) {
        return y.join("\n| ");
    })(List.map(generateTag, syntax.tags));
    const prefixed: string = prefixLines(tags, 4);
    return `type ${generateType(syntax.type)} =\n${prefixed}`;
}

function generateUnionUntaggedType(syntax: UnionUntaggedType): string {
    const values: string = (function(y: any) {
        return y.join("\n| ");
    })(List.map(generateStringValue, syntax.values));
    const prefixed: string = prefixLines(values, 4);
    return `type ${generateType(syntax.type)} =\n${prefixed}`;
}

function generateProperty(syntax: Property): string {
    const generatedType: string = generateTopLevelType(syntax.type);
    switch (syntax.type.kind) {
        case "FunctionType": {
            return `${syntax.name}: ${generatedType.slice(1, -1)}`;
        }
        default: {
            return `${syntax.name}: ${generatedType}`;
        }
    }
}

function generateTypeAlias(syntax: TypeAlias): string {
    const properties: string = (function(y: any) {
        return y.join(",\n    ");
    })(List.map(generateProperty, syntax.properties));
    const typeDef: string = generateType(syntax.type);
    if (syntax.properties.length === 0) {
        return `type alias ${typeDef} = {\n}`;
    } else {
        return `type alias ${typeDef} = {\n    ${properties}\n}`;
    }
}

function generateTypeclassFunction(syntax: TypeclassFunction): string {
    const argTypes: string[] = List.map(generateTopLevelType, syntax.args);
    const returnType: string = generateTopLevelType(syntax.returnType);
    const types: string = (function(y: any) {
        return y.join(" -> ");
    })(List.append(argTypes, [ returnType ]));
    return `${syntax.name}: ${types}`;
}

function generateTypeclass(syntax: Typeclass): string {
    const variables: string = (function(y: any) {
        return y.join(" ");
    })(List.map(generateType, syntax.variables));
    const functions: string = (function(y: any) {
        return y.join("\n\n");
    })(List.map(function(line: any) {
        return prefixLines(line, 4);
    }, List.map(generateTypeclassFunction, syntax.functions)));
    return `typeclass ${syntax.name} ${variables}\n${functions}`;
}

function generateImpl(syntax: Impl): string {
    const qualifier: string = generateType(syntax.qualifier);
    const blocks: string = (function(y: any) {
        return y.join("\n\n");
    })(List.map(function(line: any) {
        return prefixLines(line, 4);
    }, List.map(generateBlock, syntax.functions)));
    return `impl ${syntax.name} ${qualifier}\n${blocks}`;
}

function generateListType(args: Type[]): string {
    if (args.length > 0 && args[0].kind === "GenericType") {
        return `List ${generateType(args[0])}`;
    } else {
        const fixedArgs: Type[] = List.filter(function(type_: any) {
            return type_.kind === "FixedType";
        }, args);
        switch (fixedArgs.length) {
            case 0: {
                return "List any";
            }
            case fixedArgs.length: {
                if (fixedArgs.length === 1) {
                    const [ x ] = fixedArgs;
                    if (x.kind === "FixedType" && x.args.length > 0) {
                    return `List (${generateType(x)})`;
                } else {
                    return `List ${generateType(x)}`;
                };
                }
            }
            default: {
                return `List (${fixedArgs.map(generateType).join(" | ")})`;
            }
        };
    }
}

function generateTopLevelType(type_: Type): string {
    switch (type_.kind) {
        case "GenericType": {
            return generateType(type_);
        }
        case "FixedType": {
            const { name, args } = type_;
            if (args.length > 0 && args[0].kind === "FixedType" && args[0].args.length > 0) {
                function wrapper(x: string): string {
                    return `(${x})`;
                }
                return `${name} ${args.map(generateTopLevelType).map(wrapper).join(" ")}`;
            } else {
                const genericArgs: Type[] = List.filter(function(type_: any) {
                    return type_.kind === "GenericType" || type_.kind === "FixedType";
                }, args);
                if (genericArgs.length === 0) {
                    return name;
                } else {
                    function wrapper(newType: Type): string {
                        switch (newType.kind) {
                            case "FixedType": {
                                const { args } = newType;
                                if (args.length > 0) {
                                    return `(${generateTopLevelType(newType)})`;
                                } else {
                                    return `${generateTopLevelType(newType)}`;
                                };
                            }
                            default: {
                                return `${generateTopLevelType(newType)}`;
                            }
                        }
                    }
                    const wrappedArgs: string[] = genericArgs.map(wrapper);
                    return `${name} ${wrappedArgs.join(" ")}`;
                };
            };
        }
        case "FunctionType": {
            const { args } = type_;
            return `(${args.map(generateTopLevelType).join(" -> ")})`;
        }
        case "ObjectLiteralType": {
            return ``;
        }
    }
}

function generateType(type_: Type): string {
    switch (type_.kind) {
        case "GenericType": {
            const { name } = type_;
            return name;
        }
        case "FixedType": {
            const { name, args } = type_;
            if (name === "List") {
                return generateListType(args);
            } else {
                const genericArgs: Type[] = List.filter(function(type_: any) {
                    return type_.kind === "GenericType";
                }, args);
                if (genericArgs.length === 0) {
                    return name;
                } else {
                    return `${name} ${genericArgs.map(generateType).join(" ")}`;
                };
            };
        }
        case "FunctionType": {
            const { args } = type_;
            return `(${args.map(generateType).join(" -> ")})`;
        }
        case "ObjectLiteralType": {
            return ``;
        }
    }
}

function generateField(field: Field): string {
    const value: string = generateExpression(field.value);
    return `${field.name}: ${value}`;
}

function generateObjectLiteral(literal: ObjectLiteral): string {
    const fields: string = (function(y: any) {
        return y.join(",\n");
    })(literal.fields.map(generateField));
    const maybePrefixed: string = prefixLines(fields, 4);
    if (literal.base === null) {
        const _res1477476904 = `${literal.fields.length}`;
        switch (_res1477476904) {
            case "0": {
                return "{ }";
            }
            case "1": {
                return `{ ${fields} }`;
            }
            default: {
                return `{\n${maybePrefixed}\n}`;
            }
        };
    } else {
        if (literal.fields.length === 1) {
            return `{ ${literal.base.body}, ${fields} }`;
        } else {
            return `{\n    ${literal.base.body},\n${maybePrefixed}\n}`;
        };
    }
}

function generateValue(value: Value): string {
    return value.body;
}

function generateStringValue(string: StringValue): string {
    return `"${string.body}"`;
}

function generateFormatStringValue(string: FormatStringValue): string {
    const split: string[] = string.body.split("\n");
    switch (split.length) {
        case split.length: {
            if (split.length === 1) {
                const [ firstLine ] = split;
                return "`" + firstLine + "`";
            }
        }
        case split.length: {
            if (split.length >= 1) {
                const [ firstLine, ...rest ] = split;
                const indentedSplit: string[] = List.map(function(line: any) {
                    return "    " + line;
                }, split);
                const joined: string = (function(y: any) {
                    return y.join("\n");
                })(indentedSplit);
                return "`\n" + joined + "\n`";
            }
        }
        default: {
            return "`" + string.body + "`";
        }
    }
}

function generateListValue(list: ListValue): string {
    switch (list.items.length) {
        case 0: {
            return "[ ]";
        }
        case list.items.length: {
            if (list.items.length === 1) {
                const [ x ] = list.items;
                return `[ ${generateExpression(x)} ]`;
            }
        }
        default: {
            return `[\n${prefixLines(list.items.map(generateExpression).join(",\n"), 4)}\n]`;
        }
    }
}

function generateListRange(list: ListRange): string {
    return `[ ${list.start.body}..${list.end.body} ]`;
}

function generateLetBlock(body: Block[]): string {
    switch (body.length) {
        case 0: {
            return "";
        }
        case body.length: {
            if (body.length >= 1) {
                const [ x, ...ys ] = body;
                const prefixedLet: string = prefixLines("\nlet", 4);
                const prefixedBody: string = (function(y: any) {
                    return y.join("\n\n");
                })(List.map(generateBlock, body));
                const prefixedLines: string = prefixLines(prefixedBody, 8);
                const prefixedIn: string = prefixLines("\nin", 4);
                return `${prefixedLet}\n${prefixedLines}${prefixedIn}${prefixLines("", 8)}`;
            }
        }
        default: {
            return "";
        }
    }
}

function generateElseIfStatement(elseIfStatement: ElseIfStatement): string {
    const predicate: string = generateExpression(elseIfStatement.predicate);
    const maybeLetBody: string = generateLetBlock(elseIfStatement.letBody);
    const indent: number = maybeLetBody === "" ? 4 : 8;
    const body: string = (function(lines: any) {
        return prefixLines(lines, indent);
    })(generateExpression(elseIfStatement.body));
    return `else if ${predicate} then${maybeLetBody}\n${body}`;
}

function generateIfStatement(ifStatement: IfStatement): string {
    const maybeIfLetBody: string = generateLetBlock(ifStatement.ifLetBody);
    const maybeElseLetBody: string = generateLetBlock(ifStatement.elseLetBody);
    const predicate: string = generateExpression(ifStatement.predicate);
    const ifIndent: number = maybeIfLetBody === "" ? 4 : 8;
    const ifBody: string = (function(lines: any) {
        return prefixLines(lines, ifIndent);
    })(generateExpression(ifStatement.ifBody));
    const elseIndent: number = maybeElseLetBody === "" ? 4 : 8;
    const elseBody: string = (function(lines: any) {
        return prefixLines(lines, elseIndent);
    })(generateExpression(ifStatement.elseBody));
    const elseIfs: string = (function(y: any) {
        return y.join("\n");
    })(List.map(generateElseIfStatement, ifStatement.elseIf));
    const prefixedElseIfs: string = elseIfs === "" ? "" : `${elseIfs}\n`;
    return `if ${predicate} then${maybeIfLetBody}\n${ifBody}\n${prefixedElseIfs}else${maybeElseLetBody}\n${elseBody}`;
}

function generateConstructor(constructor: Constructor): string {
    switch (constructor.pattern.fields.length) {
        case 0: {
            return constructor.constructor;
        }
        default: {
            return `${constructor.constructor} ${generateObjectLiteral(constructor.pattern)}`;
        }
    }
}

function generateListDestructurePart(part: ListDestructurePart): string {
    switch (part.kind) {
        case "EmptyList": {
            return "[]";
        }
        case "StringValue": {
            const { body } = part;
            return `"${body}"`;
        }
        case "FormatStringValue": {
            const { body } = part;
            return "`" + body + "`";
        }
        case "Value": {
            const { body } = part;
            return body;
        }
        case "Destructure": {
            const { pattern } = part;
            if (pattern.length === 0) {
                return part.constructor;
            } else {
                return `${part.constructor} ${pattern}`;
            };
        }
    }
}

function generateBranchPattern(branchPattern: BranchPattern): string {
    switch (branchPattern.kind) {
        case "Destructure": {
            const { pattern } = branchPattern;
            if (pattern.length === 0) {
                return branchPattern.constructor;
            } else {
                return `${branchPattern.constructor} ${pattern}`;
            };
        }
        case "StringValue": {
            const { body } = branchPattern;
            return `"${body}"`;
        }
        case "FormatStringValue": {
            const { body } = branchPattern;
            return "`" + body + "`";
        }
        case "EmptyList": {
            return "[]";
        }
        case "ListDestructure": {
            const { parts } = branchPattern;
            return (function(y: any) {
            return y.join(" :: ");
        })(List.map(generateListDestructurePart, parts));
        }
        case "Default": {
            return "default";
        }
    }
}

function generateBranch(branch: Branch): string {
    const maybeLetBody: string = generateLetBlock(branch.letBody);
    const bodyIndent: number = maybeLetBody === "" ? 4 : 8;
    const body: string = (function(y: any) {
        return prefixLines(y, bodyIndent);
    })(generateExpression(branch.body));
    const pattern: string = generateBranchPattern(branch.pattern);
    return `${pattern} ->${maybeLetBody}\n${body}`;
}

function generateCaseStatement(caseStatement: CaseStatement): string {
    const predicate: string = generateExpression(caseStatement.predicate);
    const branches: string = (function(y: any) {
        return prefixLines(y, 4);
    })((function(y: any) {
        return y.join("\n\n");
    })(List.map(generateBranch, caseStatement.branches)));
    return `case ${predicate} of\n${branches}`;
}

function needsBrackets(expression: Expression): boolean {
    switch (expression.kind) {
        case "FunctionCall": {
            return true;
        }
        default: {
            return false;
        }
    }
}

function applyBrackets(needsBrackets: boolean, generated: string): string {
    if (needsBrackets) {
        return "(" + generated + ")";
    } else {
        return generated;
    }
}

function generateAddition(addition: Addition): string {
    const left: string = applyBrackets(needsBrackets(addition.left), generateExpression(addition.left));
    const right: string = applyBrackets(needsBrackets(addition.right), generateExpression(addition.right));
    return `${left} + ${right}`;
}

function generateSubtraction(subtraction: Subtraction): string {
    const left: string = applyBrackets(needsBrackets(subtraction.left), generateExpression(subtraction.left));
    const right: string = applyBrackets(needsBrackets(subtraction.right), generateExpression(subtraction.right));
    return `${left} - ${right}`;
}

function generateMultiplication(multiplication: Multiplication): string {
    const left: string = applyBrackets(needsBrackets(multiplication.left), generateExpression(multiplication.left));
    const right: string = applyBrackets(needsBrackets(multiplication.right), generateExpression(multiplication.right));
    return `${left} * ${right}`;
}

function generateDivision(division: Division): string {
    const left: string = applyBrackets(needsBrackets(division.left), generateExpression(division.left));
    const right: string = applyBrackets(needsBrackets(division.right), generateExpression(division.right));
    return `${left} / ${right}`;
}

function generateMod(mod: Mod): string {
    const left: string = applyBrackets(needsBrackets(mod.left), generateExpression(mod.left));
    const right: string = applyBrackets(needsBrackets(mod.right), generateExpression(mod.right));
    return `${left} % ${right}`;
}

function generateLeftPipe(leftPipe: LeftPipe): string {
    const left: string = generateExpression(leftPipe.left);
    const right: string = generateExpression(leftPipe.right);
    return `${left}\n    |> ${right}`;
}

function generateRightPipe(rightPipe: RightPipe): string {
    const left: string = generateExpression(rightPipe.left);
    const right: string = generateExpression(rightPipe.right);
    return `${left}\n    <| ${right}`;
}

function generateModuleReference(moduleReference: ModuleReference): string {
    if (moduleReference.path.length === 0) {
        return `.${generateExpression(moduleReference.value)}`;
    } else {
        const left: string = moduleReference.path.join(".");
        const right: string = generateExpression(moduleReference.value);
        return `${left}.${right}`;
    }
}

function generateFunctionCallArg(arg: Expression): string {
    switch (arg.kind) {
        case "Constructor": {
            const { pattern } = arg;
            switch (pattern.fields.length) {
                case 0: {
                    return generateExpression(arg);
                }
                default: {
                    return `(${generateExpression(arg)})`;
                }
            };
        }
        case "FunctionCall": {
            const { args } = arg;
            switch (args.length) {
                case 0: {
                    return generateExpression(arg);
                }
                default: {
                    return `(${generateExpression(arg)})`;
                }
            };
        }
        case "ModuleReference": {
            const { value } = arg;
            switch (value.kind) {
                case "Constructor": {
                    return `(${generateExpression(arg)})`;
                }
                case "FunctionCall": {
                    return `(${generateExpression(arg)})`;
                }
                default: {
                    return generateExpression(arg);
                }
            };
        }
        case "ListPrepend": {
            return `(${generateExpression(arg)})`;
        }
        case "Addition": {
            return `(${generateExpression(arg)})`;
        }
        case "Subtraction": {
            return `(${generateExpression(arg)})`;
        }
        case "Multiplication": {
            return `(${generateExpression(arg)})`;
        }
        case "Division": {
            return `(${generateExpression(arg)})`;
        }
        case "Equality": {
            return `(${generateExpression(arg)})`;
        }
        case "InEquality": {
            return `(${generateExpression(arg)})`;
        }
        case "LessThan": {
            return `(${generateExpression(arg)})`;
        }
        case "GreaterThan": {
            return `(${generateExpression(arg)})`;
        }
        case "LessThanOrEqual": {
            return `(${generateExpression(arg)})`;
        }
        case "GreaterThanOrEqual": {
            return `(${generateExpression(arg)})`;
        }
        case "LeftPipe": {
            return `(${generateExpression(arg)})`;
        }
        case "RightPipe": {
            return `(${generateExpression(arg)})`;
        }
        default: {
            return generateExpression(arg);
        }
    }
}

function generateFunctionCall(functionCall: FunctionCall): string {
    if (functionCall.args.length === 0) {
        return `${functionCall.name}()`;
    } else {
        const args: string = (function(y: any) {
            return y.join(" ");
        })(List.map(generateFunctionCallArg, functionCall.args));
        return `${functionCall.name} ${args}`;
    }
}

function generateLambda(lambda: Lambda): string {
    const args: string = (function(y: any) {
        return y.join(" ");
    })(List.map(function(arg: any) {
        return arg;
    }, lambda.args));
    const body: string = generateExpression(lambda.body);
    const indent: string = isSimpleValue(lambda.body.kind) ? ` ${body}` : (function(y: any) {
        return "\n" + y;
    })(prefixLines(body, 4));
    return `(\\${args} ->${indent})`;
}

function generateLambdaCall(lambdaCall: LambdaCall): string {
    const args: string = (function(y: any) {
        return y.join(", ");
    })(List.map(function(arg: any) {
        return `${arg}: any`;
    }, lambdaCall.args));
    const argsValues: string = (function(y: any) {
        return y.join(", ");
    })(List.map(generateExpression, lambdaCall.args));
    const body: string = generateExpression(lambdaCall.lambda.body);
    return `(function(${args}) {\n    return ${body};\n})(${argsValues})`;
}

function generateEquality(equality: Equality): string {
    const left: string = generateExpression(equality.left);
    const right: string = generateExpression(equality.right);
    return `${left} == ${right}`;
}

function generateInEquality(inEquality: InEquality): string {
    const left: string = generateExpression(inEquality.left);
    const right: string = generateExpression(inEquality.right);
    return `${left} != ${right}`;
}

function generateLessThan(lessThan: LessThan): string {
    const left: string = generateExpression(lessThan.left);
    const right: string = generateExpression(lessThan.right);
    return `${left} < ${right}`;
}

function generateLessThanOrEqual(lessThanOrEqual: LessThanOrEqual): string {
    const left: string = generateExpression(lessThanOrEqual.left);
    const right: string = generateExpression(lessThanOrEqual.right);
    return `${left} <= ${right}`;
}

function generateGreaterThan(greaterThan: GreaterThan): string {
    const left: string = generateExpression(greaterThan.left);
    const right: string = generateExpression(greaterThan.right);
    return `${left} > ${right}`;
}

function generateGreaterThanOrEqual(greaterThanOrEqual: GreaterThanOrEqual): string {
    const left: string = generateExpression(greaterThanOrEqual.left);
    const right: string = generateExpression(greaterThanOrEqual.right);
    return `${left} >= ${right}`;
}

function generateAnd(and: And): string {
    const left: string = generateExpression(and.left);
    const right: string = generateExpression(and.right);
    return `${left} && ${right}`;
}

function generateOr(or: Or): string {
    const left: string = generateExpression(or.left);
    const right: string = generateExpression(or.right);
    return `${left} || ${right}`;
}

function generateListPrepend(prepend: ListPrepend): string {
    const left: string = generateExpression(prepend.left);
    const right: string = generateExpression(prepend.right);
    return `${left} :: ${right}`;
}

function generateExpression(expression: Expression): string {
    switch (expression.kind) {
        case "Value": {
            return generateValue(expression);
        }
        case "StringValue": {
            return generateStringValue(expression);
        }
        case "FormatStringValue": {
            return generateFormatStringValue(expression);
        }
        case "ListValue": {
            return generateListValue(expression);
        }
        case "ListRange": {
            return generateListRange(expression);
        }
        case "ObjectLiteral": {
            return generateObjectLiteral(expression);
        }
        case "IfStatement": {
            return generateIfStatement(expression);
        }
        case "CaseStatement": {
            return generateCaseStatement(expression);
        }
        case "Addition": {
            return generateAddition(expression);
        }
        case "Subtraction": {
            return generateSubtraction(expression);
        }
        case "Multiplication": {
            return generateMultiplication(expression);
        }
        case "Division": {
            return generateDivision(expression);
        }
        case "Mod": {
            return generateMod(expression);
        }
        case "And": {
            return generateAnd(expression);
        }
        case "Or": {
            return generateOr(expression);
        }
        case "ListPrepend": {
            return generateListPrepend(expression);
        }
        case "LeftPipe": {
            return generateLeftPipe(expression);
        }
        case "RightPipe": {
            return generateRightPipe(expression);
        }
        case "ModuleReference": {
            return generateModuleReference(expression);
        }
        case "FunctionCall": {
            return generateFunctionCall(expression);
        }
        case "Lambda": {
            return generateLambda(expression);
        }
        case "LambdaCall": {
            return generateLambdaCall(expression);
        }
        case "Constructor": {
            return generateConstructor(expression);
        }
        case "Equality": {
            return generateEquality(expression);
        }
        case "InEquality": {
            return generateInEquality(expression);
        }
        case "LessThan": {
            return generateLessThan(expression);
        }
        case "LessThanOrEqual": {
            return generateLessThanOrEqual(expression);
        }
        case "GreaterThan": {
            return generateGreaterThan(expression);
        }
        case "GreaterThanOrEqual": {
            return generateGreaterThanOrEqual(expression);
        }
    }
}

function generateDoExpression(expression: DoExpression): string {
    switch (expression.kind) {
        case "Const": {
            return generateConst(expression);
        }
        case "Function": {
            return generateFunction(expression);
        }
        case "FunctionCall": {
            return generateFunctionCall(expression);
        }
        case "ModuleReference": {
            return generateModuleReference(expression);
        }
        case "IfStatement": {
            return generateIfStatement(expression);
        }
    }
}

function generateDoBlock(doBody: DoBlock): string {
    return (function(y: any) {
        return `do\n${y}\nreturn`;
    })((function(y: any) {
        return prefixLines(y, 4);
    })((function(y: any) {
        return y.join("\n\n");
    })(List.map(function(expression: any) {
        return generateDoExpression(expression);
    }, doBody.expressions))));
}

const openParens: string = "(";

const closeParens: string = ")";

function typeToArg(type_: Type): string {
    return (function(y: any) {
        return y.replace(closeParens, "");
    })((function(y: any) {
        return y.replace(openParens, "");
    })((function(y: any) {
        return y.replace(" ", "_");
    })((function(y: any) {
        return y[0].toLowerCase() + y.slice(1);
    })(generateTopLevelType(type_)))));
}

function generateFunctionArg(knownNames: string[], arg: FunctionArgsUnion): string {
    switch (arg.kind) {
        case "FunctionArg": {
            return arg.name;
        }
        case "AnonFunctionArg": {
            const tempName: string = (function (): any {
                switch (arg.type.kind) {
                    case "FunctionType": {
                        return "fn";
                    }
                    default: {
                        return `${typeToArg(arg.type)}`;
                    }
                }
            })();
            if (knownNames.indexOf(tempName) === -1) {
                return tempName;
            } else {
                return `_${tempName}`;
            };
        }
    }
}

function knownArgNames(args: FunctionArgsUnion[]): string[] {
    switch (args.length) {
        case 0: {
            return [ ];
        }
        case args.length: {
            if (args.length >= 1) {
                const [ arg, ...rest ] = args;
                switch (arg.kind) {
                case "FunctionArg": {
                    return [ arg.name, ...knownArgNames(rest) ];
                }
                case "AnonFunctionArg": {
                    return knownArgNames(rest);
                }
            };
            }
        }
        default: {
            return [ ];
        }
    }
}

function generateFunctionArgType(arg: FunctionArgsUnion): string {
    switch (arg.kind) {
        case "FunctionArg": {
            return generateTopLevelType(arg.type);
        }
        case "AnonFunctionArg": {
            return generateTopLevelType(arg.type);
        }
    }
}

function generateFunction(function_: Function): string {
    const argsTypes: string = (function(y: any) {
        return y.join(" -> ");
    })(List.map(generateFunctionArgType, function_.args));
    const knownNames: string[] = knownArgNames(function_.args);
    const args: string = (function(y: any) {
        return y.join(" ");
    })(List.map(function(x: any) {
        return generateFunctionArg(knownNames, x);
    }, function_.args));
    const maybeLetBody: string = generateLetBlock(function_.letBody);
    const maybeDoBody: string = function_.doBody === null ? "" : `\n${prefixLines(generateDoBlock(function_.doBody), 4)}`;
    const returnType: string = generateTopLevelType(function_.returnType);
    const bodyIndent: number = maybeLetBody === "" && maybeDoBody === "" ? 4 : 8;
    const body: string = (function(y: any) {
        return prefixLines(y, bodyIndent);
    })(generateExpression(function_.body));
    return (function(y: any) {
        return y.join("\n");
    })([ `${function_.name}: ${argsTypes} -> ${returnType}`, `${function_.name} ${args} =${maybeLetBody}${maybeDoBody}`, `${body}` ]);
}

function generateConst(constDef: Const): string {
    const maybeLetBody: string = generateLetBlock(constDef.letBody);
    const bodyIndent: number = maybeLetBody === "" ? 4 : 8;
    const body: string = (function(y: any) {
        return prefixLines(y, bodyIndent);
    })(generateExpression(constDef.value));
    const typeDef: string = generateTopLevelType(constDef.type);
    return (function(y: any) {
        return y.join("\n");
    })([ `${constDef.name}: ${typeDef}`, `${constDef.name} =${maybeLetBody}`, `${body}` ]);
}

function generateImportModule(module: ImportModule): string {
    const partExposing: string = module.exposing.length === 0 ? "" : ` exposing ( ${module.exposing.join(", ")} )`;
    const moduleName: string = (function (): any {
        switch (module.namespace) {
            case "Global": {
                if (module.name.includes("/")) {
                    return `"${module.name}"`;
                } else {
                    return module.name;
                };
            }
            case "Relative": {
                return module.name;
            }
        }
    })();
    switch (module.alias.kind) {
        case "Just": {
            const { value } = module.alias;
            return `import ${moduleName} as ${value}${partExposing}`;
        }
        case "Nothing": {
            return `import ${moduleName}${partExposing}`;
        }
    }
}

function generateImportBlock(imports: Import): string {
    return (function(y: any) {
        return y.join("\n");
    })(List.map(generateImportModule, imports.modules));
}

function generateExportBlock(exports: Export): string {
    return `exposing ( ${exports.names.join(", ")} )`;
}

function generateComment(comment: Comment): string {
    return `-- ${comment.body}`;
}

function generateMultilineComment(comment: MultilineComment): string {
    return `{-\n${comment.body}\n-}`;
}

function generateBlock(syntax: Block): string {
    switch (syntax.kind) {
        case "Import": {
            return generateImportBlock(syntax);
        }
        case "Export": {
            return generateExportBlock(syntax);
        }
        case "UnionType": {
            return generateUnionType(syntax);
        }
        case "UnionUntaggedType": {
            return generateUnionUntaggedType(syntax);
        }
        case "TypeAlias": {
            return generateTypeAlias(syntax);
        }
        case "Typeclass": {
            return generateTypeclass(syntax);
        }
        case "Impl": {
            return generateImpl(syntax);
        }
        case "Function": {
            return generateFunction(syntax);
        }
        case "Const": {
            return generateConst(syntax);
        }
        case "Comment": {
            return generateComment(syntax);
        }
        case "MultilineComment": {
            return generateMultilineComment(syntax);
        }
    }
}

function joinBlocks(blocks: Block[]): string {
    switch (blocks.length) {
        case blocks.length: {
            if (blocks.length >= 1) {
                const [ block, ...rest ] = blocks;
                const generated: string = generateBlock(block);
                const next: string = joinBlocks(rest);
                if (generated.trim().length === 0) {
                return next;
            } else {
                switch (block.kind) {
                    case "Comment": {
                        return generated + "\n" + next;
                    }
                    case "MultilineComment": {
                        return generated + "\n" + next;
                    }
                    default: {
                        return generated + "\n\n" + next;
                    }
                };
            };
            }
        }
        default: {
            return "";
        }
    }
}

function generateDerw(module: Module): string {
    const onlyImports: Import[] = List.filter(function(block: any) {
        return block.kind === "Import";
    }, module.body);
    function sorter(a: string, b: string): number {
        if (a === b) {
            return 0;
        } else {
            if (a < b) {
                return -1;
            } else {
                return 1;
            };
        }
    }
    const sortedImports: string = (function(y: any) {
        return y.join("\n");
    })((function(y: any) {
        return y.sort(sorter);
    })(List.filter(function(line: any) {
        return line.length > 0;
    }, List.map(generateBlock, onlyImports))));
    const maybeNewlines: string = onlyImports.length === 0 ? "" : "\n\n";
    const withoutImports: string = (function(y: any) {
        return y.trim();
    })((function(y: any) {
        return joinBlocks(y);
    })(List.filter(function(block: any) {
        return block.kind !== "Import";
    }, module.body)));
    return (function(y: any) {
        return y.join("");
    })([ sortedImports, maybeNewlines, withoutImports ]);
}
