/**
 * Code generator for the immutable types used by the library. We generate these
 * as a build step and check them into the repo to make the development
 * experience a little IDE-friendly and typesafe. Maybe eventually Flow will be
 * expressive enough to get everything we want, but not yet.
 *
 * TODO: Investigate if this could be done as a babel transform. Also check if
 * another thing like this already exists. Probably so, but it probably isn't
 * quite what I want, e.g. it probably wouldn't support unions like I want them.
 */
"use strict";

const generateTypes = (types) => {
    return `\
/**
 * Autogenerated; do not edit! Run "npm gen-types" to regenerate.
 *
 * @flow
 */
 
import * as Immutable from 'immutable'

import {buildUnionCaseClass, buildValueClass} from './types-lib'
import type {Updater} from './types-lib'

${joinMap(types, '\n', genType)}
`;
};

const genType = (typeName, typeData) => {
    const lowerTypeName = lowerName(typeName);
    if (lowerTypeName === typeName) {
        throw new Error('Type names must be upper-case.');
    }

    if (typeData.type === 'literal') {
        return genLiteral(typeName, typeData.value);
    } else if (typeData.type === 'struct') {
        return genStruct(typeName, typeData.fields);
    } else if (typeData.type === 'union') {
        return genUnion(typeName, typeData.cases, false);
    } else if (typeData.type === 'objectUnion') {
        return genUnion(typeName, typeData.cases, true);
    }
};

const genLiteral = (typeName, value) => {
    return `\
export type ${typeName} = ${value};
`;
};

const genStruct = (typeName, fields) => {
    const {genLines, genComma} = fieldOperators(fields);
    return `\
const ${typeName}Impl = buildValueClass('${typeName}', [${genComma((f) => `'${f}'`)}]);

export type ${typeName} = {
${genLines((f, t) => `${f}: ${t},`)}\
${genLines((f, t) => `with${upperName(f)}(${f}: ${t}): ${typeName},`)}\
${genLines((f, t) => `update${upperName(f)}(updater: Updater<${t}>): ${typeName},`)}\
    toJS(): any,
    serialize(): any,
};

export const new${typeName} = (${genComma((f, t) => `${f}: ${t}`)}): ${typeName} => (new ${typeName}Impl({
${genLines((f, t) => `${f},`)}\
}));
`;
};

const genUnion = (typeName, cases, isObject) => {
    let result = '';
    result += joinMap(cases, '\n',
        (caseName, fields) => genUnionCase(caseName, fields, isObject));

    const varName = lowerName(typeName);

    // For the match function, we use normal functions instead of arrow
    // functions because WebStorm doesn't yet like type parameters on arrow
    // functions.
    // TODO: Move to arrow functions when WebStorm gets smarter.
    result += `
export type ${typeName} = ${joinMap(cases, ' | ', (caseName) => caseName)};

export type ${typeName}Visitor<T> = {
${joinMap(cases, '\n', (caseName) => `\
    ${lowerName(caseName)}: (${lowerName(caseName)}: ${caseName}) => T,`)}
}

export const match${typeName} = function<T>(${varName}: ${typeName}, visitor: ${typeName}Visitor<T>): T {
    switch (${varName}.type) {
${joinMap(cases, '\n', (caseName) => `\
        case '${lowerName(caseName)}':
            return visitor.${lowerName(caseName)}(${varName});`)}
        default:
            throw new Error('Unexpected type: ' + ${varName}.type);
    }
};
`;
    return result;
};

/**
 * Union cases are almost like structs, but they have a tag that isn't a
 * parameter, so we generate them independently.
 *
 * Also, we need to handle a special case where Redux actions need to be plain
 * objects.
*/
const genUnionCase = (caseName, fields, isObject) => {
    const tagName = lowerName(caseName);
    const {genLines, genComma} = fieldOperators(fields);
    return `\
${isObject ? '' : `\
const ${caseName}Impl = buildUnionCaseClass('${tagName}', [${genComma((f) => `'${f}'`)}]);
`}\
export type ${caseName} = {
    type: '${tagName}',
${genLines((f, t) => `${f}: ${t},`)}\
${isObject ? '' : `\
${genLines((f, t) => `with${upperName(f)}(${f}: ${t}): ${caseName},`)}\
${genLines((f, t) => `update${upperName(f)}(updater: Updater<${t}>): ${caseName},`)}\
    toJS(): any,
    serialize(): any,
`}\
};

export const new${caseName} = (${genComma((f, t) => `${f}: ${t}`)}): ${caseName} => (${isObject ? '' : `new ${caseName}Impl(`}{
    type: '${tagName}',
${genLines((f, t) => `${f},`)}\
}${isObject ? '' : `)`});
`;
};

const fieldOperators = (fields) => ({
    genLines: (transform) => {
        return joinMap(fields, '', (k, v) => '    ' + transform(k, v) + '\n');
    },
    genComma: (transform) => {
        return joinMap(fields, ', ', transform);
    }
});

const lowerName = (name) => {
    return name[0].toLowerCase() + name.slice(1);
};

const upperName = (name) => {
    return name[0].toUpperCase() + name.slice(1);
};

const joinMap = (obj, sep, transform) => {
    return Object.keys(obj).map(
        (key) => transform(key, obj[key])
    ).join(sep);
};

export default generateTypes;
@@ -1354,122 +1354,8 @@
 ases
-, false);%0A    %7D else if (typeData.type === 'objectUnion') %7B%0A        return genUnion(typeName, typeData.cases, true
 );%0A 
@@ -2115,26 +2115,16 @@
 e, cases
-, isObject
 ) =%3E %7B%0A 
@@ -2237,26 +2237,16 @@
 , fields
-, isObject
 ));%0A%0A   
@@ -3431,18 +3431,8 @@
 elds
-, isObject
 ) =%3E
@@ -3546,37 +3546,16 @@
 turn %60%5C%0A
-$%7BisObject ? '' : %60%5C%0A
 const $%7B
@@ -3637,20 +3637,16 @@
 '%60)%7D%5D);%0A
-%60%7D%5C%0A
 export t
@@ -3731,29 +3731,8 @@
 )%7D%5C%0A
-$%7BisObject ? '' : %60%5C%0A
 $%7Bge
@@ -3928,12 +3928,8 @@
 ny,%0A
-%60%7D%5C%0A
 %7D;%0A%0A
@@ -4013,35 +4013,16 @@
 me%7D =%3E (
-$%7BisObject ? '' : %60
 new $%7Bca
@@ -4033,18 +4033,16 @@
 me%7DImpl(
-%60%7D
 %7B%0A    ty
@@ -4096,30 +4096,9 @@
 %7D%5C%0A%7D
-$%7BisObject ? '' : %60)%60%7D
+)
 );%0A%60

Remove special-case objectUnion code