// Copyright (c) 2020 Shellyl_N and Authors
// license: ISC
// https://github.com/shellyln


import { TypeAssertion,
         PrimitiveTypeAssertion,
         PrimitiveValueTypeAssertion,
         RepeatedAssertion,
         SpreadAssertion,
         SequenceAssertion,
         OneOfAssertion,
         OptionalAssertion,
         EnumAssertion,
         ObjectAssertion,
         TypeAssertionMap,
         CodegenContext } from '../../types';
import { escapeString }   from '../../lib/escape';
import { nvl2 }           from './../util';



function formatTypeName(ty: TypeAssertion, ctx: CodegenContext, typeName: string) {
    if (typeName.includes('.') || ty.kind === 'symlink' || ty.kind === 'enum') {
        return generateCSharpCodeInner(ty, false, ctx);
    }
    return typeName;
}


function formatCSharpCodeDocComment(ty: TypeAssertion | string, nestLevel: number) {
    let code = '';
    const indent = '    '.repeat(nestLevel);
    const docComment = typeof ty === 'string' ? ty : ty.docComment;
    if (docComment) {
        if (0 <= docComment.indexOf('\n')) {
            code += `${indent}/**\n${indent}  ${
                docComment
                    .split('\n')
                    .map(x => x.trimLeft())
                    .join(`\n${indent} `)}\n${indent} */\n`;
        } else {
            code += `${indent}/** ${docComment} */\n`;
        }
    }
    return code;
}


function formatMemberType(ty: TypeAssertion, ctx: CodegenContext): string {
    if (ty.typeName) {
        return formatTypeName(ty, ctx, ty.typeName);
    } else {
        switch (ty.kind) {
        case 'primitive':
            return generateCSharpCodePrimitive(ty, ctx);
        case 'primitive-value':
            return generateCSharpCodePrimitiveValue(ty, ctx);
        case 'repeated':
            return generateCSharpCodeRepeated(ty, ctx);
        case 'one-of':
            return generateCSharpCodeOneOf(ty, ctx);
        default:
            return 'object';
        }
    }
}


function appendOptionalModifier(name: string) {
    switch (name) {
    case 'decimal': case 'int': case 'double': case 'bool':
        return `${name}?`;
    default:
        return name;
    }
}


function isNullableOneOf(ty: OneOfAssertion, ctx: CodegenContext) {
    const filtered = ty.oneOf.filter(x => !(
        x.kind === 'primitive' && (x.primitiveName === 'null' || x.primitiveName === 'undefined') ||
        x.kind === 'primitive-value' && (x.value === null || x.value === void 0)));
    return (filtered.length === 1 && ty.oneOf.length !== 1 ? filtered[0] : null) ;
}


function generateCSharpCodePrimitive(ty: PrimitiveTypeAssertion, ctx: CodegenContext) {
    // TODO: Function, DateStr, DateTimeStr
    switch (ty.primitiveName) {
    case 'null': case 'undefined':
        return 'object';
    case 'integer':
        return 'int';
    case 'bigint':
        return 'decimal';
    case 'number':
        return 'double';
    case 'boolean':
        return 'bool';
    default:
        return ty.primitiveName;
    }
}


function generateCSharpCodePrimitiveValue(ty: PrimitiveValueTypeAssertion, ctx: CodegenContext) {
    if (ty.value === null || ty.value === void 0) {
        return 'object';
    }
    switch (typeof ty.primitiveName) {
    case 'bigint':
        return 'decimal';
    default:
        switch (typeof ty.value) {
        case 'number':
            return 'double';
        case 'string':
            return 'string';
        case 'boolean':
            return 'bool';
        default:
            return 'object';
        }
    }
}


function generateCSharpCodeRepeated(ty: RepeatedAssertion, ctx: CodegenContext): string {
    return `${formatMemberType(ty.repeated, ctx)}[]`;
}


function generateCSharpCodeSpread(ty: SpreadAssertion, ctx: CodegenContext) {
    return '';
}


function generateCSharpCodeSequence(ty: SequenceAssertion, ctx: CodegenContext) {
    return 'object[]';
}


function generateCSharpCodeOneOf(ty: OneOfAssertion, ctx: CodegenContext) {
    const z = isNullableOneOf(ty, ctx);
    if (z) {
        return appendOptionalModifier(formatMemberType(z, ctx));
    } else {
        return 'object';
    }
}


function generateCSharpCodeOptional(ty: OptionalAssertion, ctx: CodegenContext) {
    return appendOptionalModifier(generateCSharpCodeInner(ty.optional, false, ctx));
}


function generateCSharpCodeEnum(ty: EnumAssertion, ctx: CodegenContext) {
    return 'object';
}


