import { ChangeDetectorRef, AfterContentChecked, AfterViewInit, AfterViewChecked, Component, ElementRef, Inject, Input, NgZone, OnChanges, OnDestroy, OnInit, DoCheck, SimpleChanges, QueryList, Renderer2, ViewChild, ViewChildren, TemplateRef, EmbeddedViewRef, ViewRef, ViewContainerRef } from '@angular/core';

import { CreateKnokStepService, CreateKnokStepServiceData } from '../core/components/wizard/wizardstep.service';

import { WizardStepDirective } from '../core/components/wizard/wizard.component';

import { WizardComponent } from '../core/components/wizard/wizard.component';

import { Observer } from 'rxjs/Observer';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/empty';
import 'rxjs/add/observable/interval';
import { of } from 'rxjs/observable/of';
import { take, delay, share } from 'rxjs/operators';
import { truncateToInt } from '../core/shared/helpers/converters';

import { IntervalObservable } from 'rxjs/observable/IntervalObservable';

import { BaseComponent } from '../core/components/base.component';

import { ActivatedRoute, Router } from '@angular/router';

import { AccountService, ConfigService, CurrencyService, ExchangeService, HeaderService, IAnalysisTag, IAnalysisTags, IApiError, ICurrencies, ICurrency, IExchange, IExchanges, IKnok, IKnokAnalysis, IKnokImage, IReference, isApiResponseError, IUserAccounts, Knok, KnokService, RouterExtService, TagService, TokenService, UserCredentials, UserService } from '../core/shared';

import { Chart } from 'chart.js';
import { KnokLinearScale, KnokLinearPlugin } from '../core/shared/libs/chart_js/';
import 'chartjs-plugin-annotation';

import { DomSanitizer, SafeResourceUrl, SafeUrl } from '@angular/platform-browser';

import { ILog, Log, applyMixins } from '../core/shared/helpers';
import { CExchanges, ICurrencyPair } from '../core/shared/exchange/exchange.model';
import { IConfig, CConfig } from '../core/shared/config/config.model';
import { CCurrencies } from '../core/shared/currency/currency.model';
import { Subscription } from 'rxjs/Subscription';
import { uniqBy as _uniqBy, findIndex as _findIndex } from 'lodash/array';
import { has as _has } from 'lodash/object';
import { AbstractControl, AsyncValidatorFn, FormArray, FormBuilder, FormControl, FormGroup, NgForm, NgModel, ValidationErrors, Validators } from '@angular/forms';
import { MDCDropdownDirective, MDCDropdownToggleDirective, MDCDropdownMenuDirective, MDCDropdownMenuItemDirective } from '../core/shared/mdc';
import { ValidatorFn } from '@angular/forms/src/directives/validators';
import { TranslateService } from '@ngx-translate/core';
import { Subscriber } from 'rxjs/src/Subscriber';
import { TeardownLogic } from 'rxjs/src/Subscription';
import { IKnoker, IKnokRate, IKnokStartvalues } from '../core/shared/knok/knok.model';
import { IAccount, IUser } from '../core/shared/user/user.model';
import { getKnokserRank } from '../core/shared/helpers/converters';
import { ExpansionPanels } from '../core/shared/libs/expansion-panels';
import { Subject } from 'rxjs/Subject';
import { animate, state, style, transition, trigger } from '@angular/animations';
import { AnimationTriggerMetadata } from '@angular/animations/src/animation_metadata';
import { ContentChild, Output, EventEmitter } from '@angular/core';
import { InputNumberComponent } from '../core/shared/libs/input-number';
import { MatDialog, MatDialogRef } from '@angular/material';
import { OverlayContainer } from '@angular/cdk/overlay';

enum KEYS {
  RAW_DATA,
  RAW_DATA_TECHNICAL_TAGS,
  RAW_DATA_FUNDAMENTAL_TAGS,
  KNOK_START_VALUES,
  KNOK_VALIDATE,
  KNOK_RATE,
  CURRENCY_GROUP_STATUS,
  CURRENCY_GROUP_CHANGE,
  CURRENCY_CONTROL_CHANGE,
  PRICE_GROUP_STATUS,
  PRICE_GROUP_CHANGE,
  ENTRY_RANGES_CONTROL_CHANGE,
  EXIT_RANGES_CONTROL_CHANGE,
  STOPLOSS_CONTROL_CHANGE,
  DURATION_CONTROL_CHANGE,
  TECHNICAL_GROUP_STATUS,
  TECHNICAL_GROUP_CHANGE,
  FUNDAMENTAL_GROUP_STATUS,
  FUNDAMENTAL_GROUP_CHANGE
}

function isUnsubscribing(subscription: Subscription): boolean {
  return ((typeof subscription === 'object') && (subscription instanceof Subscription) && (typeof subscription.unsubscribe === 'function'));
}

function resourceUrlValidatorFn(control: AbstractControl): ValidationErrors | null {
  if (typeof control.value === 'string' && control.value.match(/^(www|http:|https:)+[^\s]+[\w]\/?$/i)) {
    return null;
  } else if (typeof control.value === 'string' && control.value.match(/^data:[^\s]+\/[^;]+;base64,[^\s]+$/i)) {
    return null;
  } else if (typeof control.value === 'string' && control.value.trim() === '') {
    return null;
  } else {
    return {resourceUrl: true};
  }
}

function nonzeroValidatorFn(control: AbstractControl): ValidationErrors | null {
  if (control.value !== 0) {
    return null;
  } else {
    return {nonzero: true};
  }
}

Chart.scaleService.registerScaleType('knok-linear', KnokLinearScale, Chart.scaleService.getScaleDefaults('linear'));
Chart.plugins.register(KnokLinearPlugin);

@Component({
  selector: 'knk-createknok',
  templateUrl: './createknok.component.pug',
  styleUrls: ['./createknok.component.scss']
})
export class CreateKnokComponent implements OnInit, OnDestroy, ILog {
  @ViewChild('temp', {read: TemplateRef}) tempRef: TemplateRef<any>;
  @ViewChild('continueTpl', {read: TemplateRef}) continueTpl: TemplateRef<any>;
  @ViewChild('previewTpl', {read: TemplateRef}) previewTpl: TemplateRef<any>;
  @ViewChild('canvas') canvas;
  dialogRef: MatDialogRef<TemplateRef<any>>;
  chart: any;
  panels: any;

