import {Component, Input, OnChanges, OnInit, ViewChild} from '@angular/core';
import {CompositeFilterDescriptor, FilterDescriptor} from '@progress/kendo-data-query';
import {BaseFilterCellComponent, FilterService} from '@progress/kendo-angular-grid';
import {trim} from '@progress/kendo-angular-editor/dist/es2015/util';
import {debounce} from 'ts-debounce';
import {TextBoxComponent} from '@progress/kendo-angular-inputs';



class Operator { op: string; value: string; description: string; }

@Component({
  selector: 'app-number-range-filter',
  templateUrl: './number-range-filter.component.html',
  styleUrls: ['./number-range-filter.component.scss']
})
export class NumberRangeFilterComponent extends BaseFilterCellComponent implements OnInit, OnChanges {

  constructor(filterService: FilterService) { super(filterService); }

  @Input() public filter: CompositeFilterDescriptor;
  @Input() public min = -1;
  @Input() public max = -1;
  @Input() public required = false;
  @Input() public fieldName = '';
  @Input() public options = ['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'range'];
  // ngModel control
  @ViewChild('single') ctlSingle: TextBoxComponent;
  @ViewChild('rangeFrom') ctlRangeFrom: TextBoxComponent;
  @ViewChild('rangeTo') ctlRangeTo: TextBoxComponent;

  public operator: Operator = {op: '=', value: 'eq', description: 'equal'};
  private defaultOperator: Operator = {op: '=', value: 'eq', description: 'equal'};
  public availableOperators: Operator[] = [];
  private AllOperatorMap = [
    {op: '=', value: 'eq', description: 'equal to'},
    {op: '≠', value: 'neq',  description: 'not equal to '},
    {op: '>', value: 'gt',  description: 'greater than'},
    {op: '≥', value: 'gte',  description: 'greater than or equal to'},
    {op: '<', value: 'lt',  description: 'less than'},
    {op: '≤', value: 'lte',  description: 'less than or equal to'},
    {op: '⇩', value: 'range',  description: 'range'}
  ];

  public singleMode = true;

  public valueFrom = '';
  public valueTo = '';
  public valueFromValid = true;
  public valueToValid = true;
  public fromTouched = false;
  public toTouched = false;
  public currentFocus = '';

  public showOperatorChoice = true;
  public showError = false;

  public validateResult = new Map<string, string>();
  public errorMessage = '';

  private debouncedSingleFrom = debounce(this.valueFromChanged, 500);
  private debouncedRangeFrom = debounce(this.rangeFromChanged, 500);
  private debouncedUpdateTo = debounce(this.rangeToChanged, 500);

  private isEmpty(v: string): boolean{
    return trim(v) === '';
  }

  private isNumber(v: string): boolean{
    const vf = Number(v);
    return  v !== null && ! isNaN(vf) ;
  }

  ngOnInit(): void {
    this.initAvailableOperators();
    this.showOperatorChoice = this.availableOperators.length > 1 ||
      ( this.availableOperators[0].op !== '=' && this.availableOperators[0].op !== ' ');
  }

  ngOnChanges(changes): void {
    if ( changes.options ){
      this.initAvailableOperators();
    }
  }

  private initAvailableOperators(): void {
    this.availableOperators = [];
    const self = this;
    this.options.forEach( v => this.availableOperators.push(self.getOp(v)) );

    if ( this.availableOperators.length <= 0) {
      this.availableOperators = [this.defaultOperator];
      this.operator = this.defaultOperator;
    }else{
      this.operator = this.availableOperators[0];
    }
    this.singleMode = this.operator.value !== 'range';
  }

  private getOp(key: string): Operator {
    let ret: Operator;
    this.AllOperatorMap.every( v =>  {
      if ( v.value === key ) {
        ret = v;
      }
      return  v.value !== key;
    });
    return ret;
  }

  //
  // Events handling
  //

  public onOperatorClick(op: Operator): void {
    if ( this.operator === op ) { // same op
      return;
    }
    this.operator = op;
    this.singleMode = op.value !== 'range';

    if (!this.toTouched && ! this.fromTouched) {
      return ;
    }

    this.clearError(); // start validation
    if ( this.singleMode ){
      this.valueTo = ''; // clear to
      if (this.fromTouched ){
        this.onChangeFrom(this.valueFrom);
      }
    }else if (!this.singleMode) {
      if (this.fromTouched ){
        this.validateFromAsRange(this.valueFrom);
      }
      if (this.toTouched ){
        this.validateTo(this.valueTo);
      }
    }

    this.showError = this.buildErrorMessage();

    if ( ! this.showError ){
      this.buildFilter();
    }
  }

  public onChangeFrom(v: string): void {
    this.clearError();
    this.valueFromValid = this.validateFrom(v);
    if ( this.singleMode ){
      this.debouncedSingleFrom(v).then();
    }else{
      this.debouncedRangeFrom(v).then();
    }
    this.showError = this.buildErrorMessage();
  }

  public onFromTouched(): void {
    this.fromTouched = true;
    this.currentFocus = 'from';
  }

  public onToTouched(): void {
    this.toTouched = false;
    this.currentFocus = 'to';
  }

  public onChangeTo(v: string): void {
    this.clearError();
    this.valueToValid = this.validateTo(v);
    this.debouncedUpdateTo(v).then();
    this.showError = this.buildErrorMessage();
  }

  //
  // debounced function
  //

  private valueFromChanged(v: string): void{
    if ( this.validateResult.size === 0 ) {
      this.buildFilter();
      return;
    }
    // remove filter
    this.clearOurFilter();
  }

  private rangeFromChanged(v): void{
    if ( this.validateResult.size === 0 ) {
      this.buildFilter();
      return;
    }
    // remove filter
    this.clearOurFilter();
  }

  private rangeToChanged(v: string): void {
    if ( this.validateResult.size === 0 ) {
      this.buildFilter();
      return;
    }
    // remove filter
    this.clearOurFilter();

    if ( this.valueFromValid && this.valueToValid) {
      this.buildFilter();
    }else{
      if (this.fieldName && trim(this.fieldName) !== ''){
        const f = this.removeFilter(this.fieldName);
        this.applyFilter(f);
      }
    }
  }


  private clearOurFilter(): void {
    if (this.fieldName && trim(this.fieldName) !== ''){
      const f = this.removeFilter(this.fieldName);
      this.applyFilter(f);
    }
  }


  protected buildFilter(): void {
    if (this.fieldName === '') {
      console.warn('filed name is not specified, skip update filter', this);
      return;
    }

    if (this.isEmpty(this.valueFrom)){
      this.applyFilter(this.removeFilter(this.fieldName));
      return;
    }

    if (this.singleMode) {
      this.buildSingleFilter();
    } else {
      this.buildRangeFilter();
    }

  }

  private buildSingleFilter(): void {
    if ( this.valueFrom === null ) { return; }
    const filter: FilterDescriptor = {
      field: this.fieldName,
      operator: this.operator.value,
      value: Number(this.valueFrom),
    };
    this.applyFilter(this.updateFilter(filter));
  }

  private buildRangeFilter(): void {
    const fs: FilterDescriptor[] = [];
    this.valueFromValid = this.validateFromAsRange(this.valueFrom);
    this.valueToValid = this.validateTo(this.valueTo);
    if ( this.valueFromValid && this.valueFrom !== null && this.valueFrom !== '') {
      fs.push({
        field: this.fieldName,
        operator: 'gte',
        value: Number(this.valueFrom)
      });
    }

    if ( this.valueToValid  && this.valueTo !== null && this.valueTo !== '') {
      fs.push({
        field: this.fieldName,
        operator: 'lte',
        value: Number(this.valueTo)
      });
    }
    this.removeFilter(this.fieldName);
    const root: CompositeFilterDescriptor = this.filter || { logic: 'and',
      filters: [],
    };

    if (fs.length) {
      root.filters.push(...fs);
    }
    this.filterService.filter(root);
  }

  private validateCommon(v: string, field?: string ): boolean {
    const f = field || '';
    if ( v === null ){
      this.validateResult.set(f + 'null', ' value is null');
      return false;
    }

    if (! this.isNumber(v) ) {
      this.validateResult.set(f + 'number', 'should be number');
      return false;
    }

    return this.isWithinRange(v, field);

  }

  private validateFrom(v: string): boolean {
    if ( this.singleMode ) {
      return this.validateFromAsSingle(v);
    } else {
      return this.validateFromAsRange(v);
    }
  }

  private validateFromAsSingle(v: string): boolean {
    if ( ! this.validateCommon(v) ) {
      return false;
    }

    if (this.required && this.isEmpty(v) ) {
      this.validateResult.set('required', 'value is required');
      return false;
    }
    return true;
  }

  private validateFromAsRange(v: string): boolean {
    if ( ! this.validateCommon(v, 'from_') ){
      return false;
    }

    if ( !this.isFromLessThanTo() ){
      return false;
    }
    return true;
  }

  private validateTo(v: string): boolean{
    if ( ! this.validateCommon(v, 'to_') ) {
      return false;
    }

    if ( ! this.isFromLessThanTo() ) {
      return false;
    }

    return true;
  }

  private isFromLessThanTo(): boolean{
    if (this.isEmpty( this.valueFrom) || this.isEmpty(this.valueTo)){
      return true; // if one of them is empty, we dont care about who is bigger
    }

    if ( ! (Number(this.valueFrom) <= Number(this.valueTo)) ){
      this.validateResult.set('min>max', 'min must ≤ max');
      return false;
    }
    return true;
  }

  private buildErrorMessage(): boolean {
    if (this.singleMode) {
      return this.buildSingleError();
    } else {
      return this.buildRangeError();
    }
  }

  private clearError(): void {
    this.showError = false;
    this.validateResult.clear();
    this.errorMessage = '' ;
  }

  private buildSingleError(): boolean {

    if ( this.validateResult.has('null') ){
      this.errorMessage = 'can not be null';
      return true;
    }

    if (this.validateResult.has('required')){
      this.errorMessage = 'value is required' + this.hint();
      return true;
    }

    if (this.validateResult.has('number')){
      this.errorMessage = 'should be a number';
      return true;
    }

    if (this.validateResult.has('min')){
      this.errorMessage = 'should  ≥ ' + this.min;
      return true;
    }

    if (this.validateResult.has('max')){

      this.errorMessage = 'should  ≤ ' + this.max;
      return true;
    }

    const touched = this.fromTouched || this.toTouched; // this.ctlRangeFrom
    this.showError =  this.errorMessage !== '' && ! touched;
    return this.showError;
  }

  private buildRangeError(): boolean {

    if ( this.validateResult.has('from_null') && this.validateResult.has('to_null') ){
      this.errorMessage = 'can not be null';
      return true;
    }

    if (this.validateResult.has('from_number') || this.validateResult.has( 'to_number')) {
      this.errorMessage = 'should be a number';
      return true;
    }

    if (this.validateResult.has('from_min') || this.validateResult.has('to_min')) {
      this.errorMessage = 'should  ≥ ' + this.min;
      return true;
    }

    if (this.validateResult.has('from_max') || this.validateResult.has('to_max')){
      this.errorMessage = 'should  ≤ ' + this.max;
      return true;
    }

    if (this.validateResult.has('min>max')){
      if (this.currentFocus === 'to'){
        this.errorMessage = 'should ≥ ' + this.valueFrom;
      }else{
        this.errorMessage = 'should ≤ ' + this.valueTo;
      }

      return true;
    }

    return false;
  }

  private hint(): string{
    let hint = '';

    if ( this.min !== -1  && this.max === -1) {
      hint = ' > ' + this.min + '  ';
    }

    if (this.max !== -1 && this.min === -1 ) {
      hint = ' < ' + this.max + ' ';
    }

    if (this.min !== -1 && this.max !== -1) {
      hint = this.min + '  -  ' + this.max;
    }

    if ( hint !== '' ){
      hint = ' (' + hint + ') ';
    }
    return hint;
  }


  private isWithinRange(v: string, field?: string): boolean{
    const f = field || '';
    if ( this.isEmpty(v) ){
      if (this.singleMode && this.required){
        this.validateResult.set(f + 'required', 'must input something');
        return false;
      }
      return true;
    }

    const vf = Number(v);
    if (! this.isNumber(v)) {
      this.validateResult.set(f + 'number', 'must be a number');
      return false;
    }

    if ( this.min !== -1 && vf < this.min ){
      this.validateResult.set(f + 'min', 'cannot be lower than min');
      return false;
    }

    if (this.max !== -1 && vf > this.max ) {
      this.validateResult.set(f + 'max', 'cannot be greater than max');
      return false;
    }

    // if we reached here
    return true;
  }


}
