import {
  Component,
  OnInit,
  Input,
  forwardRef,
  Output,
  EventEmitter,
  OnChanges,
  SimpleChanges,
  ViewChild,
  ElementRef,
  HostBinding,
} from '@angular/core';

import {
  NG_VALUE_ACCESSOR,
  ControlValueAccessor,
  NG_VALIDATORS,
} from '@angular/forms';

import { IonInput, Platform } from '@ionic/angular';

import { IonicSelectableComponent } from 'ionic-selectable';
import {
  PhoneNumber,
  PhoneNumberFormat,
  PhoneNumberUtil,
} from 'google-libphonenumber';

import { CountryI } from '../models/country.model';
import { IonIntlTelInputModel } from '../models/ion-intl-tel-input.model';
import { IonIntlTelInputService } from '../ion-intl-tel-input.service';
// import { ionIntlTelInputValidator } from '../ion-intl-tel-input.directive';
import { raf } from '../util/util';

/**
 * @ignore
 */
@Component({
  // tslint:disable-next-line: component-selector
  selector: 'ion-intl-tel-input',
  templateUrl: './ion-intl-tel-input.component.html',
  styleUrls: ['./ion-intl-tel-input.component.scss'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => IonIntlTelInputComponent),
      multi: true,
    },
  ],
})

/**
 * @author Azzam Asghar <[email protected]>
 */