  lang: string = null;
  selectedCurrency: string = null;
  selectedTrade: string = null;
  selectedExchange: string = null;
  selectedTA: boolean = false;
  selectedFA: boolean = false;

  knok: IKnok;
  knokser: IUser;
  account: IAccount;

  currencyForm: FormGroup;
  priceForm: FormGroup;
  technicalForm: FormGroup;
  fundamentalForm: FormGroup;

  availableCurrencies: ICurrency[] = [];
  availableExchanges: IExchange[] = [];
  availableTradingPairs: ICurrencyPair[] = [];
  availableTechnicalTags: IAnalysisTag[] = [];
  availableFundamentalTags: IAnalysisTag[] = [];
  availableFundamentalReferences: IReference[] = [];

  marEntry: number;
  marExit: number;
  marStopLoss: number;
  minProfit: number;
  maximumDuration: number;

  precision: number = 8;
  priceStep: number = 0.10000000;
  priceFractionDigits: number = 8;

  sliderDurationConfig: any = {
    orientation: 'horizontal',
    connect: true,
    tooltips: true,
    step: 1,
    start: 1,
    range: {
      min: 1,
      max: 21
    }
  };

  isContinued: boolean = false;
  isPreviewed: boolean = false;
  potentialProfitValue: number = 0;
  price: number = 0;
  priceUSD: number = 0;
  potentialProfitStatus: string = null;
  defaultPriceValues: any = {
    entryPriceFrom: null,
    entryPriceTo: null,
    exitPriceFrom: null,
    exitPriceTo: null,
    stopLoss: null
  };

  errors: any = {priceForm: {}};
  valid: any = {priceForm: false};
  validationStatuses: any = {
    entryPriceRanges: null,
    exitPriceRanges: null,
    stopLoss: null,
    duration: null,
    profit: null
  };

  /* Mixins */
  public log: (...args: any[]) => void;

  switcherValue: Array<boolean> = [false, false, false, false];

  private _indicatorValue: number = 0;
  private subscriptions: { [key: number]: Subscription } = {};
  private requiredValidator: ValidatorFn = Validators.required;
  private numberValidator: ValidatorFn = Validators.pattern(/^\d+$/);
  private decimalValidator: ValidatorFn = Validators.pattern(/^[0-9]+(\.[0-9]*){0,1}$/);
  private nonzeroValidator: ValidatorFn = nonzeroValidatorFn;
  private urlValidator: ValidatorFn = Validators.pattern(/^(www|http:|https:)+[^\s]+[\w]\/?$/i);
  private resourceUrlValidator: ValidatorFn = resourceUrlValidatorFn;
  private ngUnsubscribe: Subject<void> = new Subject<void>();

  get availableUniqueTradingPairs(): ICurrencyPair[] {
    return _uniqBy(this.availableTradingPairs, 'currency2');
  }

  get indicatorValue(): number {
    if (this.isFormValid(this.technicalForm) || this.isFormValid(this.fundamentalForm)) {
      let value: number = this._indicatorValue + 25;
      return Math.min(Math.max(value, 0), 100);
    }
    return this._indicatorValue;
  }

  set indicatorValue(value: number) {
    this._indicatorValue = Math.min(Math.max(value, 0), 100);
  }

  constructor(public dialog: MatDialog,
              public overlayContainer: OverlayContainer,
              private changeDetector: ChangeDetectorRef,
              private zone: NgZone,
              private viewContainerRef: ViewContainerRef,
              private element: ElementRef,
              private renderer: Renderer2,
              private domSanitizer: DomSanitizer,
              private router: Router,
              private routerExt: RouterExtService,
              private activateRoute: ActivatedRoute,
              private translateService: TranslateService,
              private tokenService: TokenService,
              private configService: ConfigService,
              private userService: UserService,
              private accountService: AccountService,
              private currencyService: CurrencyService,
              private exchangeService: ExchangeService,
              private knokService: KnokService,
              private tagService: TagService,
              private fb: FormBuilder) {
  }

  ngOnInit(): void {
    // todo: Observable.combineLatest(...observables) || Observable.merge(...observables) ?!
    let observables: Observable<any>[] | undefined = [this.userService.loadUser(), this.configService.config, this.currencyService.getCurrencies(null), this.exchangeService.getExchanges()];
    this.subscriptions[KEYS.RAW_DATA] = Observable.combineLatest(...observables).subscribe((result: [IUserAccounts, IConfig, ICurrencies, IExchanges]): void => {
      const user: IUser = _has(result[0], ['user']) ? result[0].user : undefined;
      const account: IAccount = _has(result[0], ['accounts']) ? result[0].accounts[0] : undefined;
      const config: IConfig = result[1];
      const currencies: ICurrencies = result[2];
      const exchanges: IExchanges = result[3];

      if (user) {
        this.knokser = user;
      }

      if (account) {
        this.account = account;
      }

      if (config) {
        this.maximumDuration = config.maximumDuration;
        this.marEntry = config.marEntry;
        this.marExit = config.marExit;
        this.marStopLoss = config.marStopLoss;
        this.minProfit = config.minProfit;
      }

      if (currencies) {
        if (_has(currencies, ['items']) && currencies.items.length) {
          currencies.items.forEach((currency: ICurrency): void => {
            const availableCurrency: ICurrency = Object.assign({}, currency);
            this.availableCurrencies.push(availableCurrency);
          });
        }
      }

      if (exchanges) {
        if (_has(exchanges, ['items']) && exchanges.items.length) {
          exchanges.items.forEach((exchange: IExchange): void => {
            const availableExchange: IExchange = Object.assign({}, exchange, {selected: false, disabled: true});
            this.availableExchanges.push(availableExchange);
          });
        }
      }

      console.warn(result);
    }, (error: any): void => {
      console.error(error);
    }, (): void => {
      this.lang = this.translateService.currentLang || this.translateService.defaultLang;
      this.knok = new Knok();
      this.initCurrencyForm();
    });
  }

