/**
 * Imagine a Fluent Builder where some subset of the fluent methods are valid to call
 * * after one or more fluent methods have been called
 * * before one or more fluent methods have been called
 * * limited number of times.
 * 
 * There is no way to enforce such constraints in statically typed languages such as C++, C# or Java when using a single builder
 * class/interface. Developers would need to author many interfaces to represent the different shapes and would likely need to 
 * author many versions of the builder itself (proxies with a specific signature delegating to an underlying source builder). 
 * 
 * By contrast, applying TypeScript Literal Types, Mapped Types, and Conditional Types this gist demonstrates the creation of
 * very specific type signatures from a dependency specification.
 * 
 * The best way to consume this gist is to jump to ExampleUsage module at the bottom to see how the API can be used. 
 * Read the ReadMe for ideas of things to try. Once you understand what the API has to offer then review the type machinery 
 * in the FluentBuilderConstraintAPI.
 * 
 * This Gist can be pasted into the TypeScript playground.
 * 
 * **NOTE** Requires TypeScript >= 3.6.3!!! This code throws errors in earlier versions of TypeScript.
 * 
 * **Fun Observation** 
 * None of the FluentBuilderConstraintAPI module (roughly 240 lines) is materialized in JavaScript.
 */
module FluentBuilderConstraintAPI {
    //-----------------------------------------------------------------------
    // General and Object
    //-----------------------------------------------------------------------
    /** Return the first if assignable to the second else never. */
    type Filter<typeToTest, predicateType> = 
        typeToTest extends predicateType 
            ? typeToTest 
            : never;

    type PropertyNamesOf<ofObject> = {
        [p in keyof ofObject]: p
    }[keyof ofObject];

    /** Given an object and a type return an object type that has the same member names of the source object but with all the members having the given type. */
    type ObjectWithMembersOfType<objectTemplate, typeOfMembers> = {
        [p in keyof objectTemplate]: typeOfMembers
    }   
            
    //-----------------------------------------------------------------------
    // Function Types
    //-----------------------------------------------------------------------
    /** Return the input argument types of a function type. */
    type ArgumentTypesOfFunction<functionType extends (...args: any[]) => any> = 
        functionType extends (...args: infer A) => any 
            ? A 
            : never;

    /** Change the return type of the given function type while preserving the argument types. */
    type ChangeFunctionReturn<functionType extends (...args: any[]) => any, returnType> = 
        (...args: ArgumentTypesOfFunction<functionType>) => returnType;

    //-----------------------------------------------------------------------
    // Boolean Types
    //-----------------------------------------------------------------------
    type And<left extends boolean, right extends boolean> = 
        left extends true
            ? right extends true
                ? true
                : false
            : false;

    type Or<left extends boolean, right extends boolean> =
        left extends true
            ? true
            : right;

    type Not<val extends boolean> =
        val extends true ? false : true;

    //--------------------------------------------------------------------
    // Unions
    //--------------------------------------------------------------------
    /** Returns true if two unions contain the same members. This is good for checking a narrowing. */
    type UnionsEquivalent<leftUnion, rightUnion> =
        And<
            leftUnion extends rightUnion ? true : false,
            rightUnion extends leftUnion ? true : false
        >

    //-----------------------------------------------------------------------
    type Increment<num extends number> = 
        num extends 0 ? 1
        : num extends 1 ? 2
        : num extends 2 ? 3
        : num extends 3 ? 4
        : num extends 4 ? 5
        : num extends 5 ? 6
        : num extends 6 ? 7
        : num extends 7 ? 8
        : num extends 8 ? 9
        : num extends 9 ? 10
        : num extends 10 ? 11
        : num extends 11 ? 12
        : num extends 12 ? 13
        : never;

    //-----------------------------------------------------------------------
    // Number Inequalities
    //-----------------------------------------------------------------------
    /** True if left equals right */
    type IsNumberEqualTo<left extends number, right extends number> = 
        left extends right
            ? right extends left
                ? true
                : false
            : false;

    type ZeroToOne = 0 | 1;
    type ZeroToThree = ZeroToOne | 2 | 3;
    type ZeroToFive = ZeroToThree | 4 | 5;
    type ZeroToSeven = ZeroToFive | 6 | 7;
    type ZeroToNine = ZeroToSeven | 8 | 9;
    type ZeroToEleven = ZeroToNine | 10 | 11;

