import java.io.File;
import java.io.IOException;
import java.io.FileOutputStream;
import java.io.PrintWriter;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Random;
import java.util.Scanner;


/* a stub for your team's fuzzer */
public class Fuzzer {
    // Blah - Test 2

    private static final String OUTPUT_FILE = "fuzz.txt";
    
    // The percentage of outputs that will start with a full stack
    private static final int STACK_FULL_PERCENTAGE = 10;
    // The percentage of inputs that will be incorrect
    private static final int INPUT_ERROR_PERCENTAGE = 5;
    // The percentage of instructions that will be incorrect
    private static final int LINE_ERROR_PERCENTAGE = 2;
    // Limit on generation of inputs
    private static final int NUMBER_TO_GENERATE = 10;
    // Instruction number range for each input
    private static final int INSTRUCTION_MIN = 0;
    private static final int INSTRUCTION_MAX = 1000;
    // Maximum variable name length
    private static final int VAR_NAME_LENGTH_MAX = 1000;
    private static final int VAR_ASCII_MIN = 33;
    private static final int VAR_ASCII_MAX = 127;
    // Variable range
    private static final int VAR_MIN = -2147483648;
    private static final int VAR_MAX = 2147483647;
    // Maximum number of items in stack
    private static final int MAX_STACK_SIZE = 512;

    // Random number generator
    private static Random rand = new Random(System.currentTimeMillis());

    private static ArrayList<String> vars = new ArrayList<>();

    // Instruction Count Array
    private static int[] counts = {0,0,0,0,0,0,0,0,0,0,0,0};
    // Instruction Added Probability Array
    private static int[] addProb = {0,0,0,0,0,0,0,0,0,0,0,0};
    // Pathway Prob Map, E.g. key = [PUSH, POP, STORE] - value = 2, this value is added to base prob
    private static HashMap<List<Instruction>, Integer> pathwayProb = new HashMap<List<Instruction>, Integer>();
    // Current stack of instructions
    private static ArrayList<Instruction> instructionStack = new ArrayList<Instruction>();
    // Max stack of instructions before resetting stack
    private static final int MAX_STACK = 2;

    public static void main(String[] args) throws IOException {
        System.out.println(Instruction.getBNF());
        FileOutputStream out = null;
        PrintWriter pw = null;
        int runCount = getRun();

        try {
            out = new FileOutputStream(OUTPUT_FILE);
            pw = new PrintWriter(out);
            
            /* We just print one instruction.
               Hint: you might want to make use of the instruction
               grammar which is effectively encoded in Instruction.java */
            // pw.print(getStaticTests());
            pw.println(generateRunInputs(runCount));
            
        }catch (Exception e){
            e.printStackTrace(System.err);
            System.exit(1);
        }finally{
            if (pw != null){
                pw.flush();
            }
            if (out != null){
                out.close();
            }
        }
        writeRun(runCount);
    }

    private static int getRun(){
        int runCount = 0;
        try {
            Scanner input = new Scanner(new File("runCount.txt"));
            runCount = input.nextInt() + 1;
        } catch (Exception e){
            // Carry on
        }
        return runCount;
    }

    private static void writeRun(int runCount) throws IOException {
        FileOutputStream out = null;
        PrintWriter pw = null;

        try {
            out = new FileOutputStream("runCount.txt");
            pw = new PrintWriter(out);

            pw.print(runCount);

        }catch (Exception e){
            e.printStackTrace(System.err);
            System.exit(1);
        }finally{
            if (pw != null){
                pw.flush();
            }
            if (out != null){
                out.close();
            }
        }
    }

    private static String generateRunInputs(int runCount){
        switch (runCount){
            case 0:
                // Test with stack full
                return generateInput(true, INSTRUCTION_MAX, MAX_STACK_SIZE, true, false);
            case 1:
                // Test with stack full
                return generateInput(true, INSTRUCTION_MAX, MAX_STACK_SIZE - 1, true, false);
            case 3:
                // Run static tests and empty stack
                return getStaticTests() + generateInput(true, INSTRUCTION_MAX, 0, true, false);
            case 4:
                // Test with dynamic probability
                return generateInput(true, INSTRUCTION_MAX, 0, true, false);
            case 5:
                // Test with long var names
                return generateInput(true, INSTRUCTION_MAX, 0, true, true);
        }
        // Run from random stack
        return generateInput(true, INSTRUCTION_MAX, 0, true, false);
    }

