package edu.caltech.cs2.project01;

import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.Scanner;

import static edu.caltech.cs2.project01.CaesarCipher.ALPHABET;

public class SubstitutionCipher {
    private String ciphertext;
    private Map<Character, Character> key;

    // Use this Random object to generate random numbers in your code,
    // but do not modify this line.
    private static final Random RANDOM = new Random();

    /**
     * Construct a SubstitutionCipher with the given cipher text and key
     * @param ciphertext the cipher text for this substitution cipher
     * @param key the map from cipher text characters to plaintext characters
     */
    public SubstitutionCipher(String ciphertext, Map<Character, Character> key) {
        this.ciphertext = ciphertext;
        this.key = key;
    }

    /**
     * Construct a SubstitutionCipher with the given cipher text and a randomly
     * initialized key.
     * @param ciphertext the cipher text for this substitution cipher
     */
    public SubstitutionCipher(String ciphertext) {
        this.ciphertext = ciphertext;
        this.key = new HashMap<>();
        for (char c : ALPHABET) {
            this.key.put(c, c);
        }
        for (int i = 0; i < 10000; i++) {
            this.key = this.randomSwap().key;
        }
    }

    /**
     * Returns the unedited cipher text that was provided by the user.
     * @return the cipher text for this substitution cipher
     */
    public String getCipherText() {
        return ciphertext;
    }

    /**
     * Applies this cipher's key onto this cipher's text.
     * That is, each letter should be replaced with whichever
     * letter it maps to in this cipher's key.
     * @return the resulting plain text after the transformation using the key
     */
    public String getPlainText() {
        String newString = "";
        for (int i = 0; i < ciphertext.length(); i++) {
            newString += key.get(ciphertext.charAt(i));
        }
        return newString;
    }

    /**
     * Returns a new SubstitutionCipher with the same cipher text as this one
     * and a modified key with exactly one random pair of characters exchanged.
     *
     * @return the new SubstitutionCipher
     */
    public SubstitutionCipher randomSwap() {
        Map<Character, Character> newMap = new HashMap<>();

        char c1 = (char) (65 + (RANDOM.nextInt(26)));
        char c2 = (char) (65 + (RANDOM.nextInt(26)));

        while(c1 == c2) {
            c2 = (char) (65 + (RANDOM.nextInt(26)));
        }

        newMap.putAll(this.key);
        char val1 = key.get(c1);
        char val2 = key.get(c2);

        newMap.put(c1, val2);
        newMap.put(c2, val1);
        return new SubstitutionCipher(this.ciphertext, newMap);
    }

    /**
     * Returns the "score" for the "plain text" for this cipher.
     * The score for each individual quadgram is calculated by
     * the provided likelihoods object. The total score for the text is just
     * the sum of these scores.
     * @param likelihoods the object used to find a score for a quadgram
     * @return the score of the plain text as calculated by likelihoods
     */
    public double getScore(QuadGramLikelihoods likelihoods) {
        String text = getPlainText();
        double score = 0;
        for (int i = 0; i < text.length() - 3; i++) {
            String quadgram = text.substring(i, i+4);
            double likelihood = likelihoods.get(quadgram);
            score += likelihood;
        }
        return score;
    }

    /**
     * Attempt to solve this substitution cipher through the hill
     * climbing algorithm. The SubstitutionCipher this is called from
     * should not be modified.
     * @param likelihoods the object used to find a score for a quadgram
     * @return a SubstitutionCipher with the same ciphertext and the optimal
     *  found through hill climbing
     */
    public SubstitutionCipher getSolution(QuadGramLikelihoods likelihoods) {
        SubstitutionCipher solution = new SubstitutionCipher(ciphertext);
        int scoreCounter = 0;

        while (scoreCounter < 1000) {
            SubstitutionCipher newSolution = solution.randomSwap();
            double scoreOld = solution.getScore(likelihoods);
            double newScore = newSolution.getScore(likelihoods);
            scoreCounter += 1;

            if (newScore > scoreOld) {
                scoreCounter = 0;
                solution = newSolution;
            }
        }

        return solution;
    }

}