  ngOnDestroy(): void {
    this.ngUnsubscribe.next();
    this.ngUnsubscribe.complete();

    const subscriptions: Subscription[] = Object.values<Subscription>(this.subscriptions);
    subscriptions.forEach((subscription: Subscription | undefined): void => {
      if (subscription) {
        subscription.unsubscribe();
      }
    });

    this.destroyChart();
    this.destroyExpansionPanels();
    this.unsetOverlap();
  }

  isApiError<T>(data: IApiError | IReference): data is IApiError {
    return (<IApiError>data).id === -1;
  }

  setPanel(): void {
    // todo: experimental
    this.changeDetector.detectChanges();
    if (this.knokService) {
      const viewRef: EmbeddedViewRef<any> = this.tempRef.createEmbeddedView(null);
      this.knokService.setPanel(viewRef);
    }
  }

  unsetPanel(): void {
    // todo: experimental
    if (this.knokService) {
      this.knokService.unsetPanel();
    }
  }

  setOverlap(tpl: TemplateRef<any>): MatDialogRef<TemplateRef<any>> {
    this.dialogRef = this.dialog.open(tpl, {
      panelClass: 'knk-overlap-pane',
      backdropClass: 'knk-overlap-backdrop',
      maxWidth: '100%',
      minWidth: '100vw',
      width: '100%',
      maxHeight: '100%',
      minHeight: '100vh',
      height: '100%',
      autoFocus: false,
      disableClose: true,
      closeOnNavigation: true
    });
    return this.dialogRef;
  }

  unsetOverlap(): void {
    if (typeof this.dialogRef === 'object') {
      this.dialogRef.close();
    }
  }

  initChart(): void {
    const oneDayTiks: number = (24 * 60 * 60 * 1000);
    const createDate: Date = new Date();
    const endDate: Date = new Date(createDate.getTime() + (this.knok.duration * oneDayTiks));

    let chartData: Array<{x: Date, y: number}> = [
      {x: createDate, y: this.knok.currentPrice},
      {x: endDate, y: null}
    ];

    const canvas: HTMLCanvasElement | undefined = (this.canvas && this.canvas.nativeElement) || undefined;
    if (canvas) {
      const ctx: CanvasRenderingContext2D = canvas.getContext('2d');
      this.chart = new Chart(ctx, {
        type: 'line',
        data: {
          datasets: [
            {
              showLine: true,
              fill: true,
              backgroundColor: this.getCanvasLinearGradient(ctx, [[0, 'rgba(0, 186, 255, 0.15)'], [1, 'rgba(255, 255, 255, 0.15)']], {y1: ctx.canvas.height}),
              borderColor: 'rgba(0, 186, 255, 1)',
              borderWidth: 2,
              pointBackgroundColor: 'rgba(0, 186, 255, 1)',
              pointBorderColor: 'rgba(255, 255, 255, 1)',
              pointBorderWidth: 2,
              pointRadius: [6, 0],
              data: chartData,
              yAxisID: 'A'
            }
          ]
        },
        options: {
          responsive: true,
          maintainAspectRatio: false,
          easing: 'easeInOutBack',
          elements: {
            line: {
              tension: 0.5
            }
          },
          plugins: ['knok-linear'],
          scales: {
            xAxes: [
              {
                id: 'D',
                type: 'time',
                display: false,
                time: {
                  unit: 'minute',
                  displayFormats: {
                    day: 'MMM yyyy'
                  }
                },
                ticks: {
                  display: false,
                  beginAtZero: false,
                  min: (new Date('07/12/2018')),
                  max: (new Date('08/12/2018'))
                },
                gridLines: {
                  display: false,
                  drawBorder: false
                }
              }
            ],
            yAxes: [
              {
                id: 'A',
                type: 'knok-linear',
                position: 'right',
                display: true,
                ticks: {
                  display: true,
                  beginAtZero: false,
                  fontFamily: 'Roboto',
                  fontSize: 10,
                  entryPriceFrom: this.knok.entryPriceFrom,
                  entryPriceTo: this.knok.entryPriceTo,
                  exitPriceFrom: this.knok.exitPriceFrom,
                  exitPriceTo: this.knok.exitPriceTo,
                  stopLoss: this.knok.stopLoss,
                  precision: this.precision
                },
                gridLines: {
                  display: true,
                  drawBorder: false,
                  zeroLineColor: 'transparent'
                }
              }
            ]
          },
          legend: {
            display: false
          },
          title: {
            display: false
          },
          tooltips: {
            enabled: false
          }
        }
      });
    }
  }

  destroyChart(): void {
    if (this.chart) {
      this.chart.destroy();
    }
  }

  prepareKnok(initialKnok: IKnok): IKnok {
    const prepareKnok: IKnok = Object.assign({}, initialKnok);

    if (!prepareKnok.hasTechnical) {
      prepareKnok.technical = null;
    }

    if (!prepareKnok.hasFundamental) {
      prepareKnok.fundamental = null;
    }

    if (_has(prepareKnok.fundamental, ['references']) && Array.isArray(prepareKnok.fundamental.references)) {
      prepareKnok.fundamental.references = JSON.stringify(prepareKnok.fundamental.references);
    }

    return prepareKnok;
  }

  publishKnok(): void {
    const knok: IKnok = this.prepareKnok(this.knok);
    Observable.timer(1000).takeUntil(this.ngUnsubscribe).subscribe((): void => {
      this.ngUnsubscribe.next();
      this.knokService.createKnok(knok).subscribe((createdKnok: IKnok) => {
        // this.routerExt.goToHome();
        this.routerExt.goToPage(['published', 'currently']);
      }, (result) => console.error(result.error));
    });
  }

