import "./form-pages.scss";
import { getOptionsSelectorAlphaChars } from "./utils.js";

/**
* @typedef {('horizontal'|'vertical')} PaginationDirection
*/

/**
 * @typedef {('next'|'prev'|'none')} Direction
 */

/**
 * @typedef {Object} FormPagesOptions
 * All properties which the suffix is "Class" can be an array, which will be
 * joined and cleaned before being added to the elements.
 * The value of `this` on all callbacks is the jQuery element you applied the plugin to.
 * @property {string} activePageClass=".form-pages__page--active" The active page selector.
 * @property {string} adaptiveContainerHeight=true Tells if the form container height must be adaptive to the active page.
 * @property {string} formPageClass=".form-pages__page" The selector which will separate the form pages.
 * @property {string} formPagesContainerAdaptiveHeightClass=".form-pages__page-container--adaptive-height" The selector for the pages container when adaptiveContainerHeight is set to true.
 * @property {string} nextButtonClass=".form-pages__next-button" The selector for the "next" button.
 * @property {string} prevButtonClass=".form-pages__prev-button" The selector for the "previous" button.
 * @property {string} submitButtonClass=".form-pages__submit-button" The selector for the form submit button.
 * @property {PaginationDirection} paginationDirection="horizontal" The direction that the form will move.
 * @property {function?} onInitialized Callback for when plugin finishes loading.
 * @property {function?} onMovedPage Callback for when page moves.
 * @property {function?} onNextPage Callback for when the form goes to the next page.
 * @property {function?} onPrevPage Callback for when the form goes to the previous page.
 * @property {function?} onSubmitForm Callback for when the form gets submitted.
 * @property {function?} onRecalculateContainerHeight Callback for when the form's container height is recalculated. Triggered **only** when `adaptiveContainerHeight` is true.
 * @property {function?} shouldMoveForwards Callback to validate if the form should move forwards. Useful for validation.
 */

/**
 * @enum {{string: string}} EventList
 */

/**
 * @typedef Dimensions
 * @property {number} width
 * @property {number} height
 */

const PLUGIN_NAME = "formPages",
  EVENT_NAMESPACE_PREFIX = "fp",

  /**
   * @type {EventList}
   * @private
   */
  Events = {
    NEXT_PAGE: `next.page.${EVENT_NAMESPACE_PREFIX}`,
    PREV_PAGE: `prev.page.${EVENT_NAMESPACE_PREFIX}`,
    INITIALIZED: `initialized.${EVENT_NAMESPACE_PREFIX}`,
    PAGE_MOVED: `moved.${EVENT_NAMESPACE_PREFIX}`,
    UPDATE_ADAPTIVE_CONTAINER_HEIGHT: `recalculate-height.${EVENT_NAMESPACE_PREFIX}`
  },
  PaginationDirection = {
    HORIZONTAL: "horizontal",
    VERTICAL: "vertical"
  },
  CALLBACKS = [
    {
      name: "onNextPage",
      associatedEvent: Events.NEXT_PAGE
    },
    {
      name: "onPrevPage",
      associatedEvent: Events.PREV_PAGE
    },
    {
      name: "onInitialized",
      associatedEvent: Events.INITIALIZED
    },
    {
      name: "onMovedPage",
      associatedEvent: Events.PAGE_MOVED
    },
    {
      name: "onRecalculateContainerHeight",
      associatedEvent: Events.UPDATE_ADAPTIVE_CONTAINER_HEIGHT
    }
  ];


/** @type {FormPagesOptions} */
let defaults = {
  activePageClass: ".form-pages__page--active",
  adaptiveContainerHeight: true,
  formPageClass: ".form-pages__page",
  formPagesContainerClass: ".form-pages__page-container",
  formPagesContainerAdaptiveHeightClass: "form-pages__page-container--adaptive-height",
  nextButtonClass: ".form-pages__next-button",
  paginationDirection: PaginationDirection.HORIZONTAL,
  prevButtonClass: ".form-pages__prev-button",
  submitButtonClass: ".form-pages__submit-button",
  onInitialized() { },
  onNextPage() { },
  onPrevPage() { },
  shouldMoveForwards() {
    return true;
  },
  onMovedPage() { },
  onRecalculateContainerHeight() { }
};

