<script setup lang="ts">
import {
  ClassroomComplianceAPIClient,
  getDefaultEntry,
  type EntriesPayload,
  type EntriesPayloadStudent,
  type EntriesResponseStudent,
  type Entry,
} from '@/api/classroom_compliance'
import { useAuthStore } from '@/stores/auth'
import { computed, onMounted, ref, watch, type Ref } from 'vue'
import EntryTableCell from '@/apps/classroom_compliance/entries_table/EntryTableCell.vue'
import StudentScoreCell from '@/apps/classroom_compliance/entries_table/StudentScoreCell.vue'
import DateHeaderCell from '@/apps/classroom_compliance/entries_table/DateHeaderCell.vue'
import StudentNameCell from '@/apps/classroom_compliance/entries_table/StudentNameCell.vue'

const authStore = useAuthStore()
const props = defineProps<{
  classId: number,
  disabled: boolean
}>()
const apiClient = new ClassroomComplianceAPIClient(authStore)

enum TableView {
  FULL = "Full",
  GRADING = "Grading",
  WHITEBOARD = "Whiteboard",
  TODAY = "Today"
}

const students: Ref<EntriesResponseStudent[]> = ref([])
const sortingChoice: Ref<string> = ref('name')
const selectedView: Ref<TableView> = ref(TableView.FULL)
const hideRemovedStudents: Ref<boolean> = ref(false)

const lastSaveState: Ref<string | null> = ref(null)
const lastSaveStateTimestamp: Ref<number> = ref(0)

const dates: Ref<string[]> = ref([])
const toDate: Ref<Date> = ref(new Date())
const fromDate: Ref<Date> = ref(new Date())

const entriesChangedSinceLastSave = computed(() => {
  const studentsClone: EntriesResponseStudent[] = JSON.parse(JSON.stringify(students.value))
  studentsClone.sort(sortEntriesByName)
  return lastSaveState.value === null || lastSaveState.value !== JSON.stringify(studentsClone)
})
const assignedDesks = computed(() => {
  return students.value.length > 0 && students.value.some(s => s.deskNumber > 0)
})

onMounted(async () => {
  showThisWeek()
  watch(sortingChoice, () => {
    if (sortingChoice.value === 'name') {
      students.value.sort(sortEntriesByName)
    } else if (sortingChoice.value === 'desk') {
      students.value.sort(sortEntriesByDeskNumber)
    }
    saveSortPreference()
  })
  // If the user selects "Today View", forcibly load the current week, so they'll only see today.
  watch(selectedView, () => {
    if (selectedView.value === TableView.TODAY) {
      showThisWeek()
    }
  })
  attemptSortByStoredPreference()
})

async function loadEntries() {
  const entries = await apiClient
    .getEntries(props.classId, fromDate.value, toDate.value)
    .handleErrorsWithAlert()
  if (entries) {
    lastSaveState.value = JSON.stringify(entries.students)
    lastSaveStateTimestamp.value = Date.now()
    if (sortingChoice.value === 'name') {
      entries.students.sort(sortEntriesByName)
    } else if (sortingChoice.value === 'desk') {
      entries.students.sort(sortEntriesByDeskNumber)
    }
    students.value = entries.students
    dates.value = entries.dates
  } else {
    students.value = []
    lastSaveState.value = null
    lastSaveStateTimestamp.value = Date.now()
    dates.value = []
  }
}

function sortEntriesByName(a: EntriesResponseStudent, b: EntriesResponseStudent): number {
  if (a.name < b.name) return -1
  if (a.name > b.name) return 1
  return 0
}

function sortEntriesByDeskNumber(a: EntriesResponseStudent, b: EntriesResponseStudent): number {
  if (a.deskNumber < b.deskNumber) return -1
  if (a.deskNumber > b.deskNumber) return 1
  return sortEntriesByName(a, b)
}

function attemptSortByStoredPreference() {
  const storedPreferences = localStorage.getItem(`entries-table.${props.classId}.preferences`)
  if (storedPreferences !== null) {
    const preferences: Record<string, string> = JSON.parse(storedPreferences)
    if ('sort' in preferences) {
      sortingChoice.value = preferences['sort']
    }
  }
}