    /*
    *   Given an instruction, adds the instruction count to the global array
    *   and adds probability to the other instructions
    */
    private static void addCountProb(Instruction instruction) {
        int index = instruction.ordinal();
        for (int i = 0; i < 12; i++) {
            if (i != index) {
                counts[i] += 1;
                addProb[i] += Instruction.values()[i].getProbability();
            }
        }
    }


    /***
     * Generates a list of different inputs for the program
     *
     * @return list of input strings
     */
    private static String generateMultipleInputs(){
        // Initiate variables
        StringBuilder result = new StringBuilder();

        // For debugging purposes
        int generated = 0;

        // Loop to create list of inputs
        while (generated < NUMBER_TO_GENERATE) {
            // Determine if the line will be correct or not
            boolean stackFull = rand.nextInt(100) < STACK_FULL_PERCENTAGE;

            result.append(generateInput(false,
                                        INSTRUCTION_MAX, MAX_STACK_SIZE, false, false));
            // Increment generated
            generated += 1;
        }
        return result.toString();
    }

    private static void incrementStackAndCount(Instruction newInstruction) {
        // Instruction values
        Instruction allInstructions[] = Instruction.values();
        // Add base prob to stack with every other instruction
        for (Instruction inst: allInstructions) {
            if (!inst.equals(newInstruction)) {
                List<Instruction> tempStack = new ArrayList<Instruction>(instructionStack);
                tempStack.add(inst);
                if (pathwayProb.containsKey(tempStack)) {
                    int oldProb = pathwayProb.get(tempStack);
                    pathwayProb.replace(tempStack, oldProb + inst.getProbability());
                } else {
                    pathwayProb.put(tempStack, inst.getProbability());
                }
            }
        }
        // Add instruction to stack
        instructionStack.add(newInstruction);
        // Check if size is bigger than max stack, then reset stack
        if (instructionStack.size() >= MAX_STACK) {
            instructionStack.clear();
        }
        System.out.println("stack: " + Arrays.toString(instructionStack.toArray()));
        for (List<Instruction> list: pathwayProb.keySet()) {
            String key = list.toString();
            String value = pathwayProb.get(list).toString();
            System.out.println("map: " + key + " " + value);
        }

    }

    /***
     * Generates a line of input for the program
     *
     * @param correct flag, which determines if the line will be correctly formulated
     * @param numInstructions for the line
     * @return the concatenated input for the program as a string
     */
    private static String generateInput(boolean correct, long numInstructions, int stackPreload, boolean dynamicProb, boolean longVarNames){
        int stackSize = 0;
        int counter = 0;

        StringBuilder result = new StringBuilder();

        for ( int i = 0 ; i < stackPreload ; i++ ) {
            result.append(completeInstruction(true, Instruction.PUSH, false));
            stackSize++;
        }

        if (correct) {
            while (counter < numInstructions) {
                Instruction newInstr = Instruction.getRandomInstruction(stackSize, instructionStack, pathwayProb);
                stackSize = stackSize + newInstr.getStackChange();
                result.append(completeInstruction(true, newInstr, false));
                if (dynamicProb) {
                    incrementStackAndCount(newInstr);
                }
                counter += 1;
            }
        } else {
            while (counter < numInstructions) {
                Instruction newInstr;
                if (rand.nextInt(100) < LINE_ERROR_PERCENTAGE){
                    newInstr = Instruction.getRandomInstruction(2, instructionStack, pathwayProb);
                    result.append(completeInstruction(false, newInstr, longVarNames));
                    if (dynamicProb) {
                        incrementStackAndCount(newInstr);
                    }
                } else {
                    newInstr = Instruction.getRandomInstruction(stackSize, instructionStack, pathwayProb);
                    result.append(completeInstruction(true, newInstr, false));
                    if (dynamicProb) {
                        incrementStackAndCount(newInstr);
                    }
                }
                stackSize = stackSize + newInstr.getStackChange();
                if (stackSize < 0){
                    stackSize = 0;
                }

                counter += 1;
            }
        }
        return result.toString();
    }

