import { Observable, Subscription } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators';
import { Store, select } from '@ngrx/store';
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
import { GlobalState } from '@app/core/state';
import * as utils from '@app/degree-planner/shared/utils';
import * as selectors from '@app/degree-planner/store/selectors';
import { DegreePlannerApiService } from '@app/degree-planner/services/api.service';
import { FormBuilder, FormGroup, AbstractControl } from '@angular/forms';
import { MatSnackBar } from '@angular/material';
import {
  Course,
  SubjectCodesTo,
  SubjectDescription,
} from '@app/core/models/course';
import { TermCode } from '@app/degree-planner/shared/term-codes/termcode';
import { MediaMatcher } from '@angular/cdk/layout';
import { ConstantsService } from '../services/constants.service';
import { TermCodeFactory } from '../services/termcode.factory';

@Component({
  selector: 'cse-course-search',
  templateUrl: './course-search.component.html',
  styleUrls: ['./course-search.component.scss'],
})
export class CourseSearchComponent implements OnInit, OnDestroy {
  @ViewChild('subject') subjectInput;

  // Internal variables used to store search info
  public queriedCourses: Course[];
  public hasResults: boolean;
  public isLoading: boolean;
  public courseSearchForm: FormGroup;

  // Observable used for drag and drop and for populating term select
  public dropZoneIds$: Observable<string[]>;
  public activeTerms: ReadonlyArray<TermCode>;
  public activeSelectedSearchTerm$: Observable<TermCode | undefined>;
  public isCourseSearchOpen$: Observable<boolean>;

  // Internal variables to store subjects and filtered subjects list
  public subjects: string[];
  public subjectMap: SubjectCodesTo<SubjectDescription>;
  public filteredSubjects: string[];

  public termSubscription: Subscription;
  public searchOpenSubscribe: Subscription;
  public mobileView: MediaQueryList;

  private shouldClearSubject: boolean;

  constructor(
    private store: Store<GlobalState>,
    private fb: FormBuilder,
    private api: DegreePlannerApiService,
    private snackBar: MatSnackBar,
    public termCodeFactory: TermCodeFactory,
    mediaMatcher: MediaMatcher,
    public constants: ConstantsService,
  ) {
    this.mobileView = mediaMatcher.matchMedia('(max-width: 900px)');
    this.shouldClearSubject = true;
  }

  public ngOnInit(): void {
    // Internal values used to manage loading state
    this.queriedCourses = [];
    this.hasResults = false;
    this.isLoading = false;

    this.activeTerms = this.termCodeFactory.active;

    // Get active term drop zones
    this.dropZoneIds$ = this.store.pipe(
      select(selectors.selectAllVisibleYears),
      utils.yearsToDropZoneIds(),
      distinctUntilChanged(utils.compareStringArrays),
    );

    // Get observable for the search open state
    this.isCourseSearchOpen$ = this.store.pipe(
      select(selectors.isCourseSearchOpen),
    );

    // Reset serach form when closed
    this.searchOpenSubscribe = this.isCourseSearchOpen$.subscribe(openState => {
      if (!openState) {
        this.resetSearchForm();
      }
    });

    // Get all subjects and set them to internal values
    const subjectDescriptions = this.constants.allSubjectDescriptions();
    this.subjectMap = {
      '-1': { short: 'All', long: 'All' },
      ...subjectDescriptions,
    };
    const ordered = Object.values(subjectDescriptions)
      .map(v => v.long)
      .sort();
    this.subjects = ['all', ...ordered];
    this.filteredSubjects = ['all', ...ordered];

    // Deafults for the search form
    this.courseSearchForm = this.fb.group({
      term: '0000',
      subject: 'All',
      search: '',
    });

    // TODO Unsubscribe from this on component disconnect
    // Get observable for currently active term
    this.activeSelectedSearchTerm$ = this.store.pipe(
      select(selectors.getActiveSelectedSearchTerm),
    );

    // Listen for changes on the active selected term and update the form when it changes
    this.termSubscription = this.activeSelectedSearchTerm$.subscribe(
      termCode => {
        const values = this.courseSearchForm.value;
        this.courseSearchForm.setValue({
          ...values,
          term: termCode ? termCode.toString() : '0000',
        });
      },
    );

    // Listen for changes to the term value
    (this.courseSearchForm.get(
      'term',
    ) as AbstractControl).valueChanges.subscribe((term: string) => {
      if (this.hasResults) {
        const { search, subject } = this.courseSearchForm.value;
        this.search(search, term, subject);
      }
    });

    // Listen for changes in the subject input
    (this.courseSearchForm.get(
      'subject',
    ) as AbstractControl).valueChanges.subscribe(value => {
      // If the subject value is blank, or if the user is typing "all"
      if (/^(a[l]{0,2})?$/i.test(value.trim())) {
        this.filteredSubjects = this.subjects;
        return;
      }

      this.filteredSubjects = this.subjects.filter(subject => {
        // Make both comparisons lowercase and remove all whitespace
        const formattedSubject = subject.replace(/\s/g, '').toLowerCase();
        const formattedValue = value.replace(/\s/g, '').toLowerCase();
        if (formattedSubject.indexOf(formattedValue) === 0) {
          return true;
        }
        return false;
      });
    });
  }

