/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
*   http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import Foundation
import CoreFoundation

public enum TCType: UInt8 {
  case stop          = 0x00
  case boolean_TRUE  = 0x01
  case boolean_FALSE = 0x02
  case i8            = 0x03
  case i16           = 0x04
  case i32           = 0x05
  case i64           = 0x06
  case double        = 0x07
  case binary        = 0x08
  case list          = 0x09
  case set           = 0x0A
  case map           = 0x0B
  case `struct`      = 0x0C
  
  public static let typeMask: UInt8 = 0xE0 // 1110 0000
  public static let typeBits: UInt8 = 0x07 // 0000 0111
  public static let typeShiftAmount = 5
 
}


public class TCompactProtocol: TProtocol {
  public static let protocolID: UInt8  = 0x82
  public static let version: UInt8     = 1
  public static let versionMask: UInt8 = 0x1F // 0001 1111
  
  public var transport: TTransport
  
  var lastField: [UInt8] = []
  var lastFieldId: UInt8 = 0
  
  var boolFieldName: String?
  var boolFieldType: TType?
  var boolFieldId: Int32?
  var booleanValue: Bool?
  
  var currentMessageName: String?

  public required init(on transport: TTransport) {
    self.transport = transport
  }

  
  /// Mark: - TCompactProtocol helpers
  
  func writebyteDirect(_ byte: UInt8) throws {
    let byte = Data([byte])
    try ProtocolTransportTry(error: TProtocolError(message: "Transport Write Failed")) {
      try self.transport.write(data: byte)
    }
  }
  
  func writeVarint32(_ val: UInt32) throws {
    var val = val
    var i32buf = [UInt8](repeating: 0, count: 5)
    var idx = 0
    while true {
      if (val & ~0x7F) == 0 {
        i32buf[idx] = UInt8(val)
        idx += 1
        break
      } else {
        i32buf[idx] = UInt8((val & 0x7F) | 0x80)
        idx += 1
        val >>= 7
      }
    }
    
    try ProtocolTransportTry(error: TProtocolError(message: "Transport Write Failed")) {
      try self.transport.write(data: Data(i32buf[0..<idx]))
    }
  }
  
  func writeVarint64(_ val: UInt64) throws {
    var val = val
    var varint64out = [UInt8](repeating: 0, count: 10)
    var idx = 0
    while true {
      if (val & ~0x7F) == 0{
        varint64out[idx] = UInt8(val)
        idx += 1
        break
      } else {
        varint64out[idx] = UInt8(val & 0x7F) | 0x80
        idx += 1
        val >>= 7
      }
    }
    
    try ProtocolTransportTry(error: TProtocolError(message: "Transport Write Failed")) {
      try self.transport.write(data: Data(varint64out[0..<idx]))
    }
  }
  
  func writeCollectionBegin(_ elementType: TType, size: Int32) throws {
    let ctype = compactType(elementType).rawValue
    if size <= 14 {
      try writebyteDirect(UInt8(size << 4) | ctype)
    } else {
      try writebyteDirect(0xF0 | ctype)
      try writeVarint32(UInt32(size))
    }
  }
  
  func readBinary(_ size: Int) throws -> Data {
    var result = Data()
    if size != 0 {
      try ProtocolTransportTry(error: TProtocolError(message: "Transport Read Failed")) {
        result = try self.transport.readAll(size: size)
      }
    }
    return result
  }
  
  func readVarint32() throws -> UInt32 {
    var result: UInt32 = 0
    var shift: UInt32 = 0
    while true {
      let byte: UInt8 = try read()
      
      result |= UInt32(byte & 0x7F) << shift
      if (byte & 0x80) == 0 {
        break
      }
      
      shift += 7
    }
    
    return result
  }
  
  func readVarint64() throws -> UInt64 {
    var result: UInt64 = 0
    var shift: UInt64 = 0
    
    while true {
      let byte: UInt8 = try read()
      
      result |= UInt64(byte & 0x7F) << shift
      if (byte & 0x80) == 0 {
        break
      }
      
      shift += 7
    }
    return result
  }
  