    /***
     * Adds parameters to an individual instruction
     *
     * @param correct flag
     * @param instruction type
     * @return string with parameter
     */
    private static String completeInstruction(boolean correct, Instruction instruction, boolean longVarNames){
        
        String name = "";
        switch (instruction) {
            case PUSH:
                if (correct) {
                    // If correct, generate as usual
                    name = " " + ((Integer) randomRange(VAR_MIN, VAR_MAX)).toString();
                } else {
                    // If incorrect, increase the range to outside +- int31_t
                    name = " " + ((Long) (randomRange(-1,1)*((long) VAR_MAX + (long) randomRange(0, VAR_MAX)))).toString();
                }
                break;
            case LOAD:
            case REM:
                // If not correct, make up a name not in the list
                if (!correct){
                    name = generateName(longVarNames);
                }
                // If no variables, return empty string
                else if (vars.size() == 0){
                    return "";
                }
                // Otherwise, get a variable name from the list
                else {
                    int index = rand.nextInt(vars.size());
                    name = " " + vars.get(index);
                    vars.remove(index);
                }
                break;
            case STORE:
                // If not correct, make up a name not in the list
                if (correct || vars.size() == 0){
                    // Make up a name
                    name = generateName(longVarNames);
                    vars.add(name);
                }
                // If no variables, return empty string
                else {
                    name = vars.get(randomRange(0,vars.size()-1));
                }


                name = " " + name;
                break;
            case SAVE:
                name = " " + generateName(longVarNames) + ".txt";
                break;
            case PLUS:
            case SUB:
            case MULT:
            case DIV:
            case POP:
            case LIST:
            case PRINT:
                break;
        }
        return instruction.getOpcode() + name + "\n";
    }

    /***
     * Generates a random name not longer than VAR_NAME_LENGTH_MAX
     * Characters are bounded by ASCII indexes at VAR_ASCII_MIN and VAR_ASCII_MAX
     *
     * @return the name generated
     */
    private static String generateName(boolean longVarName){
        StringBuilder name = new StringBuilder();
        int counter = 0;
        int length;

        if (longVarName){
            length = VAR_NAME_LENGTH_MAX + 100;
        } else {
            // Randomise the length of the string
            length = rand.nextInt(VAR_NAME_LENGTH_MAX);
        }

        // Add random bounded characters to string
        while (counter < length){
            name.append((char) randomRange(VAR_ASCII_MIN, VAR_ASCII_MAX));
            counter += 1;
        }
        return name.toString();
    }

    private static int randomRange(int min, int max){
        // Remove negatives and get around int limitations
        if (min == VAR_MIN){
            return 2* (rand.nextInt(max) + (min/2));
        } else if (min < 0){
            max = max - min;
            return rand.nextInt(max) + min;
        }
        return min + rand.nextInt(max - min);
    }