function saveSortPreference() {
  const preferences = {
    'sort': sortingChoice.value
  }
  localStorage.setItem(`entries-table.${props.classId}.preferences`, JSON.stringify(preferences))
}

function shiftDateRange(days: number) {
  toDate.value.setDate(toDate.value.getDate() + days)
  fromDate.value.setDate(fromDate.value.getDate() + days)
}

async function showPreviousWeek() {
  shiftDateRange(-7)
  await loadEntries()
}

async function showThisWeek() {
  const today = new Date()
  today.setHours(0, 0, 0, 0)
  // First set the to-date to the next upcoming end-of-week (Friday).
  toDate.value = new Date()
  toDate.value.setHours(0, 0, 0, 0)
  if (today.getDay() >= 1 && today.getDay() <= 5) {
    // If we're anywhere in the week, shift up to Friday.
    const dayDiff = 5 - today.getDay()
    toDate.value.setDate(today.getDate() + dayDiff)
  } else {
    // If it's Saturday or Sunday, shift back to the previous Friday.
    if (today.getDay() === 6) {
      toDate.value.setDate(today.getDate() - 1)
    } else {
      toDate.value.setDate(today.getDate() - 2)
    }
  }

  // Then set the from-date to the Monday of that week.
  fromDate.value = new Date()
  fromDate.value.setHours(0, 0, 0, 0)
  fromDate.value.setDate(toDate.value.getDate() - 4)
  await loadEntries()
}

async function showNextWeek() {
  shiftDateRange(7)
  await loadEntries()
}

async function saveEdits() {
  if (props.disabled) {
    console.warn('Cannot save edits when disabled.')
    await loadEntries()
    return
  }
  if (!lastSaveState.value) {
    console.warn('No lastSaveState, cannot determine what edits were made.')
    await loadEntries()
    return
  }

  const payload: EntriesPayload = { students: [] }
  // Get a list of edits which have changed.
  const lastSaveStateObj: EntriesResponseStudent[] = JSON.parse(lastSaveState.value)
  for (let i = 0; i < students.value.length; i++) {
    const student: EntriesResponseStudent = students.value[i]
    const studentPayload: EntriesPayloadStudent = { id: student.id, entries: {} }
    for (const [dateStr, entry] of Object.entries(student.entries)) {
      const lastSaveStateEntry = lastSaveStateObj[i].entries[dateStr]
      if (JSON.stringify(lastSaveStateEntry) !== JSON.stringify(entry)) {
        studentPayload.entries[dateStr] = JSON.parse(JSON.stringify(entry))
      }
    }
    if (Object.keys(studentPayload.entries).length > 0) {
      payload.students.push(studentPayload)
    }
  }
  await apiClient.saveEntries(props.classId, payload).handleErrorsWithAlert()
  await loadEntries()
}

async function discardEdits() {
  if (lastSaveState.value) {
    students.value = JSON.parse(lastSaveState.value)
    // The user's preferred sorting might have changed while editing, so we should preserve that.
    if (sortingChoice.value === 'name') {
      students.value.sort(sortEntriesByName)
    } else if (sortingChoice.value === 'desk') {
      students.value.sort(sortEntriesByDeskNumber)
    }
  } else {
    await loadEntries()
  }
}

function getTodayISOStr(): string {
  const today = new Date()
  today.setUTCHours(0, 0, 0, 0)
  return today.toISOString().substring(0, 10)
}

function getVisibleDates(): string[] {
  if (selectedView.value === TableView.FULL) return dates.value
  if (selectedView.value === TableView.TODAY) {
    const todayStr = getTodayISOStr()
    for (const date of dates.value) {
      if (date === todayStr) return [date]
    }
  }
  return []
}

function getVisibleStudents(): EntriesResponseStudent[] {
  if (hideRemovedStudents.value === true) {
    return students.value.filter(s => !s.removed && s.classId === props.classId)
  }
  return students.value
}

