/**
 * @file Hive asset type definitions and helpers.
 * @author Johan Nordberg <code@johan-nordberg.com>
 * @license
 * Copyright (c) 2017 Johan Nordberg. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 *
 *  1. Redistribution of source code must retain the above copyright notice, this
 *     list of conditions and the following disclaimer.
 *
 *  2. Redistribution in binary form must reproduce the above copyright notice,
 *     this list of conditions and the following disclaimer in the documentation
 *     and/or other materials provided with the distribution.
 *
 *  3. Neither the name of the copyright holder nor the names of its contributors
 *     may be used to endorse or promote products derived from this software without
 *     specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * You acknowledge that this software is not designed, licensed or intended for use
 * in the design, construction, operation or maintenance of any military facility.
 */

import * as assert from 'assert'
import * as ByteBuffer from 'bytebuffer'

export interface SMTAsset {
  amount: string | number
  precision: number
  nai: string
}

/**
 * Asset symbol string.
 */
export type AssetSymbol =
  | 'HIVE'
  | 'VESTS'
  | 'HBD'
  | 'TESTS'
  | 'TBD'
  | 'STEEM'
  | 'SBD'

/**
 * Class representing a hive asset, e.g. `1.000 HIVE` or `12.112233 VESTS`.
 */
export class Asset {
  constructor(
    public readonly amount: number,
    public readonly symbol: AssetSymbol
  ) {}

  /**
   * Create a new Asset instance from a string, e.g. `42.000 HIVE`.
   */
  public static fromString(string: string, expectedSymbol?: AssetSymbol) {
    const [amountString, symbol] = string.split(' ')
    if (
      !['HIVE', 'VESTS', 'HBD', 'TESTS', 'TBD', 'SBD', 'STEEM'].includes(symbol)
    ) {
      throw new Error(`Invalid asset symbol: ${symbol}`)
    }
    if (expectedSymbol && symbol !== expectedSymbol) {
      throw new Error(
        `Invalid asset, expected symbol: ${expectedSymbol} got: ${symbol}`
      )
    }
    const amount = Number.parseFloat(amountString)
    if (!Number.isFinite(amount)) {
      throw new Error(`Invalid asset amount: ${amountString}`)
    }
    return new Asset(amount, symbol as AssetSymbol)
  }

  /**
   * Convenience to create new Asset.
   * @param symbol Symbol to use when created from number. Will also be used to validate
   *               the asset, throws if the passed value has a different symbol than this.
   */
  public static from(value: string | Asset | number, symbol?: AssetSymbol) {
    if (value instanceof Asset) {
      if (symbol && value.symbol !== symbol) {
        throw new Error(
          `Invalid asset, expected symbol: ${symbol} got: ${value.symbol}`
        )
      }
      return value
    } else if (typeof value === 'number' && Number.isFinite(value)) {
      return new Asset(value, symbol || 'STEEM')
    } else if (typeof value === 'string') {
      return Asset.fromString(value, symbol)
    } else {
      throw new Error(`Invalid asset '${String(value)}'`)
    }
  }

  /**
   * Return the smaller of the two assets.
   */
  public static min(a: Asset, b: Asset) {
    assert(
      a.symbol === b.symbol,
      'can not compare assets with different symbols'
    )
    return a.amount < b.amount ? a : b
  }

  /**
   * Return the larger of the two assets.
   */
  public static max(a: Asset, b: Asset) {
    assert(
      a.symbol === b.symbol,
      'can not compare assets with different symbols'
    )
    return a.amount > b.amount ? a : b
  }

  /**
   * Return asset precision.
   */
  public getPrecision(): number {
    switch (this.symbol) {
      case 'TESTS':
      case 'TBD':
      case 'HIVE':
      case 'HBD':
      case 'SBD':
      case 'STEEM':
        return 3
      case 'VESTS':
        return 6
    }
  }

