import { NativeModules } from 'react-native'
const { RNBranch } = NativeModules

/**
 * Class for generating standard and custom events with the Branch SDK.
 * @example
 * new BranchEvent(BranchEvent.ViewEvent, buo).logEvent()
 */
export default class BranchEvent {
  /**
   * The event name. May be a standard event name or a custom event name.
   * @type {string}
   */
  name = null

  /**
   * Array containing any Branch Universal Objects associated with this event.
   * @type {Object[]}
   */
  contentItems = []

  /**
   * Transaction ID associated with this event
   * @type {?string}
   */
  transactionID = null

  /**
   * ISO currency identifier associated with this event
   * @type {?string}
   */
  currency = null

  /**
   * Revenue associated with this event
   * @type {?(string|number)}
   */
  revenue = null

  /**
   * Shipping cost associated with this event
   * @type {?(string|number)}
   */
  shipping = null

  /**
   * Tax associated with this event
   * @type {?(string|number)}
   */
  tax = null

  /**
   * Coupon associated with this event
   * @type {?string}
   */
  coupon = null

  /**
   * Affiliation associated with this event
   * @type {?string}
   */
  affiliation = null

  /**
   * Description of this event
   * @type {?string}
   */
  description = null

  /**
   * Search query associated with this event
   * @type {?string}
   */
  searchQuery = null

  /**
   * Optional object containing custom data to associate with this event.
   * Values must be strings.
   * @type {?Object}
   */
  customData = null

  /**
   * Constructs a new BranchEvent from arguments
   *
   * @param {!string} name - The name of the event. May be a standard Branch event
   *   or a custom event name.
   * @param {?(Object|Object[])} contentItems - One or more Branch Universal Objects associated with this event, or null.
   * @param {?Object} params - Object containing params to be set in the constructor
   * @param {?string} params.transactionID - Initial value for the transactionID property
   * @param {?string} params.currency - Initial value for the currency property
   * @param {?(string|number)} params.revenue - Initial value for the revenue property
   * @param {?(string|number)} params.shipping - Initial value for the shipping property
   * @param {?(string|number)} params.tax - Initial value for the tax property
   * @param {?string} params.coupon - Initial value for the coupon property
   * @param {?string} params.affiliation - Initial value for the affiliation property
   * @param {?string} params.description - Initial value for the description property
   * @param {?string} params.searchQuery - Initial value for the searchQuery property
   * @param {?Object} params.customData - Initial value for the customData property
   */
  constructor(name, contentItems = null, params = {}) {
    this.name = name

    if (Array.isArray(contentItems)) {
      this.contentItems = contentItems
    }
    else if (contentItems) {
      this.contentItems = [contentItems]
    }

    if (params.transactionID) this.transactionID = params.transactionID
    if (params.currency) this.currency = params.currency
    if (params.revenue) this.revenue = params.revenue
    if (params.shipping) this.shipping = params.shipping
    if (params.tax) this.tax = params.tax
    if (params.coupon) this.coupon = params.coupon
    if (params.affiliation) this.affiliation = params.affiliation
    if (params.description) this.description = params.description
    if (params.searchQuery) this.searchQuery = params.searchQuery
    if (params.customData) this.customData = params.customData
  }

  /**
   * Log this event. This method is always successful. It queues events to be
   * transmitted whenever the service is available. It returns a promise that
   * is resolved once the native logEvent call is complete. The promise always
   * returns null.
   *
   * @return {null} Always returns null
   */
  async logEvent() {
    const idents = this.contentItems.map((b) => b.ident)

    try {
      return await RNBranch.logEvent(idents, this.name, this._convertParams())
    }
    catch (error) {
      if (error.code != 'RNBranch::Error::BUONotFound') {
        // This is the only reason this promise should ever be rejected,
        // but in case anything else is ever thrown, throw it out to the
        // caller.
        throw error
      }

      // Native BUO not found (expired from cache). Find the JS instance and
      // have it create a new native instance with a new ident.
      const ident = this._identFromMessage(error.message)
      const buo = this.contentItems.find((b) => b.ident == ident)
      await buo._newIdent()

      // Now that a fresh BUO has been created, call this method again.
      return await this.logEvent()
    }
  }