  previewKnok(): void {
    if (!this.isPreviewed) {
      if (this.isFormValid(this.priceForm)) {
        this.availableFundamentalReferences = [];
        Observable.timer(1000).takeUntil(this.ngUnsubscribe).subscribe((): void => {
          this.ngUnsubscribe.next();
          this.isPreviewed = !this.isPreviewed;

          this.unsetOverlap();
          this.changeDetector.detectChanges();
          let dialog: MatDialogRef<TemplateRef<any>> = this.setOverlap(this.previewTpl);
          dialog.afterOpen().take(1).subscribe((): void => {
            this.changeDetector.detectChanges();
            this.initChart();
            this.initExpansionPanels(this.overlayContainer.getContainerElement());

            if (this.knokHasFundamental && this.knokFundamental.references) {
              const references: Array<{ reference: string }> = Object.assign([], this.knokFundamental.references);
              references.forEach((item: any) => {
                if (_findIndex(this.availableFundamentalReferences, (reference: IReference) => reference.url === item.reference) === -1) {
                  this.knokService.getKnokFAReference({url: item.reference}).subscribe((reference: IReference) => {
                    this.changeDetector.detectChanges();
                    this.availableFundamentalReferences.push(reference);
                  });
                }
              });
            }
          });
        });
      }
    }
  }

  back(): void {
    if (this.isPreviewed) {
      Observable.timer(1000).takeUntil(this.ngUnsubscribe).subscribe((): void => {
        this.ngUnsubscribe.next();
        this.destroyChart();
        this.destroyExpansionPanels();
        this.isPreviewed = !this.isPreviewed;

        this.unsetOverlap();
        this.changeDetector.detectChanges();
        this.setOverlap(this.continueTpl);
      });
    }
  }

  continue(): void {
    if (!this.isContinued) {
      if (this.isFormValid(this.currencyForm)) {
        this.initPriceForm();
        this.initTechnicalAnalysisForm();
        this.initFundamentalAnalysisForm();
        Observable.timer(1000).takeUntil(this.ngUnsubscribe).subscribe((): void => {
          this.ngUnsubscribe.next();
          this.isContinued = !this.isContinued;

          this.unsetOverlap();
          this.changeDetector.detectChanges();
          this.setOverlap(this.continueTpl);
        });
      }
    }
  }

  discardAndCancel(): void {
    Observable.timer(1000).takeUntil(this.ngUnsubscribe).subscribe((): void => {
      this.ngUnsubscribe.next();
      this.destroyChart();
      this.destroyExpansionPanels();
      this.unsetOverlap();
      this.router.navigate([this.lang, `discover`]);
    });
  }

  openUrlInNewTab(url: string): void {
    const win: Window | null = window.open(url, '_blank');
    if (win) {
      win.focus();
    }
  }

  updateIndicator(value: number): void {
    this._indicatorValue = +value;
  }

  initExpansionPanels(host: HTMLElement): void {
    this.panels = ExpansionPanels.init(host, this.renderer);
  }

  destroyExpansionPanels(): void {
    if (this.panels) {
      ExpansionPanels.destroy(this.panels);
    }
  }

  get availableExchangesFiltered(): Array<IExchange> {
    return this.availableExchanges.filter(e => !e.disabled);
  }

  get userTokens(): number {
    if (_has(this.account, ['currentBalance'])) {
      return this.account.currentBalance;
    } else {
      return 0;
    }
  }

  get knokExists(): boolean {
    return this.knok !== null && this.knok !== undefined;
  }

  get knokHasTechnical(): boolean {
    return this.knok.hasTechnical;
  }

  get knokHasFundamental(): boolean {
    return this.knok.hasFundamental;
  }

  get knokMarket(): string {
    return this.selectedCurrency;
  }

  get knokCurrency(): string {
    return this.selectedTrade;
  }

  get knokExchange(): string {
    return this.selectedExchange;
  }

  get knokDuration(): string {
    return `${this.knok.duration} Days`;
  }

  get knokPublishedAt(): string | null {
    const date: Date = new Date();
    let result: string = null;
    if (date) {
      let hours: string = (date.getHours() < 10) ? '0' + date.getHours() : '' + date.getHours();
      let minutes: string = (date.getMinutes() < 10) ? '0' + date.getMinutes() : '' + date.getMinutes();
      result = hours + ':' + minutes;
    }
    return (result) ? result : null;
  }

  get knokTimeLeft(): string {
    const dayTime: number = (24 * 60 * 60 * 1000);
    const currentDate: Date = new Date();
    const endDate: Date = new Date(currentDate.getTime() + (this.knok.duration * dayTime));
    const times: number = truncateToInt((endDate.getTime() - currentDate.getTime()) * 24 / dayTime);
    const daysLeft: number = truncateToInt(times / 24);

    let timeLeft: string;
    if (times > 24) {
      timeLeft = `${daysLeft} days left`;
    } else if (times === 1) {
      timeLeft = `${times} hour left`;
    } else {
      timeLeft = `${times} hours left`;
    }
    return timeLeft;
  }

  get knokCurrentPrice(): number {
    return this.knok.currentPrice as number;
  }

  get knokCurrentProfitPercent(): number {
    return 0; // this.knok.currentPrice as number;
  }

  get knokEntryPriceFrom(): number {
    return this.knok.entryPriceFrom as number;
  }

  get knokEntryPriceTo(): number {
    return this.knok.entryPriceTo as number;
  }

  get knokExitPriceFrom(): number {
    return this.knok.exitPriceFrom as number;
  }

  get knokExitPriceTo(): number {
    return this.knok.exitPriceTo as number;
  }

  get knokStopLoss(): number {
    return this.knok.stopLoss as number;
  }

  get knokTechnical(): { [key: string]: any } {
    return (this.knokHasTechnical) ? this.knok.technical : {};
  }

  get knokTechnicalImage(): SafeUrl | SafeResourceUrl | string {
    let safeUrl: SafeUrl | SafeResourceUrl | string = null;
    if (this.knokHasTechnical) {
      const image: any = this.knokTechnical.image;
      const url: string = image.url;
      const content: string = image.content;
      const contentType: string = image.contentType;

      if (url) {
        safeUrl = this.domSanitizer.bypassSecurityTrustUrl(url);
      } else if (content && contentType) {
        safeUrl = this.domSanitizer.bypassSecurityTrustResourceUrl(`data:${contentType};base64,${content}`);
      }
    }
    return safeUrl;
  }

  get knokFundamental(): { [key: string]: any } {
    return (this.knokHasFundamental) ? this.knok.fundamental : {};
  }

  get knokserExists(): boolean {
    return this.knokser !== null && this.knokser !== undefined;
  }

  get knokserAvatarUrl(): string {
    return `${SETTINGS.API_URL}` + this.knokser.avatarUrl;
  }

