/*
 * Copyright (c) 2007 BUSINESS OBJECTS SOFTWARE LIMITED
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 *     * Redistributions of source code must retain the above copyright notice,
 *       this list of conditions and the following disclaimer.
 *  
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *  
 *     * Neither the name of Business Objects nor the names of its contributors
 *       may be used to endorse or promote products derived from this software
 *       without specific prior written permission.
 *  
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */


/*
 * GemModelDemo.java
 * Created: Jul 29, 2005
 * By: Bo Ilic
 */

//package org.openquark.cal.samples;
package com.biosimilarity.magritte;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;

import org.openquark.cal.compiler.CALSourceGenerator;
import org.openquark.cal.compiler.MessageLogger;
import org.openquark.cal.compiler.ModuleName;
import org.openquark.cal.compiler.Scope;
import org.openquark.cal.compiler.SourceModel;
import org.openquark.cal.compiler.TypeException;
import org.openquark.cal.compiler.io.EntryPointSpec;
import org.openquark.cal.module.Cal.Collections.CAL_List;
import org.openquark.cal.module.Cal.Core.CAL_Prelude;
import org.openquark.cal.module.Cal.Utilities.CAL_Summary;
import org.openquark.cal.runtime.CALExecutorException;
import org.openquark.cal.services.BasicCALServices;
import org.openquark.cal.services.GemCompilationException;
import org.openquark.cal.valuenode.LiteralValueNode;
import org.openquark.gems.client.CollectorGem;
import org.openquark.gems.client.Gem;
import org.openquark.gems.client.GemGraph;
import org.openquark.gems.client.ReflectorGem;
import org.openquark.gems.client.ValueGem;
import org.openquark.gems.client.services.GemFactory;


/**
 * Contains a variety of "demo" gems expressed programmatically as {@link GemGraph}s.
 * <p>
 * The methods {@link #factorialGemGraph}, {@link #positiveOutlierDetectorGemGraph}, and
 * {@link #demoMapGemGraph} demonstrate the construction of gem graphs.
 * <p>
 * The {@link #main} method of this class demonstrates the conversion of these gem graphs into CAL source text,
 * as well as the evaluation of these gems with arguments passed into the CAL runtime from Java.
 * <p>
 * The steps for creating a GemGraph are:
 * <ol>
 * <li> Create an empty GemGraph, and set the name of its target collector
 * <li> Create a new Gem or obtain an existing Gem
 * <li> Add the Gem to the GemGraph
 * <li> Repeat steps 2-3 to add other Gems
 * <li> Connect the Gems together using the connectGems() method on the GemGraph
 * <li> Repeat steps 2-5 as necessary to add all the Gems and connections for the GemGraph
 * <li> Typecheck the GemGraph to ensure that it is consistent
 * </ol>
 * <p>
 * See SourceModelDemo.java for a demo of the same gems expressed programmatically as SourceModels.
 * See GemModelDemo.cal for the expression using CAL.
 * 
 * @author Bo Ilic
 * @author Joseph Wong
 * 
 * @see SourceModelDemo
 */
public class GemModelDemo {
    
    private static final ModuleName GEM_GRAPH_TYPE_CHECKING_MODULE = ModuleName.make("GemCutterSaveModule");
    private static final ModuleName RUN_MODULE = ModuleName.make("GemModelDemo.RunModule");
    private static final String WORKSPACE_FILE_NAME = "gemcutter.default.cws";   
    
