/*
 * Copyright 2024 Google Inc. All rights reserved.
 *
 * Licensed 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

enum FlexBuffersErrors: Error {
  case sizeOfBufferIsTooSmall
  case typeCouldNotBeDetermined
}

@inline(__always)
public func getRoot(buffer: ByteBuffer) throws -> Reference? {
  let end = buffer.count
  if buffer.count < 3 {
    throw FlexBuffersErrors.sizeOfBufferIsTooSmall
  }

  let byteWidth = buffer.read(def: UInt8.self, position: end &- 1)
  let packedType = buffer.read(def: UInt8.self, position: end &- 2)
  let offset = end &- 2 &- numericCast(byteWidth)

  return Reference(
    byteBuffer: buffer,
    offset: offset,
    parentWidth: byteWidth,
    packedType: packedType)
}

@inline(__always)
public func getRootChecked(buffer: ByteBuffer) throws -> Reference? {
  // TODO(mustiikhalil): implement verifier
  return try getRoot(buffer: buffer)
}

public struct Reference {
  private let byteBuffer: ByteBuffer
  private let offset: Int
  private let parentWidth: UInt8
  private let byteWidth: UInt8

  public let type: FlexBufferType

  @inline(__always)
  init?(
    byteBuffer: ByteBuffer,
    offset: Int,
    parentWidth: UInt8,
    packedType: UInt8)
  {
    guard let type = FlexBufferType(rawValue: UInt64(packedType >> 2)) else {
      return nil
    }
    self.byteBuffer = byteBuffer
    self.offset = offset
    self.parentWidth = parentWidth
    byteWidth = 1 << (packedType & 3)
    self.type = type
  }

  @inline(__always)
  init(
    byteBuffer: ByteBuffer,
    offset: Int,
    parentWidth: UInt8,
    byteWidth: UInt8,
    type: FlexBufferType)
  {
    self.byteBuffer = byteBuffer
    self.offset = offset
    self.parentWidth = parentWidth
    self.byteWidth = byteWidth
    self.type = type
  }

  @inline(__always)
  public var bool: Bool? {
    return switch type {
    case .bool: byteBuffer.readUInt64(offset: offset, byteWidth: byteWidth) != 0
    default: nil
    }
  }

  @inline(__always)
  public var uint: UInt64? {
    return switch type {
    case .uint: byteBuffer.readUInt64(offset: offset, byteWidth: byteWidth)
    case .indirectUInt:
      byteBuffer.readUInt64(
        offset: indirect(),
        byteWidth: byteWidth)
    default: nil
    }
  }

  @inline(__always)
  public var int: Int64? {
    return switch type {
    case .int: byteBuffer.readInt64(offset: offset, byteWidth: byteWidth)
    case .indirectInt:
      byteBuffer.readInt64(
        offset: indirect(),
        byteWidth: byteWidth)
    default: nil
    }
  }

  @inline(__always)
  public var double: Double? {
    return switch type {
    case .float: byteBuffer.readDouble(offset: offset, byteWidth: byteWidth)
    case .indirectFloat:
      byteBuffer.readDouble(
        offset: indirect(),
        byteWidth: byteWidth)
    default: nil
    }
  }

  @inline(__always)
  public var map: Map? {
    guard type == .map else { return nil }
    return Map(
      byteBuffer: byteBuffer,
      offset: indirect(),
      byteWidth: byteWidth)
  }

  @inline(__always)
  public var vector: Vector? {
    guard type == .vector || type == .map else { return nil }
    return Vector(
      byteBuffer: byteBuffer,
      offset: indirect(),
      byteWidth: byteWidth)
  }

  @inline(__always)
  public var cString: String? {
    guard type == .string || type == .key else { return nil }
    let offset = indirect()

    let count = getCount(
      buffer: byteBuffer,
      offset: offset,
      byteWidth: byteWidth)

    return byteBuffer.readString(
      at: offset,
      count: count)
  }

  @inline(__always)
  public func blob<Result>(_ completion: (UnsafeRawBufferPointer) -> Result)
    -> Result?
  {
    guard type == .blob || type == .string else { return nil }

    let offset = indirect()
    let count = getCount(
      buffer: byteBuffer,
      offset: offset,
      byteWidth: byteWidth)
    return byteBuffer.withUnsafePointerToSlice(
      index: offset,
      count: count,
      body: completion)
  }

  @inline(__always)
  public var typedVector: TypedVector? {
    guard isTypedVectorType(type: type) else { return nil }
    guard var type = toTypedVectorElementType(type: type) else { return nil }
    if type == .string {
      type = .key
    }
    return TypedVector(
      byteBuffer: byteBuffer,
      offset: indirect(),
      byteWidth: byteWidth,
      type: type)
  }

  @inline(__always)
  public var fixedTypedVector: FixedTypedVector? {
    guard isFixedTypedVectorType(type: type) else { return nil }
    let t = toFixedTypedVectorElementType(type: type)
    guard let type = t.type else { return nil }
    return FixedTypedVector(
      byteBuffer: byteBuffer,
      offset: indirect(),
      byteWidth: byteWidth,
      type: type,
      count: t.count)
  }

  @inline(__always)
  public func string(encoding: String.Encoding = .utf8) -> String? {
    guard type == .string else { return nil }
    let offset = indirect()

    let count = getCount(
      buffer: byteBuffer,
      offset: offset,
      byteWidth: byteWidth)

    return byteBuffer.readString(
      at: offset,
      count: count,
      type: encoding)
  }

  @inline(__always)
  public func asInt<T: FixedWidthInteger>() -> T? {
    guard let v = int else {
      return nil
    }
    return numericCast(v)
  }

  @inline(__always)
  public func asUInt<T: FixedWidthInteger>() -> T? {
    guard let v = uint else {
      return nil
    }
    return numericCast(v)
  }

  @inline(__always)
  public func withUnsafeRawPointer<Result>(
    _ completion: (UnsafeRawPointer) throws
      -> Result)
    rethrows -> Result?
  {
    return try byteBuffer.readWithUnsafeRawPointer(
      position: indirect(),
      completion)
  }

  private func indirect() -> Int {
    readIndirect(buffer: byteBuffer, offset: offset, parentWidth)
  }
}

extension Reference {

  public func jsonString() -> String {
    var str = ""
    jsonBuilder(json: &str)
    return str
  }

  func jsonBuilder(json: inout String) {
    switch type {
    case .null:
      json += StaticJSON.null
    case .uint, .indirectUInt:
      json += uint.valueOrNull
    case .int, .indirectInt:
      json += int.valueOrNull
    case .float, .indirectFloat:
      json += double.valueOrNull
    case .string, .key:
      json += "\"\(cString ?? StaticJSON.null)\""
    case .map:
      map?.jsonBuilder(json: &json)
    case .bool:
      json += bool.valueOrNull
    case .blob:
      if let p = blob({ String(data: Data($0), encoding: .utf8) })?
        .valueOrNull
      {
        json += "\"\(p)\""
      } else {
        json += StaticJSON.null
      }
    default:
      if type == .vector {
        vector?.jsonBuilder(json: &json)
      } else if isTypedVectorType(type: type) {
        typedVector?.jsonBuilder(json: &json)
      } else if isFixedTypedVectorType(type: type) {
        fixedTypedVector?.jsonBuilder(json: &json)
      } else {
        json += StaticJSON.null
      }
    }
  }
}