  get knokserName(): string {
    return !this.knokser.lastName && !this.knokser.firstName ? this.knokser.userName : this.knokser.firstName;
  }

  get knokserSurname(): string {
    return this.knokser.lastName;
  }

  get knokserFollowers(): number {
    return this.knokser.countFollowers as number || 0;
  }

  get knokserRankLabel(): string {
    return getKnokserRank(this.knokser.rank as number);
  }

  get accountExists(): boolean {
    return this.account !== null && this.account !== undefined;
  }

  get accountTokenBalance(): number {
    return this.account.currentBalance as number || 0;
  }

  get knokPrice(): number {
    return this.price;
  }

  get knokPriceUSD(): number {
    return this.priceUSD;
  }

  get knokPriceFormat(): string {
    if (typeof this.precision === 'number' && !Number.isNaN(this.precision)) {
      return `1.${this.precision}-${this.precision}`; // (this.precision === 8 ? '1' : '') + '.' + this.precision.toString();
    } else {
      return '1.8-8';
    }
  }

  knokPotentialProfitPercent(inPercent: boolean): number {
    if (this.potentialProfitValue && typeof this.potentialProfitValue === 'number' && !Number.isNaN(this.potentialProfitValue)) {
      if (inPercent) {
        return (this.knok.exitPriceFrom - this.knok.entryPriceTo);
      } else {
        return Math.round(this.potentialProfitValue);
      }
    } else {
      return 0;
    }
  }

  entryPriceSpread(inPercent: boolean): number {
    if (this.knok && this.knok.entryPriceFrom && this.knok.entryPriceTo && this.knok.entryPriceTo > this.knok.entryPriceFrom) {
      if (inPercent) {
        return (this.knok.entryPriceTo - this.knok.entryPriceFrom);
      } else {
        return (this.knok.entryPriceTo - this.knok.entryPriceFrom) / this.knok.entryPriceFrom * 100;
      }
    } else {
      return 0;
    }
  }

  exitPriceSpread(inPercent: boolean): number {
    if (this.knok && this.knok.exitPriceFrom && this.knok.exitPriceTo && this.knok.exitPriceTo > this.knok.exitPriceFrom) {
      if (inPercent) {
        return (this.knok.exitPriceTo - this.knok.exitPriceFrom);
      } else {
        return (this.knok.exitPriceTo - this.knok.exitPriceFrom) / this.knok.exitPriceFrom * 100;
      }
    } else {
      return 0;
    }
  }

  slPriceSpread(inPercent: boolean): number {
    if (this.knok && this.knok.exitPriceFrom && this.knok.exitPriceTo && this.knok.exitPriceTo > this.knok.exitPriceFrom) {
      if (inPercent) {
        return (this.knok.entryPriceFrom - this.knok.stopLoss);
      } else {
        return (this.knok.entryPriceFrom - this.knok.stopLoss) / this.knok.entryPriceFrom * 100;
      }
    } else {
      return 0;
    }
  }

  isFormExists(form: FormGroup | NgForm): boolean {
    return (form instanceof FormGroup) || (form instanceof NgForm);
  }

  isFormValid(form: FormGroup | NgForm): boolean {
    return (this.isFormExists(form) && form.valid);
  }

  isControlExists(control: AbstractControl | NgModel): boolean {
    return (control instanceof AbstractControl) || (control instanceof NgModel);
  }

  isControlDirtyOrTouched(control: AbstractControl | NgModel): boolean {
    return (this.isControlExists(control) && (control.dirty || control.touched));
  }

  isControlEmpty(control: AbstractControl | NgModel): boolean {
    return (this.isControlExists(control) && (control.value === ''));
  }

  protected setPotentialProfitStatus(value: number | null | undefined): void {
    // value = Math.min(Math.max(+value, 1), 3);
    switch (value) {
      case 1:
        this.potentialProfitStatus = 'Low';
        break;
      case 2:
        this.potentialProfitStatus = 'Medium';
        break;
      case 3:
        this.potentialProfitStatus = 'High';
        break;
      case 4:
        this.potentialProfitStatus = 'Very High';
        break;
      default:
        this.potentialProfitStatus = null;
        break;
    }
  }

  protected setPotentialProfitValue(value: number | null | undefined): void {
    // value = Math.min(Math.max(+value, (+this.minProfit || 0)), 100);
    this.potentialProfitValue = parseFloat(value.toFixed(2));
  }

  protected setPrice(value: number | null | undefined): void {
    this.price = parseFloat(value.toFixed(2));
  }

  protected setPriceUSD(value: number | null | undefined): void {
    this.priceUSD = parseFloat(value.toFixed(2));
  }

  protected getValidationStatusByValue(value: number | null | undefined): string {
    value = Math.min(Math.max(+value, 0), 100);
    if (value === null || value === undefined) {
      return null;
    } else if (value < 30) {
      return 'wrong';
    } else if (value < 100) {
      return 'warning';
    } else {
      return 'success';
    }
  }

  protected setValidationStatus(key: string, status: string): void {
    if (status === 'success') {
      this.validationStatuses[key] = 'success';
    } else if (status === 'warning') {
      this.validationStatuses[key] = 'warning';
    } else if (status === 'wrong') {
      this.validationStatuses[key] = 'wrong';
    } else {
      this.validationStatuses[key] = null;
    }
  }