    /**
     * This demo consists of generating the CAL source of the demo gems defined using gem graphs,
     * as well as evaluating these gems with some arguments.
     * 
     * @see #dumpAllGemGraphSourceDefinitions
     * @see #runGemGraphDemo
     */
    //public static void main(String[] args) {
    public static void entryPoint(String[] args) {
        
        System.out.println("Gem Model Demo");
        System.out.println("-------------------------------------------------------------------------------------");
        
        MessageLogger messageLogger = new MessageLogger();        
        
        // We need to set up the calServices instance that is required for the demo.
        BasicCALServices calServices =
            BasicCALServices.makeCompiled(WORKSPACE_FILE_NAME, messageLogger);
        
        if (calServices == null) {
            System.err.println(messageLogger.toString());
            System.exit(1);
        }
        
        System.out.println();
        System.out.println("The CAL source of the demo gems defined using gem graphs:");
        System.out.println("=====================================================================================");
        dumpAllGemGraphSourceDefinitions(calServices);
        
        System.out.println();
        System.out.println("Evaluating some gems defined using gem graphs:");
        System.out.println("=====================================================================================");
        runGemGraphDemo(calServices);
    }

    /**
     * Evaluates some demo gems. Here, we test out the factorial and the positiveOutlierDetector gems.
     * 
     * @param calServices The calServices instance to be used for constructing and running the GemGraphs.
     * 
     * @see #runFactorialDemo
     * @see #runPositiveOutlierDetectorDemo
     */
    public static void runGemGraphDemo(BasicCALServices calServices) {
        
        // First, we will run the factorial gem.
        runFactorialDemo(calServices);
    
        // Next, we will run the positiveOutlierDetector gem.
        runPositiveOutlierDetectorDemo(calServices);
    }

    /**
     * Runs some tests with the factorial gem defined by {@link #factorialGemGraph}.
     * 
     * @param calServices The calServices instance to be used for constructing and running the GemGraphs.
     * 
     * @see #factorialGemGraph
     */
    public static void runFactorialDemo(BasicCALServices calServices) {
        
        ////
        /// We can use the runCode method on BasicCALServices to evaluate gems defined by gem graphs.
        //

        try {
            GemFactory gemFactory = new GemFactory(calServices);
            SourceModel.FunctionDefn.Algebraic gemSource = factorialGemGraph(gemFactory, calServices).getCALSource();
    
            EntryPointSpec entryPointSpec = calServices.addNewModuleWithFunction(RUN_MODULE, gemSource);
            
            try {
                BigInteger result;
    
                result = (BigInteger)calServices.runFunction(entryPointSpec, new Object[] { BigInteger.valueOf(7) });
                System.out.println("(factorial 7) => " + result);
    
                result = (BigInteger)calServices.runFunction(entryPointSpec, new Object[] { BigInteger.valueOf(37) });
                System.out.println("(factorial 37) => " + result);
    
            } catch (CALExecutorException e) {
                e.printStackTrace(System.err);
            }
            
        } catch (TypeException e) {
            e.printStackTrace(System.err);
        } catch (GemCompilationException e) {
            e.printStackTrace(System.err);
        }
    }

    /**
     * Runs some tests with the positiveOutlierDetector gem defined by {@link #positiveOutlierDetectorGemGraph}.
     * 
     * @param calServices The GemGenerator instance to be used for constructing and running the GemGraphs.
     * 
     * @see #positiveOutlierDetectorGemGraph
     */
    public static void runPositiveOutlierDetectorDemo(BasicCALServices calServices) {
        
        ////
        /// We can use the runCode method on BasicCALServices and GemGenerator to evaluate gems defined by gem graphs.
        //

        try {
            GemFactory gemFactory = new GemFactory(calServices);

            SourceModel.FunctionDefn.Algebraic gemSource = positiveOutlierDetectorGemGraph(gemFactory, calServices).getCALSource();
            EntryPointSpec entryPointSpec = calServices.addNewModuleWithFunction(RUN_MODULE, gemSource);

            List<?> result;

            ////
            /// A Java java.util.List can be passed in as an argument to a CAL function that expects a
            /// CAL list as an argument.
            //
            List<Double> oneToFive = new ArrayList<Double>();
            for (int i = 1; i <= 5; i++) {
                oneToFive.add(new Double(i));
            }

            result = (List<?>)calServices.runFunction(entryPointSpec, new Object[] { oneToFive, new Double(2.0) });
            System.out.println("(positiveOutlierDetector [1, 2, 3, 4, 5] 2.0) => " + result);

            result = (List<?>)calServices.runFunction(entryPointSpec, new Object[] { oneToFive, new Double(1.0) });
            System.out.println("(positiveOutlierDetector [1, 2, 3, 4, 5] 1.0) => " + result);

            result = (List<?>)calServices.runFunction(entryPointSpec, new Object[] { oneToFive, new Double(0.5) });
            System.out.println("(positiveOutlierDetector [1, 2, 3, 4, 5] 0.5) => " + result);

        } catch (CALExecutorException e) {
            e.printStackTrace(System.err);
        } catch (GemCompilationException e) {
            e.printStackTrace(System.err);
        } catch (TypeException e) {
            e.printStackTrace(System.err);
        }
    }