/**
 * @class
 * @description Creates the FormPages component.
 * @property {jQuery} $element The form which the plugin is constructed upon.
 * @property {FormPagesOptions} options
 * @property {jQuery} $formPagesContainer The container of the pages
 * @property {jQuery} $pages The available pages
 * @property {number} currentPage The current page index, starting on zero.
 * @param {jQuery!} element The main form element.
 * @param {FormPagesOptions?} options
 */
function FormPages( element, options ) {
  this.$element = $( element );
  this.options = $.extend( {}, defaults, options );

  this.$formPagesContainer = $( "<div></div>" );
  this.$pages = null;

  // Control variables
  this.currentPageIndexIndex = 0;

  this._defaults = defaults;
  this._name = PLUGIN_NAME;

  this.getOptionsSelectorAlphaChars = getOptionsSelectorAlphaChars.bind( this );

  init.call( this );
}

/**
 * Initializes the plugin.
 */
function init() {
  const self = this;

  this.$pages = this.$element.find( this.options.formPageClass );

  // Adding the form pages container class to the $formPagesContainer.
  this.$formPagesContainer.addClass(
    this.getOptionsSelectorAlphaChars( "formPagesContainerClass" ) );

  // Step 1: Add the correspondent classes
  // Removing the active classes from the pages as a way to prevent wrongly
  // presented pages, then we add the active page class to the first page.
  this.$pages
    .removeClass( this.getOptionsSelectorAlphaChars( "activePageClass" ) )
    .first()
    .addClass( this.getOptionsSelectorAlphaChars( "activePageClass" ) );

  // Step 2: Move the pages to the container
  this.$pages.appendTo( this.$formPagesContainer );
  this.$element.append( this.$formPagesContainer );

  // Step 3: Configuring the default triggers.
  function configureDefaultTriggers() {

    // Proxying the configured event callbacks
    $.each( CALLBACKS, function( i, callback ) {
      const callbackFn = self.options[ callback.name ];
      if ( !callbackFn ) {
        return;
      }

      let eventData = { currentPage: self.currentPageIndexIndex };

      // Some special eventData treatment for some events.
      if ( callback.associatedEvent === Events.INITIALIZED ||
        callback.associatedEvent === Events.PAGE_MOVED ) {
        eventData = { instance: self };
      }

      // Adding the default configured callbacks to the events
      self.options[ callback.name ] = callbackFn.bind( self.$element,
        eventData );

      callback.associatedEvent && self.on( callback.associatedEvent,
        self.options[ callback.name ] );
    } );

    self.options.shouldMoveForwards = self.options.shouldMoveForwards.bind( self );

    // If valid, we always move on next or previous events.
    self.on( Events.PREV_PAGE, function() {
      self.goToPrevPage();
    } );

    self.on( Events.NEXT_PAGE, function() {
      self.goToNextPage();
    } );

    self.on( Events.UPDATE_ADAPTIVE_CONTAINER_HEIGHT, updateAdaptiveContainerHeight.bind( self ) );

    self.on( "click", function( e ) {
      const $currentTarget = $( e.currentTarget );

      // We should prevent default when clicked on "next" or "prev" buttons.
      // to avoid sending the form.
      // We manually check if the form can move forwards or backwards so
      // that we avoid triggering the event when the movement is out of
      // boundaries.
      console.group( "Responding to the 'click' navigation buttons:" );
      if ( $currentTarget.is( self.options.prevButtonClass ) ) {
        console.log( "Previous button clicked" );
        e.preventDefault();
        console.log( `self.canMoveBackwards(): ${self.canMoveBackwards()}` );
        self.canMoveBackwards() && self.trigger( Events.PREV_PAGE );
      } else if ( $currentTarget.is( self.options.nextButtonClass ) ) {
        console.log( "Next button clicked" );
        e.preventDefault();
        console.log( `self.canMoveForwards(): ${self.canMoveForwards()}` );
        console.log( `self.options.shouldMoveForwards(): ${self.options.shouldMoveForwards()}` );
        self.canMoveForwards() &&
          self.options.shouldMoveForwards() &&
          self.trigger( Events.NEXT_PAGE );
      }
      console.groupEnd();
    }, `${self.options.prevButtonClass}, ${self.options.nextButtonClass}` );
  }

  function configureContainerFormClasses() {
    let classes = "form-pages--active";
    if ( self.options.paginationDirection === PaginationDirection.VERTICAL ) {
      classes += " form-pages--vertical";
    }
    self.$element.addClass( classes );
  }

  configureDefaultTriggers();
  configureContainerFormClasses();

  if ( self.options.adaptiveContainerHeight ) {
    console.log( `Should have adaptive height: ${self.options.adaptiveContainerHeight}` );
    self.$formPagesContainer
      .addClass( self.options.formPagesContainerAdaptiveHeightClass )
      .height( self.getCurrentPageElement().height() );
    self.on( Events.PAGE_MOVED, adaptContainerHeightOnChangePage.bind( self ) );
  }

  self.options.onInitialized.call( this, this );

  // Configuring window resize trigger to recalculate the pages translation
  $( window ).on( "resize", function() {
    self.translateToPage( self.currentPageIndexIndex );
  } );
}