    /***
     * Get the desired static boundary tests
     *
     * @return a string containing all of the boundary tests' instructions
     */
    private static String getStaticTests() {
        // Note. Some operations will crash dc if performed, so they cannot be tested
        // Examples include *, /, -, +, pop, store operations on empty stack

        StringBuilder result = new StringBuilder();

        // Test opreations that need variables on no variables defined
        result.append(Instruction.LIST.getOpcode() + "\n");
        result.append(Instruction.LOAD.getOpcode() + " VAR1\n");
        result.append(Instruction.REM.getOpcode() + " VAR1\n");

        // Test operations that use stack on empty stack
        result.append(Instruction.PRINT.getOpcode() + "\n");

        // Prep for next tests
        result.append(Instruction.PUSH.getOpcode() + " 5\n");
        result.append(Instruction.STORE.getOpcode() + " VAR1\n");
        result.append(Instruction.PUSH.getOpcode() + " 10\n");
        result.append(Instruction.STORE.getOpcode() + " VAR2\n");
        result.append(Instruction.SAVE.getOpcode() + "\n");

        // Test incorrect arguements for operation
        result.append(Instruction.LOAD.getOpcode() + " VAR3\n");

        // Test Variables stored and used corectly
        result.append(Instruction.LIST.getOpcode() + "\n");
        result.append(Instruction.PRINT.getOpcode() + "\n");
        result.append(Instruction.LOAD.getOpcode() + " VAR2\n");
        result.append(Instruction.LOAD.getOpcode() + " VAR1\n");
        result.append(Instruction.PRINT.getOpcode() + "\n");
        result.append(Instruction.POP.getOpcode() + "\n");
        result.append(Instruction.PRINT.getOpcode() + "\n");
        result.append(Instruction.REM.getOpcode() + " VAR2\n");
        result.append(Instruction.LIST.getOpcode() + "\n");
        result.append(Instruction.LOAD.getOpcode() + " VAR2\n");
        result.append(Instruction.PRINT.getOpcode() + "\n");
        result.append(Instruction.STORE.getOpcode() + " VAR1\n");
        result.append(Instruction.LIST.getOpcode() + "\n");
        result.append(Instruction.LOAD.getOpcode() + " VAR1\n");
        result.append(Instruction.STORE.getOpcode() + " VAR1\n");
        result.append(Instruction.LIST.getOpcode() + "\n");
        result.append(Instruction.REM.getOpcode() + " VAR1\n");

        // Test Plus operation
        result.append(Instruction.PUSH.getOpcode() + " "+ VAR_MAX + "\n");
        result.append(Instruction.PUSH.getOpcode() + " "+ VAR_MIN + "\n");
        result.append(Instruction.PLUS.getOpcode() + "\n");
        result.append(Instruction.POP.getOpcode() + "\n");
        result.append(Instruction.PUSH.getOpcode() + " "+ VAR_MAX + "\n");
        result.append(Instruction.PUSH.getOpcode() + " 1\n");
        result.append(Instruction.PLUS.getOpcode() + "\n");
        result.append(Instruction.POP.getOpcode() + "\n");

        // Test Subtract operation
        result.append(Instruction.PUSH.getOpcode() + " "+ VAR_MAX + "\n");
        result.append(Instruction.PUSH.getOpcode() + " "+ VAR_MAX + "\n");
        result.append(Instruction.SUB.getOpcode() + "\n");
        result.append(Instruction.POP.getOpcode() + "\n");
        result.append(Instruction.PUSH.getOpcode() + " 1\n");
        result.append(Instruction.PUSH.getOpcode() + " "+ VAR_MIN + "\n");
        result.append(Instruction.SUB.getOpcode() + "\n");
        result.append(Instruction.POP.getOpcode() + "\n");

        // Test Divide operation
        result.append(Instruction.PUSH.getOpcode() + " "+ VAR_MAX + "\n");
        result.append(Instruction.PUSH.getOpcode() + " "+ VAR_MAX + "\n");
        result.append(Instruction.DIV.getOpcode() + "\n");
        result.append(Instruction.POP.getOpcode() + "\n");
        result.append(Instruction.PUSH.getOpcode() + " 1\n");
        result.append(Instruction.PUSH.getOpcode() + " "+ VAR_MIN + "\n");
        result.append(Instruction.DIV.getOpcode() + "\n");
        result.append(Instruction.POP.getOpcode() + "\n");

        // Test Multiply operation
        result.append(Instruction.PUSH.getOpcode() + " "+ VAR_MAX + "\n");
        result.append(Instruction.PUSH.getOpcode() + " 2\n");
        result.append(Instruction.MULT.getOpcode() + "\n");
        result.append(Instruction.POP.getOpcode() + "\n");
        result.append(Instruction.PUSH.getOpcode() + " 2\n");
        result.append(Instruction.PUSH.getOpcode() + " "+ Math.floorDiv(VAR_MIN, 2) + "\n");
        result.append(Instruction.MULT.getOpcode() + "\n");
        result.append(Instruction.POP.getOpcode() + "\n");

        return result.toString();
    }

}