    /**
     * Prints out the CAL source text generated by the various demo GemGraphs.
     * 
     * @param calServices 
     * 
     * @see #factorialGemGraph
     * @see #positiveOutlierDetectorGemGraph
     * @see #demoMapGemGraph
     */
    public static void dumpAllGemGraphSourceDefinitions(BasicCALServices calServices) {
        GemFactory gemFactory = new GemFactory(calServices);

        try {
            System.out.println(getGemGraphSourceDefinition(factorialGemGraph(gemFactory, calServices)));
            System.out.println(getGemGraphSourceDefinition(positiveOutlierDetectorGemGraph(gemFactory, calServices)));
            System.out.println(getGemGraphSourceDefinition(demoMapGemGraph(gemFactory, calServices)));
        } catch (TypeException e) {
            e.printStackTrace(System.err);
        }
    }

    /** Generates CAL source text from a GemGraph. */ 
    private static String getGemGraphSourceDefinition(GemGraph gemGraph) {
        return CALSourceGenerator.getFunctionText(
            gemGraph.getTargetCollector().getUnqualifiedName(),
            gemGraph.getTargetCollector(),
            Scope.PUBLIC);
    }

    /**
     * Creates a GemGraph that defines the factorial function.
     * <p>
     * Corresponding CAL source for the gem graph:
     * <pre>
     * public factorial end = List.product (Prelude.upFromTo (1 :: Prelude.Integer) end);
     * </pre>
     * @param calServices 
     */
    public static GemGraph factorialGemGraph(GemFactory gemFactory, BasicCALServices calServices) throws TypeException {
        
        // This demonstrates the creation of a simple gem graph. The steps required to build a gem graph are:
        // 1) Create an empty GemGraph, and set the name of its target collector
        // 2) Create a new Gem or obtain an existing Gem
        // 3) Add the Gem to the GemGraph
        // 4) Repeat steps 2-3 to add other Gems
        // 5) Connect the Gems together using the connectGems() method on the GemGraph
        // 6) Repeat steps 2-5 as necessary to add all the Gems and connections for the GemGraph
        // 7) Typecheck the GemGraph to ensure that it is consistent
        
        GemGraph gemGraph = new GemGraph();
        gemGraph.getTargetCollector().setName("factorial");
        
        Gem productGem = gemFactory.makeFunctionalAgentGem(CAL_List.Functions.product);
        gemGraph.addGem(productGem);
        
        Gem upFromToGem = gemFactory.makeFunctionalAgentGem(CAL_Prelude.Functions.upFromTo);
        gemGraph.addGem(upFromToGem);

        // Create a Value gem to represent a BigInteger (Prelude.Integer) value for the number 1.
        Gem oneGem = new ValueGem(new LiteralValueNode(BigInteger.valueOf(1), calServices.getPreludeTypeConstants().getIntegerType()));
            
        gemGraph.addGem(oneGem);
        
        gemGraph.connectGems(oneGem.getOutputPart(), upFromToGem.getInputPart(0));
        
        gemGraph.connectGems(upFromToGem.getOutputPart(), productGem.getInputPart(0));
        
        gemGraph.connectGems(productGem.getOutputPart(), gemGraph.getTargetCollector().getInputPart(0));
        
        gemGraph.typeGemGraph(calServices.getTypeCheckInfo(GEM_GRAPH_TYPE_CHECKING_MODULE));
        
        return gemGraph;
    }
    
