package registrar;

import tester.Tester;

class Examples {

    ILo<Integer> testList = new Cons<Integer>(
        1,
        new Cons<Integer>(2, new Cons<Integer>(3, new Mt<Integer>()))
    );

    Teacher singer;
    Teacher wormwood;

    Course singing;
    Course arithmetic;
    Course clobbering;

    Student calvin;
    Student hobbes;
    Student susie;

    void init() {
        singer = new Teacher("Mr. Singer");
        wormwood = new Teacher("Ms. Wormwood");

        singing = new Course("Singing", singer);
        arithmetic = new Course("Arithmetic", wormwood);
        clobbering = new Course("Introduction to Clobbering", wormwood);

        calvin = new Student("Calvin");
        hobbes = new Student("Hobbes");
        susie = new Student("Susie Derkins");
    }

    void testILoUniqAppend(Tester t) {
        t.checkExpect(testList.uniqAppend(2), testList);
        t.checkExpect(
            testList.uniqAppend(4),
            new Cons<Integer>(
                1,
                new Cons<Integer>(
                    2,
                    new Cons<Integer>(
                        3,
                        new Cons<Integer>(4, new Mt<Integer>())
                    )
                )
            )
        );
    }

    void testILoHas(Tester t) {
        t.checkExpect(testList.has(2), true);
        t.checkExpect(testList.has(6), false);
    }

    void testILoDropFirst(Tester t) {
        t.checkExpect(
            testList.dropFirst(2),
            new Cons<Integer>(1, new Cons<Integer>(3, new Mt<Integer>()))
        );
    }

    void testCourse(Tester t) {
        init();
        t.checkExpect(singing.teacher, singer);
        t.checkExpect(
            singer.courses,
            new Cons<Course>(singing, new Mt<Course>())
        );
    }

    void testStudentEnroll(Tester t) {
        init();
        calvin.enroll(arithmetic);
        t.checkExpect(
            calvin.courses,
            new Cons<Course>(arithmetic, new Mt<Course>())
        );
        t.checkExpect(
            arithmetic.students,
            new Cons<Student>(calvin, new Mt<Student>())
        );
        t.checkExpect(calvin.takesCourse(arithmetic), true);
        calvin.enroll(arithmetic);
        t.checkExpect(
            calvin.courses,
            new Cons<Course>(arithmetic, new Mt<Course>())
        );
        t.checkExpect(
            arithmetic.students,
            new Cons<Student>(calvin, new Mt<Student>())
        );

        singing.setTeacher(wormwood);
        t.checkExpect(singing.teacher, wormwood);
        t.checkExpect(
            wormwood.courses,
            new Cons<Course>(
                arithmetic,
                new Cons<Course>(
                    clobbering,
                    new Cons<Course>(singing, new Mt<Course>())
                )
            )
        );
        t.checkExpect(singer.courses, new Mt<Course>());
    }

    void testStudentClassmates(Tester t) {
        init();

        calvin.enroll(singing);
        hobbes.enroll(singing);
        susie.enroll(arithmetic);

        t.checkExpect(calvin.classmates(hobbes), true);
        t.checkExpect(hobbes.classmates(calvin), true);
        t.checkExpect(calvin.classmates(susie), false);
        t.checkExpect(susie.classmates(hobbes), false);
    }

    void testTeacherDejaVu(Tester t) {
        init();

        calvin.enroll(arithmetic);
        calvin.enroll(clobbering);
        hobbes.enroll(clobbering);
        susie.enroll(singing);

        t.checkExpect(wormwood.dejavu(calvin), true);
        t.checkExpect(singer.dejavu(susie), false);
        t.checkExpect(wormwood.dejavu(hobbes), false);
    }
}

class School {

    static int cid = 0;

    // Returns the current ID, then increments it for the next time.
    static int getThenInc() {
        return School.cid++;
    }
}

class Course {

    String name;
    Teacher teacher;
    ILo<Student> students;

    Course(String name, Teacher teacher, ILo<Student> students) {
        this.name = name;
        this.teacher = teacher;
        teacher.assignCourse(this);
        this.students = students;
    }

    // Convenience constructor for initial course construction.
    Course(String name, Teacher teacher) {
        this(name, teacher, new Mt<Student>());
    }

    // Effect: Adds a student.
    void addStudent(Student student) {
        // Make sure there's no way to add a student to a course without
        // being enrolled.
        if (!student.takesCourse(this)) student.enroll(this);
        this.students = this.students.uniqAppend(student);
    }

