// Generates combinations of different block types and operations for
// non-defaultable locals (local.set / .tee / .get).
// See the function references specification on the updated algorithm
// for validating non-null references in locals.

const KINDS = [
  "block",
  "loop",
  "try",
  "catch",
  "delegate",
  "if",
  "else",
]
const INITIALIZED = [
  "nowhere",
  "outer",
  "inner",
  "outer-tee",
  "inner-tee",
];
const USED = [
  "outer",
  "inner",
  "after-inner",
  "after-outer",
];

function generateBlock(kind, contents) {
  switch (kind) {
    case "block": {
      return `block\n${contents}end\n`
    }
    case "loop": {
      return `loop\n${contents}end\n`
    }
    case "try": {
      return `try\n${contents}end\n`
    }
    case "catch": {
      return `try\ncatch_all\n${contents}end\n`
    }
    case "delegate": {
      return `try\n${contents}\ndelegate 0\n`
    }
    case "if": {
      return `i32.const 0\nif\n${contents}end\n`
    }
    case "else": {
      return `i32.const 0\nif\nelse\n${contents}end\n`
    }
  }
}

// Generate a variation of the module below:
//
// (func
//  (block
//    $outer
//    (block
//      $inner
//    )
//    $after-inner
//  )
//  $after-outer
// )
//
// Where a local is used and initialized at different points depending on the
// parameters. The block kinds of the inner and outer block may also be
// customized.
function generateModule(outerBlockKind, innerBlockKind, initializedWhere, usedWhere) {
  const INITIALIZE_STMT = '(local.set 0 ref.func 0)\n';
  const INITIALIZE_STMT2 = '(drop (local.tee 0 ref.func 0))\n';
  const USE_STMT = '(drop local.get 0)\n';

  // inner block
  let innerBlockContents = '';
  if (initializedWhere === 'inner') {
    innerBlockContents += INITIALIZE_STMT;
  } else if (initializedWhere === 'inner-tee') {
    innerBlockContents += INITIALIZE_STMT2;
  }
  if (usedWhere === 'inner') {
    innerBlockContents += USE_STMT;
  }
  let innerBlock = generateBlock(innerBlockKind, innerBlockContents);

  // outer block
  let outerBlockContents = '';
  if (initializedWhere === 'outer') {
    outerBlockContents += INITIALIZE_STMT;
  } else if (initializedWhere === 'outer-tee') {
    outerBlockContents += INITIALIZE_STMT2;
  }
  if (usedWhere === 'outer') {
    outerBlockContents += USE_STMT;
  }
  outerBlockContents += innerBlock;
  if (usedWhere === 'after-inner') {
    outerBlockContents += USE_STMT;
  }
  let outerBlock = generateBlock(outerBlockKind, outerBlockContents);

  // after outer block
  let afterOuterBlock = '';
  if (usedWhere === 'after-outer') {
    afterOuterBlock += USE_STMT;
  }

  return `(module
  (type $t (func))
  (func (export "test")
    (local (ref $t))
${outerBlock}${afterOuterBlock}  )
)`;
}

const LOGGING = false;

for (let outer of KINDS) {
  for (let inner of KINDS) {
    for (let initialized of INITIALIZED) {
      for (let used of USED) {
        let text = generateModule(outer, inner, initialized, used);

        let expectPass;
        switch (initialized) {
          case "outer":
          case "outer-tee": {
            // Defining the local in the outer block makes it valid
            // in the outer block, the inner block, and after the
            // inner block
            expectPass = used !== "after-outer";
            break;
          }
          case "inner":
          case "inner-tee": {
            // Defining the local in the inner block makes it valid
            // in the inner block
            //
            // NOTE: an extension to typing could make this valid
            // after the inner block in some cases
            expectPass = used === "inner";
            break;
          }
          case "nowhere": {
            // Not defining the local makes it always invalid to
            // use
            expectPass = false;
            break;
          }
        }

        if (LOGGING) {
          console.log();
          console.log(`TEST: outer=${outer}, inner=${inner}, initialized=${initialized}, used=${used}`);
          console.log(expectPass ? "EXPECT PASS" : "EXPECT FAIL");
          console.log(text);
        }

        let binary = wasmTextToBinary(text);
        assertEq(WebAssembly.validate(binary), expectPass);
        if (!expectPass) {
          // Check if the error message is right.
          try {
            new WebAssembly.Module(binary);
          } catch (ex) {
            assertEq(true, /local\.get read from unset local/.test(ex.message));
          }
        }
      }
    }
  }
}
