import { Component, Element, Event, EventEmitter, Listen, Method, Prop } from '@stencil/core';
import { Config, DomController, GestureDetail } from '../../index';

@Component({
  tag: 'ion-scroll',
  styleUrls: {
    ios: 'scroll.ios.scss',
    md: 'scroll.md.scss'
  },
  host: {
    theme: 'scroll'
  }
})
export class Scroll {

  private watchDog: any;
  private isScrolling = false;
  private lastScroll = 0;
  private detail: ScrollDetail;
  private queued = false;
  @Element() private el: HTMLElement;

  @Prop({ context: 'config'}) config: Config;
  @Prop({ context: 'dom' }) dom: DomController;
  @Prop({ context: 'isServer' }) isServer: boolean;

  @Prop() mode: string;


  /**
   * If true and the content does not cause an overflow scroll, the scroll interaction will cause a bounce.
   * If the content exceeds the bounds of ionScroll, nothing will change.
   * Note, the does not disable the system bounce on iOS. That is an OS level setting.
   */
  @Prop({mutable: true}) forceOverscroll: boolean;

  @Prop() scrollEvents = false;

  /**
   * Emitted when the scroll has started.
   */
  @Event() ionScrollStart: EventEmitter<ScrollBaseDetail>;

  /**
   * Emitted while scrolling. This event is disabled by default.
   * Look at the property: `scrollEvents`
   */
  @Event({bubbles: false}) ionScroll: EventEmitter<ScrollDetail>;

  /**
   * Emitted when the scroll has ended.
   */
  @Event() ionScrollEnd: EventEmitter<ScrollBaseDetail>;

  constructor() {
    // Detail is used in a hot loop in the scroll event, by allocating it here
    // V8 will be able to inline any read/write to it since it's a monomorphic class.
    // https://mrale.ph/blog/2015/01/11/whats-up-with-monomorphism.html
    this.detail = {
      positions: [],
      scrollTop: 0,
      scrollLeft: 0,
      type: 'scroll',
      event: undefined,
      startX: 0,
      startY: 0,
      startTimeStamp: 0,
      currentX: 0,
      currentY: 0,
      velocityX: 0,
      velocityY: 0,
      deltaX: 0,
      deltaY: 0,
      timeStamp: 0,
      data: undefined,
      isScrolling: true,
    };
  }

  componentWillLoad() {
    if (this.isServer) {
      return;
    }
    if (this.forceOverscroll === undefined) {
      this.forceOverscroll = this.mode === 'ios' && ('ontouchstart' in window);
    }
  }

  componentDidUnload() {
    if (this.watchDog) {
      clearInterval(this.watchDog);
    }
  }

  @Listen('scroll', { passive: true })
  onScroll(ev: UIEvent) {
    const timeStamp = Date.now();
    const didStart = !this.isScrolling;
    this.lastScroll = timeStamp;
    if (didStart) {
      this.onScrollStart();
    }
    if (!this.queued && this.scrollEvents) {
      this.queued = true;
      this.dom.read(timeStamp => {
        this.queued = false;
        this.detail.event = ev;
        updateScrollDetail(this.detail, this.el, timeStamp, didStart);
        this.ionScroll.emit(this.detail);
      });
    }
  }

  @Method()
  scrollToTop(duration: number): Promise<void> {
    return this.scrollToPoint(0, 0, duration);
  }

  @Method()
  scrollToBottom(duration: number): Promise<void> {
    const y = (this.el)
      ? this.el.scrollHeight - this.el.clientHeight
      : 0;

    return this.scrollToPoint(0, y, duration);
  }

  @Method()
  scrollByPoint(x: number, y: number, duration: number, done?: Function): Promise<any> {
    return this.scrollToPoint(x + this.el.scrollLeft, y + this.el.scrollTop, duration, done);
  }