  func ttype(_ compactTypeVal: UInt8) throws -> TType {
    guard let compactType = TCType(rawValue: compactTypeVal) else {
      throw TProtocolError(message: "Unknown TCType value: \(compactTypeVal)")
    }
    
    switch compactType {
    case .stop: return .stop;
    case .boolean_FALSE, .boolean_TRUE: return .bool;
    case .i8: return .i8;
    case .i16: return .i16;
    case .i32: return .i32;
    case .i64: return .i64;
    case .double: return .double;
    case .binary: return .string;
    case .list: return .list;
    case .set: return .set;
    case .map: return .map;
    case .struct: return .struct;
    }
  }
  
  func compactType(_ ttype: TType) -> TCType {
    switch ttype {
    case .stop:   return .stop
    case .void:   return .i8
    case .bool:   return .boolean_FALSE
    case .i8:   return .i8
    case .double: return .double
    case .i16:    return .i16
    case .i32:    return .i32
    case .i64:    return .i64
    case .string: return .binary
    case .struct: return .struct
    case .map:    return .map
    case .set:    return .set
    case .list:   return .list
    case .utf8:   return .binary
    case .utf16:  return .binary
    }
  }
  
  /// ZigZag encoding maps signed integers to unsigned integers so that
  /// numbers with a small absolute value (for instance, -1) have
  /// a small varint encoded value too. It does this in a way that
  /// "zig-zags" back and forth through the positive and negative integers,
  /// so that -1 is encoded as 1, 1 is encoded as 2, -2 is encoded as 3, and so
  ///
  /// - parameter n: number to zigzag
  ///
  /// - returns: zigzaged UInt32
  func i32ToZigZag(_ n : Int32) -> UInt32 {
    return UInt32(bitPattern: Int32(n << 1) ^ Int32(n >> 31))
  }

  func i64ToZigZag(_ n : Int64) -> UInt64 {
    return UInt64(bitPattern: Int64(n << 1) ^ Int64(n >> 63))
  }

  func zigZagToi32(_ n: UInt32) -> Int32 {
    return Int32(n >> 1) ^ (-Int32(n & 1))
  }
  
  func zigZagToi64(_ n: UInt64) -> Int64 {
    return Int64(n >> 1) ^ (-Int64(n & 1))
  }
  
  
  
  /// Mark: - TProtocol  
  
  public func readMessageBegin() throws -> (String, TMessageType, Int32) {
    let protocolId: UInt8 = try read()
    
    if protocolId != TCompactProtocol.protocolID {
      let expected = String(format:"%2X", TCompactProtocol.protocolID)
      let got      = String(format:"%2X", protocolId)
      throw TProtocolError(message: "Wrong Protocol ID \(got)",
                           extendedError: .mismatchedProtocol(expected: expected, got: got))
    }

    let versionAndType: UInt8 = try read()
    let version: UInt8 = versionAndType & TCompactProtocol.versionMask
    if version != TCompactProtocol.version {
      throw TProtocolError(error: .badVersion(expected: "\(TCompactProtocol.version)",
                                              got:"\(version)"))
    }
    
    let type = (versionAndType >> UInt8(TCType.typeShiftAmount)) & TCType.typeBits
    guard let mtype = TMessageType(rawValue: Int32(type)) else {
      throw TProtocolError(message: "Unknown TMessageType value: \(type)")
    }
    let sequenceId = try readVarint32()
    let name: String = try read()
    
    return (name, mtype, Int32(sequenceId))
  }
  
  public func readMessageEnd() throws { }
  
  public func readStructBegin() throws -> String {
    lastField.append(lastFieldId)
    lastFieldId = 0
    return ""
  }
  
  public func readStructEnd() throws {
    lastFieldId = lastField.last ?? 0
    lastField.removeLast()
  }
  