    /** True if left is greater than right. Only handles left from [0, 12] but since this is static code analysis that is enough. */
    type IsNumberGreaterThan<left extends number, right extends number> =
        left extends 0 ? false
        : left extends 1 ? right extends 0 ? true : false
        : left extends 2 ? right extends ZeroToOne ? true : false
        : left extends 3 ? right extends ZeroToOne | 2 ? true : false
        : left extends 4 ? right extends ZeroToThree ? true : false
        : left extends 5 ? right extends ZeroToThree | 4 ? true : false
        : left extends 6 ? right extends ZeroToFive ? true : false
        : left extends 7 ? right extends ZeroToFive | 6 ? true : false
        : left extends 8 ? right extends ZeroToSeven ? true : false
        : left extends 9 ? right extends ZeroToSeven | 8 ? true : false
        : left extends 10 ? right extends ZeroToNine ? true : false
        : left extends 11 ? right extends ZeroToNine | 10 ? true : false
        : left extends 12 ? right extends ZeroToEleven ? true : false
        : left extends 13 ? right extends ZeroToEleven | 12 ? true : false
        : never;
        
    type IsNumberGreaterThanOrEqualTo<left extends number, right extends number> = 
        Or<
            IsNumberEqualTo<left, right>,
            IsNumberGreaterThan<left, right>
        >;

    type IsNumberLessThanOrEqualTo<left extends number, right extends number> =
        Not<IsNumberGreaterThan<left, right>>;

    //------------------------------------------------------------------------------------------------
    // Numeric Range
    //------------------------------------------------------------------------------------------------
    type NumericRange = { min?: number; max?: number; }
    
    type IsNumberInRange<num extends number, range extends NumericRange> =
        And<
            range["min"] extends number 
                ? IsNumberGreaterThanOrEqualTo<num, range["min"]>
                : true,
            range["max"] extends number
                ? IsNumberLessThanOrEqualTo<num, range["max"]>
                : true
        >;

    //------------------------------------------------------------------------------------------------
    // Fluent Builder: Requirements for each Member
    //------------------------------------------------------------------------------------------------
    /** Number of times each method has been called */
    type MembersCalledCountMap = { [memberName: string]: number };

    type IncrementIfNumber<num> = num extends number ? Increment<num> : never;

    type IncrementedCallCount<memberToIncrement, membersCalledCountMap> = 
    {
        [memberName in keyof membersCalledCountMap]: memberName extends memberToIncrement
            ? IncrementIfNumber<membersCalledCountMap[memberName]>          
            : membersCalledCountMap[memberName];
    }

    type MemberCallCountRequirementsOfOtherMembers = { [memberName: string]: NumericRange };

    type _AreMemberCallCountRequirementsMet<requirementsOnOtherMembers extends MemberCallCountRequirementsOfOtherMembers, membersCalledCountMap extends MembersCalledCountMap> =
        {
            [
                member 
                in 
                keyof requirementsOnOtherMembers
            ]: member extends string 
                ? IsNumberInRange<
                    membersCalledCountMap[member], 
                    requirementsOnOtherMembers[member]
                >
                : false
        }[keyof requirementsOnOtherMembers]

    type AreMemberCallCountRequirementsMet<requirementsOnOtherMembers extends MemberCallCountRequirementsOfOtherMembers, membersCalledCountMap extends MembersCalledCountMap> = 
        PropertyNamesOf<requirementsOnOtherMembers> extends never
            // when there are no requirements we pass
            ? true
            // otherwise determine if all of the requirements are met - UnionsEquivalent required because the return type is the union of results.
            : UnionsEquivalent<
                true, 
                _AreMemberCallCountRequirementsMet<requirementsOnOtherMembers, membersCalledCountMap>
            >;        

    //------------------------------------------------------------------------------------------------
    // Fluent Builder: Top Level Builder
    //------------------------------------------------------------------------------------------------
    /** For all members, what is each of their requirements on other members. */
    type ObjectMemberCallCountRequirements = { [memberName: string]: MemberCallCountRequirementsOfOtherMembers };

    type NamesOfMembersWithSatisfiedCallCounts<membersCalledCountMap extends MembersCalledCountMap, objectMemberCallCountRequirements extends ObjectMemberCallCountRequirements> =
    {
        [
            memberName 
            in keyof membersCalledCountMap
        ]: 
            memberName extends string
            ? UnionsEquivalent<
                true, 
                AreMemberCallCountRequirementsMet<objectMemberCallCountRequirements[memberName], membersCalledCountMap>
            > extends true
                ? memberName
                : never
            : never
    }[keyof membersCalledCountMap];