    // Effect: Sets the teacher.
    void setTeacher(Teacher newTeacher) {
        if (!newTeacher.equals(this.teacher)) {
            this.teacher.removeCourse(this);
            this.teacher = newTeacher;
            this.teacher.assignCourse(this);
        }
        // If the teacher is the same, do nothing.
    }

    // Is this Course taught by the Teacher?
    boolean taughtBy(Teacher teacher) {
        return this.teacher.equals(teacher);
    }
}

class Teacher {

    String name;
    ILo<Course> courses;

    Teacher(String name, ILo<Course> courses) {
        this.name = name;
        this.courses = courses;
    }

    // Convenience constructor for initial teacher construction.
    Teacher(String name) {
        this(name, new Mt<Course>());
    }

    // Effect: Assigns the given Course to this Teacher.
    void assignCourse(Course course) {
        if (!course.taughtBy(this)) course.setTeacher(this);
        this.courses = this.courses.uniqAppend(course);
    }

    // Does this Teacher teach the Course?
    boolean teaches(Course course) {
        return this.courses.has(course);
    }

    // Effect: Removes a Course from this Teacher.
    // ASSUMES THIS TEACHER WILL BE REPLACED.
    void removeCourse(Course course) {
        this.courses = this.courses.dropFirst(course);
    }

    // Is the Student in one or more of this Teacher's Courses?
    boolean dejavu(Student student) {
        return this.courses.moreMap(new StudentTakesCourse(student));
    }
}

class Student {

    String name;
    int id;
    ILo<Course> courses;

    Student(String name, int id, ILo<Course> courses) {
        this.name = name;
        this.id = id;
        this.courses = courses;
    }

    // Convenience constructor for initial student construction.
    Student(String name) {
        this(name, School.cid, new Mt<Course>());
    }

    // Effect: Enrolls this student in the given course.
    void enroll(Course course) {
        this.courses = this.courses.uniqAppend(course);
        course.addStudent(this);
    }

    // Does the student take this course?
    boolean takesCourse(Course course) {
        return this.courses.has(course);
    }

    // Are they in any of the same Courses as this Student?
    boolean classmates(Student student) {
        return this.courses.orMap(new StudentTakesCourse(student));
    }
}

interface Question<A> {
    boolean ask(A a);
}

class StudentTakesCourse implements Question<Course> {

    Student student;

    StudentTakesCourse(Student student) {
        this.student = student;
    }

    public boolean ask(Course course) {
        return this.student.takesCourse(course);
    }
}

interface ILo<A> {
    // Uniquely append an element (i.e., leaves list unchanged if element is already present.)
    ILo<A> uniqAppend(A a);

    // Drop first instance of element from a list.
    ILo<A> dropFirst(A a);

    // Does the list have this?
    boolean has(A a);

    // Do any of the elements satisfy the predicate?
    boolean orMap(Question<A> question);

    // Do more than one of the elements satisfy the predicate?
    boolean moreMap(Question<A> question);
}

class Cons<A> implements ILo<A> {

    A first;
    ILo<A> rest;

    Cons(A first, ILo<A> rest) {
        this.first = first;
        this.rest = rest;
    }

    public ILo<A> uniqAppend(A a) {
        if (this.first.equals(a)) return this;
        else return new Cons<A>(this.first, this.rest.uniqAppend(a));
    }

    public ILo<A> dropFirst(A a) {
        if (this.first.equals(a)) return this.rest;
        else return new Cons<A>(this.first, this.rest.dropFirst(a));
    }

    public boolean has(A a) {
        return this.first.equals(a) || this.rest.has(a);
    }

    public boolean orMap(Question<A> question) {
        return question.ask(this.first) || this.rest.orMap(question);
    }

    public boolean moreMap(Question<A> question) {
        return question.ask(this.first) && this.rest.orMap(question);
    }
}

class Mt<A> implements ILo<A> {

    public ILo<A> uniqAppend(A a) {
        return new Cons<A>(a, this);
    }

    public ILo<A> dropFirst(A a) {
        throw new Error("Could not find element to drop in list.");
    }

    public boolean has(A a) {
        return false;
    }

    public boolean orMap(Question<A> question) {
        return false;
    }

    public boolean moreMap(Question<A> question) {
        return false;
    }
}