  @Method()
  scrollToPoint(x: number, y: number, duration: number, done?: Function): Promise<any> {
    // scroll animation loop w/ easing
    // credit https://gist.github.com/dezinezync/5487119

    let promise: Promise<any>;
    if (done === undefined) {
      // only create a promise if a done callback wasn't provided
      // done can be a null, which avoids any functions
      promise = new Promise(resolve => {
        done = resolve;
      });
    }

    const self = this;
    const el = self.el;
    if (!el) {
      // invalid element
      done();
      return promise;
    }

    if (duration < 32) {
      el.scrollTop = y;
      el.scrollLeft = x;
      done();
      return promise;
    }

    const fromY = el.scrollTop;
    const fromX = el.scrollLeft;

    const maxAttempts = (duration / 16) + 100;

    let startTime: number;
    let attempts = 0;
    let stopScroll = false;

    // scroll loop
    function step(timeStamp: number) {
      attempts++;

      if (!self.el || stopScroll || attempts > maxAttempts) {
        self.isScrolling = false;
        el.style.transform = el.style.webkitTransform = '';
        done();
        return;
      }

      let time = Math.min(1, ((timeStamp - startTime) / duration));

      // where .5 would be 50% of time on a linear scale easedT gives a
      // fraction based on the easing method
      const easedT = (--time) * time * time + 1;

      if (fromY !== y) {
        el.scrollTop = (easedT * (y - fromY)) + fromY;
      }

      if (fromX !== x) {
        el.scrollLeft = Math.floor((easedT * (x - fromX)) + fromX);
      }

      if (easedT < 1) {
        // do not use DomController here
        // must use nativeRaf in order to fire in the next frame
        self.dom.raf(step);

      } else {
        stopScroll = true;
        self.isScrolling = false;
        el.style.transform = el.style.webkitTransform = '';
        done();
      }
    }

    // start scroll loop
    self.isScrolling = true;

    // chill out for a frame first
    this.dom.write(() => {
      this.dom.write(timeStamp => {
        startTime = timeStamp;
        step(timeStamp);
      });
    });

    return promise;
  }


  private onScrollStart() {
    this.isScrolling = true;
    this.ionScrollStart.emit({
      isScrolling: true
    });

    if (this.watchDog) {
      clearInterval(this.watchDog);
    }
    // watchdog
    this.watchDog = setInterval(() => {
      if (this.lastScroll < Date.now() - 120) {
        this.onScrollEnd();
      }
    }, 100);
  }

  private onScrollEnd() {

    clearInterval(this.watchDog);
    this.watchDog = null;
    this.isScrolling = false;
    this.ionScrollEnd.emit({
      isScrolling: false
    });
  }

  hostData() {
    return {
      class: {
        overscroll: this.forceOverscroll
      }
    };
  }

  render() {
    return [
      // scroll-inner is used to keep custom user padding
      <div class='scroll-inner'>
        <slot></slot>
      </div>
    ];
  }
}

// ******** DOM READ ****************
function updateScrollDetail(
  detail: ScrollDetail,
  el: HTMLElement,
  timeStamp: number,
  didStart: boolean
) {
  const scrollTop = el.scrollTop;
  const scrollLeft = el.scrollLeft;
  const positions = detail.positions;
  if (didStart) {
    // remember the start positions
    detail.startTimeStamp = timeStamp;
    detail.startY = scrollTop;
    detail.startX = scrollLeft;
    positions.length = 0;
  }

  detail.timeStamp = timeStamp;
  detail.currentY = detail.scrollTop = scrollTop;
  detail.currentX = detail.scrollLeft = scrollLeft;
  detail.deltaY = scrollTop - detail.startY;
  detail.deltaX = scrollLeft - detail.startX;

  // actively scrolling
  positions.push(scrollTop, scrollLeft, timeStamp);

  // move pointer to position measured 100ms ago
  const timeRange = timeStamp - 100;
  let startPos = positions.length - 1;

  while (startPos > 0 && positions[startPos] > timeRange) {
    startPos -= 3;
  }

  if (startPos > 3) {
    // compute relative movement between these two points
    const frequency = 1 / (positions[startPos] - timeStamp);
    const movedX = positions[startPos - 1] - scrollLeft;
    const movedY = positions[startPos - 2] - scrollTop;

    // based on XXms compute the movement to apply for each render step
    // velocity = space/time = s*(1/t) = s*frequency
    detail.velocityX = movedX * frequency;
    detail.velocityY = movedY * frequency;
  } else {
    detail.velocityX = 0;
    detail.velocityY = 0;
  }
}

export interface ScrollDetail extends GestureDetail, ScrollBaseDetail {
  positions: number[];
  scrollTop: number;
  scrollLeft: number;
}

export interface ScrollBaseDetail {
  isScrolling: boolean;
}

export interface ScrollCallback {
  (detail?: ScrollDetail): boolean|void;
}