  protected initTechnicalAnalysisForm(): void {
    this.technicalForm = this.fb.group({
      header: this.fb.control('', [this.requiredValidator]),
      image: this.fb.control('', Validators.compose([this.requiredValidator, this.resourceUrlValidator])),
      description: this.fb.control(''),
      tags: this.fb.control([])
    });

    this.subscriptions[KEYS.RAW_DATA_TECHNICAL_TAGS] = this.tagService.taTags.publishReplay(1, 30000).refCount().subscribe((tags: IAnalysisTags) => {
      if (tags) {
        this.availableTechnicalTags = tags.items;
      }
    });

    // todo: set knok properties from form
    this.subscriptions[KEYS.TECHNICAL_GROUP_STATUS] = this.technicalForm.statusChanges.distinctUntilChanged().subscribe((status: any): void => {
      this.selectedTA = (status !== 'INVALID');
      this.knok = Object.assign({}, this.knok, {
        hasTechnical: (status !== 'INVALID')
      });
    });

    this.subscriptions[KEYS.TECHNICAL_GROUP_CHANGE] = this.technicalForm.valueChanges.distinctUntilChanged().subscribe((values: any): void => {
      const tags: string[] = (Array.isArray(values.tags)) ? values.tags : [];
      const image: any = {url: '', contentType: '', content: null};

      if (values.image) {
        if (values.image.startsWith('http://') || values.image.startsWith('https://')) {
          image.url = values.image;
        } else {
          const contentType: string = values.image.substr(values.image.indexOf('data:') + 5, values.image.indexOf(';') - 5);
          const content: string = values.image.substr(values.image.indexOf('base64,') + 7);
          image.contentType = contentType;
          image.content = content;
        }
      }

      this.knok = Object.assign({}, this.knok, {
        technical: Object.assign({}, this.knok.technical, {
          header: values.header,
          description: values.description,
          image: Object.assign({}, this.knok.technical.image, image),
          tags: tags
        })
      });
    });

    this.technicalForm.setAsyncValidators((form: FormGroup): Promise<ValidationErrors | null> => {
      return new Promise((resolve, reject) => {
        const timeout: any = setTimeout(() => {
          let errorThrowable: boolean = false;
          let errors: ValidationErrors = {};

          // todo: validation

          if (errorThrowable) {
            resolve(errors);
          } else {
            resolve(null);
          }

          clearTimeout(timeout);
        }, 500);
      });
    });
  }

  protected initFundamentalAnalysisForm(): void {
    this.fundamentalForm = this.fb.group({
      header: this.fb.control('', [this.requiredValidator]),
      references: this.fb.array([]),
      description: this.fb.control(''),
      tags: this.fb.control([])
    });

    this.subscriptions[KEYS.RAW_DATA_FUNDAMENTAL_TAGS] = this.tagService.faTags.publishReplay(1, 30000).refCount().subscribe((tags: IAnalysisTags) => {
      if (tags) {
        this.availableFundamentalTags = tags.items;
      }
    });

    // todo: set knok properties from form
    this.subscriptions[KEYS.FUNDAMENTAL_GROUP_STATUS] = this.fundamentalForm.statusChanges.distinctUntilChanged().subscribe((status: any): void => {
      this.selectedFA = (status !== 'INVALID');
      this.knok = Object.assign({}, this.knok, {
        hasFundamental: (status !== 'INVALID')
      });
    });

    this.subscriptions[KEYS.FUNDAMENTAL_GROUP_CHANGE] = this.fundamentalForm.valueChanges.distinctUntilChanged().subscribe((values: any): void => {
      const tags: string[] = (Array.isArray(values.tags)) ? values.tags : [];
      const references: Array<{ reference: string }> = (Array.isArray(values.references)) ? values.references : [];

      this.knok = Object.assign({}, this.knok, {
        fundamental: Object.assign({}, this.knok.fundamental, {
          header: values.header,
          description: values.description,
          references: references,
          tags: tags
        })
      });
    });

    this.fundamentalForm.setAsyncValidators((form: FormGroup): Observable<ValidationErrors | null> => {
      return of(null).pipe(delay(500));
    });
  }