  /**
   * returns a representation of this asset using only STEEM SBD for
   * legacy purposes
   */
  public steem_symbols(): Asset {
    switch (this.symbol) {
      case 'HIVE':
        return Asset.from(this.amount, 'STEEM')
      case 'HBD':
        return Asset.from(this.amount, 'SBD')
      default:
        return this
    }
  }

  /**
   * Return a string representation of this asset, e.g. `42.000 HIVE`.
   */
  public toString(): string {
    return `${this.amount.toFixed(this.getPrecision())} ${this.symbol}`
  }

  /**
   * Return a new Asset instance with amount added.
   */
  public add(amount: Asset | string | number): Asset {
    const other = Asset.from(amount, this.symbol)
    assert(this.symbol === other.symbol, 'can not add with different symbols')
    return new Asset(this.amount + other.amount, this.symbol)
  }

  /**
   * Return a new Asset instance with amount subtracted.
   */
  public subtract(amount: Asset | string | number): Asset {
    const other = Asset.from(amount, this.symbol)
    assert(
      this.symbol === other.symbol,
      'can not subtract with different symbols'
    )
    return new Asset(this.amount - other.amount, this.symbol)
  }

  /**
   * Return a new Asset with the amount multiplied by factor.
   */
  public multiply(factor: Asset | string | number): Asset {
    const other = Asset.from(factor, this.symbol)
    assert(
      this.symbol === other.symbol,
      'can not multiply with different symbols'
    )
    return new Asset(this.amount * other.amount, this.symbol)
  }

  /**
   * Return a new Asset with the amount divided.
   */
  public divide(divisor: Asset | string | number): Asset {
    const other = Asset.from(divisor, this.symbol)
    assert(
      this.symbol === other.symbol,
      'can not divide with different symbols'
    )
    return new Asset(this.amount / other.amount, this.symbol)
  }

  /**
   * For JSON serialization, same as toString().
   */
  public toJSON(): string {
    return this.toString()
  }
}

export type PriceType = Price | { base: Asset | string; quote: Asset | string }

/**
 * Represents quotation of the relative value of asset against another asset.
 * Similar to 'currency pair' used to determine value of currencies.
 *
 *  For example:
 *  1 EUR / 1.25 USD where:
 *  1 EUR is an asset specified as a base
 *  1.25 USD us an asset specified as a qute
 *
 *  can determine value of EUR against USD.
 */
export class Price {
  /**
   * @param base  - represents a value of the price object to be expressed relatively to quote
   *                asset. Cannot have amount == 0 if you want to build valid price.
   * @param quote - represents an relative asset. Cannot have amount == 0, otherwise
   *                asertion fail.
   *
   * Both base and quote shall have different symbol defined.
   */
  constructor(public readonly base: Asset, public readonly quote: Asset) {
    assert(
      base.amount !== 0 && quote.amount !== 0,
      'base and quote assets must be non-zero'
    )
    assert(
      base.symbol !== quote.symbol,
      'base and quote can not have the same symbol'
    )
  }

  /**
   * Convenience to create new Price.
   */
  public static from(value: PriceType) {
    if (value instanceof Price) {
      return value
    } else {
      return new Price(Asset.from(value.base), Asset.from(value.quote))
    }
  }

  /**
   * Return a string representation of this price pair.
   */
  public toString() {
    return `${this.base}:${this.quote}`
  }

  /**
   * Return a new Asset with the price converted between the symbols in the pair.
   * Throws if passed asset symbol is not base or quote.
   */
  public convert(asset: Asset) {
    if (asset.symbol === this.base.symbol) {
      assert(this.base.amount > 0)
      return new Asset(
        (asset.amount * this.quote.amount) / this.base.amount,
        this.quote.symbol
      )
    } else if (asset.symbol === this.quote.symbol) {
      assert(this.quote.amount > 0)
      return new Asset(
        (asset.amount * this.base.amount) / this.quote.amount,
        this.base.symbol
      )
    } else {
      throw new Error(`Can not convert ${asset} with ${this}`)
    }
  }
}
