//===--- SimplifyDestructure.swift ----------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import SIL

extension DestructureTupleInst : OnoneSimplifiable, SILCombineSimplifiable {
  func simplify(_ context: SimplifyContext) {
    foldWithAggregateConstruction(context)
  }
}

extension DestructureStructInst : OnoneSimplifiable, SILCombineSimplifiable {
  func simplify(_ context: SimplifyContext) {
    foldWithAggregateConstruction(context)
  }
}

private protocol DestructureInstruction : MultipleValueInstruction {
  func createExtract(of aggregate: Value, elementIndex: Int, using builder: Builder) -> Value
}

extension DestructureTupleInst: DestructureInstruction {
  func createExtract(of aggregate: Value, elementIndex: Int, using builder: Builder) -> Value {
    return builder.createTupleExtract(tuple: aggregate, elementIndex: elementIndex)
  }
}

extension DestructureStructInst: DestructureInstruction {
  func createExtract(of aggregate: Value, elementIndex: Int, using builder: Builder) -> Value {
    return builder.createStructExtract(struct: aggregate, fieldIndex: elementIndex)
  }
}

private protocol ConstructureInstruction : SingleValueInstruction {}

extension TupleInst: ConstructureInstruction {}
extension StructInst: ConstructureInstruction {}

private extension DestructureInstruction {
  var aggregate: Value { operands[0].value }

  func foldWithAggregateConstruction(_ context: SimplifyContext) {

    if aggregate.type.isTrivial(in: parentFunction) {
      // ```
      //   (%1, %2) = destructure_tuple %t
      // ```
      // ->
      // ```
      //   %1 = tuple_extract %t, 0
      //   %2 = tuple_extract %t, 1
      // ```
      replaceWithAggregateExtract(context)
      return
    }

    switch aggregate {
    case let constructInst as ConstructureInstruction:
      // Eliminate the redundant instruction pair
      // ```
      //   %t = tuple (%0, %1, %2)
      //   (%3, %4, %5) = destructure_tuple %t
      // ```
      // and replace the results %3, %4, %5 with %0, %1, %2, respectively
      //
      tryFoldWithConstructure(constructure: constructInst, context)

    case let copy as CopyValueInst:
      // Similar to the pattern above, but with a copy_value:
      // Replace
      // ```
      //   %t = tuple (%0, %1, %2)
      //   %c = copy_value %t           // can also be a chain of multiple copies
      //   (%3, %4, %5) = destructure_tuple %c
      // ```
      // with
      // ```
      //   %c0 = copy_value %0
      //   %c1 = copy_value %1
      //   %c2 = copy_value %2
      //   %s = tuple (%0, %1, %2)      // keep the original tuple/struct instruction
      // ```
      // and replace the results %3, %4, %5 with %c0, %c1, %c2, respectively.
      //
      // This transformation has the advantage that we can do it even if the `tuple`/`struct`
      // has other uses than the `copy_value`.
      //
      tryFoldWithCopyOfConstructure(copy: copy, context)

    default:
      break
    }
  }

  private func replaceWithAggregateExtract(_ context: SimplifyContext) {
    let builder = Builder(before: self, context)
    for (elementIdx, result) in results.enumerated() {
      let elementValue = createExtract(of: aggregate, elementIndex: elementIdx, using: builder)
      result.uses.replaceAll(with: elementValue, context)
    }
    context.erase(instruction: self)
  }

  private func tryFoldWithConstructure(constructure: ConstructureInstruction, _ context: SimplifyContext) {
    let singleConstructureUse = context.preserveDebugInfo ? constructure.uses.singleUse : constructure.uses.ignoreDebugUses.singleUse
    let canEraseConstructure = singleConstructureUse?.instruction == self

    if !canEraseConstructure && constructure.ownership == .owned {
      // We cannot add more uses to this tuple/struct without inserting a copy.
      return
    }

    for (result, operand) in zip(self.results, constructure.operands) {
      result.uses.replaceAll(with: operand.value, context)
    }

    context.erase(instruction: self)
    if canEraseConstructure {
      context.erase(instructionIncludingDebugUses: constructure)
    }
  }

  private func tryFoldWithCopyOfConstructure(copy: CopyValueInst, _ context: SimplifyContext) {
    guard copy.uses.singleUse?.instruction == self,
          let constructure = copy.fromValue.lookThroughCopy as? ConstructureInstruction,
          constructure.parentBlock == self.parentBlock
    else {
      return
    }
    for (result, operand) in zip(self.results, constructure.operands) {
      if operand.value.type.isTrivial(in: parentFunction) {
        result.uses.replaceAll(with: operand.value, context)
      } else {
        let builder = Builder(before: constructure, context)
        let copiedOperand = builder.createCopyValue(operand: operand.value)
        result.uses.replaceAll(with: copiedOperand, context)
      }
    }
    context.erase(instruction: self)
    context.erase(instruction: copy)
  }
}