/**
 * Adapts the container height when page changed.
 */
function adaptContainerHeightOnChangePage() {
  console.group( "adaptContainerHeightOnChangePage" );
  console.log( "this.getCurrentPageElement():", this.getCurrentPageElement() );
  this.trigger( Events.UPDATE_ADAPTIVE_CONTAINER_HEIGHT );
  console.groupEnd();
};

/**
 * If the `adaptiveContainerHeight` is set to true, this function recalculates
 * the height of the container.
 */
function updateAdaptiveContainerHeight() {
  if ( !this.options.adaptiveContainerHeight ) {
    return;
  }

  this.$formPagesContainer.css( "height", this.getCurrentPageElement().height() );
};

/**
 * Makes a proxy and calls events to the main `$element` object, passing the
 * current page as event data.
 * @param {string} eventName
 * @param {object} params Params passed to the jQuery trigger function to be attached as event data.
 */
FormPages.prototype.trigger = function( eventName, params = {} ) {
  this.$element.trigger( eventName, $.extend( {}, params,
    {
      currentPage: this.currentPageIndexIndex,
      formPagesContainer: this.$formPagesContainer,
      currentPageElement: this.getCurrentPageElement()
    } ) );
};

/**
 * Configures events to the plugin
 * @param {string} eventName
 * @param {function} cb Event callback
 * @param {string} filter
 * See {@tutorial event-handling}
 */
FormPages.prototype.on = function( eventName, cb, filter = null ) {
  this.$element.on( eventName, filter, {
    currentPage: this.currentPageIndexIndex,
    formPagesContainer: this.$formPagesContainer,
    currentPageElement: this.getCurrentPageElement()
  }, cb );
};

/**
 * Checks if the pages can move forwards.
 * @return {boolean}
 */
FormPages.prototype.canMoveForwards = function() {
  return this.currentPageIndexIndex + 1 < this.getTotalPages();
};

/**
 * Checks if the pages can move backwards.
 * @return {boolean}
 */
FormPages.prototype.canMoveBackwards = function() {
  return this.currentPageIndexIndex - 1 >= 0;
};

/**
 * Checks the amount of the elements that matches to the
 * `this.options.formPageClass` option value.
 * @return {number}
 */
FormPages.prototype.getTotalPages = function() {
  return this.$element.find( this.options.formPageClass ).length;
};

/**
 * Tries to move the form to a specific page.
 * This also validates if the move is allowed (not out of bounds).
 * In case the component can't move to the desired page, it returns the
 * current page.
 * @param {number} page the page index - 1
 * @return {number} The current page or the page the component moved to.
 */