    /**
     * Creates a GemGraph that defines the positiveOutlierDetector function.
     * <p>
     * Corresponding CAL source for the gem graph:
     * <pre>
     * public positiveOutlierDetector sourceData x_2 = 
     *     let
     *         isPositiveOutlier x_1 = x_1 - avg >= x_2 * stdDev;
     *         stdDev = Summary.populationStandardDeviation sourceData;
     *         avg = Summary.average sourceData;
     *     in
     *         List.filter isPositiveOutlier sourceData
     *     ;
     * </pre>
     * @param gemFactory used for creating value and function gems
     * @param calServices 
     */
    public static GemGraph positiveOutlierDetectorGemGraph(GemFactory gemFactory, BasicCALServices calServices) throws TypeException {

        GemGraph gemGraph = new GemGraph();
        gemGraph.getTargetCollector().setName("positiveOutlierDetector");
        
        // A collector targeting the target collector. It will be an argument of the positiveOutlierDetector gem.
        CollectorGem sourceDataCollector = new CollectorGem();
        sourceDataCollector.setName("sourceData");
        gemGraph.addGem(sourceDataCollector);
        
        // Local collector: avg
        // Corresponding source: avg = Summary.average sourceData;
        CollectorGem avgCollector = new CollectorGem();
        avgCollector.setName("avg");
        gemGraph.addGem(avgCollector);
        
        // a ReflectorGem provides as output the value that is collected by the corresponding CollectorGem
        ReflectorGem sourceDataReflector1 = new ReflectorGem(sourceDataCollector);
        gemGraph.addGem(sourceDataReflector1);
        
        Gem averageGem = gemFactory.makeFunctionalAgentGem(CAL_Summary.Functions.average);
        gemGraph.addGem(averageGem);
        
        gemGraph.connectGems(sourceDataReflector1.getOutputPart(), averageGem.getInputPart(0));
        
        gemGraph.connectGems(averageGem.getOutputPart(), avgCollector.getInputPart(0));
        
        // Local collector: stdDev
        // Corresponding source: stdDev = Summary.populationStandardDeviation sourceData;
        CollectorGem stdDevCollector = new CollectorGem();
        stdDevCollector.setName("stdDev");
        gemGraph.addGem(stdDevCollector);
        
        ReflectorGem sourceDataReflector2 = new ReflectorGem(sourceDataCollector);
        gemGraph.addGem(sourceDataReflector2);
        
        Gem populationStdDevGem = gemFactory.makeFunctionalAgentGem(CAL_Summary.Functions.populationStandardDeviation);
        gemGraph.addGem(populationStdDevGem);
        
        gemGraph.connectGems(sourceDataReflector2.getOutputPart(), populationStdDevGem.getInputPart(0));
        
        gemGraph.connectGems(populationStdDevGem.getOutputPart(), stdDevCollector.getInputPart(0));
        
        // Local collector: isPositiveOutlier
        // Corresponding source: isPositiveOutlier x_1 = x_1 - avg >= x_2 * stdDev;
        CollectorGem isPositiveOutlierCollector = new CollectorGem();
        isPositiveOutlierCollector.setName("isPositiveOutlier");
        gemGraph.addGem(isPositiveOutlierCollector);
        
        Gem subtractGem = gemFactory.makeFunctionalAgentGem(CAL_Prelude.Functions.subtract);
        gemGraph.addGem(subtractGem);
        
        ReflectorGem avgReflector = new ReflectorGem(avgCollector);
        gemGraph.addGem(avgReflector);
        
        // Retarget the first input of subtract to the isPositiveOutlier collector
        //
        // This means that the first input of subtract is no longer an argument for the overall Gem defined by the
        // GemGraph's target collector, but rather a local argument for the isPositiveOutlier collector.
        gemGraph.retargetInputArgument(subtractGem.getInputPart(0), isPositiveOutlierCollector, -1);
        gemGraph.connectGems(avgReflector.getOutputPart(), subtractGem.getInputPart(1));
        
        Gem multiplyGem = gemFactory.makeFunctionalAgentGem(CAL_Prelude.Functions.multiply);
        gemGraph.addGem(multiplyGem);
        
        ReflectorGem stdDevReflector = new ReflectorGem(stdDevCollector);
        gemGraph.addGem(stdDevReflector);
        
        // Leave the first input of multiply targeting the target collector (it will be an argument of the positiveOutlierDetector gem),
        // but hook up the second input of multiply to the stdDev reflector
        gemGraph.connectGems(stdDevReflector.getOutputPart(), multiplyGem.getInputPart(1));
        
        Gem greaterThanEqualsGem = gemFactory.makeFunctionalAgentGem(CAL_Prelude.Functions.greaterThanEquals);
        gemGraph.addGem(greaterThanEqualsGem);
        
        gemGraph.connectGems(subtractGem.getOutputPart(), greaterThanEqualsGem.getInputPart(0));
        gemGraph.connectGems(multiplyGem.getOutputPart(), greaterThanEqualsGem.getInputPart(1));
        
        gemGraph.connectGems(greaterThanEqualsGem.getOutputPart(), isPositiveOutlierCollector.getInputPart(0));
        
        // Update the reflected inputs of the collector because one of the inputs in the gem tree was retargeted
        isPositiveOutlierCollector.updateReflectedInputs();
        
        // Construct the gem tree connected to the target collector
        ReflectorGem isPositiveOutlierReflector = new ReflectorGem(isPositiveOutlierCollector);
        gemGraph.addGem(isPositiveOutlierReflector);

        isPositiveOutlierReflector.getInputPart(0).setBurnt(true);
        
        ReflectorGem sourceDataReflector3 = new ReflectorGem(sourceDataCollector);
        gemGraph.addGem(sourceDataReflector3);
        
        Gem filterGem = gemFactory.makeFunctionalAgentGem(CAL_List.Functions.filter);
        gemGraph.addGem(filterGem);
        
        gemGraph.connectGems(isPositiveOutlierReflector.getOutputPart(), filterGem.getInputPart(0));
        gemGraph.connectGems(sourceDataReflector3.getOutputPart(), filterGem.getInputPart(1));
        
        gemGraph.connectGems(filterGem.getOutputPart(), gemGraph.getTargetCollector().getInputPart(0));
        
        gemGraph.typeGemGraph(calServices.getTypeCheckInfo(GEM_GRAPH_TYPE_CHECKING_MODULE));
        
        return gemGraph;
    }
    
