import java.util.Random;
import java.util.Collections;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.lang.*;
import java.text.DecimalFormat;

/**
 * Code to test submitted solutions for balancing bike messenger saddlebags.
 *
 * @author Kyle Burke
 */
public class SpeedGrader {

    //whether we're using Java's internal nanosecond timers
    private static final boolean USE_NANOS = true;
    
    
    public SpeedGrader() {
        //no state
    }

    /**
     * Runs the tests and prints feedback if asserts are enabled.
     */
    public void runTests(CodeTester tester, double multiplier, int maxPoints, int[] sizes) {
        List<Integer> sizeList = new ArrayList<>();
        for (int i = 0; i < sizes.length; i++)  {
            sizeList.add(sizes[i]);
        }
        runTests(tester, multiplier, maxPoints, sizeList);
    }

    /**
     * Runs the tests and prints feedback if asserts are enabled.
     */
    public void runTests(CodeTester tester, double multiplier, int maxPoints, List<Integer> sizes) {
        //check that asserts are enforced
        boolean assertsEnabled = false;
        try {
            assert false;
        } catch (Throwable e) {
            assertsEnabled = true;
        }
        System.out.println("Are asserts enabled?  " + (assertsEnabled ? "Yes!" : "No!  You won't get the normal feedback."));
    
        //benchmark and set up for the tests
        tester.setUseNanos(USE_NANOS);
        long benchmarkMillis = BenchmarkJava.benchmark(USE_NANOS);
        System.out.println("Benchmark took: " + benchmarkMillis + " ms.");
        double allowedMillis = multiplier * benchmarkMillis;
        System.out.println("Each test will be capped at " + ((long) allowedMillis) + " ms.");
        Collections.sort(sizes); //make sure the sizes are in order!  (Just in case we messed up!)
        double score = 0.0;
        int numSizes = sizes.size();
        System.out.println("There are " + numSizes + " different problem sizes.");
        double pointsPerSize = ((double) maxPoints) / sizes.size();
        System.out.println("Each trial is worth " + pointsPerSize + " points.");
        DecimalFormat pointsFormat = new DecimalFormat("###.##");
        DecimalFormat sizeFormat = new DecimalFormat("###,###,###");
        
        //run the tests
        int numSuccesses = 0;
        long testTime = -1;
        for (int i = 0; i < sizes.size(); i++) {
            int problemSize = sizes.get(i);
            tester.setProblemSize(problemSize);
            try {
                testTime = tester.call();
            } catch (Error e) {
                System.out.println("Error on size " + sizeFormat.format(problemSize) + ":");
                e.printStackTrace(System.out);
                throw new RuntimeException(e);
            }
            if (testTime < allowedMillis) {
                score += pointsPerSize;
                numSuccesses = i+1;
                if (numSuccesses == sizes.size()) {
                    //make the total a clean perfect score
                    score = (double) maxPoints;
                }
                System.out.println("Success on size " + sizeFormat.format(problemSize) + " in " + testTime + " ms.");
            } else {
                System.out.println("Timed out on size " + sizeFormat.format(problemSize) + ".  (Took " + testTime + " > " + allowedMillis + ".)");
                System.out.println("Remaining sizes: " + sizes.subList(i+1, sizes.size()));
                break;
            }
        }
        System.out.println("The last test ran in " + testTime + " ms.");
            
        if (assertsEnabled) {
        
            String teamMembers = tester.getCodeAuthors();
            
            System.out.println();
            System.out.println("*~~~~~~~ Summary for " + teamMembers + " ~~~~~~~~~~~*");
            System.out.println("Total score: " + pointsFormat.format(score) + "/" + maxPoints + "     (" + pointsFormat.format(pointsPerSize) + " x " + numSuccesses + ")");
        } else {
            System.out.println("You didn't enable asserts.  Please run again with them enabled to get summary feedback.");
        }
        
    }
    
    public static abstract class CodeTester implements Callable<Long> {
    
        protected int problemSize;
        
        protected boolean useNanos;
        
        public abstract Long call();
        
        public void setProblemSize(int problemSize) {
            this.problemSize = problemSize;
        }
        
        public abstract String getCodeAuthors();
        
        public void setUseNanos(boolean useNanos) {
            this.useNanos = useNanos;
        }
        
        protected long getSystemMillis() {
            if (this.useNanos) {
                return System.nanoTime() / 1000000;
            } else {
                return System.currentTimeMillis();
            }
        }
    
    }
}