function addAttributes(ty: TypeAssertion, ctx: CodegenContext, typeName: string) {
    const attrs: string[] = [];
    let ty2: TypeAssertion = ty;

    if (ty2.kind !== 'optional') {
        switch (typeName) {
        case 'decimal': case 'int': case 'double': case 'bool':
            break;
        default:
            if (ty2.kind === 'one-of') {
                if (! isNullableOneOf(ty2, ctx)) {
                    attrs.push('Required');
                }
            } else {
                attrs.push('Required');
            }
            break;
        }
        ty2 = ty;
    }

    switch (ty2.kind) {
    case 'primitive':
        {
            if (typeof ty2.minLength === 'number') {
                attrs.push(`MinLength(${ty2.minLength})`);
            }
            if (typeof ty2.maxLength === 'number') {
                attrs.push(`MaxLength(${ty2.maxLength})`);
            }
            if (ty2.minValue !== null && ty2.minValue !== void 0 ||
                ty2.maxValue !== null && ty2.maxValue !== void 0) {
                switch (ty2.primitiveName) {
                case 'string':
                    attrs.push(`Range(typeof(string), "${
                        nvl2(ty2.minValue, x => escapeString(x), '')}", "${
                        nvl2(ty2.maxValue, x => escapeString(x), '\\U00010FFFF')}")`);
                    break;
                case 'bigint':
                    attrs.push(`Range(typeof(decimal), ${
                        nvl2(ty2.minValue, x => `new decimal(@"${String(x)}").ToString()`, 'Decimal.MinValue')}, ${
                        nvl2(ty2.maxValue, x => `new decimal(@"${String(x)}").ToString()`, 'Decimal.MaxValue')})`);
                    break;
                case 'integer':
                    attrs.push(`Range(${
                        nvl2(ty2.minValue, x => `(int)${String(x)}`, 'Int32.MinValue')}, ${
                        nvl2(ty2.maxValue, x => `(int)${String(x)}`, 'Int32.MaxValue')})`);
                    break;
                case 'number':
                    attrs.push(`Range(${
                        nvl2(ty2.minValue, x => `(double)${String(x)}`, 'Double.MinValue')}, ${
                        nvl2(ty2.maxValue, x => `(double)${String(x)}`, 'Double.MaxValue')})`);
                    break;
                }
            }
            if (ty2.pattern) {
                attrs.push(`RegularExpression(@"${ty2.pattern.source.replace(/"/g, '""')}")`);
            }
        }
        break;
    case 'repeated':
        {
            if (typeof ty2.min === 'number') {
                attrs.push(`MinLength(${ty2.min})`);
            }
            if (typeof ty2.max === 'number') {
                attrs.push(`MaxLength(${ty2.max})`);
            }
        }
        break;
    }

    if (0 < attrs.length) {
        return `[${attrs.join(', ')}]\n${'    '.repeat(ctx.nestLevel + 1)}`;
    } else{
        return '';
    }
}


function generateCSharpCodeObject(ty: ObjectAssertion, isInterface: boolean, ctx: CodegenContext) {
    const sep = '\n\n';

    const memberLines =
        ty.members.filter(x => !(x[2]))
        .map(x => {
            const typeName =
                x[1].typeName ?
                    formatTypeName(x[1], {...ctx, nestLevel: ctx.nestLevel + 1}, x[1].typeName) :
                    generateCSharpCodeInner(x[1], false, {...ctx, nestLevel: ctx.nestLevel + 1});

            return (
                `${formatCSharpCodeDocComment(x[3] || '', ctx.nestLevel + 1)}${
                    '    '.repeat(ctx.nestLevel + 1)}${addAttributes(x[1], ctx, typeName)}public ${
                    typeName} ${x[0]} { get; set; }`
            );
        });

    if (memberLines.length === 0) {
        return (`\n${
            '    '.repeat(ctx.nestLevel)}{\n${
            '    '.repeat(ctx.nestLevel)}}`
        );
    }
    return (`\n${
        '    '.repeat(ctx.nestLevel)}{\n${memberLines.join(sep)}\n${
        '    '.repeat(ctx.nestLevel)}}`
    );
}


function generateCSharpCodeInner(ty: TypeAssertion, isInterface: boolean, ctx: CodegenContext): string {
    switch (ty.kind) {
    case 'never': case 'any': case 'unknown':
        return 'object';
    case 'primitive':
        return generateCSharpCodePrimitive(ty, ctx);
    case 'primitive-value':
        return generateCSharpCodePrimitiveValue(ty, ctx);
    case 'repeated':
        return generateCSharpCodeRepeated(ty, ctx);
    case 'spread':
        return generateCSharpCodeSpread(ty, ctx);
    case 'sequence':
        return generateCSharpCodeSequence(ty, ctx);
    case 'one-of':
        return generateCSharpCodeOneOf(ty, ctx);
    case 'optional':
        return generateCSharpCodeOptional(ty, ctx);
    case 'enum':
        return generateCSharpCodeEnum(ty, ctx);
    case 'object':
        return generateCSharpCodeObject(ty, isInterface, ctx);
    case 'symlink':
        if (ctx.schema?.has(ty.symlinkTargetName)) {
            const target = ctx.schema.get(ty.symlinkTargetName);
            switch (target?.ty.kind) {
            case 'enum':
                return 'object';
            }
        }
        return ty.symlinkTargetName;
    case 'operator':
        throw new Error(`Unexpected type assertion: ${(ty as any).kind}`);
    default:
        throw new Error(`Unknown type assertion: ${(ty as any).kind}`);
    }
}


