import numpy as np


class GnomesPlayer:
    def __init__(self):
        self.lowest_gnome_nook = 100
        self.highest_gnome_nook = -1

    def __repr__(self) -> str:
        if self.lowest_gnome_nook == 100 and self.highest_gnome_nook == -1:
            return "GnomesPlayer[untrained]"
        return (f"GnomesPlayer[{self.lowest_gnome_nook}" +
                f", {self.highest_gnome_nook}]")

    def fit(self, X: np.ndarray, y: np.ndarray) -> None:
        """
        Requires: X is a 1D numpy array with all values between 0 and 99
                  y is a 1D numpy array with all values 0 or 1
                  X and y are parallel arrays, i.e. the i-th element of X and y
                  correspond to the same data point
        Modifies: self.lowest_gnome_nook and self.highest_gnome_nook
        Effects: Updates the lowest and highest nook indices that contain a
                 gnome from the training data. If a nook contains a gnome, the
                 value in the y array is 1, and 0 otherwise. Note that the nook
                 indices in X are not sorted.
        """
        # TODO: Implement this function
        pass

    def predict(self, X: np.ndarray) -> np.ndarray:
        """
        Requires: X is a 1D numpy array with values between 0 and 99
        Modifies: Nothing
        Effects:  Returns predictions for whether or not there is a gnome
                  in the nooks in X. We predict that a nook has a gnome if
                  it is in the interval [lowest_gnome_nook, highest_gnome_nook]
                  and we predict that it does not have a gnome if it is outside
                  this interval. Predictions are returned as numpy
                  array of 0/1 values. For example if lowest_gnome_nook is 18,
                  highest_gnome_nook is 20, and X is [16, 20, 18, 21, 19, 23],
                  then this function should return a 1D numpy array that
                  contains [0, 1, 1, 0, 1, 0].
        """
        # TODO: Implement this function
        return np.full(X.shape, -1)

    def compare(
            self, X: np.ndarray, actual: np.ndarray, predicted: np.ndarray
            ) -> None:
        """
        Requires: X is a 1D numpy array with values between 0 and 99
                  actual is a 1D numpy array with values 0 or 1
                  predicted is a 1D numpy array with values 0 or 1
                  X and actual are parallel arrays, i.e. the i-th element of X
                  and actual correspond to the same data point
        Modifies: Nothing
        Effects: Prints the actual and predicted values for each data point,
                 and prints the accuracy of the predictions.
        """
        print("Actual, predicted")
        right = (actual == predicted).sum()
        wrong = len(actual) - right

        # Create the entire string block at once, then print it
        lines_to_print = [
            f"  {x}: {a}, {p}" for x, a, p in zip(X, actual, predicted)
        ]
        print('\n'.join(lines_to_print))

        acc = 0.0
        acc = right / len(actual)
        print(f"Right: {right}, Wrong: {wrong}")
        print(f"Accuracy (higher is better): {acc}")


def main() -> None:
    print("========== Game #1 ==========")
    X_train = np.array([16, 18, 26, 31, 7, 10, 3, 28, 13])
    y_train = np.array([1, 1, 0, 0, 0, 0, 0, 0, 1])
    X_test = np.array([17, 12, 27, 22, 5, 24, 21, 4, 9, 6])
    y_test = np.array([1, 1, 0, 1, 0, 0, 1, 0, 0, 0])

    print("\n--- GnomesPlayer ---")
    gp = GnomesPlayer()
    print(f"Before training: {gp}")
    gp.fit(X_train, y_train)
    print(f"After training: {gp}")
    preds_gp = gp.predict(X_test)
    print(f"predictions: {preds_gp.tolist()}")
    gp.compare(X_test, y_test, preds_gp)

    print("\n========== Game #2 ==========")
    X_train = np.array([9, 13, 24, 26, 2, 6, 29, 4, 19, 22])
    y_train = np.array([1, 1, 0, 0, 0, 1, 0, 1, 1, 0])
    X_test = np.array([15, 23, 22, 5, 20, 12, 14, 17])
    y_test = np.array([1, 0, 0, 1, 1, 1, 1, 1])

    print("\n--- GnomesPlayer ---")
    gp = GnomesPlayer()
    print(f"Before training: {gp}")
    gp.fit(X_train, y_train)
    print(f"After training: {gp}")
    preds_gp = gp.predict(X_test)
    print(f"predictions: {preds_gp.tolist()}")
    gp.compare(X_test, y_test, preds_gp)


if __name__ == "__main__":
    main()
