import java.util.Arrays;
import java.util.InputMismatchException;
import java.util.Random;
import java.util.Scanner;

/**
 * Slalom
 * <p>
 * Converted from BASIC to Java by Aldrin Misquitta (@aldrinm)
 *
 * There is a bug in the original version where the data pointer doesn't reset after a race is completed. This causes subsequent races to error at
 * some future point on line "540    READ Q"
 */
public class Slalom {

    private static final int MAX_NUM_GATES = 25;
    private static final int[] MAX_SPEED = {
            14, 18, 26, 29, 18,
            25, 28, 32, 29, 20,
            29, 29, 25, 21, 26,
            29, 20, 21, 20, 18,
            26, 25, 33, 31, 22
    };

    public static void main(String[] args) {
        var random = new Random();

        printIntro();
        Scanner scanner = new Scanner(System.in);

        int numGates = readNumberOfGatesChoice(scanner);

        printMenu();
        MenuChoice menuChoice;
        do {
            menuChoice = readMenuOption(scanner);
            switch (menuChoice) {
                case INS:
                    printInstructions();
                    break;
                case MAX:
                    printApproxMaxSpeeds(numGates);
                    break;
                case RUN:
                    run(numGates, scanner, random);
                    break;
            }
        } while (menuChoice != MenuChoice.RUN);
    }

    private static void run(int numGates, Scanner scan, Random random) {
        int rating = readSkierRating(scan);
        boolean gameInProgress = true;
        var medals = new Medals(0, 0, 0);

        while (gameInProgress) {
            System.out.println("THE STARTER COUNTS DOWN...5...4...3...2...1...GO!");
            System.out.println("YOU'RE OFF!");

            int speed = random.nextInt(18 - 9) + 9;

            float totalTimeTaken = 0;
            try {
                totalTimeTaken = runThroughGates(numGates, scan, random, speed);
                System.out.printf("%nYOU TOOK %.2f SECONDS.%n", totalTimeTaken + random.nextFloat());

                medals = evaluateAndUpdateMedals(totalTimeTaken, numGates, rating, medals);
            } catch (WipedOutOrSnaggedAFlag | DisqualifiedException e) {
                //end of this race! Print time taken and stop
                System.out.printf("%nYOU TOOK %.2f SECONDS.%n", totalTimeTaken + random.nextFloat());
            }

            gameInProgress = readRaceAgainChoice(scan);
        }

        System.out.println("THANKS FOR THE RACE");
        if (medals.getGold() >= 1) System.out.printf("GOLD MEDALS: %d%n", medals.getGold());
        if (medals.getSilver() >= 1) System.out.printf("SILVER MEDALS: %d%n", medals.getSilver());
        if (medals.getBronze() >= 1) System.out.printf("BRONZE MEDALS: %d%n", medals.getBronze());
    }

    private static Medals evaluateAndUpdateMedals(float totalTimeTaken, int numGates, int rating,
                                                  Medals medals) {
        var m = totalTimeTaken;
        m = m / numGates;
        int goldMedals = medals.getGold();
        int silverMedals = medals.getSilver();
        int bronzeMedals = medals.getBronze();
        if (m < 1.5 - (rating * 0.1)) {
            System.out.println("YOU WON A GOLD MEDAL!");
            goldMedals++;
        } else if (m < 2.9 - rating * 0.1) {
            System.out.println("YOU WON A SILVER MEDAL");
            silverMedals++;
        } else if (m < 4.4 - rating * 0.01) {
            System.out.println("YOU WON A BRONZE MEDAL");
            bronzeMedals++;
        }
        return new Medals(goldMedals, silverMedals, bronzeMedals);
    }