FormPages.prototype.goTo = function( page ) {
  console.group( `goTo(${page})` );

  /** @type {Direction} */
  let movingDirection = "next";

  // Page must be bigger than zero and less than the total pages
  if ( !( page < 0 || page > this.getTotalPages() ) ) {
    movingDirection = page > this.currentPageIndexIndex ? "next" : "prev";
    this.currentPageIndexIndex = page;
    console.log( `page: ${page}` );
    console.log( `this.currentPageIndexIndex: ${this.currentPageIndexIndex}` );
  } else {
    movingDirection = "none";
  }

  if ( movingDirection === "none" ) {
    return this.currentPageIndexIndex;
  }

  console.log( `movingDirection: ${movingDirection}` );

  // Animating the pages
  const $activePage = this.$formPagesContainer.find( this.options.activePageClass );

  $activePage
    .removeClass( this.getOptionsSelectorAlphaChars( "activePageClass" ) );
  this.translateToPage( page );

  console.groupEnd();

  // Triggering PAGE_MOVED event.
  this.trigger( Events.PAGE_MOVED );

  return this.currentPageIndexIndex;
};

/**
 * Translates the page to show the active one. Useful when page resizes.
 * @param {number} page
 * @todo make the translation works on Y axis.
 * @private
 */
FormPages.prototype.translateToPage = function( page ) {
  console.group( `translateToPage(${page})` );
  console.log( `currentPage: ${this.currentPageIndexIndex}` );

  // $nextPageToBeShown can be the previous page also. "next" in this case does
  // not imply direction or position.
  let $nextPageToBeShown = this.$formPagesContainer
    .find( this.options.formPageClass ).eq( page ),
    translationX =
      `${this.getPageDimensions().width * ( this.currentPageIndexIndex )}`;

  console.log( "$nextPageToBeShown: ", $nextPageToBeShown );

  if ( page > this.currentPageIndexIndex - 1 ) {
    console.log( "this.currentPageIndexIndex > page: ", this.currentPageIndexIndex > page );
    translationX = `-${translationX}`;
  }

  $nextPageToBeShown.addClass(
    this.getOptionsSelectorAlphaChars( "activePageClass" ) );
  this.$formPagesContainer.css( "transform",
    `translateX(${translationX}px)` );
  console.groupEnd();
};

/**
 * Tries to move the form to the next page and returns the current page.
 * @return {number} The page the component moved to or the current page.
 */
FormPages.prototype.goToNextPage = function() {
  console.group( "goToNextPage()" );
  console.log( "shouldMoveForwards() ", this.options.shouldMoveForwards() );
  console.log( "this.currentPageIndexIndex: " + this.currentPageIndexIndex );
  console.groupEnd();
  return this.options.shouldMoveForwards() ?
    this.goTo( this.currentPageIndexIndex + 1 ) :
    this.currentPageIndexIndex;
};

/**
 * Tries to move the form to the previous page and returns the current page.
 * @return {number} The page the component moved to or the current page.
 */
FormPages.prototype.goToPrevPage = function() {
  console.group( "goToNextPage()" );
  console.log( "shouldMoveForwards() ", this.options.shouldMoveForwards() );
  console.log( "this.currentPageIndexIndex: " + this.currentPageIndexIndex );
  console.groupEnd();
  return this.goTo( this.currentPageIndexIndex - 1 );
};

/**
 * Gets the pages' parent's dimensions.
 * @return {Dimensions}
 */
FormPages.prototype.getParentDimensions = function() {
  return {
    width: this.$element.outerWidth(),
    height: this.$element.outerHeight()
  };
};

/**
 * Gets the dimensions of the page to help adjust possible animations.
 * @param {number} pageNumber
 * @returns {Dimensions}
 */
FormPages.prototype.getPageDimensions = function( pageNumber = 1 ) {

  /** @type {Dimensions} */
  let result = {},
    $pageEl = this.$element.find( this.options.formPageClass )
      .eq( pageNumber - 1 );
  result.width = $pageEl.outerWidth();
  result.height = $pageEl.outerHeight();

  return result;
};

/**
 * Gets the active page as jQuery object.
 *
 * @returns {jQuery|null}
 */
FormPages.prototype.getCurrentPageElement = function() {
  return this.$element.find( this.options.formPageClass ).eq( this.currentPageIndexIndex );
};


( function( $, window, document, undefined ) {

  /**
   * @param {FormPagesOptions} options
   */
  $.fn[ PLUGIN_NAME ] = function( options ) {
    return this.each( function() {
      if ( !$.data( this, "plugin_" + PLUGIN_NAME ) ) {
        $.data( this, "plugin_" + PLUGIN_NAME, new FormPages( this, options ) );
      }
    } );
  };
} )( jQuery, window, document );