export function generateCSharpCode(schema: TypeAssertionMap): string {
    let code =
`using System.ComponentModel.DataAnnotations;

namespace Tynder.UserSchema
{
`;

    const ctx: CodegenContext = {
        nestLevel: 1,
        schema,
    };

    for (const ty of schema.entries()) {
        const indent0 = '    '.repeat(ctx.nestLevel);

        if (ty[1].ty.kind === 'object') {
            // nothing to do
        } else if (ty[1].ty.kind === 'enum') {
            // nothing to do
        } else if (ty[1].ty.kind === 'never' && ty[1].ty.passThruCodeBlock) {
            // nothing to do
        } else {
            code += formatCSharpCodeDocComment(ty[1].ty, ctx.nestLevel);
            let tyName = 'System.Object';
            switch (ty[1].ty.kind) {
            case 'primitive':
                switch (ty[1].ty.primitiveName) {
                case 'integer':
                    tyName =  'System.Int32';
                    break;
                case 'bigint':
                    tyName =  'System.Decimal';
                    break;
                case 'number':
                    tyName =  'System.Double';
                    break;
                case 'boolean':
                    tyName =  'System.Boolean';
                    break;
                case 'string':
                    tyName =  'System.String';
                    break;
                }
                break;
            case 'primitive-value':
                if (ty[1].ty.value !== null && ty[1].ty.value !== void 0) {
                    switch (typeof ty[1].ty.primitiveName) {
                    case 'bigint':
                        tyName =  'System.Decimal';
                        break;
                    default:
                        switch (typeof ty[1].ty.value) {
                        case 'number':
                            tyName =  'System.Double';
                            break;
                        case 'boolean':
                            tyName =  'System.Boolean';
                            break;
                        case 'string':
                            tyName =  'System.String';
                            break;
                        }
                    }
                }
                break;
            }
            code += `${indent0}using ${ty[0]} = ${tyName};\n\n`;
        }
    }

    let isFirst = true;
    for (const ty of schema.entries()) {
        const accessModifier = ty[1].exported ? 'public' : 'public';
        const indent0 = '    '.repeat(ctx.nestLevel);
        const indent1 = '    '.repeat(ctx.nestLevel + 1);

        if (ty[1].ty.kind === 'object' || ty[1].ty.kind === 'enum') {
            if (isFirst) {
                isFirst = false;
                code += '\n';
            } else {
                code += '\n\n';
            }
            code += formatCSharpCodeDocComment(ty[1].ty, ctx.nestLevel);
        }

        if (ty[1].ty.kind === 'object') {
            code += `${indent0}${accessModifier} class ${ty[0]}${
                ty[1].ty.baseTypes && ty[1].ty.baseTypes.length ? ` : ${
                    ty[1].ty.baseTypes
                        .filter(x => x.typeName)
                        .map(x => formatTypeName(x, {...ctx, nestLevel: ctx.nestLevel + 1}, x.typeName as string))
                        .join(', ')}` : ''} ${
                generateCSharpCodeInner(ty[1].ty, true, ctx)}\n`;
        } else if (ty[1].ty.kind === 'enum') {
            let value: number | null = 0;
            code += `${indent0}${accessModifier} static class ${ty[0]}\n${indent0}{\n${
                ty[1].ty.values
                    .map(x => `${
                        formatCSharpCodeDocComment(x[2] || '', ctx.nestLevel + 1)}${
                        indent1}${(() => {
                            if (value !== null && x[1] === value) {
                                value++;
                                return `public static double ${x[0]} { get { return ${x[1]}; } }`;
                            } else {
                                if (typeof x[1] === 'number') {
                                    value = x[1] + 1;
                                    return `public static double ${x[0]} { get { return ${x[1]}; } }`;
                                } else {
                                    return `public static string ${x[0]} { get { return "${escapeString(x[1])}"; } }`;
                                }
                            }
                        })()}`)
                    .join('\n\n')}\n${indent0}}\n`;
        } else if (ty[1].ty.kind === 'never' && ty[1].ty.passThruCodeBlock) {
            // nothing to do
        } else {
            // nothing to do
        }
    }
    return code + '}\n';
}