    type _FluentBuilderWithRequirementCountSpecification<
        BuilderClass, 
        ResultMethodName extends keyof BuilderClass, 
        membersCalledCountMap extends MembersCalledCountMap, 
        objectMemberCallCountRequirements extends ObjectMemberCallCountRequirements
    > = 
    {
        readonly [
            memberName 
            in 
            ( 
                // always include the result method
                ResultMethodName 
                // only the fluent methods whose requirements have been satisfied
                | Filter<
                    keyof BuilderClass, 
                    NamesOfMembersWithSatisfiedCallCounts<membersCalledCountMap, objectMemberCallCountRequirements>
                > 
            )
        ]: 
            memberName extends ResultMethodName
                // straight pass-thru of the result method
                ? BuilderClass[memberName]    
                : BuilderClass[memberName] extends (...args: any[]) => any
                    // for other functions/methods, assume fluent builder methods
                    ? ChangeFunctionReturn<
                        BuilderClass[memberName], 
                        _FluentBuilderWithRequirementCountSpecification<
                            BuilderClass, 
                            ResultMethodName,
                            IncrementedCallCount<memberName, membersCalledCountMap>,
                            objectMemberCallCountRequirements
                        >
                    >
                    // non-methods are pass-thru
                    : BuilderClass[memberName];        
    }

    export type FluentBuilderWithRequirementCountSpecification<
        BuilderClass, 
        ResultMethodName extends keyof BuilderClass,     
        objectMemberCallCountRequirements extends ObjectMemberCallCountRequirements
    > = _FluentBuilderWithRequirementCountSpecification<BuilderClass, ResultMethodName, ObjectWithMembersOfType<BuilderClass, 0>, objectMemberCallCountRequirements>;
}

module ExampleUsage {

    /** Silly example fluent builder for demonstration purposes. This would be your builder. */
    class SillyFluentBuilder {

        private _messages: string[] = [];

        withWidget(x: number): SillyFluentBuilder {
            this._messages.push(`Widget of ${x}`);
            return this;
        }
        
        withGadget(x: string): SillyFluentBuilder {
            this._messages.push(`Gadget of ${x}`);
            return this;
        }

        withGidget(x: boolean): SillyFluentBuilder {
            this._messages.push(`Gidget is ${x}`);
            return this;
        }

        withCake(x: number): SillyFluentBuilder {
            this._messages.push(`Cake of ${x}`);
            return this;
        }

        getResult(): string {
            return this._messages.join("\n");
        }    
    }

    /**
     * This type specifies the call count dependencies of every property of the fluent builder 
     * against every other method (and even potentially itself) of the fluent builder. 
     * 
     * This specification is fed to the constrained fluent builder API to generate the very specific
     * types which enforce these call count requirements. 
     */
    type SillyFluentBuilderMemberRequirements = {
        // withWidget will be available for use once the following constraints are met
        withWidget: {
            // withWidget is available to be called up to when it has been called 2 times before and no more
            // ...which means it can be called 3 times in total. 
            // If it should only called once then set max to 0.
            // If you do not care then simpl exclude.            
            withWidget: { max: 2 }; 

            // withWidget is available to be called when withCake has NOT been called i.e. as soon as withCake is called it is no longer available.
            withCake: { max: 0 }; 

            // withWidget is available once withGadget has been called once i.e. a dependency and only 
            // while withGadget has been called 2 or fewer times. 
            withGadget: { min: 1, max: 2 }; 
        };

        withGidget: {
            // requires withWidget to have been called twice
            withWidget: { min: 2 }
        };           
    }

    /**
     * A factory function that constrains the original fluent builder class.
     * This is where the magic happens: All of the work is in the mapped type application of FluentBuilderWithRequirementCountSpecification
     * which constructs a complex type based upon the SillyFluentBuilderMemberRequirements.
     **/
    function createConstrainedSillyFluentBuilder(): FluentBuilderConstraintAPI.FluentBuilderWithRequirementCountSpecification<
        SillyFluentBuilder, 
        "getResult", 
        SillyFluentBuilderMemberRequirements
    > {
        return new SillyFluentBuilder();
    }

    /**
     * ## First Try
     * 1. Move withWidget after a call to withCake call to observe a violation of withWidget's dependendency { withCake: { max: 0 } }.
     * 2. Adding another call to withWidget to observe a violation of the withWidget's own max count { withWidget: { max: 2 } }
     * 3. Move withWidget to before the first call to withGadget to observe a violation of the dependency of withWidget on withGadget { withGadget: { min: 1 } }   
     * 
     * ## Next Try
     * 1. Change the counts or specifications of an existing member specification in SillyFluentBuilderMemberRequirements (above).
     * 2. Add a new specification for another method such as withGadget.
     */
    module ReadMe {}

    createConstrainedSillyFluentBuilder()
        .withGadget("Bonjour")
        .withWidget(20)
        .withWidget(20)
        .withGadget("Bonjour")
        .withWidget(20)
        .withGidget(false)
        .withCake(1)
        .withCake(2)
        .withGadget("Bonsoir")
        .withGidget(false)
        .withGadget("Bon matin")
        .withCake(4)
        .getResult();
}