  // Parse the ident of the missing BUO out of the error text.
  _identFromMessage(message) {
    const match = /^.*ident\s([A-Fa-f0-9-]+).*$/.exec(message)
    if (match) return match[1]
    return null
  }

  _convertParams() {
    let params = {}

    if (this.transactionID) params.transactionID = this.transactionID
    if (this.currency) params.currency = this.currency

    // for the benefit of the NSDecimalNumber on iOS
    if (this.revenue) params.revenue = '' + this.revenue
    if (this.shipping) params.shipping = '' + this.shipping
    if (this.tax) params.tax = '' + this.tax

    if (this.coupon) params.coupon = this.coupon
    if (this.affiliation) params.affiliation = this.affiliation
    if (this.description) params.description = this.description
    if (this.searchQuery) params.searchQuery = this.searchQuery
    if (this.customData) {
      params.customData = this.customData
      for (const key in params.customData) {
        const valueType = typeof params.customData[key]
        if (valueType == 'string') continue
        console.warn('[Branch] customMetadata values must be strings. Value for property ' + key + ' has type ' + valueType + '.')
        // TODO: throw?
      }
    }

    return params
  }
}

// --- Standard event definitions ---

// Commerce events

/**
 * Standard Add to Cart event
 * @type {string}
 */
BranchEvent.AddToCart = RNBranch.STANDARD_EVENT_ADD_TO_CART

/**
 * Standard Add to Wishlist event
 * @type {string}
 */
BranchEvent.AddToWishlist = RNBranch.STANDARD_EVENT_ADD_TO_WISHLIST

/**
 * Standard View Cart event
 * @type {string}
 */
BranchEvent.ViewCart = RNBranch.STANDARD_EVENT_VIEW_CART

/**
 * Standard Initiate Purchase event
 * @type {string}
 */
BranchEvent.InitiatePurchase = RNBranch.STANDARD_EVENT_INITIATE_PURCHASE

/**
 * Standard Add Payment Info event
 * @type {string}
 */
BranchEvent.AddPaymentInfo = RNBranch.STANDARD_EVENT_ADD_PAYMENT_INFO

/**
 * Standard Purchase event
 * @type {string}
 */
BranchEvent.Purchase = RNBranch.STANDARD_EVENT_PURCHASE

/**
 * Standard Spend Credits event
 * @type {string}
 */
BranchEvent.SpendCredits = RNBranch.STANDARD_EVENT_SPEND_CREDITS

// Content events

/**
 * Standard Search event
 * @type {string}
 */
BranchEvent.Search = RNBranch.STANDARD_EVENT_SEARCH

/**
 * Standard View Item event for a single Branch Universal Object
 * @type {string}
 */
BranchEvent.ViewItem = RNBranch.STANDARD_EVENT_VIEW_ITEM

/**
 * Standard View Items event for multiple Branch Universal Objects
 * @type {string}
 */
BranchEvent.ViewItems = RNBranch.STANDARD_EVENT_VIEW_ITEMS

/**
 * Standard Rate event
 * @type {string}
 */
BranchEvent.Rate = RNBranch.STANDARD_EVENT_RATE

/**
 * Standard Share event
 * @type {string}
 */
BranchEvent.Share = RNBranch.STANDARD_EVENT_SHARE

// User Lifecycle Events

/**
 * Standard Complete Registration event
 * @type {string}
 */
BranchEvent.CompleteRegistration = RNBranch.STANDARD_EVENT_COMPLETE_REGISTRATION

/**
 * Standard Complete Tutorial event
 * @type {string}
 */
BranchEvent.CompleteTutorial = RNBranch.STANDARD_EVENT_COMPLETE_TUTORIAL

/**
 * Standard Achieve Level event
 * @type {string}
 */
BranchEvent.AchieveLevel = RNBranch.STANDARD_EVENT_ACHIEVE_LEVEL

/**
 * Standard Unlock Achievement event
 * @type {string}
 */
BranchEvent.UnlockAchievement = RNBranch.STANDARD_EVENT_UNLOCK_ACHIEVEMENT