  protected initPriceForm(): void {
    this.priceForm = this.fb.group({
      entryPriceFrom: this.fb.control(this.defaultPriceValues.entryPriceFrom || '', Validators.compose([this.requiredValidator, this.decimalValidator, this.nonzeroValidator])),
      entryPriceTo: this.fb.control(this.defaultPriceValues.entryPriceTo || '', Validators.compose([this.requiredValidator, this.decimalValidator, this.nonzeroValidator])),
      exitPriceFrom: this.fb.control(this.defaultPriceValues.exitPriceFrom || '', Validators.compose([this.requiredValidator, this.decimalValidator, this.nonzeroValidator])),
      exitPriceTo: this.fb.control(this.defaultPriceValues.exitPriceTo || '', Validators.compose([this.requiredValidator, this.decimalValidator, this.nonzeroValidator])),
      stopLoss: this.fb.control(this.defaultPriceValues.stopLoss || '', Validators.compose([this.requiredValidator, this.decimalValidator, this.nonzeroValidator])),
      duration: this.fb.control(this.sliderDurationConfig.start || 1, Validators.compose([this.requiredValidator, this.numberValidator, this.nonzeroValidator]))
    });

    if (typeof this.precision === 'number' && !Number.isNaN(this.precision)) {
      switch (this.precision) {
        case 2:
          this.priceStep = 1.00;
          this.priceFractionDigits = 2;
          break;
        default:
          this.priceStep = 0.10000000;
          this.priceFractionDigits = 8;
      }
    }

    const entryPriceFromControl: AbstractControl = this.priceForm.get('entryPriceFrom');
    const entryPriceToControl: AbstractControl = this.priceForm.get('entryPriceTo');
    const exitPriceFromControl: AbstractControl = this.priceForm.get('exitPriceFrom');
    const exitPriceToControl: AbstractControl = this.priceForm.get('exitPriceTo');
    const stopLossControl: AbstractControl = this.priceForm.get('stopLoss');
    const durationControl: AbstractControl = this.priceForm.get('duration');
    let observables: Observable<any>[] | undefined;

    // todo: set knok properties from form
    observables = [this.priceForm.valueChanges.distinctUntilChanged(), this.priceForm.statusChanges.distinctUntilChanged()];
    this.subscriptions[KEYS.PRICE_GROUP_CHANGE] = Observable.combineLatest(...observables).debounceTime(500).subscribe((result: [any, string]): void => {
      const form: FormGroup = this.priceForm;
      const values: any = result[0];
      const status: string = result[1];

      if (values) {
        this.knok = Object.assign({}, this.knok, {
          entryPriceFrom: +values.entryPriceFrom,
          entryPriceTo: +values.entryPriceTo,
          exitPriceFrom: +values.exitPriceFrom,
          exitPriceTo: +values.exitPriceTo,
          stopLoss: +values.stopLoss,
          duration: +values.duration
        });
      }

      if (status === 'VALID') {
        const knok: IKnok = this.prepareKnok(this.knok);
        if (isUnsubscribing(this.subscriptions[KEYS.KNOK_RATE])) {
          this.subscriptions[KEYS.KNOK_RATE].unsubscribe();
        }
        this.subscriptions[KEYS.KNOK_RATE] = this.knokService.rateKnok(knok).publishReplay(1, 30000).refCount().subscribe((rate: IKnokRate): void => {
          if (entryPriceFromControl.enabled && entryPriceToControl.enabled) {
            this.setValidationStatus('entryPriceRanges', this.getValidationStatusByValue(+rate.entry));
          }

          if (exitPriceFromControl.enabled && exitPriceToControl.enabled) {
            this.setValidationStatus('exitPriceRanges', this.getValidationStatusByValue(+rate.exit));
          }

          if (stopLossControl.enabled) {
            this.setValidationStatus('stopLoss', this.getValidationStatusByValue(+rate.entryToSL));
          }

          if (durationControl.enabled) {
            const duration: number = parseInt(values.duration, 10);
            let status: string = null;
            switch (true) {
              case duration >= 1 && duration < 4:
                status = 'success';
                break;
              case duration >= 4 && duration < 11:
                status = 'warning';
                break;
              default:
                status = 'wrong';
                break;
            }
            this.setValidationStatus('duration', status);
          }

          this.setPotentialProfitStatus(+rate.potentialProfit);
          this.setPotentialProfitValue(+rate.potentialProfitValue);
          this.setPrice(+rate.price);
          this.setPriceUSD(+rate.priceUSD);
          this.updateIndicator(+rate.total);
        });
      }

      this.valid.priceForm = (status === 'VALID');
    });

    this.priceForm.setAsyncValidators(Validators.composeAsync([
      (form: FormGroup): Observable<ValidationErrors | null> => {
        const values: any = form.value;
        this.knok = Object.assign({}, this.knok, {
          entryPriceFrom: +values.entryPriceFrom,
          entryPriceTo: +values.entryPriceTo,
          exitPriceFrom: +values.exitPriceFrom,
          exitPriceTo: +values.exitPriceTo,
          stopLoss: +values.stopLoss,
          duration: +values.duration
        });

        const knok: IKnok = this.prepareKnok(this.knok);
        return this.knokService.validateKnock(knok).publishReplay(1, 30000).refCount().map((error: IApiError): ValidationErrors | null => {
          if (error && error.id === 5 && error.errors) {
            return error.errors;
          }
          return null;
        }).map((errors: ValidationErrors | null): ValidationErrors | null => {
          this.priceForm.setErrors(errors, {emitEvent: true});
          this.errors.priceForm = errors || {};

          if (entryPriceFromControl.enabled && entryPriceToControl.enabled) {
            if (_has(errors, ['entryPriceFrom']) || _has(errors, ['entryPriceTo'])) {
              this.setValidationStatus('entryPriceRanges', 'wrong');
            }
          }

          if (exitPriceFromControl.enabled && exitPriceToControl.enabled) {
            if (_has(errors, ['exitPriceFrom']) || _has(errors, ['exitPriceTo'])) {
              this.setValidationStatus('exitPriceRanges', 'wrong');
            }
          }

          if (stopLossControl.enabled) {
            if (_has(errors, ['stopLoss'])) {
              this.setValidationStatus('stopLoss', 'wrong');
            }
          }

          if (durationControl.enabled) {
            if (_has(errors, ['duration'])) {
              this.setValidationStatus('duration', 'wrong');
            }
          }

          return errors;
        }).first();
      }
    ]));

    this.priceForm.updateValueAndValidity({emitEvent: true});
  }

  protected initCurrencyForm(): void {
    this.currencyForm = this.fb.group({
      currency: this.fb.control(null, [this.requiredValidator]),
      trade: this.fb.control(null, [this.requiredValidator]),
      exchange: this.fb.control(null, [this.requiredValidator])
    });

    const currencyControl: AbstractControl = this.currencyForm.get('currency');
    this.subscriptions[KEYS.CURRENCY_CONTROL_CHANGE] = currencyControl.valueChanges.distinctUntilChanged().subscribe((value: any): void => {
      if (currencyControl.enabled) {
        if (currencyControl.valid) {
          this.fillTradingPairs(value);
        } else {
          this.fillTradingPairs(null);
        }
        this.prepareExchangesByTradeValue(null);
      }
    });

    // todo: set knok properties from form
    this.subscriptions[KEYS.CURRENCY_GROUP_CHANGE] = this.currencyForm.valueChanges.distinctUntilChanged().subscribe((values: any): void => {
      this.knok = Object.assign({}, this.knok, {
        currency1: values.currency,
        currency2: values.trade,
        exchangeId: values.exchange
      });
    });

    this.currencyForm.setAsyncValidators((form: FormGroup): Observable<ValidationErrors | null> => {
      const exchange: IExchange = this.availableExchanges.find((exchange: IExchange) => (+exchange.exchangeId === +form.value.exchange));
      if (exchange) {
        this.selectedExchange = exchange.exchangeName;
      }
      this.selectedCurrency = form.value.currency;
      this.selectedTrade = form.value.trade;

      const observable: Observable<ValidationErrors | null> = Observable.create((observer: Subscriber<ValidationErrors | null>): TeardownLogic => {
        observer.next(null);
        observer.complete();
        return (): void => {
          if (this.isFormValid(this.currencyForm)) {
            this.subscriptions[KEYS.KNOK_START_VALUES] = this.knokService.getKnokStartvalues(+form.value.exchange, form.value.currency, form.value.trade).subscribe((result: IKnokStartvalues): void => {
              this.defaultPriceValues.entryPriceFrom = +result.entryFrom;
              this.defaultPriceValues.entryPriceTo = +result.entryTo;
              this.defaultPriceValues.exitPriceFrom = +result.exitFrom;
              this.defaultPriceValues.exitPriceTo = +result.exitTo;
              this.defaultPriceValues.stopLoss = +result.stopLoss;
              this.knok.currentPrice = +result.entryFrom;
              this.precision = +result.precision;
            });
          }
        };
      });
      return observable;
    });

    currencyControl.updateValueAndValidity({emitEvent: false});
  }