  // Unsubscribe from subs to prevent memeory leaks
  public ngOnDestroy() {
    this.termSubscription.unsubscribe();
    this.searchOpenSubscribe.unsubscribe();
  }

  public subjectChange($event, input) {
    const { search, term, subject } = this.courseSearchForm.value;
    // blur the element so that the field shows the first characters of long options
    input.blur();

    this.search(search, term, subject);
    this.shouldClearSubject = true;
  }

  // Get a subject code from text
  public getSubjectCode(text: string): string | undefined {
    const pair = Object.entries(this.subjectMap).find(([_, value]) => {
      return value.long.toLowerCase() === text.toLowerCase();
    });

    if (pair === undefined) {
      return undefined;
    } else {
      return pair[0];
    }
  }

  // Function that runs on form submit
  // Get form values and run a search
  public formSubmit() {
    const { search, term, subject } = this.courseSearchForm.value;
    this.search(search, term, subject);
  }

  // Run a search and display all results if the serach is valid
  public search(search, term, subject) {
    // Check if subject is valid
    const subjectCode = this.getSubjectCode(subject);

    if (!subjectCode) {
      this.courseSearchForm.controls['subject'].setErrors({ invalid: true });
      return;
    }

    // Set the internal UI state
    this.isLoading = true;
    this.hasResults = false;
    this.queriedCourses = [];

    // Hit the search API
    this.api
      .searchCourses({
        subjectCode,
        searchText: search,
        termCode: term === '' ? '0000' : term,
      })
      .toPromise()
      .then(res => {
        // TODO add error handeling

        // Update the internal state
        this.hasResults = true;
        this.isLoading = false;

        // Map out the results and update the course object to match the needed structure
        this.queriedCourses = res.hits.map(course => {
          // FIXME
          return ({
            ...course,
            subjectCode: course.subject.subjectCode,
          } as unknown) as Course;
        });
      })
      .catch(console.log.bind(console));
  }

  // Reset the search form values and clear results
  public resetSearchForm() {
    this.queriedCourses = [];
    this.hasResults = false;

    // Ensure that the search form has been created before attempting to set values
    if (this.courseSearchForm) {
      this.courseSearchForm.setValue({
        subject: 'All',
        search: '',
        term: this.courseSearchForm.value.term,
      });
    }
  }

  public clearSubject() {
    if (!this.shouldClearSubject) {
      return;
    }

    const { term, search } = this.courseSearchForm.value;
    this.shouldClearSubject = false;
    this.courseSearchForm.setValue({
      subject: '',
      search,
      term,
    });
  }
}