/*
 * 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

/// `TableVerifier` verifies a table object is within a provided memory.
/// It checks if all the objects for a specific generated table, are within
/// the bounds of the buffer, aligned.
public struct TableVerifier {

  /// position of current table in `ByteBuffer`
  fileprivate var _position: Int

  /// Current VTable position
  fileprivate var _vtable: Int

  /// Length of current VTable
  fileprivate var _vtableLength: Int

  /// `Verifier` object created in the base verifable call.
  fileprivate var _verifier: Verifier

  /// Creates a `TableVerifier` verifier that allows the Flatbuffer object
  /// to verify the buffer before accessing any of the data.
  ///
  /// - Parameters:
  ///   - position: Current table Position
  ///   - vtable: Current `VTable` position
  ///   - vtableLength: Current `VTable` length
  ///   - verifier: `Verifier` Object  that caches the data of the verifiable object
  internal init(
    position: Int,
    vtable: Int,
    vtableLength: Int,
    verifier: inout Verifier
  ) {
    _position = position
    _vtable = vtable
    _vtableLength = vtableLength
    _verifier = verifier
  }

  /// Dereference the current object position from the `VTable`
  /// - Parameter field: Current VTable refrence to  position.
  /// - Throws: A `FlatbuffersErrors` incase the voffset is not aligned/outOfBounds/apparentSizeTooLarge
  /// - Returns: An optional position for current field
  internal mutating func dereference(_ field: VOffset) throws -> Int? {
    if field >= _vtableLength {
      return nil
    }

    /// Reading the offset for the field needs to be read.
    let offset: VOffset = try _verifier.getValue(
      at: Int(clamping: _vtable &+ Int(field)))

    if offset > 0 {
      return Int(clamping: _position &+ Int(offset))
    }
    return nil
  }

  /// Visits all the fields within the table to validate the integrity
  /// of the data
  /// - Parameters:
  ///   - field: voffset of the current field to be read
  ///   - fieldName: fieldname to report data Errors.
  ///   - required: If the field has to be available in the buffer
  ///   - type: Type of field to be read
  /// - Throws: A `FlatbuffersErrors` where the field is corrupt
  public mutating func visit<T>(
    field: VOffset,
    fieldName: String,
    required: Bool,
    type: T.Type
  ) throws where T: Verifiable {
    let derefValue = try dereference(field)

    if let value = derefValue {
      try T.verify(&_verifier, at: value, of: T.self)
      return
    }
    if required {
      throw FlatbuffersErrors.requiredFieldDoesntExist(
        position: field,
        name: fieldName)
    }
  }

  /// Visits all the fields for a union object within the table to
  /// validate the integrity of the data
  /// - Parameters:
  ///   - key: Current Key Voffset
  ///   - field: Current field Voffset
  ///   - unionKeyName: Union key name
  ///   - fieldName: Field key name
  ///   - required: indicates if an object is required to be present
  ///   - completion: Completion is a handler that WILL be called in the generated
  /// - Throws: A `FlatbuffersErrors` where the field is corrupt
  public mutating func visit<T>(
    unionKey key: VOffset,
    unionField field: VOffset,
    unionKeyName: String,
    fieldName: String,
    required: Bool,
    completion: @escaping (inout Verifier, T, Int) throws -> Void
  ) throws
  where T: UnionEnum {
    let keyPos = try dereference(key)
    let valPos = try dereference(field)

    if keyPos == nil && valPos == nil {
      if required {
        throw FlatbuffersErrors.requiredFieldDoesntExist(
          position: key,
          name: unionKeyName)
      }
      return
    }

    if let _key = keyPos,
      let _val = valPos
    {
      /// verifiying that the key is within the buffer
      try T.T.verify(&_verifier, at: _key, of: T.T.self)
      guard
        let _enum = try T.init(
          value: _verifier._buffer.read(
            def: T.T.self,
            position: _key))
      else {
        throw FlatbuffersErrors.unknownUnionCase
      }
      /// we are assuming that Unions will always be of type Uint8
      try completion(
        &_verifier,
        _enum,
        _val)
      return
    }
    throw FlatbuffersErrors.valueNotFound(
      key: keyPos,
      keyName: unionKeyName,
      field: valPos,
      fieldName: fieldName)
  }

  /// Visits and validates all the objects within a union vector
  /// - Parameters:
  ///   - key: Current Key Voffset
  ///   - field: Current field Voffset
  ///   - unionKeyName: Union key name
  ///   - fieldName: Field key name
  ///   - required: indicates if an object is required to be present
  ///   - completion: Completion is a handler that WILL be called in the generated
  /// - Throws: A `FlatbuffersErrors` where the field is corrupt
  public mutating func visitUnionVector<T>(
    unionKey key: VOffset,
    unionField field: VOffset,
    unionKeyName: String,
    fieldName: String,
    required: Bool,
    completion: @escaping (inout Verifier, T, Int) throws -> Void
  ) throws
  where T: UnionEnum {
    let keyVectorPosition = try dereference(key)
    let offsetVectorPosition = try dereference(field)

    if let keyPos = keyVectorPosition,
      let valPos = offsetVectorPosition
    {
      try UnionVector<T>.verify(
        &_verifier,
        keyPosition: keyPos,
        fieldPosition: valPos,
        unionKeyName: unionKeyName,
        fieldName: fieldName,
        completion: completion)
      return
    }
    if required {
      throw FlatbuffersErrors.requiredFieldDoesntExist(
        position: field,
        name: fieldName)
    }
  }

  /// Finishs the current Table verifier, and subtracts the current
  /// table from the incremented depth.
  public mutating func finish() {
    _verifier.finish()
  }
}