  protected fillTradingPairs(currency: string | null): void {
    if (this.currencyForm) {
      const tradeControl: AbstractControl = this.currencyForm.get('trade');
      if (tradeControl) {
        tradeControl.reset(null, {emitEvent: true});
      }
    }

    if (this.availableTradingPairs.length) {
      this.availableTradingPairs = [];
    }

    if (currency) {
      this.availableExchanges.forEach((exchange: IExchange): void => {
        if (_has(exchange, ['exchangeId'])) {
          if (_has(exchange, ['currencyPairs']) && exchange.currencyPairs.length) {
            exchange.currencyPairs.forEach((trade: ICurrencyPair): void => {
              if (_has(trade, ['currency1']) && trade.currency1 === currency) {
                const availableTrade: ICurrencyPair = Object.assign({}, trade, {exchangeId: +exchange.exchangeId});
                this.availableTradingPairs.push(availableTrade);
              }
            });
          }
        }
      });
    }
  }

  protected prepareExchangesByTradeValue(value: string | null): number[] {
    let selectedExchangesId: number[] = [];
    let selectedTrade: string | null = value;
    let selectedFirstExchange: IExchange | undefined = undefined;

    if (selectedTrade) {
      if (this.availableTradingPairs.length) {
        this.availableTradingPairs.forEach((trade: ICurrencyPair | undefined): void => {
          if (_has(trade, ['exchangeId']) && _has(trade, ['currency2'])) {
            if (selectedTrade === trade.currency2) {
              selectedExchangesId.push(+trade.exchangeId);
            }
          }
        }, this);
      }
    }

    if (this.availableExchanges.length) {
      this.availableExchanges.forEach((exchange: IExchange): void => {
        exchange.disabled = selectedExchangesId.indexOf(exchange.exchangeId) === -1;
        if (!selectedFirstExchange && !exchange.disabled) {
          selectedFirstExchange = exchange;
        }
      }, this);
    }

    if (this.currencyForm) {
      if (!selectedExchangesId.length) {
        this.selectSomeExchange(undefined);
      } else {
        this.selectSomeExchange(selectedFirstExchange);
      }
    }

    return selectedExchangesId;
  }

  protected prepareExchangesByTradeIndex(index: number | undefined): number[] {
    let selectedExchangesId: number[] = [];
    let selectedTrade: string | null = null;

    if (typeof index === 'number' && index >= 0) {
      if (this.availableUniqueTradingPairs.length) {
        let trade: ICurrencyPair | undefined = this.availableUniqueTradingPairs[index];
        if (_has(trade, ['currency2'])) {
          selectedTrade = trade.currency2;
        }
      }
    }

    if (selectedTrade) {
      selectedExchangesId = this.prepareExchangesByTradeValue(selectedTrade);
    }

    return selectedExchangesId;
  }

  protected selectSomeExchange(selectedExchange: IExchange | undefined): void {
    const exchangeControl: AbstractControl = this.currencyForm.get('exchange');
    if (selectedExchange === undefined) {
      if (this.availableExchanges.length) {
        this.availableExchanges.forEach((exchange: IExchange): void => {
          exchange.selected = false;
        }, this);
      }
      exchangeControl.reset(null, {emitEvent: true});
    } else {
      if (this.availableExchanges.length) {
        this.availableExchanges.forEach((exchange: IExchange): void => {
          exchange.selected = +selectedExchange.exchangeId === +exchange.exchangeId;
        }, this);
      }
      exchangeControl.patchValue(selectedExchange.exchangeId, {emitEvent: true});
    }
  }

  private getCanvasLinearGradient(ctx: CanvasRenderingContext2D, colors: [number, string][], options?: { [key: string]: any }): CanvasGradient | null {
    if (ctx && ctx.canvas) {
      const y0: number = options.y0 || 0;
      const y1: number = options.y1 || 100;
      const gradient: CanvasGradient = ctx.createLinearGradient(0, y0, 0, y1);
      colors.forEach((value: [number, string]): void => {
        const index: number = value[0];
        const color: string = value[1];
        gradient.addColorStop(index, color);
      });
      return gradient;
    } else {
      return null;
    }
  }
}

@Component({
  selector: 'knk-price-switcher',
  template:  '<ng-content></ng-content>'
})
export class PriceInputSwitcherComponent {
  @Input('parentPrice') parentPrice: InputNumberComponent;
  @Input('selectedTrade') selectedTrade: string;
  @Input() set parentValue(value: any) {
    if (this.showInPercentInt) {
      this.currentPrice.baseValue = value;
    }
  }
  @ContentChild(InputNumberComponent) currentPrice: InputNumberComponent;

  @Output() showInPercentChange = new EventEmitter<boolean>();
  private showInPercentInt: boolean;
  @Input('showInPercent') set showInPercent(value: boolean) {
    if (this.showInPercentInt === value) {
      return;
    }
    this.showInPercentInt = value;
    if (this.showInPercentInt) {
      this.currentPrice.baseValue = this.parentPrice.priceValue;
    } else {
      this.currentPrice.baseValue = null;
    }
  }

  get showInPercent(): boolean {
    return this.showInPercentInt;
  }

  switch(): void {
    this.showInPercentInt = !this.showInPercentInt;
    if (this.showInPercentInt) {
      this.currentPrice.baseValue = this.parentPrice.priceValue;
    } else {
      this.currentPrice.baseValue = null;
    }
    this.showInPercentChange.emit(this.showInPercentInt);
  }

  get label(): string {
    if (!this.showInPercentInt) {
      return this.selectedTrade;
    } else {
      return '%';
    }
  }

  // get priceSpread(): string {
  //   const base: number = +parseFloat(this.parentPrice.value);
  //   const curr: number = +parseFloat(this.currentPrice.value);
  //   if (this.showInPercentInt) {
  //     // curr - in percent
  //     return ((base / 100) * curr + base).toFixed(8) + ` / ${this.selectedTrade}`; // number
  //   } else {
  //     // curr - in number
  //     return (Math.abs(curr - base) / (base / 100)).toFixed(2) + '%'; // percent
  //   }
  // }
}

applyMixins(CreateKnokComponent, [Log]);