export class IonIntlTelInputComponent
  implements ControlValueAccessor, OnInit, OnChanges {
  @HostBinding('class.ion-intl-tel-input')
  cssClass = true;
  @HostBinding('class.ion-intl-tel-input-ios')
  isIos: boolean;
  @HostBinding('class.ion-intl-tel-input-md')
  isMD: boolean;
  @HostBinding('class.has-focus')
  hasFocus;
  @HostBinding('class.ion-intl-tel-input-has-value')
  get hasValueCssClass(): boolean {
    return this.hasValue();
  }
  @HostBinding('class.ion-intl-tel-input-is-enabled')
  @Input('isEnabled')
  get isEnabled(): boolean {
    return !this.disabled;
  }

  /**
   * Iso Code of default selected Country.
   * See more on.
   *
   * @default ''
   * @memberof IonIntlTelInputComponent
   */
  @Input()
  defaultCountryiso = '';

  /**
   * Determines whether to use `00` or `+` as dial code prefix.
   * Available attributes are '+' | '00'.
   * See more on.
   *
   * @default +
   * @memberof IonIntlTelInputComponent
   */
  @Input()
  dialCodePrefix: '+' | '00' = '+';

  /**
   * Determines whether to select automatic country based on user input.
   * See more on.
   *
   * @default false
   * @memberof IonIntlTelInputComponent
   */
  @Input()
  enableAutoCountrySelect = false;

  /**
   * Determines whether an example number will be shown as a placeholder in input.
   * See more on.
   *
   * @default true
   * @memberof IonIntlTelInputComponent
   */
  @Input()
  enablePlaceholder = true;

  /**
   * A fallaback placeholder to be used if no example number is found for a country.
   * See more on.
   *
   * @default ''
   * @memberof IonIntlTelInputComponent
   */
  @Input()
  fallbackPlaceholder = '';

  /**
   * If a custom placeholder is needed for input.
   * If this property is set it will override `enablePlaceholder` and only this placeholder will be shown.
   * See more on.
   *
   * @default ''
   * @memberof IonIntlTelInputComponent
   */
  @Input()
  inputPlaceholder = '';

  /**
   * Maximum Length for input.
   * See more on.
   *
   * @default '15'
   * @memberof IonIntlTelInputComponent
   */
  @Input()
  maxLength = '15';

  /**
   * Title of modal opened to select country dial code.
   * See more on.
   *
   * @default 'Select Country'
   * @memberof IonIntlTelInputComponent
   */
  @Input()
  modalTitle = 'Select Country';

  /**
   * CSS class to attach to dial code selectionmodal.
   * See more on.
   *
   * @default ''
   * @memberof IonIntlTelInputComponent
   */
  @Input()
  modalCssClass = '';

  /**
   * Placeholder for input in dial code selection modal.
   * See more on.
   *
   * @default 'Enter country name'
   * @memberof IonIntlTelInputComponent
   */
  @Input()
  modalSearchPlaceholder = 'Enter country name';

  /**
   * Text for close button in dial code selection modal.
   * See more on.
   *
   * @default 'Close'
   * @memberof IonIntlTelInputComponent
   */
  @Input()
  modalCloseText = 'Close';

  /**
   * Slot for close button in dial code selection modal. [Ionic slots](https://ionicframework.com/docs/api/item) are supported
   * See more on.
   *
   * @default 'end'
   * @memberof IonIntlTelInputComponent
   */
  @Input()
  modalCloseButtonSlot: 'start' | 'end' | 'primary' | 'secondary' = 'end';

  /**
   * Determines whether dial code selection modal should be searchable or not.
   * See more on.
   *
   * @default 'true'
   * @memberof IonIntlTelInputComponent
   */
  @Input()
  modalCanSearch = true;

  /**
   * Determines whether dial code selection modal is closed on backdrop click.
   * See more on.
   *
   * @default 'true'
   * @memberof IonIntlTelInputComponent
   */
  @Input()
  modalShouldBackdropClose = true;

  /**
   * Determines whether input should be focused when dial code selection modal is opened.
   * See more on.
   *
   * @default 'true'
   * @memberof IonIntlTelInputComponent
   */
  @Input()
  modalShouldFocusSearchbar = true;

  /**
   * Message to show when no countries are found for search in dial code selection modal.
   * See more on.
   *
   * @default 'true'
   * @memberof IonIntlTelInputComponent
   */
  @Input()
  modalSearchFailText = 'No countries found';

  /**
   * List of iso codes of manually selected countries as string, which will appear in the dropdown.
   * **Note**: `onlyCountries` should be a string array of country iso codes.
   * See more on.
   *
   * @default null
   * @memberof IonIntlTelInputComponent
   */
  @Input()
  onlyCountries: Array<string> = [];

  /**
   * List of iso codesn as string of  countries, which will appear at the top in dial code selection modal.
   * **Note**: `preferredCountries` should be a string array of country iso codes.
   * See more on.
   *
   * @default null
   * @memberof IonIntlTelInputComponent
   */
  @Input()
  preferredCountries: Array<string> = [];

  /**
   * Determines whether first country should be selected in dial code select or not.
   * See more on.
   *
   * @default true
   * @memberof IonIntlTelInputComponent
   */
  @Input()
  selectFirstCountry = true;

  /**
   * Determines whether to visually separate dialcode into the drop down element.
   * See more on.
   *
   * @default true
   * @memberof IonIntlTelInputComponent
   */
  @Input()
  separateDialCode = true;

  /**
   * Fires when the Phone number Input is changed.
   * See more on.
   *
   * @memberof IonIntlTelInputComponent
   */
  @Output()
  readonly numberChange = new EventEmitter<Event>();

  /**
   * Fires when the Phone number Input is blurred.
   * See more on.
   *
   * @memberof IonIntlTelInputComponent
   */
  @Output()
  readonly numberBlur = new EventEmitter<void>();

  /**
   * Fires when the Phone number Input is focused.
   * See more on.
   *
   * @memberof IonIntlTelInputComponent
   */
  @Output()
  readonly numberFocus = new EventEmitter<void>();

  /**
   * Fires when the user is typing in Phone number Input.
   * See more on.
   *
   * @memberof IonIntlTelInputComponent
   */
  @Output()
  readonly numberInput = new EventEmitter<KeyboardEvent>();

  /**
   * Fires when the dial code selection is changed.
   * See more on.
   *
   * @memberof IonIntlTelInputComponent
   */
  @Output()
  readonly codeChange = new EventEmitter<any>();

  /**
   * Fires when the dial code selection modal is opened.
   * See more on.
   *
   * @memberof IonIntlTelInputComponent
   */
  @Output()
  readonly codeOpen = new EventEmitter<any>();

  /**
   * Fires when the dial code selection modal is closed.
   * See more on.
   *
   * @memberof IonIntlTelInputComponent
   */
  @Output()
  readonly codeClose = new EventEmitter<any>();

  /**
   * Fires when a dial code is selected in dial code selection modal.
   * See more on.
   *
   * @memberof IonIntlTelInputComponent
   */
  @Output()
  readonly codeSelect = new EventEmitter<any>();

  @ViewChild('numberInput', { static: false }) numberInputEl: IonInput;

  // tslint:disable-next-line: variable-name
  private _value: IonIntlTelInputModel = null;

  country: CountryI;
  phoneNumber = '';
  countries: CountryI[] = [];
  disabled = false;
  phoneUtil: any = PhoneNumberUtil.getInstance();

  onTouched: () => void = () => {};
  propagateChange = (_: IonIntlTelInputModel | null) => {};

  constructor(
    private el: ElementRef,
    private platform: Platform,
    private ionIntlTelInputService: IonIntlTelInputService
  ) {}

  get value(): IonIntlTelInputModel | null {
    return this._value;
  }

  set value(value: IonIntlTelInputModel | null) {
    this._value = value;
    this.setIonicClasses(this.el);
  }

  emitValueChange(change: IonIntlTelInputModel | null) {
    this.propagateChange(change);
  }

  ngOnInit() {
    this.isIos = this.platform.is('ios');
    this.isMD = !this.isIos;
    this.setItemClass(this.el, 'item-interactive', true);

    this.fetchAllCountries();
    this.setPreferredCountries();

    if (this.onlyCountries.length) {
      this.countries = this.countries.filter((country: CountryI) =>
        this.onlyCountries.includes(country.isoCode)
      );
    }

    if (this.selectFirstCountry) {
      if (this.defaultCountryiso) {
        this.setCountry(this.getCountryByIsoCode(this.defaultCountryiso));
      } else {
        if (
          this.preferredCountries.length &&
          this.preferredCountries.includes(this.defaultCountryiso)
        ) {
          this.setCountry(this.getCountryByIsoCode(this.preferredCountries[0]));
        } else {
          this.setCountry(this.countries[0]);
        }
      }
    }
  }

  ngOnChanges(changes: SimpleChanges) {
    if (
      this.countries &&
      changes.defaulyCountryisoCode &&
      changes.defaulyCountryisoCode.currentValue !==
        changes.defaulyCountryisoCode.previousValue
    ) {
      this.setCountry(changes.defaulyCountryisoCode.currentValue);
    }
  }

  registerOnChange(fn: any): void {
    this.propagateChange = fn;
  }

  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }

  writeValue(obj: IonIntlTelInputModel): void {
    this.fillValues(obj);
  }

  setDisabledState(isDisabled: boolean): void {
    this.disabled = isDisabled;
  }

  fillValues(value: IonIntlTelInputModel) {
    if (
      value &&
      value !== null &&
      typeof value === 'object' &&
      !this.isNullOrWhiteSpace(value)
    ) {
      this.phoneNumber = value.nationalNumber;
      this.setCountry(this.getCountryByIsoCode(value.isoCode));
      this.value = value;
    } else if (
      this.value &&
      this.value !== null &&
      typeof this.value === 'object' &&
      !this.isNullOrWhiteSpace(this.value)
    ) {
      this.phoneNumber = this.value.nationalNumber;
      this.setCountry(this.getCountryByIsoCode(this.value.isoCode));
    }
    setTimeout(() => {
      this.onNumberChange();
    }, 1);
  }

  hasValue(): boolean {
    return !this.isNullOrWhiteSpace(this.value);
  }

  onCodeOpen() {
    this.codeOpen.emit();
  }

  onCodeChange(event: {
    component: IonicSelectableComponent;
    value: any;
  }): void {
    if (this.isNullOrWhiteSpace(this.phoneNumber)) {
      this.emitValueChange(null);
    } else {
      let googleNumber: PhoneNumber;
      try {
        googleNumber = this.phoneUtil.parse(
          this.phoneNumber,
          this.country.isoCode.toUpperCase()
        );
      } catch (e) {}

      const internationallNo = googleNumber
        ? this.phoneUtil.format(googleNumber, PhoneNumberFormat.INTERNATIONAL)
        : '';
      const nationalNo = googleNumber
        ? this.phoneUtil.format(googleNumber, PhoneNumberFormat.NATIONAL)
        : '';

      if (this.separateDialCode && internationallNo) {
        this.phoneNumber = this.removeDialCode(internationallNo);
      }

      this.emitValueChange({
        internationalNumber: internationallNo,
        nationalNumber: nationalNo,
        isoCode: this.country.isoCode,
        dialCode: this.dialCodePrefix + this.country.dialCode,
      });
      this.codeChange.emit();
    }
    setTimeout(() => {
      this.numberInputEl.setFocus();
    }, 400);
  }

  onCodeClose() {
    this.onTouched();
    this.setIonicClasses(this.el);
    this.hasFocus = false;
    this.setItemClass(this.el, 'item-has-focus', false);
    this.codeClose.emit();
  }

  public onCodeSearchCountries(event: {
    component: IonicSelectableComponent;
    text: string;
  }): void {
    const text = event.text.trim().toLowerCase();
    event.component.startSearch();

    event.component.items = this.filterCountries(text);
    event.component.endSearch();
  }

  onCodeSelect() {
    this.codeSelect.emit();
  }

  onIonNumberChange(event: Event) {
    this.setIonicClasses(this.el);
    this.numberChange.emit(event);
  }

  onIonNumberBlur() {
    this.onTouched();
    this.setIonicClasses(this.el);
    this.hasFocus = false;
    this.setItemClass(this.el, 'item-has-focus', false);
    this.numberBlur.emit();
  }

  onIonNumberFocus() {
    this.hasFocus = true;
    this.setItemClass(this.el, 'item-has-focus', true);
    this.numberFocus.emit();
  }

  onIonNumberInput(event: KeyboardEvent) {
    this.numberInput.emit(event);
  }

  onNumberChange(): void {
    if (!this.phoneNumber) {
      this.value = null;
      this.emitValueChange(null);
      return;
    }
    if (this.country) {
      this.emitValueChange({
        internationalNumber:
          this.dialCodePrefix + this.country.dialCode + ' ' + this.phoneNumber,
        nationalNumber: this.phoneNumber,
        isoCode: this.country.isoCode,
        dialCode: this.dialCodePrefix + this.country.dialCode,
      });
    }
    let googleNumber: PhoneNumber;
    try {
      googleNumber = this.phoneUtil.parse(
        this.phoneNumber,
        this.country.isoCode.toUpperCase()
      );
    } catch (e) {
      return;
    }

    let isoCode = this.country ? this.country.isoCode : null;
    // auto select country based on the extension (and areaCode if needed) (e.g select Canada if number starts with +1 416)
    if (this.enableAutoCountrySelect) {
      isoCode =
        googleNumber && googleNumber.getCountryCode()
          ? this.getCountryIsoCode(googleNumber.getCountryCode(), googleNumber)
          : this.country.isoCode;
      if (isoCode && isoCode !== this.country.isoCode) {
        const newCountry = this.countries.find(
          (country: CountryI) => country.isoCode === isoCode
        );
        if (newCountry) {
          this.country = newCountry;
        }
      }
    }
    isoCode = isoCode ? isoCode : this.country ? this.country.isoCode : null;

    if (!this.phoneNumber || !isoCode) {
      this.emitValueChange(null);
    } else {
      const internationallNo = googleNumber
        ? this.phoneUtil.format(googleNumber, PhoneNumberFormat.INTERNATIONAL)
        : '';
      const nationalNo = googleNumber
        ? this.phoneUtil.format(googleNumber, PhoneNumberFormat.NATIONAL)
        : '';

      if (this.separateDialCode && internationallNo) {
        this.phoneNumber = this.removeDialCode(internationallNo);
      }

      this.emitValueChange({
        internationalNumber: internationallNo,
        nationalNumber: nationalNo,
        isoCode: this.country.isoCode,
        dialCode: this.dialCodePrefix + this.country.dialCode,
      });
    }
  }

  onNumberKeyDown(event: KeyboardEvent) {
    const allowedChars = /^[0-9\+\-\ ]/;
    const allowedCtrlChars = /[axcv]/;
    const allowedOtherKeys = [
      'ArrowLeft',
      'ArrowUp',
      'ArrowRight',
      'ArrowDown',
      'Home',
      'End',
      'Insert',
      'Delete',
      'Backspace',
    ];

    if (
      !allowedChars.test(event.key) &&
      !(event.ctrlKey && allowedCtrlChars.test(event.key)) &&
      !allowedOtherKeys.includes(event.key)
    ) {
      event.preventDefault();
    }
  }

  private filterCountries(text: string): CountryI[] {
    return this.countries.filter((country) => {
      return (
        country.name.toLowerCase().indexOf(text) !== -1 ||
        country.name.toLowerCase().indexOf(text) !== -1 ||
        country.dialCode.toString().toLowerCase().indexOf(text) !== -1
      );
    });
  }

  private getCountryIsoCode(
    countryCode: number,
    googleNumber: PhoneNumber
  ): string | undefined {
    const rawNumber = (googleNumber as any).values_[2].toString();

    const countries = this.countries.filter(
      (country: CountryI) => country.dialCode === countryCode.toString()
    );
    const mainCountry = countries.find(
      (country: CountryI) => country.areaCodes === undefined
    );
    const secondaryCountries = countries.filter(
      (country: CountryI) => country.areaCodes !== undefined
    );

    let matchedCountry = mainCountry ? mainCountry.isoCode : undefined;

    secondaryCountries.forEach((country) => {
      country.areaCodes.forEach((areaCode) => {
        if (rawNumber.startsWith(areaCode)) {
          matchedCountry = country.isoCode;
        }
      });
    });
    return matchedCountry;
  }

  private fetchAllCountries() {
    this.countries = this.ionIntlTelInputService.getListOfCountries();
  }

  private getCountryByIsoCode(isoCode: string): CountryI {
    for (const country of this.countries) {
      if (country.isoCode === isoCode) {
        return country;
      }
    }
    return;
  }

  private isNullOrWhiteSpace(value: any): boolean {
    if (value === null || value === undefined) {
      return true;
    }
    if (typeof value === 'string' && value === '') {
      return true;
    }
    if (typeof value === 'object' && Object.keys(value).length === 0) {
      return true;
    }
    return false;
  }

  private removeDialCode(phoneNumber: string): string {
    if (this.separateDialCode && phoneNumber) {
      phoneNumber = phoneNumber.substr(phoneNumber.indexOf(' ') + 1);
    }
    return phoneNumber;
  }

  private setCountry(country: CountryI): void {
    this.country = country;
    this.codeChange.emit(this.country);
  }

  private setPreferredCountries(): void {
    for (const preferedCountryIsoCode of this.preferredCountries) {
      const country = this.getCountryByIsoCode(preferedCountryIsoCode);
      country.priority = country ? 1 : country.priority;
    }
    this.countries.sort((a, b) =>
      a.priority > b.priority ? -1 : a.priority < b.priority ? 1 : 0
    );
  }

  private startsWith = (input: string, search: string): boolean => {
    return input.substr(0, search.length) === search;
  };

  private getClasses = (element: HTMLElement) => {
    const classList = element.classList;
    const classes = [];
    for (let i = 0; i < classList.length; i++) {
      const item = classList.item(i);
      if (item !== null && this.startsWith(item, 'ng-')) {
        classes.push(`ion-${item.substr(3)}`);
      }
    }
    return classes;
  };

  private setClasses = (element: HTMLElement, classes: string[]) => {
    const classList = element.classList;
    [
      'ion-valid',
      'ion-invalid',
      'ion-touched',
      'ion-untouched',
      'ion-dirty',
      'ion-pristine',
    ].forEach((c) => classList.remove(c));

    classes.forEach((c) => classList.add(c));
  };

  private setIonicClasses = (element: ElementRef) => {
    raf(() => {
      const input = element.nativeElement as HTMLElement;
      const classes = this.getClasses(input);
      this.setClasses(input, classes);

      const item = input.closest('ion-item');
      if (item) {
        this.setClasses(item, classes);
      }
    });
  };

  private setItemClass = (
    element: ElementRef,
    className: string,
    addClass: boolean
  ) => {
    const input = element.nativeElement as HTMLElement;
    const item = input.closest('ion-item');
    if (item) {
      const classList = item.classList;
      if (addClass) {
        classList.add(className);
      } else {
        classList.remove(className);
      }
    }
  };
}