  public func readFieldBegin() throws -> (String, TType, Int32) {
    let byte: UInt8 = try read()
    guard let type = TCType(rawValue: byte & 0x0F) else {
      throw TProtocolError(message: "Unknown TCType \(byte & 0x0F)")
    }
    
    // if it's a stop, then we can return immediately, as the struct is over
    if type == .stop {
      return ("", .stop, 0)
    }
    
    var fieldId: Int16 = 0
    
    // mask off the 4MSB of the type header.  it could contain a field id delta
    let modifier = (byte & 0xF0) >> 4
    if modifier == 0 {
      // not a delta.  look ahead for the zigzag varint field id
      fieldId = try read()
    } else {
      // has a delta.  add the delta to the last Read field id.
      fieldId = Int16(lastFieldId + modifier)
    }
    
    let fieldType = try ttype(type.rawValue)
    
    // if this happens to be a boolean field, the value is encoded in the type
    if type == .boolean_TRUE || type == .boolean_FALSE {
      // save the boolean value in a special instance variable
      booleanValue = type == .boolean_TRUE
    }
    
    // push the new field onto the field stack so we can keep the deltas going
    lastFieldId = UInt8(fieldId)
    return ("", fieldType, Int32(fieldId))
  }
  
  public func readFieldEnd() throws { }
  
  public func read() throws -> String {
    let length = try readVarint32()
    
    var result: String
    
    if length != 0 {
      let data = try readBinary(Int(length))
      result = String(data: data, encoding: String.Encoding.utf8) ?? ""
    } else {
      result = ""
    }
    
    return result
  }
  
  public func read() throws -> Bool {
    if let val = booleanValue {
      self.booleanValue = nil
      return val
    } else {
      let result = try read() as UInt8
      return TCType(rawValue: result) == .boolean_TRUE
    }
  }
  
  public func read() throws -> UInt8 {
    var buff: UInt8 = 0
    try ProtocolTransportTry(error: TProtocolError(message: "Transport Read Failed")) {
      buff = try self.transport.readAll(size: 1)[0]
    }
    return buff
  }
  
  public func read() throws -> Int16 {
    let v = try readVarint32()
    return Int16(zigZagToi32(v))
  }
  
  public func read() throws -> Int32 {
    let v = try readVarint32()
    return zigZagToi32(v)
  }
  
  public func read() throws -> Int64 {
    let v = try readVarint64()
    return zigZagToi64(v)
  }
  
  public func read() throws -> Double {
    var buff = Data()
    try ProtocolTransportTry(error: TProtocolError(message: "Transport Read Failed")) {
      buff = try self.transport.readAll(size: 8)
    }
    let i64: UInt64 = buff.withUnsafeBytes { $0.load(as: UInt64.self) }
    let bits = CFSwapInt64LittleToHost(i64)
    return Double(bitPattern: bits)
  }
  
  public func read() throws -> Data {
    let length = try readVarint32()
    return try readBinary(Int(length))
  }
  
  public func readMapBegin() throws -> (TType, TType, Int32) {
    var keyAndValueType: UInt8 = 8
    let size = try readVarint32()
    if size != 0 {
      keyAndValueType = try read()
    }
    
    let keyType = try ttype(keyAndValueType >> 4)
    let valueType = try ttype(keyAndValueType & 0xF)
    
    return (keyType, valueType, Int32(size))
  }
  
  public func readMapEnd() throws { }
  
  public func readSetBegin() throws -> (TType, Int32) {
    return try readListBegin()
  }
  
  public func readSetEnd() throws { }
  
  public func readListBegin() throws -> (TType, Int32) {
    let sizeAndType: UInt8 = try read()
    var size: UInt32 = UInt32(sizeAndType >> 4) & 0x0f
    if size == 15 {
      size = try readVarint32()
    }
    let elementType = try ttype(sizeAndType & 0x0F)
    
    return (elementType, Int32(size))
  }
  
  public func readListEnd() throws { }
  
  public func writeMessageBegin(name: String,
                                type messageType: TMessageType,
                                sequenceID: Int32) throws {
    try writebyteDirect(TCompactProtocol.protocolID)
    let nextByte: UInt8 = (TCompactProtocol.version & TCompactProtocol.versionMask) |
                          (UInt8((UInt32(messageType.rawValue) << UInt32(TCType.typeShiftAmount))) &
                          TCType.typeMask)
    try writebyteDirect(nextByte)
    try writeVarint32(UInt32(sequenceID))
    try write(name)
    
    currentMessageName = name
  }
  
  public func writeMessageEnd() throws {
    currentMessageName = nil
  }
  
  public func writeStructBegin(name: String) throws {
    lastField.append(lastFieldId)
    lastFieldId = 0
  }
  