    /**
     * @return the total time taken through all the gates.
     */
    private static float runThroughGates(int numGates, Scanner scan, Random random, int speed) throws DisqualifiedException, WipedOutOrSnaggedAFlag {
        float totalTimeTaken = 0.0f;
        for (int i = 0; i < numGates; i++) {
            var gateNum = i + 1;
            boolean stillInRace = true;
            boolean gateCompleted = false;
            while (!gateCompleted) {
                System.out.printf("%nHERE COMES GATE # %d:%n", gateNum);
                printSpeed(speed);

                var tmpSpeed = speed;

                int chosenOption = readOption(scan);
                switch (chosenOption) {
                    case 0:
                        //how long
                        printHowLong(totalTimeTaken, random);
                        break;
                    case 1:
                        //speed up a lot
                        speed = speed + random.nextInt(10 - 5) + 5;
                        break;
                    case 2:
                        //speed up a little
                        speed = speed + random.nextInt(5 - 3) + 3;
                        break;
                    case 3:
                        //speed up a teensy
                        speed = speed + random.nextInt(4 - 1) + 1;
                        break;
                    case 4:
                        //keep going at the same speed
                        break;
                    case 5:
                        //check a teensy
                        speed = speed - random.nextInt(4 - 1) + 1;
                        break;
                    case 6:
                        //check a little
                        speed = speed - random.nextInt(5 - 3) + 3;
                        break;
                    case 7:
                        //check a lot
                        speed = speed - random.nextInt(10 - 5) + 5;
                        break;
                    case 8:
                        //cheat
                        System.out.println("***CHEAT");
                        if (random.nextFloat() < 0.7) {
                            System.out.println("AN OFFICIAL CAUGHT YOU!");
                            stillInRace = false;
                        } else {
                            System.out.println("YOU MADE IT!");
                            totalTimeTaken = totalTimeTaken + 1.5f;
                        }
                        break;
                }

                if (stillInRace) {
                    printSpeed(speed);
                    stillInRace = checkAndProcessIfOverMaxSpeed(random, speed, MAX_SPEED[i]);
                    if (!stillInRace) throw new WipedOutOrSnaggedAFlag();
                } else {
                    throw new DisqualifiedException();//we've been dis-qualified
                }

                if (speed < 7) {
                    System.out.println("LET'S BE REALISTIC, OK?  LET'S GO BACK AND TRY AGAIN...");
                    speed = tmpSpeed;
                    gateCompleted = false;
                } else {
                    totalTimeTaken = totalTimeTaken + (MAX_SPEED[i] - speed + 1);
                    if (speed > MAX_SPEED[i]) {
                        totalTimeTaken = totalTimeTaken + 0.5f;
                    }
                    gateCompleted = true;
                }
            }

        }
        return totalTimeTaken;
    }

    private static boolean checkAndProcessIfOverMaxSpeed(Random random, int speed, int maxSpeed) {
        boolean stillInRace = true;
        if (speed > maxSpeed) {
            if (random.nextFloat() >= (speed - maxSpeed) * 0.1 + 0.2) {
                System.out.println("YOU WENT OVER THE MAXIMUM SPEED AND MADE IT!");
            } else {
                System.out.print("YOU WENT OVER THE MAXIMUM SPEED AND ");
                if (random.nextBoolean()) {
                    System.out.println("WIPED OUT!");
                } else {
                    System.out.println("SNAGGED A FLAG!");
                }
                stillInRace = false;
            }
        } else if (speed > maxSpeed - 1) {
            System.out.println("CLOSE ONE!");
        }
        return stillInRace;
    }

    private static boolean readRaceAgainChoice(Scanner scan) {
        System.out.print("\nDO YOU WANT TO RACE AGAIN? ");
        String raceAgain = "";
        final String YES = "YES";
        final String NO = "NO";
        while (!YES.equals(raceAgain) && !NO.equals(raceAgain)) {
            raceAgain = scan.nextLine();
            if (!(YES.equals(raceAgain) || NO.equals(raceAgain))) {
                System.out.println("PLEASE TYPE 'YES' OR 'NO'");
            }
        }
        return raceAgain.equals(YES);
    }

    private static void printSpeed(int speed) {
        System.out.printf("%3d M.P.H.%n", speed);
    }

    private static void printHowLong(float t, Random random) {
        System.out.printf("YOU'VE TAKEN %.2f SECONDS.%n", t + random.nextFloat());
    }

    private static int readOption(Scanner scan) {
        Integer option = null;

        while (option == null) {
            System.out.print("OPTION? ");
            try {
                option = scan.nextInt();
            } catch (InputMismatchException ex) {
                System.out.println("!NUMBER EXPECTED - RETRY INPUT LINE\n");
            }
            scan.nextLine();
            if (option != null && (option > 8 || option < 0)) {
                System.out.println("WHAT?");
                option = null;
            }
        }
        return option;
    }

