import { AccountID } from './account-id'
import { Currency } from './currency'
import { BinaryParser } from '../serdes/binary-parser'
import { SerializedType, JsonObject } from './serialized-type'
import { bytesToHex, concat } from '@xrplf/isomorphic/utils'

/**
 * Constants for separating Paths in a PathSet
 */
const PATHSET_END_BYTE = 0x00
const PATH_SEPARATOR_BYTE = 0xff

/**
 * Constant for masking types of a Hop
 */
const TYPE_ACCOUNT = 0x01
const TYPE_CURRENCY = 0x10
const TYPE_ISSUER = 0x20

/**
 * The object representation of a Hop, an issuer AccountID, an account AccountID, and a Currency
 */
interface HopObject extends JsonObject {
  issuer?: string
  account?: string
  currency?: string
}

/**
 * TypeGuard for HopObject
 */
function isHopObject(arg): arg is HopObject {
  return (
    arg.issuer !== undefined ||
    arg.account !== undefined ||
    arg.currency !== undefined
  )
}

/**
 * TypeGuard for PathSet
 */
function isPathSet(arg): arg is Array<Array<HopObject>> {
  return (
    (Array.isArray(arg) && arg.length === 0) ||
    (Array.isArray(arg) && Array.isArray(arg[0]) && arg[0].length === 0) ||
    (Array.isArray(arg) && Array.isArray(arg[0]) && isHopObject(arg[0][0]))
  )
}

/**
 * Serialize and Deserialize a Hop
 */
class Hop extends SerializedType {
  /**
   * Create a Hop from a HopObject
   *
   * @param value Either a hop or HopObject to create a hop with
   * @returns a Hop
   */
  static from(value: Hop | HopObject): Hop {
    if (value instanceof Hop) {
      return value
    }

    const bytes: Array<Uint8Array> = [Uint8Array.from([0])]

    if (value.account) {
      bytes.push(AccountID.from(value.account).toBytes())
      bytes[0][0] |= TYPE_ACCOUNT
    }

    if (value.currency) {
      bytes.push(Currency.from(value.currency).toBytes())
      bytes[0][0] |= TYPE_CURRENCY
    }

    if (value.issuer) {
      bytes.push(AccountID.from(value.issuer).toBytes())
      bytes[0][0] |= TYPE_ISSUER
    }

    return new Hop(concat(bytes))
  }

  /**
   * Construct a Hop from a BinaryParser
   *
   * @param parser BinaryParser to read the Hop from
   * @returns a Hop
   */
  static fromParser(parser: BinaryParser): Hop {
    const type = parser.readUInt8()
    const bytes: Array<Uint8Array> = [Uint8Array.from([type])]

    if (type & TYPE_ACCOUNT) {
      bytes.push(parser.read(AccountID.width))
    }

    if (type & TYPE_CURRENCY) {
      bytes.push(parser.read(Currency.width))
    }

    if (type & TYPE_ISSUER) {
      bytes.push(parser.read(AccountID.width))
    }

    return new Hop(concat(bytes))
  }

  /**
   * Get the JSON interpretation of this hop
   *
   * @returns a HopObject, an JS object with optional account, issuer, and currency
   */
  toJSON(): HopObject {
    const hopParser = new BinaryParser(bytesToHex(this.bytes))
    const type = hopParser.readUInt8()

    let account, currency, issuer
    if (type & TYPE_ACCOUNT) {
      account = (AccountID.fromParser(hopParser) as AccountID).toJSON()
    }

    if (type & TYPE_CURRENCY) {
      currency = (Currency.fromParser(hopParser) as Currency).toJSON()
    }

    if (type & TYPE_ISSUER) {
      issuer = (AccountID.fromParser(hopParser) as AccountID).toJSON()
    }

    const result: HopObject = {}
    if (account) {
      result.account = account
    }

    if (issuer) {
      result.issuer = issuer
    }

    if (currency) {
      result.currency = currency
    }

    return result
  }

  /**
   * get a number representing the type of this hop
   *
   * @returns a number to be bitwise and-ed with TYPE_ constants to describe the types in the hop
   */
  type(): number {
    return this.bytes[0]
  }
}

/**
 * Class for serializing/deserializing Paths
 */
class Path extends SerializedType {
  /**
   * construct a Path from an array of Hops
   *
   * @param value Path or array of HopObjects to construct a Path
   * @returns the Path
   */
  static from(value: Path | Array<HopObject>): Path {
    if (value instanceof Path) {
      return value
    }

    const bytes: Array<Uint8Array> = []
    value.forEach((hop: HopObject) => {
      bytes.push(Hop.from(hop).toBytes())
    })

    return new Path(concat(bytes))
  }

  /**
   * Read a Path from a BinaryParser
   *
   * @param parser BinaryParser to read Path from
   * @returns the Path represented by the bytes read from the BinaryParser
   */
  static fromParser(parser: BinaryParser): Path {
    const bytes: Array<Uint8Array> = []
    while (!parser.end()) {
      bytes.push(Hop.fromParser(parser).toBytes())

      if (
        parser.peek() === PATHSET_END_BYTE ||
        parser.peek() === PATH_SEPARATOR_BYTE
      ) {
        break
      }
    }
    return new Path(concat(bytes))
  }

  /**
   * Get the JSON representation of this Path
   *
   * @returns an Array of HopObject constructed from this.bytes
   */
  toJSON(): Array<HopObject> {
    const json: Array<HopObject> = []
    const pathParser = new BinaryParser(this.toString())

    while (!pathParser.end()) {
      json.push(Hop.fromParser(pathParser).toJSON())
    }

    return json
  }
}

/**
 * Deserialize and Serialize the PathSet type
 */
class PathSet extends SerializedType {
  /**
   * Construct a PathSet from an Array of Arrays representing paths
   *
   * @param value A PathSet or Array of Array of HopObjects
   * @returns the PathSet constructed from value
   */
  static from<T extends PathSet | Array<Array<HopObject>>>(value: T): PathSet {
    if (value instanceof PathSet) {
      return value
    }

    if (isPathSet(value)) {
      const bytes: Array<Uint8Array> = []

      value.forEach((path: Array<HopObject>) => {
        bytes.push(Path.from(path).toBytes())
        bytes.push(Uint8Array.from([PATH_SEPARATOR_BYTE]))
      })

      bytes[bytes.length - 1] = Uint8Array.from([PATHSET_END_BYTE])

      return new PathSet(concat(bytes))
    }

    throw new Error('Cannot construct PathSet from given value')
  }

  /**
   * Construct a PathSet from a BinaryParser
   *
   * @param parser A BinaryParser to read PathSet from
   * @returns the PathSet read from parser
   */
  static fromParser(parser: BinaryParser): PathSet {
    const bytes: Array<Uint8Array> = []

    while (!parser.end()) {
      bytes.push(Path.fromParser(parser).toBytes())
      bytes.push(parser.read(1))

      if (bytes[bytes.length - 1][0] == PATHSET_END_BYTE) {
        break
      }
    }

    return new PathSet(concat(bytes))
  }

  /**
   * Get the JSON representation of this PathSet
   *
   * @returns an Array of Array of HopObjects, representing this PathSet
   */
  toJSON(): Array<Array<HopObject>> {
    const json: Array<Array<HopObject>> = []
    const pathParser = new BinaryParser(this.toString())

    while (!pathParser.end()) {
      json.push(Path.fromParser(pathParser).toJSON())
      pathParser.skip(1)
    }

    return json
  }
}

export { PathSet }