function getVisibleStudentEntries(student: EntriesResponseStudent): Record<string, Entry | null> {
  if (selectedView.value === TableView.FULL) return student.entries
  if (selectedView.value === TableView.TODAY) {
    // Only show today's date, if we have it, or null otherwise.
    const todayStr = getTodayISOStr()
    const obj: Record<string, Entry | null> = {}
    obj[todayStr] = student.entries[todayStr]
    return obj
  }
  return {}
}

function addAllEntriesForDate(dateStr: string) {
  if (props.disabled) return
  for (let i = 0; i < students.value.length; i++) {
    const student = students.value[i]
    if (student.removed) continue
    if (!(dateStr in student.entries) || student.entries[dateStr] === null) {
      student.entries[dateStr] = getDefaultEntry(dateStr)
    }
  }
}

function isWhiteboardView() {
  return selectedView.value === TableView.WHITEBOARD
}

defineExpose({
  loadEntries,
  isWhiteboardView
})
</script>
<template>
  <div>
    <div class="button-bar" style="text-align: left;">
      <button type="button" @click="showPreviousWeek" :disabled="selectedView === TableView.TODAY">Previous
        Week</button>
      <button type="button" @click="showThisWeek" :disabled="selectedView === TableView.TODAY">This Week</button>
      <button type="button" @click="showNextWeek" :disabled="selectedView === TableView.TODAY">Next Week</button>
      <button type="button" @click="saveEdits" :disabled="!entriesChangedSinceLastSave">
        Save
      </button>
      <button type="button" @click="discardEdits" :disabled="!entriesChangedSinceLastSave">
        Discard Edits
      </button>

      <select style="margin-left: 0.5em;" v-model="sortingChoice">
        <option value="name">Sort by Name</option>
        <option value="desk">Sort by Desk</option>
      </select>

      <select v-model="selectedView">
        <option :value="TableView.FULL">Full View</option>
        <option :value="TableView.GRADING">Grading View</option>
        <option :value="TableView.WHITEBOARD">Whiteboard View</option>
        <option :value="TableView.TODAY">Today View</option>
      </select>

      <span>
        <input type="checkbox" id="show-removed-students-checkbox" v-model="hideRemovedStudents" />
        <label for="show-removed-students-checkbox" style="display: inline; font-size: small;">Hide Removed
          Students</label>
      </span>
    </div>

    <p v-if="selectedView === TableView.GRADING">
      Grading for the week from {{ fromDate.toLocaleDateString() }} to {{ toDate.toLocaleDateString() }}.
    </p>

    <table class="entries-table" :class="{ 'entries-table-reduced-view': selectedView !== TableView.FULL }">
      <thead>
        <tr class="align-center">
          <th v-if="selectedView !== TableView.WHITEBOARD">#</th>
          <th>Student</th>
          <th v-if="assignedDesks">Desk</th>
          <DateHeaderCell v-for="date in getVisibleDates()" :key="date" :date-str="date" :disabled="disabled"
            @add-all-entries-clicked="addAllEntriesForDate(date)" />
          <th v-if="selectedView !== TableView.WHITEBOARD">Score</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="(student, idx) in getVisibleStudents()" :key="student.id" style="height: 2em;">
          <td v-if="selectedView !== TableView.WHITEBOARD" style="text-align: right; padding-right: 0.5em;">{{ idx + 1
          }}.</td>
          <StudentNameCell :student="student" :class-id="classId"
            :show-labels="selectedView !== TableView.WHITEBOARD" />
          <td v-if="assignedDesks" v-text="student.deskNumber"></td>
          <EntryTableCell v-for="(entry, date) in getVisibleStudentEntries(student)" :key="date"
            v-model="student.entries[date]" :date-str="date" :last-save-state-timestamp="lastSaveStateTimestamp"
            :disabled="disabled" :student="student" />
          <StudentScoreCell :score="student.score" v-if="selectedView !== TableView.WHITEBOARD" />
        </tr>
      </tbody>
    </table>
  </div>
</template>
<style scoped>
.entries-table {
  margin-top: 0.5em;
  margin-bottom: 1em;
  width: 100%;
}

.entries-table-reduced-view {
  width: auto;
  max-width: 100%;
}

.entries-table,
.entries-table th,
.entries-table td {
  border: 1px solid black;
  border-collapse: collapse;
}
</style>