    private static int readSkierRating(Scanner scan) {
        int rating = 0;

        while (rating < 1 || rating > 3) {
            System.out.print("RATE YOURSELF AS A SKIER, (1=WORST, 3=BEST)? ");
            try {
                rating = scan.nextInt();
                if (rating < 1 || rating > 3) {
                    System.out.println("THE BOUNDS ARE 1-3");
                }
            } catch (InputMismatchException ex) {
                System.out.println("!NUMBER EXPECTED - RETRY INPUT LINE\n");
            }
            scan.nextLine();
        }
        return rating;
    }

    private static void printApproxMaxSpeeds(int numGates) {
        System.out.println("GATE MAX");
        System.out.println(" #  M.P.H.");
        System.out.println("---------");
        for (int i = 0; i < numGates; i++) {
            System.out.println((i+1) + "  " + MAX_SPEED[i]);
        }
    }

    private static void printInstructions() {
        System.out.println("\n*** SLALOM: THIS IS THE 1976 WINTER OLYMPIC GIANT SLALOM.  YOU ARE");
        System.out.println("            THE AMERICAN TEAM'S ONLY HOPE OF A GOLD MEDAL.");
        System.out.println();
        System.out.println("     0 -- TYPE THIS IS YOU WANT TO SEE HOW LONG YOU'VE TAKEN.");
        System.out.println("     1 -- TYPE THIS IF YOU WANT TO SPEED UP A LOT.");
        System.out.println("     2 -- TYPE THIS IF YOU WANT TO SPEED UP A LITTLE.");
        System.out.println("     3 -- TYPE THIS IF YOU WANT TO SPEED UP A TEENSY.");
        System.out.println("     4 -- TYPE THIS IF YOU WANT TO KEEP GOING THE SAME SPEED.");
        System.out.println("     5 -- TYPE THIS IF YOU WANT TO CHECK A TEENSY.");
        System.out.println("     6 -- TYPE THIS IF YOU WANT TO CHECK A LITTLE.");
        System.out.println("     7 -- TYPE THIS IF YOU WANT TO CHECK A LOT.");
        System.out.println("     8 -- TYPE THIS IF YOU WANT TO CHEAT AND TRY TO SKIP A GATE.");
        System.out.println();
        System.out.println(" THE PLACE TO USE THESE OPTIONS IS WHEN THE COMPUTER ASKS:");
        System.out.println();
        System.out.println("OPTION?");
        System.out.println();
        System.out.println("                GOOD LUCK!");
    }

    private static MenuChoice readMenuOption(Scanner scan) {
        System.out.print("COMMAND--? ");
        MenuChoice menuChoice = null;

        while (menuChoice == null) {
            String choice = scan.next();
            if (Arrays.stream(MenuChoice.values()).anyMatch(a -> a.name().equals(choice))) {
                menuChoice = MenuChoice.valueOf(choice);
            } else {
                System.out.print("\""+ choice + "\" IS AN ILLEGAL COMMAND--RETRY? ");
            }
            scan.nextLine();
        }
        return menuChoice;
    }

    private static void printMenu() {
        System.out.println("TYPE INS FOR INSTRUCTIONS");
        System.out.println("TYPE MAX FOR APPROXIMATE MAXIMUM SPEEDS");
        System.out.println("TYPE RUN FOR THE BEGINNING OF THE RACE");
    }

    private static int readNumberOfGatesChoice(Scanner scan) {
        int numGates = 0;
        while (numGates < 1) {
            System.out.print("HOW MANY GATES DOES THIS COURSE HAVE (1 TO 25)? ");
            numGates = scan.nextInt();
            if (numGates > MAX_NUM_GATES) {
                System.out.println(MAX_NUM_GATES + " IS THE LIMIT.");
                numGates = MAX_NUM_GATES;
            }
        }
        return numGates;
    }

    private static void printIntro() {
        System.out.println("                                SLALOM");
        System.out.println("              CREATIVE COMPUTING  MORRISTOWN, NEW JERSEY");
        System.out.println("\n\n");
    }

    private enum MenuChoice {
        INS, MAX, RUN
    }

    private static class DisqualifiedException extends Exception {
    }

    private static class WipedOutOrSnaggedAFlag extends Exception {
    }

    private static class Medals {
        private int gold = 0;
        private int silver = 0;
        private int bronze = 0;

        public Medals(int gold, int silver, int bronze) {
            this.gold = gold;
            this.silver = silver;
            this.bronze = bronze;
        }

        public int getGold() {
            return gold;
        }

        public int getSilver() {
            return silver;
        }

        public int getBronze() {
            return bronze;
        }
    }


}