  public func writeStructEnd() throws {
    lastFieldId = lastField.last ?? 0
    lastField.removeLast()
  }
  
  public func writeFieldBegin(name: String,
                              type fieldType: TType,
                              fieldID: Int32) throws {
    if fieldType == .bool {
      boolFieldName = name
      boolFieldType = fieldType
      boolFieldId = fieldID
      return
    } else {
      try writeFieldBeginInternal(name: name,
                                  type: fieldType,
                                  fieldID: fieldID,
                                  typeOverride: 0xFF)
    }
  }
  
  func writeFieldBeginInternal(name: String,
                               type fieldType: TType,
                               fieldID: Int32,
                               typeOverride: UInt8) throws {
    
    let typeToWrite = typeOverride == 0xFF ? compactType(fieldType).rawValue : typeOverride
    
    // check if we can use delta encoding for the field id
    let diff = UInt8(fieldID) - lastFieldId
    if (UInt8(fieldID) > lastFieldId) && (diff <= 15) {
      // Write them together
      try writebyteDirect((UInt8(fieldID) - lastFieldId) << 4 | typeToWrite)
      
    } else {
      // Write them separate
      try writebyteDirect(typeToWrite)
      try write(Int16(fieldID))
    }
    
    lastFieldId = UInt8(fieldID)
  }
  
  public func writeFieldStop() throws {
    try writebyteDirect(TCType.stop.rawValue)
  }
  
  public func writeFieldEnd() throws { }
  
  public func writeMapBegin(keyType: TType, valueType: TType, size: Int32) throws {
    if size == 0 {
      try writebyteDirect(0)
    } else {
      try writeVarint32(UInt32(size))
      
      let compactedTypes = compactType(keyType).rawValue << 4 | compactType(valueType).rawValue
      try writebyteDirect(compactedTypes)
    }
  }
  
  public func writeMapEnd() throws { }
  
  public func writeSetBegin(elementType: TType, size: Int32) throws {
    try writeCollectionBegin(elementType, size: size)
  }
  
  public func writeSetEnd() throws { }
  
  public func writeListBegin(elementType: TType, size: Int32) throws {
    try writeCollectionBegin(elementType, size: size)
  }
  
  public func writeListEnd() throws { }
  
  public func write(_ value: String) throws {
    try write(value.data(using: String.Encoding.utf8)!)
  }
  
  public func write(_ value: Bool) throws {
    if let boolFieldId = boolFieldId, let boolFieldType = boolFieldType,
       let boolFieldName = boolFieldName {
      
      // we haven't written the field header yet
      let compactType: TCType = value ? .boolean_TRUE : .boolean_FALSE
      try writeFieldBeginInternal(name: boolFieldName, type: boolFieldType, fieldID: boolFieldId,
                                  typeOverride: compactType.rawValue)
      self.boolFieldId = nil
      self.boolFieldType = nil
      self.boolFieldName = nil
    } else {
      // we're not part of a field, so just write the value.
      try writebyteDirect(value ? TCType.boolean_TRUE.rawValue : TCType.boolean_FALSE.rawValue)
    }
  }

  public func write(_ value: UInt8) throws {
    try writebyteDirect(value)
  }

  public func write(_ value: Int16) throws {
    try writeVarint32(i32ToZigZag(Int32(value)))
  }
  
  public func write(_ value: Int32) throws {
    try writeVarint32(i32ToZigZag(value))
  }
  
  public func write(_ value: Int64) throws {
    try writeVarint64(i64ToZigZag(value))
  }
  
  public func write(_ value: Double) throws {
    var bits = CFSwapInt64HostToLittle(value.bitPattern)
    let data = withUnsafePointer(to: &bits) {
      return Data(bytes: UnsafePointer<UInt8>(OpaquePointer($0)), count: MemoryLayout<UInt64>.size)
    }
    try ProtocolTransportTry(error: TProtocolError(message: "Transport Write Failed")) {
      try self.transport.write(data: data)
    }
  }
  
  public func write(_ data: Data) throws {
    try writeVarint32(UInt32(data.count))
    try ProtocolTransportTry(error: TProtocolError(message: "Transport Write Failed")) {
      try self.transport.write(data: data)
    }
  }
}