    /**
     * Creates a GemGraph that defines the demoMap function.
     * <p>
     * Corresponding CAL source for the gem graph:
     * <pre>
     * public demoMap mapFunction list = Prelude.iff (Prelude.isEmpty list) [] (Prelude.Cons (mapFunction (List.head list)) (demoMap mapFunction (List.tail list)));
     * </pre>
     * @param gemFactory 
     * @param calServices 
     */
    public static GemGraph demoMapGemGraph(GemFactory gemFactory, BasicCALServices calServices) throws TypeException {

        GemGraph gemGraph = new GemGraph();
        gemGraph.getTargetCollector().setName("demoMap");
        
        // Two collectors forming the two arguments of the demoMap gem.
        CollectorGem mapFunctionCollector = new CollectorGem();
        mapFunctionCollector.setName("mapFunction");
        gemGraph.addGem(mapFunctionCollector);
        
        CollectorGem listCollector = new CollectorGem();
        listCollector.setName("list");
        gemGraph.addGem(listCollector);
        
        // Construct the test for checking whether the 'list' value is the empty list.
        Gem isEmptyGem = gemFactory.makeFunctionalAgentGem(CAL_Prelude.Functions.isEmpty);
        gemGraph.addGem(isEmptyGem);
        
        ReflectorGem listReflector1 = new ReflectorGem(listCollector);
        gemGraph.addGem(listReflector1);

        // Construct the gem tree for the case when 'list' is not empty.
        
        // The head of the returned Cons is: mapFunction (List.head list)
        // which uses the apply gem to apply the mapFunction to its argument.
        gemGraph.connectGems(listReflector1.getOutputPart(), isEmptyGem.getInputPart(0));

        Gem headGem = gemFactory.makeFunctionalAgentGem(CAL_List.Functions.head);
        gemGraph.addGem(headGem);
        
        ReflectorGem listReflector2 = new ReflectorGem(listCollector);
        gemGraph.addGem(listReflector2);
        
        gemGraph.connectGems(listReflector2.getOutputPart(), headGem.getInputPart(0));
        
        Gem applyGem = gemFactory.makeFunctionalAgentGem(CAL_Prelude.Functions.apply);
        gemGraph.addGem(applyGem);
        
        ReflectorGem mapFunctionReflector1 = new ReflectorGem(mapFunctionCollector);
        gemGraph.addGem(mapFunctionReflector1);
        
        gemGraph.connectGems(mapFunctionReflector1.getOutputPart(), applyGem.getInputPart(0));
        gemGraph.connectGems(headGem.getOutputPart(), applyGem.getInputPart(1));
        
        // The tail of the returned Cons is: demoMap mapFunction (List.tail list) 
        Gem tailGem = gemFactory.makeFunctionalAgentGem(CAL_List.Functions.tail);
        gemGraph.addGem(tailGem);

        ReflectorGem listReflector3 = new ReflectorGem(listCollector);
        gemGraph.addGem(listReflector3);
        
        gemGraph.connectGems(listReflector3.getOutputPart(), tailGem.getInputPart(0));
        
        ReflectorGem demoMapReflector = new ReflectorGem(gemGraph.getTargetCollector());
        gemGraph.addGem(demoMapReflector);
        
        ReflectorGem mapFunctionReflector2 = new ReflectorGem(mapFunctionCollector);
        gemGraph.addGem(mapFunctionReflector2);
        
        Gem consGem = gemFactory.makeFunctionalAgentGem(CAL_Prelude.DataConstructors.Cons);
        gemGraph.addGem(consGem);
        
        gemGraph.connectGems(applyGem.getOutputPart(), consGem.getInputPart(0));
        gemGraph.connectGems(demoMapReflector.getOutputPart(), consGem.getInputPart(1));
        
        // Construct the conditional branch (using the iff gem). The false case returns the value
        // generated by the gem tree defined above. In the true case, Nil (the empty list) is returned.
        Gem iffGem = gemFactory.makeFunctionalAgentGem(CAL_Prelude.Functions.iff);
        gemGraph.addGem(iffGem);
        
        Gem nilGem = gemFactory.makeFunctionalAgentGem(CAL_Prelude.DataConstructors.Nil);
        gemGraph.addGem(nilGem);
        
        gemGraph.connectGems(isEmptyGem.getOutputPart(), iffGem.getInputPart(0));
        gemGraph.connectGems(nilGem.getOutputPart(), iffGem.getInputPart(1));
        gemGraph.connectGems(consGem.getOutputPart(), iffGem.getInputPart(2));
        
        // Connect the gems to the target collector
        gemGraph.connectGems(iffGem.getOutputPart(), gemGraph.getTargetCollector().getInputPart(0));
        
        gemGraph.connectGems(mapFunctionReflector2.getOutputPart(), demoMapReflector.getInputPart(0));
        
        gemGraph.connectGems(tailGem.getOutputPart(), demoMapReflector.getInputPart(1));
        
        gemGraph.typeGemGraph(calServices.getTypeCheckInfo(GEM_GRAPH_TYPE_CHECKING_MODULE));
        
        return gemGraph;
    }
}
